Merge SDK 1.10.0 feature branch into main
This commit is contained in:
commit
126ac23824
|
@ -81,3 +81,4 @@ DecompileChecker.kt
|
|||
backup-dbs/
|
||||
*.db
|
||||
.DS_Store
|
||||
/.idea/androidTestResultsUserPreferences.xml
|
||||
|
|
67
CHANGELOG.md
67
CHANGELOG.md
|
@ -1,6 +1,73 @@
|
|||
Change Log
|
||||
==========
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
- `cash.z.ecc.android.sdk`:
|
||||
- `Synchronizer.getUnifiedAddress`
|
||||
- `Synchronizer.getSaplingAddress`
|
||||
- `Synchronizer.isValidUnifiedAddr`
|
||||
- `Synchronizer.getMemos(TransactionOverview)`
|
||||
- `Synchronizer.getReceipients(TransactionOverview)`
|
||||
- `cash.z.ecc.android.sdk.model`:
|
||||
- `Account`
|
||||
- `FirstClassByteArray`
|
||||
- `PendingTransaction`
|
||||
- `Transaction`
|
||||
- `UnifiedSpendingKey`
|
||||
- `cash.z.ecc.android.sdk.tool`:
|
||||
- `DerivationTool.deriveUnifiedSpendingKey`
|
||||
- `DerivationTool.deriveUnifiedFullViewingKey`
|
||||
- `DerivationTool.deriveTransparentAccountPrivateKey`
|
||||
- `DerivationTool.deriveTransparentAddressFromAccountPrivateKey`
|
||||
- `DerivationTool.deriveUnifiedAddress`
|
||||
- `DerivationTool.deriveUnifiedFullViewingKeys`
|
||||
- `DerivationTool.validateUnifiedFullViewingKey`
|
||||
- Still unimplemented.
|
||||
- `cash.z.ecc.android.sdk.type`:
|
||||
- `AddressType.Unified`
|
||||
- `UnifiedFullViewingKey`, representing a Unified Full Viewing Key as specified in
|
||||
[ZIP 316](https://zips.z.cash/zip-0316#encoding-of-unified-full-incoming-viewing-keys).
|
||||
|
||||
### Changed
|
||||
- The following methods now take or return `UnifiedFullViewingKey` instead of
|
||||
`UnifiedViewingKey`:
|
||||
- `cash.z.ecc.android.sdk`:
|
||||
- `Initializer.Config.addViewingKey`
|
||||
- `Initializer.Config.importWallet`
|
||||
- `Initializer.Config.newWallet`
|
||||
- `Initializer.Config.setViewingKeys`
|
||||
- `cash.z.ecc.android.sdk`:
|
||||
- `Synchronizer.Companion.new` now takes many of the arguments previously passed to `Initializer`. In addition, an optional `seed` argument is required for first-time initialization or if `Synchronizer.new` throws an exception indicating that an internal migration requires the wallet seed. (This second case will be true the first time existing clients upgrade to this new version of the SDK).
|
||||
- `Synchronizer.sendToAddress` now takes a `UnifiedSpendingKey` instead of an encoded
|
||||
Sapling extended spending key, and the `fromAccountIndex` argument is now implicit in
|
||||
the `UnifiedSpendingKey`.
|
||||
- `Synchronizer.shieldFunds` now takes a `UnifiedSpendingKey` instead of separately
|
||||
encoded Sapling and transparent keys.
|
||||
- `Synchronizer` methods that previously took an `Int` for account index now take an `Account` object
|
||||
|
||||
### Removed
|
||||
- `cash.z.ecc.android.sdk`:
|
||||
- `Initializer` (use `Synchronizer.new` instead)
|
||||
- `Synchronizer.getAddress` (use `Synchronizer.getUnifiedAddress` instead).
|
||||
- `Synchronizer.getShieldedAddress` (use `Synchronizer.getSaplingAddress` instead)
|
||||
- `Synchronizer.cancel`
|
||||
- `cash.z.ecc.android.sdk.type.UnifiedViewingKey`
|
||||
- This type had a bug where the `extpub` field actually was storing a plain transparent
|
||||
public key, and not the extended public key as intended. This made it incompatible
|
||||
with ZIP 316.
|
||||
- `cash.z.ecc.android.sdk.tool`:
|
||||
- `DerivationTool.deriveSpendingKeys` (use `DerivationTool.deriveUnifiedSpendingKey` instead)
|
||||
- `DerivationTool.deriveViewingKey` (use `DerivationTool.deriveUnifiedFullViewingKey` instead)
|
||||
- `DerivationTool.deriveTransparentAddress` (use `Synchronizer.getLegacyTransparentAddress` instead).
|
||||
- `DerivationTool.deriveTransparentAddressFromPrivateKey` (use `Synchronizer.getLegacyTransparentAddress` instead).
|
||||
- `DerivationTool.deriveTransparentAddressFromPublicKey` (use `Synchronizer.getLegacyTransparentAddress` instead).
|
||||
- `DerivationTool.deriveTransparentSecretKey` (use `DerivationTool.deriveUnifiedSpendingKey` instead).
|
||||
- `DerivationTool.deriveShieldedAddress`
|
||||
- `DerivationTool.deriveUnifiedViewingKeys` (use `DerivationTool.deriveUnifiedFullViewingKey` instead)
|
||||
- `DerivationTool.validateUnifiedViewingKey`
|
||||
|
||||
Version 1.9.0-beta05
|
||||
------------------------------------
|
||||
- The minimum version of Android supported is now API 21
|
||||
|
|
|
@ -1,7 +1,32 @@
|
|||
Troubleshooting Migrations
|
||||
==========
|
||||
|
||||
Upcoming
|
||||
Migration to Version 1.10
|
||||
---------------------------------
|
||||
The way the SDK is initialized has changed. The `Initializer` object has been removed and `Synchronizer.new` now takes a longer parameter list which includes the parameters previously passed to `Initializer`.
|
||||
|
||||
SDK initialization also now requires access to the seed bytes at two times: 1. during new wallet creation and 2. during upgrade of an existing wallet to SDK 1.10 due to internal data migrations. To handle case #2, client should wrap `Synchronizer.new()` with a try-catch for `InitializerException.SeedRequired`. Clients can pass `null` to try to initialize the SDK without the seed, then try again if the exception is thrown to indicate the seed is needed. This pattern future-proofs initialization, as the seed may be required by future SDK updates.
|
||||
|
||||
To improve type safety of the public API, Zcash account indexes are now represented by an `Account` object. The SDK currently only supports the default account, `Account.DEFAULT`. Migration will effectively require replacing APIs with an account `0` with `Account.DEFAULT`.
|
||||
|
||||
To support Network Upgrade 5, the way keys are generated has changed.
|
||||
|
||||
For SDK clients that regenerate the keys from a mnemonic, migration might look like:
|
||||
* Replace usage of `UnifiedViewingKey` with `UnifiedFullViewingKey`
|
||||
* Replace `DerivationTool.deriveUnifiedViewingKeys` with `DerivationTool.deriveUnifiedFullViewingKeys`
|
||||
|
||||
For SDK clients that store the key separately from the mnemonic, the migration might look like:
|
||||
* Replace usage of `UnifiedViewingKey(extfvk: String, extpub: String)` with `UnifiedFullViewingKey(encoding: String)`
|
||||
* Replace `DerivationTool.deriveUnifiedViewingKeys` with `DerivationTool.deriveUnifiedFullViewingKeys`
|
||||
* Delete any previously persisted values for `UnifiedViewingKey(extfvk: String, extpub: String)`,
|
||||
provided that they can be rederived from the mnemonic.
|
||||
* Re-generate the key from the mnemonic using `DerivationTool.deriveUnifiedFullViewingKeys`
|
||||
|
||||
To support Unified Addresses (UAs), some APIs have been modified. In particular, `Synchronizer.getUnifiedAddress()` returns the unified address while `Synchronizer.getSaplingAddress()` and `Synchronizer.getTransparentAddress()` return the sapling or transparent components of the unified address. Due to this change and the derivation of different addresses from UAs, clients may notice that the transparent address returned by this API is different from the transparent address returned by older versions of the SDK. Note that UA support does not yet encompass orchard addresses.
|
||||
|
||||
Due to internal changes in the SDK, the way transactions are queried and represented works differently. The previous ConfirmedTransaction object has been replaced with `TransactionOverview` which contains less information. Missing fields, such as memos and recipients, can be queried with `Synchronizer.getMemos(TransactionOverview)` and `Synchronizer.getReceipients(TransactionOverview)`.
|
||||
|
||||
Migration to Version 1.9
|
||||
--------------------------------------
|
||||
`ZcashNetwork` is no longer an enum. The prior enum values are now declared as object properties `ZcashNetwork.Mainnet` and `ZcashNetwork.Testnet`. For the most part, this change should have minimal impact. ZcashNetwork was also moved from the package `cash.z.ecc.android.sdk.type` to `cash.z.ecc.android.sdk.model`, which will require a change to your import statements. The server fields have been removed from `ZcashNetwork`, allowing server and network configuration to be done independently.
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.darkside.test
|
|||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.Darkside
|
||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
||||
|
@ -214,8 +215,8 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
|
|||
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.processor.getBalanceInfo(accountIndex)
|
||||
suspend fun validateBalance(available: Long = -1, total: Long = -1, account: Account) {
|
||||
val balance = synchronizer.processor.getBalanceInfo(account)
|
||||
if (available > 0) {
|
||||
assertEquals("invalid available balance", available, balance.available)
|
||||
}
|
||||
|
|
|
@ -3,19 +3,19 @@ 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.Account
|
||||
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.WalletBalance
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.isPending
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
|
@ -64,22 +64,24 @@ class TestWallet(
|
|||
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 = runBlocking { Synchronizer.new(initializer) } as SdkSynchronizer
|
||||
runBlocking { DerivationTool.deriveUnifiedSpendingKey(seed, network = network, Account.DEFAULT) }
|
||||
val synchronizer: SdkSynchronizer = Synchronizer.newBlocking(
|
||||
context,
|
||||
network,
|
||||
alias,
|
||||
endpoint,
|
||||
seed,
|
||||
startHeight
|
||||
)
|
||||
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 unifiedAddress =
|
||||
runBlocking { synchronizer.getUnifiedAddress(Account.DEFAULT) }
|
||||
val transparentAddress =
|
||||
runBlocking { DerivationTool.deriveTransparentAddress(seed, network = network) }
|
||||
runBlocking { synchronizer.getTransparentAddress(Account.DEFAULT) }
|
||||
val birthdayHeight get() = synchronizer.latestBirthdayHeight
|
||||
val networkName get() = synchronizer.network.networkName
|
||||
|
||||
|
@ -109,9 +111,9 @@ class TestWallet(
|
|||
return this
|
||||
}
|
||||
|
||||
suspend fun send(address: String = transparentAddress, memo: String = "", amount: Zatoshi = Zatoshi(500L), fromAccountIndex: Int = 0): TestWallet {
|
||||
suspend fun send(address: String = transparentAddress, memo: String = "", amount: Zatoshi = Zatoshi(500L)): TestWallet {
|
||||
Twig.sprout("$alias sending")
|
||||
synchronizer.sendToAddress(shieldedSpendingKey, amount, address, memo, fromAccountIndex)
|
||||
synchronizer.sendToAddress(shieldedSpendingKey, amount, address, memo)
|
||||
.takeWhile { it.isPending(null) }
|
||||
.collect {
|
||||
twig("Updated transaction: $it")
|
||||
|
@ -135,7 +137,7 @@ class TestWallet(
|
|||
twig("FOUND utxo balance of total: ${walletBalance.total} available: ${walletBalance.available}")
|
||||
|
||||
if (walletBalance.available.value > 0L) {
|
||||
synchronizer.shieldFunds(shieldedSpendingKey, transparentSecretKey)
|
||||
synchronizer.shieldFunds(shieldedSpendingKey)
|
||||
.onCompletion { twig("done shielding funds") }
|
||||
.catch { twig("Failed with $it") }
|
||||
.collect()
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
package cash.z.wallet.sdk.sample.demoapp
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import cash.z.ecc.android.sdk.Initializer
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.db.entity.isFailure
|
||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||
import cash.z.ecc.android.sdk.ext.convertZecToZatoshi
|
||||
import cash.z.ecc.android.sdk.ext.toHex
|
||||
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
|
||||
import cash.z.ecc.android.sdk.internal.Twig
|
||||
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
||||
import cash.z.ecc.android.sdk.model.Mainnet
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
||||
import cash.z.ecc.android.sdk.model.isFailure
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
@ -60,23 +62,10 @@ class SampleCodeTest {
|
|||
// log(entropy.asSeedPhrase())
|
||||
}
|
||||
|
||||
// ///////////////////////////////////////////////////
|
||||
// Derive Extended Spending Key
|
||||
@Test fun deriveSpendingKey() {
|
||||
val spendingKeys = runBlocking {
|
||||
DerivationTool.deriveSpendingKeys(
|
||||
seed,
|
||||
ZcashNetwork.Mainnet
|
||||
)
|
||||
}
|
||||
assertEquals(1, spendingKeys.size)
|
||||
log("Spending Key: ${spendingKeys[0]}")
|
||||
}
|
||||
|
||||
// ///////////////////////////////////////////////////
|
||||
// Get Address
|
||||
@Test fun getAddress() = runBlocking {
|
||||
val address = synchronizer.getAddress()
|
||||
val address = synchronizer.getUnifiedAddress()
|
||||
assertFalse(address.isBlank())
|
||||
log("Address: $address")
|
||||
}
|
||||
|
@ -142,7 +131,7 @@ class SampleCodeTest {
|
|||
val amount = 0.123.convertZecToZatoshi()
|
||||
val address = "ztestsapling1tklsjr0wyw0d58f3p7wufvrj2cyfv6q6caumyueadq8qvqt8lda6v6tpx474rfru9y6u75u7qnw"
|
||||
val memo = "Test Transaction"
|
||||
val spendingKey = DerivationTool.deriveSpendingKeys(seed, ZcashNetwork.Mainnet)[0]
|
||||
val spendingKey = DerivationTool.deriveUnifiedSpendingKey(seed, ZcashNetwork.Mainnet, Account.DEFAULT)
|
||||
val transactionFlow = synchronizer.sendToAddress(spendingKey, amount, address, memo)
|
||||
transactionFlow.collect {
|
||||
log("pending transaction updated $it")
|
||||
|
@ -160,8 +149,14 @@ class SampleCodeTest {
|
|||
|
||||
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
private val synchronizer: Synchronizer = run {
|
||||
val initializer = runBlocking { Initializer.new(context) {} }
|
||||
Synchronizer.newBlocking(initializer)
|
||||
val network = ZcashNetwork.fromResources(context)
|
||||
Synchronizer.newBlocking(
|
||||
context,
|
||||
network,
|
||||
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
|
||||
seed = seed,
|
||||
birthday = null
|
||||
)
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
|
|
|
@ -2,6 +2,8 @@ package cash.z.ecc.android.sdk.demoapp
|
|||
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
|
@ -30,11 +32,9 @@ import com.google.android.material.navigation.NavigationView
|
|||
@Suppress("TooManyFunctions")
|
||||
class MainActivity :
|
||||
AppCompatActivity(),
|
||||
ClipboardManager.OnPrimaryClipChangedListener,
|
||||
DrawerLayout.DrawerListener {
|
||||
private lateinit var appBarConfiguration: AppBarConfiguration
|
||||
private lateinit var clipboard: ClipboardManager
|
||||
private var clipboardListener: ((String?) -> Unit)? = null
|
||||
var fabListener: BaseDemoFragment<out ViewBinding>? = null
|
||||
|
||||
/**
|
||||
|
@ -49,7 +49,6 @@ class MainActivity :
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboard.addPrimaryClipChangedListener(this)
|
||||
setContentView(R.layout.activity_main)
|
||||
val toolbar: Toolbar = findViewById(R.id.toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
|
@ -86,6 +85,10 @@ class MainActivity :
|
|||
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)
|
||||
|
||||
if (ZcashNetwork.Mainnet == ZcashNetwork.fromResources(applicationContext)) {
|
||||
menu.findItem(R.id.action_faucet).isVisible = false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -94,6 +97,11 @@ class MainActivity :
|
|||
val navController = findNavController(R.id.nav_host_fragment)
|
||||
navController.navigate(R.id.nav_home)
|
||||
true
|
||||
} else if (item.itemId == R.id.action_faucet) {
|
||||
runCatching {
|
||||
startActivity(newBrowserIntent("https://faucet.zecpages.com/"))
|
||||
}
|
||||
true
|
||||
} else {
|
||||
super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
@ -136,19 +144,6 @@ class MainActivity :
|
|||
}
|
||||
}
|
||||
|
||||
override fun onPrimaryClipChanged() {
|
||||
clipboardListener?.invoke(getClipboardText())
|
||||
}
|
||||
|
||||
fun setClipboardListener(block: (String?) -> Unit) {
|
||||
clipboardListener = block
|
||||
block(getClipboardText())
|
||||
}
|
||||
|
||||
fun removeClipboardListener() {
|
||||
clipboardListener = null
|
||||
}
|
||||
|
||||
fun hideKeyboard() {
|
||||
val windowToken = window.decorView.rootView.windowToken
|
||||
getSystemService<InputMethodManager>()?.hideSoftInputFromWindow(windowToken, 0)
|
||||
|
@ -173,3 +168,12 @@ class MainActivity :
|
|||
hideKeyboard()
|
||||
}
|
||||
}
|
||||
|
||||
private fun newBrowserIntent(url: String): Intent {
|
||||
val uri = Uri.parse(url)
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
return intent
|
||||
}
|
||||
|
|
|
@ -1,21 +1,38 @@
|
|||
package cash.z.ecc.android.sdk.demoapp
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
* Shared mutable state for the demo
|
||||
*/
|
||||
class SharedViewModel : ViewModel() {
|
||||
class SharedViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
private val _seedPhrase = MutableStateFlow(DemoConstants.INITIAL_SEED_WORDS)
|
||||
|
||||
private val _blockHeight = MutableStateFlow<BlockHeight?>(
|
||||
runBlocking {
|
||||
BlockHeight.ofLatestCheckpoint(
|
||||
getApplication(),
|
||||
ZcashNetwork.fromResources(application)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
// publicly, this is read-only
|
||||
val seedPhrase: StateFlow<String> get() = _seedPhrase
|
||||
|
||||
// publicly, this is read-only
|
||||
val birthdayHeight: StateFlow<BlockHeight?> get() = _blockHeight
|
||||
|
||||
fun updateSeedPhrase(newPhrase: String?): Boolean {
|
||||
return if (isValidSeedPhrase(newPhrase)) {
|
||||
_seedPhrase.value = newPhrase!!
|
||||
|
|
|
@ -5,13 +5,16 @@ import android.view.LayoutInflater
|
|||
import androidx.lifecycle.lifecycleScope
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.bip39.toSeed
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
||||
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetAddressBinding
|
||||
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
|
||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
|
||||
import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
|
@ -21,7 +24,8 @@ import kotlinx.coroutines.runBlocking
|
|||
*/
|
||||
class GetAddressFragment : BaseDemoFragment<FragmentGetAddressBinding>() {
|
||||
|
||||
private lateinit var viewingKey: UnifiedViewingKey
|
||||
private lateinit var synchronizer: Synchronizer
|
||||
private lateinit var viewingKey: UnifiedFullViewingKey
|
||||
private lateinit var seed: ByteArray
|
||||
|
||||
/**
|
||||
|
@ -36,33 +40,44 @@ class GetAddressFragment : BaseDemoFragment<FragmentGetAddressBinding>() {
|
|||
// have the seed stored
|
||||
seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
|
||||
|
||||
// the derivation tool can be used for generating keys and addresses
|
||||
// converting seed into viewingKey
|
||||
viewingKey = runBlocking {
|
||||
DerivationTool.deriveUnifiedViewingKeys(
|
||||
DerivationTool.deriveUnifiedFullViewingKeys(
|
||||
seed,
|
||||
ZcashNetwork.fromResources(requireApplicationContext())
|
||||
).first()
|
||||
}
|
||||
|
||||
val network = ZcashNetwork.fromResources(requireApplicationContext())
|
||||
synchronizer = Synchronizer.newBlocking(
|
||||
requireApplicationContext(),
|
||||
network,
|
||||
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
|
||||
seed = seed,
|
||||
birthday = network.saplingActivationHeight
|
||||
)
|
||||
}
|
||||
|
||||
private fun displayAddress() {
|
||||
// a full fledged app would just get the address from the synchronizer
|
||||
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
|
||||
val zaddress = DerivationTool.deriveShieldedAddress(
|
||||
seed,
|
||||
ZcashNetwork.fromResources(requireApplicationContext())
|
||||
)
|
||||
val taddress = DerivationTool.deriveTransparentAddress(
|
||||
seed,
|
||||
ZcashNetwork.fromResources(requireApplicationContext())
|
||||
)
|
||||
binding.textInfo.text = "z-addr:\n$zaddress\n\n\nt-addr:\n$taddress"
|
||||
binding.unifiedAddress.apply {
|
||||
val uaddress = synchronizer.getUnifiedAddress()
|
||||
text = uaddress
|
||||
setOnClickListener { copyToClipboard(uaddress) }
|
||||
}
|
||||
binding.saplingAddress.apply {
|
||||
val sapling = synchronizer.getSaplingAddress()
|
||||
text = sapling
|
||||
setOnClickListener { copyToClipboard(sapling) }
|
||||
}
|
||||
binding.transparentAddress.apply {
|
||||
val transparent = synchronizer.getTransparentAddress()
|
||||
text = transparent
|
||||
setOnClickListener { copyToClipboard(transparent) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO [#677]: Show an example with the synchronizer
|
||||
// TODO [#677]: https://github.com/zcash/zcash-android-wallet-sdk/issues/677
|
||||
|
||||
//
|
||||
// Android Lifecycle overrides
|
||||
//
|
||||
|
@ -84,11 +99,11 @@ class GetAddressFragment : BaseDemoFragment<FragmentGetAddressBinding>() {
|
|||
override fun onActionButtonClicked() {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
copyToClipboard(
|
||||
DerivationTool.deriveShieldedAddress(
|
||||
viewingKey.extfvk,
|
||||
DerivationTool.deriveUnifiedAddress(
|
||||
viewingKey.encoding,
|
||||
ZcashNetwork.fromResources(requireApplicationContext())
|
||||
),
|
||||
"Shielded address copied to clipboard!"
|
||||
"Unified address copied to clipboard!"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import android.view.LayoutInflater
|
|||
import androidx.lifecycle.lifecycleScope
|
||||
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.Synchronizer
|
||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
||||
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
||||
|
@ -18,14 +17,12 @@ import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
|||
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
* Displays the available balance && total balance associated with the seed defined by the default config.
|
||||
* comments.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
|
||||
|
||||
private lateinit var synchronizer: Synchronizer
|
||||
|
@ -46,27 +43,14 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
|
|||
// have the seed stored
|
||||
val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
|
||||
|
||||
// converting seed into viewingKey
|
||||
val viewingKey = runBlocking {
|
||||
DerivationTool.deriveUnifiedViewingKeys(
|
||||
seed,
|
||||
ZcashNetwork.fromResources(requireApplicationContext())
|
||||
).first()
|
||||
}
|
||||
|
||||
// using the ViewingKey to initialize
|
||||
runBlocking {
|
||||
Initializer.new(requireApplicationContext(), null) {
|
||||
val network = ZcashNetwork.fromResources(requireApplicationContext())
|
||||
it.newWallet(
|
||||
viewingKey,
|
||||
network = network,
|
||||
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network)
|
||||
)
|
||||
}
|
||||
}.let { initializer ->
|
||||
synchronizer = Synchronizer.newBlocking(initializer)
|
||||
}
|
||||
val network = ZcashNetwork.fromResources(requireApplicationContext())
|
||||
synchronizer = Synchronizer.newBlocking(
|
||||
requireApplicationContext(),
|
||||
network,
|
||||
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
|
||||
seed = seed,
|
||||
birthday = sharedViewModel.birthdayHeight.value
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
@ -80,25 +64,40 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
|
|||
synchronizer.status.collectWith(lifecycleScope, ::onStatus)
|
||||
synchronizer.progress.collectWith(lifecycleScope, ::onProgress)
|
||||
synchronizer.processorInfo.collectWith(lifecycleScope, ::onProcessorInfoUpdated)
|
||||
synchronizer.saplingBalances.filterNotNull().collectWith(lifecycleScope, ::onBalance)
|
||||
synchronizer.orchardBalances.collectWith(lifecycleScope, ::onOrchardBalance)
|
||||
synchronizer.saplingBalances.collectWith(lifecycleScope, ::onSaplingBalance)
|
||||
synchronizer.transparentBalances.collectWith(lifecycleScope, ::onTransparentBalance)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun onBalance(balance: WalletBalance) {
|
||||
binding.textBalance.text = """
|
||||
Available balance: ${balance.available.convertZatoshiToZecString(12)}
|
||||
Total balance: ${balance.total.convertZatoshiToZecString(12)}
|
||||
""".trimIndent()
|
||||
private fun onOrchardBalance(
|
||||
orchardBalance: WalletBalance?
|
||||
) {
|
||||
binding.orchardBalance.apply {
|
||||
text = orchardBalance.humanString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSaplingBalance(
|
||||
saplingBalance: WalletBalance?
|
||||
) {
|
||||
binding.saplingBalance.apply {
|
||||
text = saplingBalance.humanString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTransparentBalance(
|
||||
transparentBalance: WalletBalance?
|
||||
) {
|
||||
binding.transparentBalance.apply {
|
||||
text = transparentBalance.humanString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onStatus(status: Synchronizer.Status) {
|
||||
binding.textStatus.text = "Status: $status"
|
||||
val balance: WalletBalance? = synchronizer.saplingBalances.value
|
||||
if (null == balance) {
|
||||
binding.textBalance.text = "Calculating balance..."
|
||||
} else {
|
||||
onBalance(balance)
|
||||
}
|
||||
onOrchardBalance(synchronizer.orchardBalances.value)
|
||||
onSaplingBalance(synchronizer.saplingBalances.value)
|
||||
onTransparentBalance(synchronizer.transparentBalances.value)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
|
@ -112,3 +111,13 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
|
|||
if (info.isScanning) binding.textStatus.text = "Scanning blocks...${info.scanProgress}%"
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun WalletBalance?.humanString() = if (null == this) {
|
||||
"Calculating balance"
|
||||
} else {
|
||||
"""
|
||||
Available balance: ${available.convertZatoshiToZecString(12)}
|
||||
Total balance: ${total.convertZatoshiToZecString(12)}
|
||||
""".trimIndent()
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
|||
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetPrivateKeyBinding
|
||||
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
|
||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -43,14 +44,14 @@ class GetPrivateKeyFragment : BaseDemoFragment<FragmentGetPrivateKeyBinding>() {
|
|||
// demonstrate deriving spending keys for five accounts but only take the first one
|
||||
lifecycleScope.launchWhenStarted {
|
||||
@Suppress("MagicNumber")
|
||||
val spendingKey = DerivationTool.deriveSpendingKeys(
|
||||
val spendingKey = DerivationTool.deriveUnifiedSpendingKey(
|
||||
seed,
|
||||
ZcashNetwork.fromResources(requireApplicationContext()),
|
||||
5
|
||||
).first()
|
||||
Account(5)
|
||||
)
|
||||
|
||||
// derive the key that allows you to view but not spend transactions
|
||||
val viewingKey = DerivationTool.deriveViewingKey(
|
||||
val viewingKey = DerivationTool.deriveUnifiedFullViewingKey(
|
||||
spendingKey,
|
||||
ZcashNetwork.fromResources(requireApplicationContext())
|
||||
)
|
||||
|
@ -86,11 +87,11 @@ class GetPrivateKeyFragment : BaseDemoFragment<FragmentGetPrivateKeyBinding>() {
|
|||
override fun onActionButtonClicked() {
|
||||
lifecycleScope.launch {
|
||||
copyToClipboard(
|
||||
DerivationTool.deriveUnifiedViewingKeys(
|
||||
DerivationTool.deriveUnifiedFullViewingKeys(
|
||||
seed,
|
||||
ZcashNetwork.fromResources(requireApplicationContext())
|
||||
).first().extpub,
|
||||
"ViewingKey copied to clipboard!"
|
||||
).first().encoding,
|
||||
"UnifiedFullViewingKey copied to clipboard!"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,6 @@ class HomeFragment : BaseDemoFragment<FragmentHomeBinding>() {
|
|||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
mainActivity()?.setClipboardListener(::updatePasteButton)
|
||||
|
||||
lifecycleScope.launch {
|
||||
sharedViewModel.seedPhrase.collect {
|
||||
|
@ -43,7 +42,6 @@ class HomeFragment : BaseDemoFragment<FragmentHomeBinding>() {
|
|||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
mainActivity()?.removeClipboardListener()
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
|
@ -95,17 +93,6 @@ class HomeFragment : BaseDemoFragment<FragmentHomeBinding>() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun updatePasteButton(clipboardText: String? = mainActivity()?.getClipboardText()) {
|
||||
clipboardText.let {
|
||||
val isEditing = binding.groupEdit.visibility == View.VISIBLE
|
||||
if (isEditing && (it != null && it.split(' ').size > 2)) {
|
||||
binding.buttonPaste.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.buttonPaste.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toAbbreviatedPhrase(): String {
|
||||
this.trim().apply {
|
||||
val firstSpace = indexOf(' ')
|
||||
|
|
|
@ -8,17 +8,17 @@ import androidx.lifecycle.lifecycleScope
|
|||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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.Synchronizer
|
||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
||||
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
|
||||
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
||||
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentListTransactionsBinding
|
||||
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
|
||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||
import cash.z.ecc.android.sdk.ext.collectWith
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
||||
import cash.z.ecc.android.sdk.model.TransactionOverview
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
|
@ -33,7 +33,6 @@ import kotlinx.coroutines.runBlocking
|
|||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBinding>() {
|
||||
private lateinit var initializer: Initializer
|
||||
private lateinit var synchronizer: Synchronizer
|
||||
private lateinit var adapter: TransactionAdapter
|
||||
private lateinit var address: String
|
||||
|
@ -51,28 +50,21 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
|
|||
// Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already
|
||||
// have the seed stored
|
||||
val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
|
||||
|
||||
initializer = Initializer.newBlocking(
|
||||
requireApplicationContext(),
|
||||
Initializer.Config {
|
||||
val network = ZcashNetwork.fromResources(requireApplicationContext())
|
||||
runBlocking {
|
||||
it.importWallet(
|
||||
seed,
|
||||
birthday = null,
|
||||
network = network,
|
||||
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
val network = ZcashNetwork.fromResources(requireApplicationContext())
|
||||
address = runBlocking {
|
||||
DerivationTool.deriveShieldedAddress(
|
||||
DerivationTool.deriveUnifiedAddress(
|
||||
seed,
|
||||
ZcashNetwork.fromResources(requireApplicationContext())
|
||||
ZcashNetwork.fromResources(requireApplicationContext()),
|
||||
Account.DEFAULT
|
||||
)
|
||||
}
|
||||
synchronizer = Synchronizer.newBlocking(initializer)
|
||||
synchronizer = Synchronizer.newBlocking(
|
||||
requireApplicationContext(),
|
||||
network,
|
||||
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
|
||||
seed = seed,
|
||||
birthday = sharedViewModel.birthdayHeight.value
|
||||
)
|
||||
}
|
||||
|
||||
private fun initTransactionUI() {
|
||||
|
@ -87,7 +79,10 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
|
|||
synchronizer.status.collectWith(lifecycleScope, ::onStatus)
|
||||
synchronizer.processorInfo.collectWith(lifecycleScope, ::onProcessorInfoUpdated)
|
||||
synchronizer.progress.collectWith(lifecycleScope, ::onProgress)
|
||||
synchronizer.clearedTransactions.collectWith(lifecycleScope, ::onTransactionsUpdated)
|
||||
|
||||
synchronizer.clearedTransactions.collectWith(lifecycleScope) {
|
||||
onTransactionsUpdated(it)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -113,7 +108,7 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
|
|||
binding.textInfo.visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
private fun onTransactionsUpdated(transactions: List<ConfirmedTransaction>) {
|
||||
private fun onTransactionsUpdated(transactions: List<TransactionOverview>) {
|
||||
twig("got a new paged list of transactions")
|
||||
adapter.submitList(transactions)
|
||||
|
||||
|
|
|
@ -4,22 +4,22 @@ import android.view.LayoutInflater
|
|||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
|
||||
import cash.z.ecc.android.sdk.demoapp.R
|
||||
import cash.z.ecc.android.sdk.model.TransactionOverview
|
||||
|
||||
/**
|
||||
* Simple adapter implementation that knows how to bind a recyclerview to ClearedTransactions.
|
||||
*/
|
||||
class TransactionAdapter : ListAdapter<ConfirmedTransaction, TransactionViewHolder>(
|
||||
object : DiffUtil.ItemCallback<ConfirmedTransaction>() {
|
||||
class TransactionAdapter : ListAdapter<TransactionOverview, TransactionViewHolder>(
|
||||
object : DiffUtil.ItemCallback<TransactionOverview>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: ConfirmedTransaction,
|
||||
newItem: ConfirmedTransaction
|
||||
oldItem: TransactionOverview,
|
||||
newItem: TransactionOverview
|
||||
) = oldItem.minedHeight == newItem.minedHeight
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: ConfirmedTransaction,
|
||||
newItem: ConfirmedTransaction
|
||||
oldItem: TransactionOverview,
|
||||
newItem: TransactionOverview
|
||||
) = oldItem == newItem
|
||||
}
|
||||
) {
|
||||
|
|
|
@ -5,10 +5,10 @@ import android.widget.ImageView
|
|||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
|
||||
import cash.z.ecc.android.sdk.db.entity.valueInZatoshi
|
||||
import cash.z.ecc.android.sdk.demoapp.R
|
||||
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
|
||||
import cash.z.ecc.android.sdk.model.TransactionOverview
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
|
@ -17,22 +17,24 @@ import java.util.Locale
|
|||
*/
|
||||
class TransactionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
private val amountText = itemView.findViewById<TextView>(R.id.text_transaction_amount)
|
||||
private val infoText = itemView.findViewById<TextView>(R.id.text_transaction_info)
|
||||
private val timeText = itemView.findViewById<TextView>(R.id.text_transaction_timestamp)
|
||||
private val icon = itemView.findViewById<ImageView>(R.id.image_transaction_type)
|
||||
private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault())
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun bindTo(transaction: ConfirmedTransaction?) {
|
||||
val isInbound = transaction?.toAddress.isNullOrEmpty()
|
||||
amountText.text = transaction?.valueInZatoshi.convertZatoshiToZecString()
|
||||
fun bindTo(transaction: TransactionOverview) {
|
||||
bindTo(!transaction.isSentTransaction, transaction.blockTimeEpochSeconds, transaction.netValue)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun bindTo(isInbound: Boolean, time: Long, value: Zatoshi) {
|
||||
amountText.text = value.convertZatoshiToZecString()
|
||||
timeText.text =
|
||||
if (transaction == null || transaction.blockTimeInSeconds == 0L) {
|
||||
if (time == 0L) {
|
||||
"Pending"
|
||||
} else {
|
||||
formatter.format(transaction.blockTimeInSeconds * 1000L)
|
||||
formatter.format(time * 1000L)
|
||||
}
|
||||
infoText.text = getMemoString(transaction)
|
||||
|
||||
icon.rotation = if (isInbound) 0f else 180f
|
||||
icon.rotation = if (isInbound) 0f else 180f
|
||||
|
@ -40,8 +42,4 @@ class TransactionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
|||
ContextCompat.getColor(itemView.context, if (isInbound) R.color.tx_inbound else R.color.tx_outbound)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getMemoString(transaction: ConfirmedTransaction?): String {
|
||||
return transaction?.memo?.takeUnless { it[0] < 0 }?.let { String(it) } ?: "no memo"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,11 +8,9 @@ import androidx.lifecycle.lifecycleScope
|
|||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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.block.CompactBlockProcessor
|
||||
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
|
||||
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
||||
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentListUtxosBinding
|
||||
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
|
||||
|
@ -20,15 +18,16 @@ import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
|||
import cash.z.ecc.android.sdk.demoapp.util.mainActivity
|
||||
import cash.z.ecc.android.sdk.ext.collectWith
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
||||
import cash.z.ecc.android.sdk.model.TransactionOverview
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.max
|
||||
|
||||
|
@ -47,7 +46,6 @@ import kotlin.math.max
|
|||
@Suppress("TooManyFunctions")
|
||||
class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
|
||||
private lateinit var seed: ByteArray
|
||||
private lateinit var initializer: Initializer
|
||||
private lateinit var synchronizer: Synchronizer
|
||||
private lateinit var adapter: UtxoAdapter
|
||||
private val address: String = "t1RwbKka1CnktvAJ1cSqdn7c6PXWG4tZqgd"
|
||||
|
@ -66,20 +64,15 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
|
|||
// Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already
|
||||
// have the seed stored
|
||||
seed = Mnemonics.MnemonicCode(sharedViewModel.seedPhrase.value).toSeed()
|
||||
initializer = runBlocking {
|
||||
val network = ZcashNetwork.fromResources(requireApplicationContext())
|
||||
Initializer.new(requireApplicationContext()) {
|
||||
runBlocking {
|
||||
it.newWallet(
|
||||
seed,
|
||||
network = network,
|
||||
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network)
|
||||
)
|
||||
}
|
||||
it.alias = "Demo_Utxos"
|
||||
}
|
||||
}
|
||||
synchronizer = runBlocking { Synchronizer.new(initializer) }
|
||||
val network = ZcashNetwork.fromResources(requireApplicationContext())
|
||||
synchronizer = Synchronizer.newBlocking(
|
||||
requireApplicationContext(),
|
||||
network,
|
||||
alias = "Demo_Utxos",
|
||||
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
|
||||
seed = seed,
|
||||
birthday = sharedViewModel.birthdayHeight.value
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
@ -149,7 +142,7 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
|
|||
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
finalCount = (synchronizer as SdkSynchronizer).getTransactionCount()
|
||||
finalCount = (synchronizer as SdkSynchronizer).getTransactionCount().toInt()
|
||||
withContext(Dispatchers.Main) {
|
||||
@Suppress("MagicNumber")
|
||||
delay(100)
|
||||
|
@ -179,13 +172,9 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
|
|||
override fun onResume() {
|
||||
super.onResume()
|
||||
resetInBackground()
|
||||
val seed = Mnemonics.MnemonicCode(sharedViewModel.seedPhrase.value).toSeed()
|
||||
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
|
||||
binding.inputAddress.setText(
|
||||
DerivationTool.deriveTransparentAddress(
|
||||
seed,
|
||||
ZcashNetwork.fromResources(requireApplicationContext())
|
||||
)
|
||||
synchronizer.getTransparentAddress(Account.DEFAULT)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -199,10 +188,12 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
|
|||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
synchronizer.prepare()
|
||||
initialCount = (synchronizer as SdkSynchronizer).getTransactionCount()
|
||||
initialCount = (synchronizer as SdkSynchronizer).getTransactionCount().toInt()
|
||||
}
|
||||
|
||||
onTransactionsUpdated(synchronizer.clearedTransactions.first())
|
||||
}
|
||||
synchronizer.clearedTransactions.collectWith(lifecycleScope, ::onTransactionsUpdated)
|
||||
|
||||
// synchronizer.receivedTransactions.collectWith(lifecycleScope, ::onTransactionsUpdated)
|
||||
} catch (t: Throwable) {
|
||||
twig("failed to start the synchronizer!!! due to : $t")
|
||||
|
@ -263,7 +254,7 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
|
|||
binding.textStatus.visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
private fun onTransactionsUpdated(transactions: List<ConfirmedTransaction>) {
|
||||
private fun onTransactionsUpdated(transactions: List<TransactionOverview>) {
|
||||
twig("got a new paged list of transactions of size ${transactions.size}")
|
||||
adapter.submitList(transactions)
|
||||
}
|
||||
|
|
|
@ -4,22 +4,22 @@ import android.view.LayoutInflater
|
|||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
|
||||
import cash.z.ecc.android.sdk.demoapp.R
|
||||
import cash.z.ecc.android.sdk.model.TransactionOverview
|
||||
|
||||
/**
|
||||
* Simple adapter implementation that knows how to bind a recyclerview to ClearedTransactions.
|
||||
*/
|
||||
class UtxoAdapter : ListAdapter<ConfirmedTransaction, UtxoViewHolder>(
|
||||
object : DiffUtil.ItemCallback<ConfirmedTransaction>() {
|
||||
class UtxoAdapter : ListAdapter<TransactionOverview, UtxoViewHolder>(
|
||||
object : DiffUtil.ItemCallback<TransactionOverview>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: ConfirmedTransaction,
|
||||
newItem: ConfirmedTransaction
|
||||
oldItem: TransactionOverview,
|
||||
newItem: TransactionOverview
|
||||
) = oldItem.minedHeight == newItem.minedHeight
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: ConfirmedTransaction,
|
||||
newItem: ConfirmedTransaction
|
||||
oldItem: TransactionOverview,
|
||||
newItem: TransactionOverview
|
||||
) = oldItem == newItem
|
||||
}
|
||||
) {
|
||||
|
|
|
@ -3,10 +3,10 @@ package cash.z.ecc.android.sdk.demoapp.demos.listutxos
|
|||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
|
||||
import cash.z.ecc.android.sdk.db.entity.valueInZatoshi
|
||||
import cash.z.ecc.android.sdk.demoapp.R
|
||||
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
|
||||
import cash.z.ecc.android.sdk.model.TransactionOverview
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
|
@ -15,23 +15,21 @@ import java.util.Locale
|
|||
*/
|
||||
class UtxoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
private val amountText = itemView.findViewById<TextView>(R.id.text_transaction_amount)
|
||||
private val infoText = itemView.findViewById<TextView>(R.id.text_transaction_info)
|
||||
private val timeText = itemView.findViewById<TextView>(R.id.text_transaction_timestamp)
|
||||
private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault())
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun bindTo(transaction: ConfirmedTransaction?) {
|
||||
amountText.text = transaction?.valueInZatoshi.convertZatoshiToZecString()
|
||||
timeText.text =
|
||||
if (transaction == null || transaction.blockTimeInSeconds == 0L) {
|
||||
"Pending"
|
||||
} else {
|
||||
formatter.format(transaction.blockTimeInSeconds * 1000L)
|
||||
}
|
||||
infoText.text = getMemoString(transaction)
|
||||
fun bindTo(transaction: TransactionOverview) {
|
||||
bindToHelper(transaction.netValue, transaction.blockTimeEpochSeconds)
|
||||
}
|
||||
|
||||
private fun getMemoString(transaction: ConfirmedTransaction?): String {
|
||||
return transaction?.memo?.takeUnless { it[0] < 0 }?.let { String(it) } ?: "no memo"
|
||||
@Suppress("MagicNumber")
|
||||
private fun bindToHelper(amount: Zatoshi, time: Long) {
|
||||
amountText.text = amount.convertZatoshiToZecString()
|
||||
timeText.text = if (time == 0L) {
|
||||
"Pending"
|
||||
} else {
|
||||
formatter.format(time * 1000L)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,16 +8,8 @@ import android.widget.TextView
|
|||
import androidx.lifecycle.lifecycleScope
|
||||
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.Synchronizer
|
||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
||||
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
|
||||
import cash.z.ecc.android.sdk.db.entity.isCreated
|
||||
import cash.z.ecc.android.sdk.db.entity.isCreating
|
||||
import cash.z.ecc.android.sdk.db.entity.isFailedEncoding
|
||||
import cash.z.ecc.android.sdk.db.entity.isFailedSubmit
|
||||
import cash.z.ecc.android.sdk.db.entity.isMined
|
||||
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
|
||||
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
||||
import cash.z.ecc.android.sdk.demoapp.DemoConstants
|
||||
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentSendBinding
|
||||
|
@ -30,11 +22,21 @@ import cash.z.ecc.android.sdk.ext.convertZecToZatoshi
|
|||
import cash.z.ecc.android.sdk.ext.toZecString
|
||||
import cash.z.ecc.android.sdk.internal.Twig
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
||||
import cash.z.ecc.android.sdk.model.PendingTransaction
|
||||
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
||||
import cash.z.ecc.android.sdk.model.isCreated
|
||||
import cash.z.ecc.android.sdk.model.isCreating
|
||||
import cash.z.ecc.android.sdk.model.isFailedEncoding
|
||||
import cash.z.ecc.android.sdk.model.isFailedSubmit
|
||||
import cash.z.ecc.android.sdk.model.isMined
|
||||
import cash.z.ecc.android.sdk.model.isSubmitSuccess
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
|
@ -54,7 +56,7 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
|
|||
|
||||
// in a normal app, this would be stored securely with the trusted execution environment (TEE)
|
||||
// but since this is a demo, we'll derive it on the fly
|
||||
private lateinit var spendingKey: String
|
||||
private lateinit var spendingKey: UnifiedSpendingKey
|
||||
|
||||
/**
|
||||
* Initialize the required values that would normally live outside the demo but are repeated
|
||||
|
@ -68,22 +70,20 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
|
|||
// have the seed stored
|
||||
val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
|
||||
|
||||
runBlocking {
|
||||
Initializer.new(requireApplicationContext()) {
|
||||
val network = ZcashNetwork.fromResources(requireApplicationContext())
|
||||
runBlocking {
|
||||
it.newWallet(
|
||||
seed,
|
||||
network = network,
|
||||
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network)
|
||||
)
|
||||
}
|
||||
}
|
||||
}.let { initializer ->
|
||||
synchronizer = Synchronizer.newBlocking(initializer)
|
||||
}
|
||||
val network = ZcashNetwork.fromResources(requireApplicationContext())
|
||||
synchronizer = Synchronizer.newBlocking(
|
||||
requireApplicationContext(),
|
||||
network,
|
||||
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
|
||||
seed = seed,
|
||||
birthday = sharedViewModel.birthdayHeight.value
|
||||
)
|
||||
spendingKey = runBlocking {
|
||||
DerivationTool.deriveSpendingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first()
|
||||
DerivationTool.deriveUnifiedSpendingKey(
|
||||
seed,
|
||||
ZcashNetwork.fromResources(requireApplicationContext()),
|
||||
Account.DEFAULT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -173,26 +173,28 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
|
|||
isSending = true
|
||||
val amount = amountInput.text.toString().toDouble().convertZecToZatoshi()
|
||||
val toAddress = addressInput.text.toString().trim()
|
||||
synchronizer.sendToAddress(
|
||||
spendingKey,
|
||||
amount,
|
||||
toAddress,
|
||||
"Funds from Demo App"
|
||||
).collectWith(lifecycleScope, ::onPendingTxUpdated)
|
||||
lifecycleScope.launch {
|
||||
synchronizer.sendToAddress(
|
||||
spendingKey,
|
||||
amount,
|
||||
toAddress,
|
||||
"Funds from Demo App"
|
||||
).collectWith(lifecycleScope, ::onPendingTxUpdated)
|
||||
}
|
||||
|
||||
mainActivity()?.hideKeyboard()
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
private fun onPendingTxUpdated(pendingTransaction: PendingTransaction?) {
|
||||
val id = pendingTransaction?.id ?: -1
|
||||
val message = when {
|
||||
pendingTransaction == null -> "Transaction not found"
|
||||
pendingTransaction.isMined() -> "Transaction Mined (id: $id)!\n\nSEND COMPLETE".also { isSending = false }
|
||||
pendingTransaction.isMined() -> "Transaction Mined!\n\nSEND COMPLETE".also { isSending = false }
|
||||
pendingTransaction.isSubmitSuccess() -> "Successfully submitted transaction!\nAwaiting confirmation..."
|
||||
pendingTransaction.isFailedEncoding() ->
|
||||
"ERROR: failed to encode transaction! (id: $id)".also { isSending = false }
|
||||
"ERROR: failed to encode transaction!".also { isSending = false }
|
||||
pendingTransaction.isFailedSubmit() ->
|
||||
"ERROR: failed to submit transaction! (id: $id)".also { isSending = false }
|
||||
"ERROR: failed to submit transaction!".also { isSending = false }
|
||||
pendingTransaction.isCreated() -> "Transaction creation complete! (id: $id)"
|
||||
pendingTransaction.isCreating() -> "Creating transaction!".also { onResetInfo() }
|
||||
else -> "Transaction updated!".also { twig("Unhandled TX state: $pendingTransaction") }
|
||||
|
|
|
@ -1,21 +1,62 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_info"
|
||||
android:layout_width="wrap_content"
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:textSize="18sp"
|
||||
android:text="loading address..."
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.2"/>
|
||||
android:orientation="vertical">
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:text="@string/unified_address"
|
||||
android:textSize="18sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/unified_address"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:text=""
|
||||
android:textSize="18sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:text="@string/sapling_address"
|
||||
android:textSize="18sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sapling_address"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:text="@string/sapling_address"
|
||||
android:textSize="18sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:text="@string/transparent_address"
|
||||
android:textSize="18sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/transparent_address"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:textSize="18sp" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
</ScrollView>
|
||||
|
|
|
@ -1,31 +1,60 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<ScrollView 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">
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_status"
|
||||
android:layout_width="wrap_content"
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Initializing wallet..."
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_balance"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="Balance"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@+id/text_status"
|
||||
app:layout_constraintHorizontal_bias="0.492"
|
||||
app:layout_constraintStart_toStartOf="@+id/text_status"
|
||||
app:layout_constraintTop_toBottomOf="@+id/text_status"
|
||||
app:layout_constraintVertical_bias="0.0" />
|
||||
<TextView
|
||||
android:id="@+id/text_status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Initializing wallet..."
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="Orchard balance" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/orchard_balance"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="Sapling balance" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sapling_balance"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="Transparent balance" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/transparent_balance"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
</ScrollView>
|
||||
|
|
|
@ -7,4 +7,9 @@
|
|||
android:orderInCategory="100"
|
||||
android:title="@string/action_settings"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/action_faucet"
|
||||
android:orderInCategory="100"
|
||||
android:title="@string/action_faucet"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
<string name="nav_header_subtitle">v1.1.0</string>
|
||||
<string name="nav_header_desc">Navigation header</string>
|
||||
<string name="action_settings">Change Seed Phrase</string>
|
||||
<string name="action_faucet">Testnet Faucet</string>
|
||||
|
||||
<!-- Drawer Menu -->
|
||||
<string name="menu_home">Home</string>
|
||||
|
@ -25,4 +26,7 @@
|
|||
<string name="load">Load</string>
|
||||
<string name="apply">Apply</string>
|
||||
<string name="loading">⌛ Loading</string>
|
||||
<string name="unified_address">Unified address</string>
|
||||
<string name="sapling_address">Sapling address</string>
|
||||
<string name="transparent_address">Transparent address</string>
|
||||
</resources>
|
||||
|
|
|
@ -15,8 +15,7 @@ Thankfully, the only thing an app developer has to be concerned with is the foll
|
|||
| **CompactBlockStore** | Stores compact blocks that have been downloaded from the `LightWalletService` |
|
||||
| **CompactBlockProcessor** | Validates and scans the compact blocks in the `CompactBlockStore` for transaction details |
|
||||
| **OutboundTransactionManager** | Creates, Submits and manages transactions for spending funds |
|
||||
| **Initializer** | Responsible for all setup that must happen before synchronization can begin. Loads the rust library and helps initialize databases. |
|
||||
| **DerivationTool**, **BirthdayTool** | Utilities for deriving keys, addresses and loading wallet checkpoints, called "birthdays." |
|
||||
| **DerivationTool** | Utilities for deriving keys and addresses |
|
||||
| **RustBackend** | Wraps and simplifies the rust library and exposes its functionality to the Kotlin SDK |
|
||||
|
||||
# Checkpoints
|
||||
|
|
|
@ -14,7 +14,7 @@ Start by making sure the command line with Gradle works first, because **all the
|
|||
1. Install JVM 11 or greater on your system. Our setup has been tested with Java 11-17. Although a variety of JVM distributions are available and should work, we have settled on recommending [Adoptium/Temurin](https://adoptium.net), because this is the default distribution used by Gradle toolchains. For Windows or Linux, be sure that the `JAVA_HOME` environment variable points to the right Java version. Note: If you switch from a newer to an older JVM version, you may see an error like the following `> com.android.ide.common.signing.KeytoolException: Failed to read key AndroidDebugKey from store "~/.android/debug.keystore": Integrity check failed: java.security.NoSuchAlgorithmException: Algorithm HmacPBESHA256 not available`. A solution is to delete the debug keystore and allow it to be re-generated.
|
||||
1. Android Studio has an embedded JVM, although running Gradle tasks from the command line requires a separate JVM to be installed. Our Gradle scripts are configured to use toolchains to automatically install the correct JVM version.
|
||||
1. Configure Rust
|
||||
1. [Install rust](https://www.rust-lang.org/learn/get-started)
|
||||
1. [Install Rust](https://www.rust-lang.org/learn/get-started). You will need Rust 1.59 or greater. If you install with `rustup` then you are guaranteed to get a compatible Rust version. If you use system packages, check the provided version.
|
||||
1. macOS with Homebrew
|
||||
1. `brew install rustup`
|
||||
1. `rustup-init`
|
||||
|
|
|
@ -22,7 +22,7 @@ ZCASH_ASCII_GPG_KEY=
|
|||
# Configures whether release is an unstable snapshot, therefore published to the snapshot repository.
|
||||
IS_SNAPSHOT=true
|
||||
|
||||
LIBRARY_VERSION=1.9.0-beta05
|
||||
LIBRARY_VERSION=1.10.0-beta01
|
||||
|
||||
# Kotlin compiler warnings can be considered errors, failing the build.
|
||||
ZCASH_IS_TREAT_WARNINGS_AS_ERRORS=true
|
||||
|
@ -84,6 +84,7 @@ ANDROIDX_ANNOTATION_VERSION=1.3.0
|
|||
ANDROIDX_APPCOMPAT_VERSION=1.4.2
|
||||
ANDROIDX_CONSTRAINT_LAYOUT_VERSION=2.1.4
|
||||
ANDROIDX_CORE_VERSION=1.8.0
|
||||
ANDROIDX_DATABASE_VERSION=2.2.0
|
||||
ANDROIDX_ESPRESSO_VERSION=3.4.0
|
||||
ANDROIDX_LIFECYCLE_VERSION=2.4.1
|
||||
ANDROIDX_MULTIDEX_VERSION=2.0.1
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -8,35 +8,39 @@ authors = [
|
|||
description = "JNI backend for the Android wallet SDK"
|
||||
publish = false
|
||||
edition = "2018"
|
||||
rust-version = "1.59"
|
||||
|
||||
[dependencies]
|
||||
android_logger = "0.9"
|
||||
android_logger = "0.11"
|
||||
failure = "0.1"
|
||||
hdwallet = "0.3.1"
|
||||
hdwallet-bitcoin = "0.3"
|
||||
hex = "0.4"
|
||||
jni = { version = "0.17", default-features = false }
|
||||
jni = { version = "0.20", default-features = false }
|
||||
log = "0.4"
|
||||
log-panics = "2.0.0"
|
||||
zcash_client_backend = "0.5"
|
||||
zcash_client_sqlite = "0.3"
|
||||
zcash_primitives = "0.5"
|
||||
zcash_proofs = "0.5"
|
||||
schemer = "0.2"
|
||||
secp256k1 = "0.21"
|
||||
secrecy = "0.8"
|
||||
zcash_address = "0.2"
|
||||
zcash_client_backend = { version = "0.5", features = ["transparent-inputs", "unstable"] }
|
||||
zcash_client_sqlite = { version = "0.3", features = ["transparent-inputs", "unstable"] }
|
||||
zcash_primitives = "0.8"
|
||||
zcash_proofs = "0.8"
|
||||
|
||||
#### Temporary additions: ####################################
|
||||
secp256k1 = "0.19"
|
||||
##############################################################
|
||||
|
||||
# Revision corresponds to the partially-complete auto-shielding version, with the NU5
|
||||
# consensus branch ID and activation heights, and v5 transaction parsing, backported.
|
||||
# https://github.com/zcash/librustzcash/pull/555
|
||||
# https://github.com/zcash/librustzcash/pull/558
|
||||
# Revision corresponds to the pending zcash_primitives 0.8.0.
|
||||
[patch.crates-io]
|
||||
zcash_client_backend = { git = 'https://github.com/zcash/librustzcash.git', rev='0a1ed9b8acf336bdc20ba49083c66f114ecf6877' }
|
||||
zcash_client_sqlite = { git = 'https://github.com/zcash/librustzcash.git', rev='0a1ed9b8acf336bdc20ba49083c66f114ecf6877' }
|
||||
zcash_primitives = { git = 'https://github.com/zcash/librustzcash.git', rev='0a1ed9b8acf336bdc20ba49083c66f114ecf6877' }
|
||||
zcash_proofs = { git = 'https://github.com/zcash/librustzcash.git', rev='0a1ed9b8acf336bdc20ba49083c66f114ecf6877' }
|
||||
schemer = { git = "https://github.com/aschampion/schemer.git", rev = "6726b60f43f72c6e24a18d31be0ec7d42829e5e1" }
|
||||
schemer-rusqlite = { git = "https://github.com/aschampion/schemer.git", rev = "6726b60f43f72c6e24a18d31be0ec7d42829e5e1" }
|
||||
zcash_address = { git = 'https://github.com/zcash/librustzcash.git', rev='6f4a6aa00cc2db7864d01c7403cbcd75dd45bda4' }
|
||||
zcash_client_backend = { git = 'https://github.com/zcash/librustzcash.git', rev='6f4a6aa00cc2db7864d01c7403cbcd75dd45bda4' }
|
||||
zcash_client_sqlite = { git = 'https://github.com/zcash/librustzcash.git', rev='6f4a6aa00cc2db7864d01c7403cbcd75dd45bda4' }
|
||||
zcash_primitives = { git = 'https://github.com/zcash/librustzcash.git', rev='6f4a6aa00cc2db7864d01c7403cbcd75dd45bda4' }
|
||||
zcash_proofs = { git = 'https://github.com/zcash/librustzcash.git', rev='6f4a6aa00cc2db7864d01c7403cbcd75dd45bda4' }
|
||||
|
||||
## Uncomment this to test librustzcash changes locally
|
||||
#[patch.crates-io]
|
||||
#zcash_address = { path = '../../clones/librustzcash/components/zcash_address' }
|
||||
#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' }
|
||||
|
@ -44,6 +48,7 @@ zcash_proofs = { git = 'https://github.com/zcash/librustzcash.git', rev='0a1ed9b
|
|||
|
||||
## Uncomment this to test someone else's librustzcash changes in a branch
|
||||
#[patch.crates-io]
|
||||
#zcash_address = { git = "https://github.com/zcash/librustzcash", branch = "branch-name" }
|
||||
#zcash_client_backend = { git = "https://github.com/zcash/librustzcash", branch = "branch-name" }
|
||||
#zcash_client_sqlite = { git = "https://github.com/zcash/librustzcash", branch = "branch-name" }
|
||||
#zcash_primitives = { git = "https://github.com/zcash/librustzcash", branch = "branch-name" }
|
||||
|
|
|
@ -262,6 +262,12 @@ dependencies {
|
|||
implementation(libs.androidx.paging)
|
||||
ksp(libs.androidx.room.compiler)
|
||||
|
||||
// For direct database access
|
||||
// TODO [#703]: Eliminate this dependency
|
||||
// https://github.com/zcash/zcash-android-wallet-sdk/issues/703
|
||||
implementation(libs.androidx.sqlite)
|
||||
implementation(libs.androidx.sqlite.framework)
|
||||
|
||||
// Kotlin
|
||||
implementation(libs.kotlin.stdlib)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
|
@ -284,8 +290,6 @@ dependencies {
|
|||
// Tests
|
||||
testImplementation(libs.kotlin.reflect)
|
||||
testImplementation(libs.kotlin.test)
|
||||
testImplementation(libs.mockito.junit)
|
||||
testImplementation(libs.mockito.kotlin)
|
||||
testImplementation(libs.bundles.junit)
|
||||
testImplementation(libs.grpc.testing)
|
||||
|
||||
|
|
|
@ -1,96 +0,0 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 2,
|
||||
"identityHash": "11cfa01fe0b00e5d1e61a46e78f68ee2",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "compactblocks",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`height` INTEGER NOT NULL, `data` BLOB NOT NULL, PRIMARY KEY(`height`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "height",
|
||||
"columnName": "height",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data",
|
||||
"columnName": "data",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"height"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "utxos",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `address` TEXT NOT NULL, `txid` BLOB, `tx_index` INTEGER, `script` BLOB, `value` INTEGER NOT NULL, `height` INTEGER)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "address",
|
||||
"columnName": "address",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "txid",
|
||||
"columnName": "txid",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "transactionIndex",
|
||||
"columnName": "tx_index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "script",
|
||||
"columnName": "script",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "height",
|
||||
"columnName": "height",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '11cfa01fe0b00e5d1e61a46e78f68ee2')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,345 +0,0 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 3,
|
||||
"identityHash": "d6e9b05e0607d399f821058adb43dc15",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "transactions",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_tx` INTEGER, `txid` BLOB NOT NULL, `tx_index` INTEGER, `created` TEXT, `expiry_height` INTEGER, `block` INTEGER, `raw` BLOB, PRIMARY KEY(`id_tx`), FOREIGN KEY(`block`) REFERENCES `blocks`(`height`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id_tx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "transactionId",
|
||||
"columnName": "txid",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "transactionIndex",
|
||||
"columnName": "tx_index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "created",
|
||||
"columnName": "created",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "expiryHeight",
|
||||
"columnName": "expiry_height",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "minedHeight",
|
||||
"columnName": "block",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "raw",
|
||||
"columnName": "raw",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id_tx"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "blocks",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"block"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"height"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "blocks",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`height` INTEGER, `hash` BLOB NOT NULL, `time` INTEGER NOT NULL, `sapling_tree` BLOB NOT NULL, PRIMARY KEY(`height`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "height",
|
||||
"columnName": "height",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "hash",
|
||||
"columnName": "hash",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "time",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "saplingTree",
|
||||
"columnName": "sapling_tree",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"height"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "received_notes",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_note` INTEGER, `tx` INTEGER NOT NULL, `output_index` INTEGER NOT NULL, `account` INTEGER NOT NULL, `value` INTEGER NOT NULL, `spent` INTEGER, `diversifier` BLOB NOT NULL, `rcm` BLOB NOT NULL, `nf` BLOB NOT NULL, `is_change` INTEGER NOT NULL, `memo` BLOB, PRIMARY KEY(`id_note`), FOREIGN KEY(`tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`account`) REFERENCES `accounts`(`account`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`spent`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id_note",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "transactionId",
|
||||
"columnName": "tx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "outputIndex",
|
||||
"columnName": "output_index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "account",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "spent",
|
||||
"columnName": "spent",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "diversifier",
|
||||
"columnName": "diversifier",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "rcm",
|
||||
"columnName": "rcm",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "nf",
|
||||
"columnName": "nf",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isChange",
|
||||
"columnName": "is_change",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "memo",
|
||||
"columnName": "memo",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id_note"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "transactions",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"tx"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id_tx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "accounts",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"account"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"account"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "transactions",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"spent"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id_tx"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "accounts",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`account` INTEGER, `extfvk` TEXT NOT NULL, `address` TEXT NOT NULL, PRIMARY KEY(`account`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "account",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "extendedFullViewingKey",
|
||||
"columnName": "extfvk",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "address",
|
||||
"columnName": "address",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"account"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "sent_notes",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_note` INTEGER, `tx` INTEGER NOT NULL, `output_index` INTEGER NOT NULL, `from_account` INTEGER NOT NULL, `address` TEXT NOT NULL, `value` INTEGER NOT NULL, `memo` BLOB, PRIMARY KEY(`id_note`), FOREIGN KEY(`tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`from_account`) REFERENCES `accounts`(`account`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id_note",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "transactionId",
|
||||
"columnName": "tx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "outputIndex",
|
||||
"columnName": "output_index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "from_account",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "address",
|
||||
"columnName": "address",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "memo",
|
||||
"columnName": "memo",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id_note"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "transactions",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"tx"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id_tx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "accounts",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"from_account"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"account"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd6e9b05e0607d399f821058adb43dc15')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,345 +0,0 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 4,
|
||||
"identityHash": "d6e9b05e0607d399f821058adb43dc15",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "transactions",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_tx` INTEGER, `txid` BLOB NOT NULL, `tx_index` INTEGER, `created` TEXT, `expiry_height` INTEGER, `block` INTEGER, `raw` BLOB, PRIMARY KEY(`id_tx`), FOREIGN KEY(`block`) REFERENCES `blocks`(`height`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id_tx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "transactionId",
|
||||
"columnName": "txid",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "transactionIndex",
|
||||
"columnName": "tx_index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "created",
|
||||
"columnName": "created",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "expiryHeight",
|
||||
"columnName": "expiry_height",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "minedHeight",
|
||||
"columnName": "block",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "raw",
|
||||
"columnName": "raw",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id_tx"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "blocks",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"block"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"height"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "blocks",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`height` INTEGER, `hash` BLOB NOT NULL, `time` INTEGER NOT NULL, `sapling_tree` BLOB NOT NULL, PRIMARY KEY(`height`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "height",
|
||||
"columnName": "height",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "hash",
|
||||
"columnName": "hash",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "time",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "saplingTree",
|
||||
"columnName": "sapling_tree",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"height"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "received_notes",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_note` INTEGER, `tx` INTEGER NOT NULL, `output_index` INTEGER NOT NULL, `account` INTEGER NOT NULL, `value` INTEGER NOT NULL, `spent` INTEGER, `diversifier` BLOB NOT NULL, `rcm` BLOB NOT NULL, `nf` BLOB NOT NULL, `is_change` INTEGER NOT NULL, `memo` BLOB, PRIMARY KEY(`id_note`), FOREIGN KEY(`tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`account`) REFERENCES `accounts`(`account`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`spent`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id_note",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "transactionId",
|
||||
"columnName": "tx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "outputIndex",
|
||||
"columnName": "output_index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "account",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "spent",
|
||||
"columnName": "spent",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "diversifier",
|
||||
"columnName": "diversifier",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "rcm",
|
||||
"columnName": "rcm",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "nf",
|
||||
"columnName": "nf",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isChange",
|
||||
"columnName": "is_change",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "memo",
|
||||
"columnName": "memo",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id_note"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "transactions",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"tx"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id_tx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "accounts",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"account"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"account"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "transactions",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"spent"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id_tx"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "accounts",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`account` INTEGER, `extfvk` TEXT NOT NULL, `address` TEXT NOT NULL, PRIMARY KEY(`account`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "account",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "extendedFullViewingKey",
|
||||
"columnName": "extfvk",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "address",
|
||||
"columnName": "address",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"account"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "sent_notes",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_note` INTEGER, `tx` INTEGER NOT NULL, `output_index` INTEGER NOT NULL, `from_account` INTEGER NOT NULL, `address` TEXT NOT NULL, `value` INTEGER NOT NULL, `memo` BLOB, PRIMARY KEY(`id_note`), FOREIGN KEY(`tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`from_account`) REFERENCES `accounts`(`account`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id_note",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "transactionId",
|
||||
"columnName": "tx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "outputIndex",
|
||||
"columnName": "output_index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "from_account",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "address",
|
||||
"columnName": "address",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "memo",
|
||||
"columnName": "memo",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id_note"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "transactions",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"tx"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id_tx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "accounts",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"from_account"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"account"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd6e9b05e0607d399f821058adb43dc15')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,407 +0,0 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 5,
|
||||
"identityHash": "9431cf7a9bc49395e07834e4c81c5ed1",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "transactions",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_tx` INTEGER, `txid` BLOB NOT NULL, `tx_index` INTEGER, `created` TEXT, `expiry_height` INTEGER, `block` INTEGER, `raw` BLOB, PRIMARY KEY(`id_tx`), FOREIGN KEY(`block`) REFERENCES `blocks`(`height`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id_tx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "transactionId",
|
||||
"columnName": "txid",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "transactionIndex",
|
||||
"columnName": "tx_index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "created",
|
||||
"columnName": "created",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "expiryHeight",
|
||||
"columnName": "expiry_height",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "minedHeight",
|
||||
"columnName": "block",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "raw",
|
||||
"columnName": "raw",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id_tx"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "blocks",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"block"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"height"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "blocks",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`height` INTEGER, `hash` BLOB NOT NULL, `time` INTEGER NOT NULL, `sapling_tree` BLOB NOT NULL, PRIMARY KEY(`height`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "height",
|
||||
"columnName": "height",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "hash",
|
||||
"columnName": "hash",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "time",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "saplingTree",
|
||||
"columnName": "sapling_tree",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"height"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "received_notes",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_note` INTEGER, `tx` INTEGER NOT NULL, `output_index` INTEGER NOT NULL, `account` INTEGER NOT NULL, `value` INTEGER NOT NULL, `spent` INTEGER, `diversifier` BLOB NOT NULL, `rcm` BLOB NOT NULL, `nf` BLOB NOT NULL, `is_change` INTEGER NOT NULL, `memo` BLOB, PRIMARY KEY(`id_note`), FOREIGN KEY(`tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`account`) REFERENCES `accounts`(`account`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`spent`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id_note",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "transactionId",
|
||||
"columnName": "tx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "outputIndex",
|
||||
"columnName": "output_index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "account",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "spent",
|
||||
"columnName": "spent",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "diversifier",
|
||||
"columnName": "diversifier",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "rcm",
|
||||
"columnName": "rcm",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "nf",
|
||||
"columnName": "nf",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isChange",
|
||||
"columnName": "is_change",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "memo",
|
||||
"columnName": "memo",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id_note"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "transactions",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"tx"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id_tx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "accounts",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"account"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"account"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "transactions",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"spent"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id_tx"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "accounts",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`account` INTEGER, `extfvk` TEXT NOT NULL, `address` TEXT NOT NULL, PRIMARY KEY(`account`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "account",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "extendedFullViewingKey",
|
||||
"columnName": "extfvk",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "address",
|
||||
"columnName": "address",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"account"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "sent_notes",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_note` INTEGER, `tx` INTEGER NOT NULL, `output_index` INTEGER NOT NULL, `from_account` INTEGER NOT NULL, `address` TEXT NOT NULL, `value` INTEGER NOT NULL, `memo` BLOB, PRIMARY KEY(`id_note`), FOREIGN KEY(`tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`from_account`) REFERENCES `accounts`(`account`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id_note",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "transactionId",
|
||||
"columnName": "tx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "outputIndex",
|
||||
"columnName": "output_index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "from_account",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "address",
|
||||
"columnName": "address",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "memo",
|
||||
"columnName": "memo",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id_note"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "transactions",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"tx"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id_tx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "accounts",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"from_account"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"account"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "utxos",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `address` TEXT NOT NULL, `prevout_txid` BLOB, `prevout_idx` INTEGER, `script` BLOB, `value_zat` INTEGER NOT NULL, `height` INTEGER, `spent_in_tx` INTEGER)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "address",
|
||||
"columnName": "address",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "txid",
|
||||
"columnName": "prevout_txid",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "transactionIndex",
|
||||
"columnName": "prevout_idx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "script",
|
||||
"columnName": "script",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value_zat",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "height",
|
||||
"columnName": "height",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "spent",
|
||||
"columnName": "spent_in_tx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9431cf7a9bc49395e07834e4c81c5ed1')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,419 +0,0 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 6,
|
||||
"identityHash": "fa97f2995039ee4a382a54d224f4d8b9",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "transactions",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_tx` INTEGER, `txid` BLOB NOT NULL, `tx_index` INTEGER, `created` TEXT, `expiry_height` INTEGER, `block` INTEGER, `raw` BLOB, PRIMARY KEY(`id_tx`), FOREIGN KEY(`block`) REFERENCES `blocks`(`height`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id_tx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "transactionId",
|
||||
"columnName": "txid",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "transactionIndex",
|
||||
"columnName": "tx_index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "created",
|
||||
"columnName": "created",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "expiryHeight",
|
||||
"columnName": "expiry_height",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "minedHeight",
|
||||
"columnName": "block",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "raw",
|
||||
"columnName": "raw",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id_tx"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "blocks",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"block"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"height"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "blocks",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`height` INTEGER, `hash` BLOB NOT NULL, `time` INTEGER NOT NULL, `sapling_tree` BLOB NOT NULL, PRIMARY KEY(`height`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "height",
|
||||
"columnName": "height",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "hash",
|
||||
"columnName": "hash",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "time",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "saplingTree",
|
||||
"columnName": "sapling_tree",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"height"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "received_notes",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_note` INTEGER, `tx` INTEGER NOT NULL, `output_index` INTEGER NOT NULL, `account` INTEGER NOT NULL, `value` INTEGER NOT NULL, `spent` INTEGER, `diversifier` BLOB NOT NULL, `rcm` BLOB NOT NULL, `nf` BLOB NOT NULL, `is_change` INTEGER NOT NULL, `memo` BLOB, PRIMARY KEY(`id_note`), FOREIGN KEY(`tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`account`) REFERENCES `accounts`(`account`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`spent`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id_note",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "transactionId",
|
||||
"columnName": "tx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "outputIndex",
|
||||
"columnName": "output_index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "account",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "spent",
|
||||
"columnName": "spent",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "diversifier",
|
||||
"columnName": "diversifier",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "rcm",
|
||||
"columnName": "rcm",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "nf",
|
||||
"columnName": "nf",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isChange",
|
||||
"columnName": "is_change",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "memo",
|
||||
"columnName": "memo",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id_note"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "transactions",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"tx"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id_tx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "accounts",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"account"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"account"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "transactions",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"spent"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id_tx"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "accounts",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`account` INTEGER, `extfvk` TEXT NOT NULL, `address` TEXT NOT NULL, PRIMARY KEY(`account`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "account",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "extendedFullViewingKey",
|
||||
"columnName": "extfvk",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "address",
|
||||
"columnName": "address",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"account"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "sent_notes",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_note` INTEGER, `tx` INTEGER NOT NULL, `output_index` INTEGER NOT NULL, `from_account` INTEGER NOT NULL, `address` TEXT NOT NULL, `value` INTEGER NOT NULL, `memo` BLOB, PRIMARY KEY(`id_note`), FOREIGN KEY(`tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`from_account`) REFERENCES `accounts`(`account`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id_note",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "transactionId",
|
||||
"columnName": "tx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "outputIndex",
|
||||
"columnName": "output_index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "from_account",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "address",
|
||||
"columnName": "address",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "memo",
|
||||
"columnName": "memo",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id_note"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "transactions",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"tx"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id_tx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "accounts",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"from_account"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"account"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "utxos",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_utxo` INTEGER, `address` TEXT NOT NULL, `prevout_txid` BLOB NOT NULL, `prevout_idx` INTEGER NOT NULL, `script` BLOB NOT NULL, `value_zat` INTEGER NOT NULL, `height` INTEGER NOT NULL, `spent_in_tx` INTEGER, PRIMARY KEY(`id_utxo`), FOREIGN KEY(`spent_in_tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id_utxo",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "address",
|
||||
"columnName": "address",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "txid",
|
||||
"columnName": "prevout_txid",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "transactionIndex",
|
||||
"columnName": "prevout_idx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "script",
|
||||
"columnName": "script",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value_zat",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "height",
|
||||
"columnName": "height",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "spent",
|
||||
"columnName": "spent_in_tx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id_utxo"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "transactions",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"spent_in_tx"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id_tx"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fa97f2995039ee4a382a54d224f4d8b9')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,425 +0,0 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 7,
|
||||
"identityHash": "8e6d7ff0e82352e1fa54e951a5006ba9",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "transactions",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_tx` INTEGER, `txid` BLOB NOT NULL, `tx_index` INTEGER, `created` TEXT, `expiry_height` INTEGER, `block` INTEGER, `raw` BLOB, PRIMARY KEY(`id_tx`), FOREIGN KEY(`block`) REFERENCES `blocks`(`height`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id_tx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "transactionId",
|
||||
"columnName": "txid",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "transactionIndex",
|
||||
"columnName": "tx_index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "created",
|
||||
"columnName": "created",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "expiryHeight",
|
||||
"columnName": "expiry_height",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "minedHeight",
|
||||
"columnName": "block",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "raw",
|
||||
"columnName": "raw",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id_tx"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "blocks",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"block"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"height"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "blocks",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`height` INTEGER, `hash` BLOB NOT NULL, `time` INTEGER NOT NULL, `sapling_tree` BLOB NOT NULL, PRIMARY KEY(`height`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "height",
|
||||
"columnName": "height",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "hash",
|
||||
"columnName": "hash",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "time",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "saplingTree",
|
||||
"columnName": "sapling_tree",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"height"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "received_notes",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_note` INTEGER, `tx` INTEGER NOT NULL, `output_index` INTEGER NOT NULL, `account` INTEGER NOT NULL, `value` INTEGER NOT NULL, `spent` INTEGER, `diversifier` BLOB NOT NULL, `rcm` BLOB NOT NULL, `nf` BLOB NOT NULL, `is_change` INTEGER NOT NULL, `memo` BLOB, PRIMARY KEY(`id_note`), FOREIGN KEY(`tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`account`) REFERENCES `accounts`(`account`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`spent`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id_note",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "transactionId",
|
||||
"columnName": "tx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "outputIndex",
|
||||
"columnName": "output_index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "account",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "spent",
|
||||
"columnName": "spent",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "diversifier",
|
||||
"columnName": "diversifier",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "rcm",
|
||||
"columnName": "rcm",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "nf",
|
||||
"columnName": "nf",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isChange",
|
||||
"columnName": "is_change",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "memo",
|
||||
"columnName": "memo",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id_note"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "transactions",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"tx"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id_tx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "accounts",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"account"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"account"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "transactions",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"spent"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id_tx"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "accounts",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`account` INTEGER, `extfvk` TEXT NOT NULL, `address` TEXT NOT NULL, `transparent_address` TEXT NOT NULL, PRIMARY KEY(`account`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "account",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "extendedFullViewingKey",
|
||||
"columnName": "extfvk",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "address",
|
||||
"columnName": "address",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "transparentAddress",
|
||||
"columnName": "transparent_address",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"account"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "sent_notes",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_note` INTEGER, `tx` INTEGER NOT NULL, `output_index` INTEGER NOT NULL, `from_account` INTEGER NOT NULL, `address` TEXT NOT NULL, `value` INTEGER NOT NULL, `memo` BLOB, PRIMARY KEY(`id_note`), FOREIGN KEY(`tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`from_account`) REFERENCES `accounts`(`account`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id_note",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "transactionId",
|
||||
"columnName": "tx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "outputIndex",
|
||||
"columnName": "output_index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "from_account",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "address",
|
||||
"columnName": "address",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "memo",
|
||||
"columnName": "memo",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id_note"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "transactions",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"tx"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id_tx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "accounts",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"from_account"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"account"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "utxos",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_utxo` INTEGER, `address` TEXT NOT NULL, `prevout_txid` BLOB NOT NULL, `prevout_idx` INTEGER NOT NULL, `script` BLOB NOT NULL, `value_zat` INTEGER NOT NULL, `height` INTEGER NOT NULL, `spent_in_tx` INTEGER, PRIMARY KEY(`id_utxo`), FOREIGN KEY(`spent_in_tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id_utxo",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "address",
|
||||
"columnName": "address",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "txid",
|
||||
"columnName": "prevout_txid",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "transactionIndex",
|
||||
"columnName": "prevout_idx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "script",
|
||||
"columnName": "script",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value_zat",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "height",
|
||||
"columnName": "height",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "spent",
|
||||
"columnName": "spent_in_tx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id_utxo"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "transactions",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"spent_in_tx"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id_tx"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8e6d7ff0e82352e1fa54e951a5006ba9')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 2,
|
||||
"identityHash": "5152277ebe83665392b173731792ccc8",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "pending_transactions",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `to_address` TEXT, `to_internal_account_index` INTEGER, `value` INTEGER NOT NULL, `fee` INTEGER, `memo` BLOB, `sent_from_account_index` INTEGER NOT NULL, `mined_height` INTEGER NOT NULL, `expiry_height` INTEGER NOT NULL, `cancelled` INTEGER NOT NULL, `encode_attempts` INTEGER NOT NULL, `submit_attempts` INTEGER NOT NULL, `error_message` TEXT, `error_code` INTEGER, `create_time` INTEGER NOT NULL, `raw` BLOB NOT NULL, `raw_transaction_id` BLOB)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "toAddress",
|
||||
"columnName": "to_address",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "toInternalAccountIndex",
|
||||
"columnName": "to_internal_account_index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "fee",
|
||||
"columnName": "fee",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "memo",
|
||||
"columnName": "memo",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sentFromAccountIndex",
|
||||
"columnName": "sent_from_account_index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "minedHeight",
|
||||
"columnName": "mined_height",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "expiryHeight",
|
||||
"columnName": "expiry_height",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "cancelled",
|
||||
"columnName": "cancelled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "encodeAttempts",
|
||||
"columnName": "encode_attempts",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "submitAttempts",
|
||||
"columnName": "submit_attempts",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "errorMessage",
|
||||
"columnName": "error_message",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "errorCode",
|
||||
"columnName": "error_code",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "createTime",
|
||||
"columnName": "create_time",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "raw",
|
||||
"columnName": "raw",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "rawTransactionId",
|
||||
"columnName": "raw_transaction_id",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5152277ebe83665392b173731792ccc8')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
package cash.z.ecc.android.sdk
|
||||
|
||||
class InitializerTest {
|
||||
|
||||
// lateinit var initializer: Initializer
|
||||
//
|
||||
// @After
|
||||
// fun cleanUp() {
|
||||
// // don't leave databases sitting around after this test is run
|
||||
// if (::initializer.isInitialized) initializer.erase()
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// fun testInit() {
|
||||
// val height = 980000
|
||||
//
|
||||
// initializer = Initializer(context) { config ->
|
||||
// config.importedWalletBirthday(height)
|
||||
// config.setViewingKeys(
|
||||
// "zxviews1qvn6j50dqqqqpqxqkvqgx2sp63jccr4k5t8zefadpzsu0yy73vczfznwc794xz6lvy3yp5ucv43lww48zz95ey5vhrsq83dqh0ky9junq0cww2wjp9c3cd45n5l5x8l2g9atnx27e9jgyy8zasjy26gugjtefphan9al3tx208m8ekev5kkx3ug6pd0qk4gq4j4wfuxajn388pfpq54wklwktqkyjz9e6gam0n09xjc35ncd3yah5aa9ezj55lk4u7v7hn0v86vz7ygq4qj2v",
|
||||
// "zxviews1qv886f6hqqqqpqy2ajg9sm22vs4gm4hhajthctfkfws34u45pjtut3qmz0eatpqzvllgsvlk3x0y35ktx5fnzqqzueyph20k3328kx46y3u5xs4750cwuwjuuccfp7la6rh8yt2vjz6tylsrwzy3khtjjzw7etkae6gw3vq608k7quka4nxkeqdxxsr9xxdagv2rhhwugs6w0cquu2ykgzgaln2vyv6ah3ram2h6lrpxuznyczt2xl3lyxcwlk4wfz5rh7wzfd7642c2ae5d7"
|
||||
// )
|
||||
// config.alias = "VkInitTest1"
|
||||
// }
|
||||
// assertEquals(height, initializer.birthday.height)
|
||||
// initializer.erase()
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// fun testErase() {
|
||||
// val alias = "VkInitTest2"
|
||||
// initializer = Initializer(context) { config ->
|
||||
// config.importedWalletBirthday(1_419_900)
|
||||
// config.setViewingKeys(
|
||||
// "zxviews1qvn6j50dqqqqpqxqkvqgx2sp63jccr4k5t8zefadpzsu0yy73vczfznwc794xz6lvy3yp5ucv43lww48zz95ey5vhrsq83dqh0ky9junq0cww2wjp9c3cd45n5l5x8l2g9atnx27e9jgyy8zasjy26gugjtefphan9al3tx208m8ekev5kkx3ug6pd0qk4gq4j4wfuxajn388pfpq54wklwktqkyjz9e6gam0n09xjc35ncd3yah5aa9ezj55lk4u7v7hn0v86vz7ygq4qj2v",
|
||||
// "zxviews1qv886f6hqqqqpqy2ajg9sm22vs4gm4hhajthctfkfws34u45pjtut3qmz0eatpqzvllgsvlk3x0y35ktx5fnzqqzueyph20k3328kx46y3u5xs4750cwuwjuuccfp7la6rh8yt2vjz6tylsrwzy3khtjjzw7etkae6gw3vq608k7quka4nxkeqdxxsr9xxdagv2rhhwugs6w0cquu2ykgzgaln2vyv6ah3ram2h6lrpxuznyczt2xl3lyxcwlk4wfz5rh7wzfd7642c2ae5d7"
|
||||
// )
|
||||
// config.alias = alias
|
||||
// }
|
||||
//
|
||||
// assertTrue("Failed to erase initializer", Initializer.erase(context, alias))
|
||||
// assertFalse("Expected false when erasing nothing.", Initializer.erase(context))
|
||||
// }
|
||||
//
|
||||
// @Test(expected = InitializerException.MissingDefaultBirthdayException::class)
|
||||
// fun testMissingBirthday() {
|
||||
// val config = Initializer.Config { config ->
|
||||
// config.setViewingKeys("vk1")
|
||||
// }
|
||||
// config.validate()
|
||||
// }
|
||||
//
|
||||
// @Test(expected = InitializerException.InvalidBirthdayHeightException::class)
|
||||
// fun testOutOfBoundsBirthday() {
|
||||
// val config = Initializer.Config { config ->
|
||||
// config.setViewingKeys("vk1")
|
||||
// config.setBirthdayHeight(ZcashSdk.SAPLING_ACTIVATION_HEIGHT - 1)
|
||||
// }
|
||||
// config.validate()
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// fun testImportedWalletUsesSaplingActivation() {
|
||||
// initializer = Initializer(context) { config ->
|
||||
// config.setViewingKeys("vk1")
|
||||
// config.importWallet(ByteArray(32))
|
||||
// }
|
||||
// assertEquals("Incorrect height used for import.", ZcashSdk.SAPLING_ACTIVATION_HEIGHT, initializer.birthday.height)
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// fun testDefaultToOldestHeight_true() {
|
||||
// initializer = Initializer(context) { config ->
|
||||
// config.setViewingKeys("vk1")
|
||||
// config.setBirthdayHeight(null, true)
|
||||
// }
|
||||
// assertEquals("Height should equal sapling activation height when defaultToOldestHeight is true", ZcashSdk.SAPLING_ACTIVATION_HEIGHT, initializer.birthday.height)
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// fun testDefaultToOldestHeight_false() {
|
||||
// val initialHeight = 750_000
|
||||
// initializer = Initializer(context) { config ->
|
||||
// config.setViewingKeys("vk1")
|
||||
// config.setBirthdayHeight(initialHeight, false)
|
||||
// }
|
||||
// val h = initializer.birthday.height
|
||||
// assertNotEquals("Height should not equal sapling activation height when defaultToOldestHeight is false", ZcashSdk.SAPLING_ACTIVATION_HEIGHT, h)
|
||||
// assertTrue("expected $h to be higher", h >= initialHeight)
|
||||
// }
|
||||
//
|
||||
// companion object {
|
||||
// private val context = InstrumentationRegistry.getInstrumentation().context
|
||||
// init {
|
||||
// Twig.plant(TroubleshootingTwig())
|
||||
// }
|
||||
// }
|
||||
}
|
|
@ -2,7 +2,9 @@ package cash.z.ecc.android.sdk.db
|
|||
|
||||
import androidx.test.filters.SmallTest
|
||||
import cash.z.ecc.android.sdk.internal.AndroidApiVersion
|
||||
import cash.z.ecc.android.sdk.internal.db.PendingTransactionDb
|
||||
import cash.z.ecc.android.sdk.internal.db.DatabaseCoordinator
|
||||
import cash.z.ecc.android.sdk.internal.db.commonDatabaseBuilder
|
||||
import cash.z.ecc.android.sdk.internal.db.pending.PendingTransactionDb
|
||||
import cash.z.ecc.android.sdk.test.getAppContext
|
||||
import cash.z.ecc.fixture.DatabaseNameFixture
|
||||
import cash.z.ecc.fixture.DatabasePathFixture
|
||||
|
|
|
@ -3,7 +3,7 @@ package cash.z.ecc.android.sdk.db
|
|||
import androidx.test.filters.FlakyTest
|
||||
import androidx.test.filters.MediumTest
|
||||
import androidx.test.filters.SmallTest
|
||||
import cash.z.ecc.android.sdk.internal.ext.createNewFileSuspend
|
||||
import cash.z.ecc.android.sdk.internal.db.DatabaseCoordinator
|
||||
import cash.z.ecc.android.sdk.internal.ext.existsSuspend
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.test.getAppContext
|
||||
|
@ -192,14 +192,14 @@ class DatabaseCoordinatorTest {
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun getEmptyFile(parent: File, fileName: String): File {
|
||||
private fun getEmptyFile(parent: File, fileName: String): File {
|
||||
return File(parent, fileName).apply {
|
||||
assertTrue(parentFile != null)
|
||||
parentFile!!.mkdirs()
|
||||
assertTrue(parentFile!!.existsSuspend())
|
||||
assertTrue(parentFile!!.exists())
|
||||
|
||||
createNewFileSuspend()
|
||||
assertTrue(existsSuspend())
|
||||
createNewFile()
|
||||
assertTrue(exists())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,20 +1,12 @@
|
|||
package cash.z.ecc.android.sdk.ext
|
||||
|
||||
import cash.z.ecc.android.sdk.Initializer
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.util.SimpleMnemonics
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONObject
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
fun Initializer.Config.seedPhrase(seedPhrase: String, network: ZcashNetwork) {
|
||||
runBlocking { setSeed(SimpleMnemonics().toSeed(seedPhrase.toCharArray()), network) }
|
||||
}
|
||||
|
||||
object BlockExplorer {
|
||||
suspend fun fetchLatestHeight(): Long {
|
||||
val url = URL("https://api.blockchair.com/zcash/blocks?limit=1")
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package cash.z.ecc.android.sdk.fixture
|
||||
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
|
||||
object WalletFixture {
|
||||
val NETWORK = ZcashNetwork.Mainnet
|
||||
const val SEED_PHRASE =
|
||||
"kitchen renew wide common vague fold vacuum tilt amazing pear square gossip jewel month tree shock scan alpha just spot fluid toilet view dinner"
|
||||
|
||||
suspend fun getUnifiedSpendingKey(
|
||||
seed: String = SEED_PHRASE,
|
||||
network: ZcashNetwork = NETWORK,
|
||||
account: Account = Account.DEFAULT
|
||||
) = DerivationTool.deriveUnifiedSpendingKey(Mnemonics.MnemonicCode(seed).toEntropy(), network, account)
|
||||
}
|
|
@ -1,9 +1,12 @@
|
|||
package cash.z.ecc.android.sdk.integration
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import cash.z.ecc.android.sdk.DefaultSynchronizerFactory
|
||||
import cash.z.ecc.android.sdk.annotation.MaintainedTest
|
||||
import cash.z.ecc.android.sdk.annotation.TestPurpose
|
||||
import cash.z.ecc.android.sdk.db.DatabaseCoordinator
|
||||
import cash.z.ecc.android.sdk.ext.BlockExplorer
|
||||
import cash.z.ecc.android.sdk.internal.SaplingParamTool
|
||||
import cash.z.ecc.android.sdk.internal.db.DatabaseCoordinator
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
|
@ -13,11 +16,9 @@ import org.junit.Ignore
|
|||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import kotlin.test.DefaultAsserter.assertEquals
|
||||
import kotlin.test.DefaultAsserter.assertTrue
|
||||
|
||||
// TODO [#650]: https://github.com/zcash/zcash-android-wallet-sdk/issues/650
|
||||
// TODO [#650]: Move integration tests to separate module
|
||||
|
||||
/**
|
||||
* This test is intended to run to make sure that basic things are functional and pinpoint what is
|
||||
|
@ -27,8 +28,7 @@ import kotlin.test.DefaultAsserter.assertTrue
|
|||
@RunWith(Parameterized::class)
|
||||
class SanityTest(
|
||||
private val wallet: TestWallet,
|
||||
private val extfvk: String,
|
||||
private val extpub: String,
|
||||
private val encoding: String,
|
||||
private val birthday: BlockHeight
|
||||
|
||||
) {
|
||||
|
@ -38,54 +38,42 @@ class SanityTest(
|
|||
|
||||
@Test
|
||||
fun testFilePaths() {
|
||||
val rustBackend = runBlocking {
|
||||
DefaultSynchronizerFactory.defaultRustBackend(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
wallet.network,
|
||||
"TestWallet",
|
||||
birthday,
|
||||
SaplingParamTool.new(ApplicationProvider.getApplicationContext())
|
||||
)
|
||||
}
|
||||
|
||||
assertTrue(
|
||||
"$name has invalid DataDB file",
|
||||
wallet.initializer.rustBackend.dataDbFile.absolutePath.endsWith(
|
||||
"$name has invalid DataDB file actual=${rustBackend.dataDbFile.absolutePath}" +
|
||||
"expected suffix=no_backup/co.electricoin.zcash/TestWallet_${networkName}_${DatabaseCoordinator.DB_DATA_NAME}",
|
||||
rustBackend.dataDbFile.absolutePath.endsWith(
|
||||
"no_backup/co.electricoin.zcash/TestWallet_${networkName}_${DatabaseCoordinator.DB_DATA_NAME}"
|
||||
)
|
||||
)
|
||||
assertTrue(
|
||||
"$name has invalid CacheDB file",
|
||||
wallet.initializer.rustBackend.cacheDbFile.absolutePath.endsWith(
|
||||
"$name has invalid CacheDB file $rustBackend.cacheDbFile.absolutePath",
|
||||
rustBackend.cacheDbFile.absolutePath.endsWith(
|
||||
"no_backup/co.electricoin.zcash/TestWallet_${networkName}_${DatabaseCoordinator.DB_CACHE_NAME}"
|
||||
)
|
||||
)
|
||||
assertTrue(
|
||||
"$name has invalid CacheDB params dir",
|
||||
wallet.initializer.rustBackend.saplingParamDir.endsWith(
|
||||
"$name has invalid params dir ${rustBackend.saplingParamDir.absolutePath}",
|
||||
rustBackend.saplingParamDir.absolutePath.endsWith(
|
||||
"no_backup/co.electricoin.zcash"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBirthday() {
|
||||
assertEquals(
|
||||
"$name has invalid birthday height",
|
||||
birthday,
|
||||
wallet.initializer.checkpoint.height
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testViewingKeys() {
|
||||
assertEquals(
|
||||
"$name has invalid extfvk",
|
||||
extfvk,
|
||||
wallet.initializer.viewingKeys[0].extfvk
|
||||
)
|
||||
assertEquals(
|
||||
"$name has invalid extpub",
|
||||
extpub,
|
||||
wallet.initializer.viewingKeys[0].extpub
|
||||
)
|
||||
}
|
||||
|
||||
@Ignore(
|
||||
"This test needs to be refactored to a separate test module. It causes SSLHandshakeException: Chain " +
|
||||
"validation failed on CI"
|
||||
)
|
||||
@Test
|
||||
fun testLatestHeight() = runBlocking {
|
||||
if (wallet.networkName == "mainnet") {
|
||||
val expectedHeight = BlockExplorer.fetchLatestHeight()
|
||||
|
@ -105,11 +93,11 @@ class SanityTest(
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore(
|
||||
"This test needs to be refactored to a separate test module. It causes SSLHandshakeException: Chain " +
|
||||
"validation failed on CI"
|
||||
)
|
||||
@Test
|
||||
fun testSingleBlockDownload() = runBlocking {
|
||||
// Fetch height directly because the synchronizer hasn't started, yet. Then we test the
|
||||
// result, only if there is no server communication problem.
|
||||
|
@ -123,7 +111,10 @@ class SanityTest(
|
|||
runCatching {
|
||||
wallet.service.getLatestBlockHeight()
|
||||
}.getOrNull() ?: return@runBlocking
|
||||
assertTrue("$networkName failed to return a proper block. Height was ${block.height} but we expected $height", block.height == height.value)
|
||||
assertTrue(
|
||||
"$networkName failed to return a proper block. Height was ${block.height} but we expected $height",
|
||||
block.height == height.value
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -133,15 +124,13 @@ class SanityTest(
|
|||
// Testnet wallet
|
||||
arrayOf(
|
||||
TestWallet(TestWallet.Backups.SAMPLE_WALLET),
|
||||
"zxviewtestsapling1qv0ue89kqqqqpqqyt4cl5wvssx4wqq30e5m948p07dnwl9x3u75vvnzvjwwpjkrf8yk2gva0kkxk9p8suj4xawlzw9pajuxgap83wykvsuyzfrm33a2p2m4jz2205kgzx0l2lj2kyegtnuph6crkyvyjqmfxut84nu00wxgrstu5fy3eu49nzl8jzr4chmql4ysgg2t8htn9dtvxy8c7wx9rvcerqsjqm6lqln9syk3g8rr3xpy3l4nj0kawenzpcdtnv9qmy98vdhqzaf063",
|
||||
"0234965f30c8611253d035f44e68d4e2ce82150e8665c95f41ccbaf916b16c69d8",
|
||||
"uviewtest1m3cyp6tdy3rewtpqazdxlsqkmu7xjedtqmp4da8mvxm87h4as38v5kz4ulw7x7nmgv5d8uwk743a5zt7aurtz2z2g74fu740ecp5fhdgakm6hgzr5jzcl75cmddlufmjpykrpkzj84yz8j5qe9c5935qt2tvd9dpx3m0zw5dwn3t2dtsdyqvy5jstf88w799qre549yyxw7dvk3murm3568ah6wqg5tdjka2ujtgct4q62hw7mfcxcyaeu8l6882hxkt9x4025mx3w35whcrmpxy8fqsh62esatczj8awxtrgnj8h2vj65r8595qt9jl4gz84w4mja74tymt8xxaguckeam",
|
||||
BlockHeight.new(ZcashNetwork.Testnet, 1330000)
|
||||
),
|
||||
// Mainnet wallet
|
||||
arrayOf(
|
||||
TestWallet(TestWallet.Backups.SAMPLE_WALLET, ZcashNetwork.Mainnet),
|
||||
"zxviews1q0hxkupsqqqqpqzsffgrk2smjuccedua7zswf5e3rgtv3ga9nhvhjug670egshd6me53r5n083s2m9mf4va4z7t39ltd3wr7hawnjcw09eu85q0ammsg0tsgx24p4ma0uvr4p8ltx5laum2slh2whc23ctwlnxme9w4dw92kalwk5u4wyem8dynknvvqvs68ktvm8qh7nx9zg22xfc77acv8hk3qqll9k3x4v2fa26puu2939ea7hy4hh60ywma69xtqhcy4037ne8g2sg8sq",
|
||||
"031c6355641237643317e2d338f5e8734c57e8aa8ce960ee22283cf2d76bef73be",
|
||||
"uview1n8j8hckdh4rpxsa8qswmcv8mgu6g3f4l4se6ympej3qr6k5k5xlw47u02s3h2sy5aplkzuwysvum2p6weakvyc72udsuvplaq8r5jkw5h6cjfp26j8rudam7suzu6lwalzakpps2jv2x5v08gf3la02dtdlq75ca7k4urg6t0yncyly5wu26t6mfdfvxvhckr2qxzcwllnh947gn6wzg92f0mlhfds239q50gm4398n02anm23qgk8st49u0wmmw7flathr49h2twxvfm6gauasuq6z2fvs3t8g9ut4duk7tp7ry88dwacsutxzpwnm674y06mf3mz3tnu8s2fx4vatmcs9",
|
||||
BlockHeight.new(ZcashNetwork.Mainnet, 1000000)
|
||||
)
|
||||
)
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
package cash.z.ecc.android.sdk.integration
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.android.sdk.DefaultSynchronizerFactory
|
||||
import cash.z.ecc.android.sdk.annotation.MaintainedTest
|
||||
import cash.z.ecc.android.sdk.annotation.TestPurpose
|
||||
import cash.z.ecc.android.sdk.db.DatabaseCoordinator
|
||||
import cash.z.ecc.android.sdk.internal.SaplingParamTool
|
||||
import cash.z.ecc.android.sdk.internal.db.DatabaseCoordinator
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.util.TestWallet
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
|
@ -22,41 +25,35 @@ class SmokeTest {
|
|||
|
||||
@Test
|
||||
fun testFilePaths() {
|
||||
val rustBackend = runBlocking {
|
||||
DefaultSynchronizerFactory.defaultRustBackend(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
ZcashNetwork.Testnet,
|
||||
"TestWallet",
|
||||
TestWallet.Backups.SAMPLE_WALLET.testnetBirthday,
|
||||
SaplingParamTool.new(ApplicationProvider.getApplicationContext())
|
||||
)
|
||||
}
|
||||
assertTrue(
|
||||
"Invalid DataDB file",
|
||||
wallet.initializer.rustBackend.dataDbFile.absolutePath.endsWith(
|
||||
rustBackend.dataDbFile.absolutePath.endsWith(
|
||||
"no_backup/co.electricoin.zcash/TestWallet_testnet_${DatabaseCoordinator.DB_DATA_NAME}"
|
||||
)
|
||||
)
|
||||
assertTrue(
|
||||
"Invalid CacheDB file",
|
||||
wallet.initializer.rustBackend.cacheDbFile.absolutePath.endsWith(
|
||||
rustBackend.cacheDbFile.absolutePath.endsWith(
|
||||
"no_backup/co.electricoin.zcash/TestWallet_testnet_${DatabaseCoordinator.DB_CACHE_NAME}"
|
||||
)
|
||||
)
|
||||
assertTrue(
|
||||
"Invalid CacheDB params dir",
|
||||
wallet.initializer.rustBackend.saplingParamDir.endsWith(
|
||||
rustBackend.saplingParamDir.endsWith(
|
||||
"no_backup/co.electricoin.zcash"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBirthday() {
|
||||
assertEquals(
|
||||
"Invalid birthday height",
|
||||
1_330_000,
|
||||
wallet.initializer.checkpoint.height.value
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testViewingKeys() {
|
||||
assertEquals("Invalid extfvk", "zxviewtestsapling1qv0ue89kqqqqpqqyt4cl5wvssx4wqq30e5m948p07dnwl9x3u75vvnzvjwwpjkrf8yk2gva0kkxk9p8suj4xawlzw9pajuxgap83wykvsuyzfrm33a2p2m4jz2205kgzx0l2lj2kyegtnuph6crkyvyjqmfxut84nu00wxgrstu5fy3eu49nzl8jzr4chmql4ysgg2t8htn9dtvxy8c7wx9rvcerqsjqm6lqln9syk3g8rr3xpy3l4nj0kawenzpcdtnv9qmy98vdhqzaf063", wallet.initializer.viewingKeys[0].extfvk)
|
||||
assertEquals("Invalid extpub", "0234965f30c8611253d035f44e68d4e2ce82150e8665c95f41ccbaf916b16c69d8", wallet.initializer.viewingKeys[0].extpub)
|
||||
}
|
||||
|
||||
// This test takes an extremely long time
|
||||
// Does its runtime grow over time based on growth of the blockchain?
|
||||
@Test
|
||||
|
|
|
@ -2,20 +2,20 @@ package cash.z.ecc.android.sdk.integration
|
|||
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import cash.z.ecc.android.sdk.Initializer
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.Synchronizer.Status.SYNCED
|
||||
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
|
||||
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
||||
import cash.z.ecc.android.sdk.ext.onFirst
|
||||
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
|
||||
import cash.z.ecc.android.sdk.internal.Twig
|
||||
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.isSubmitSuccess
|
||||
import cash.z.ecc.android.sdk.test.ScopedTest
|
||||
import cash.z.ecc.android.sdk.tool.CheckpointTool
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
|
@ -63,7 +63,7 @@ class TestnetIntegrationTest : ScopedTest() {
|
|||
@Test
|
||||
@Ignore("This test is broken")
|
||||
fun getAddress() = runBlocking {
|
||||
assertEquals(address, synchronizer.getAddress())
|
||||
assertEquals(address, synchronizer.getUnifiedAddress())
|
||||
}
|
||||
|
||||
// This is an extremely slow test; it is disabled so that we can get CI set up
|
||||
|
@ -98,7 +98,7 @@ class TestnetIntegrationTest : ScopedTest() {
|
|||
}
|
||||
|
||||
private suspend fun sendFunds(): Boolean {
|
||||
val spendingKey = DerivationTool.deriveSpendingKeys(seed, synchronizer.network)[0]
|
||||
val spendingKey = DerivationTool.deriveUnifiedSpendingKey(seed, synchronizer.network, Account.DEFAULT)
|
||||
log("sending to address")
|
||||
synchronizer.sendToAddress(
|
||||
spendingKey,
|
||||
|
@ -117,29 +117,33 @@ class TestnetIntegrationTest : ScopedTest() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
init { Twig.plant(TroubleshootingTwig()) }
|
||||
init {
|
||||
Twig.plant(TroubleshootingTwig())
|
||||
}
|
||||
|
||||
val lightWalletEndpoint = LightWalletEndpoint("lightwalletd.testnet.z.cash", 9087, true)
|
||||
private const val birthdayHeight = 963150L
|
||||
private const val targetHeight = 663250
|
||||
private const val seedPhrase = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"
|
||||
private const val seedPhrase =
|
||||
"still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"
|
||||
val seed = "cash.z.ecc.android.sdk.integration.IntegrationTest.seed.value.64bytes".toByteArray()
|
||||
val address = "zs1m30y59wxut4zk9w24d6ujrdnfnl42hpy0ugvhgyhr8s0guszutqhdj05c7j472dndjstulph74m"
|
||||
val toAddress = "zs1vp7kvlqr4n9gpehztr76lcn6skkss9p8keqs3nv8avkdtjrcctrvmk9a7u494kluv756jeee5k0"
|
||||
|
||||
private val context = InstrumentationRegistry.getInstrumentation().context
|
||||
private val initializer = runBlocking {
|
||||
Initializer.new(context) { config ->
|
||||
config.setNetwork(ZcashNetwork.Testnet, lightWalletEndpoint)
|
||||
runBlocking { config.importWallet(seed, BlockHeight.new(ZcashNetwork.Testnet, birthdayHeight), ZcashNetwork.Testnet, lightWalletEndpoint) }
|
||||
}
|
||||
}
|
||||
private lateinit var synchronizer: Synchronizer
|
||||
|
||||
@JvmStatic
|
||||
@BeforeClass
|
||||
fun startUp() {
|
||||
synchronizer = Synchronizer.newBlocking(initializer)
|
||||
synchronizer = Synchronizer.newBlocking(
|
||||
context,
|
||||
ZcashNetwork.Testnet,
|
||||
lightWalletEndpoint =
|
||||
lightWalletEndpoint,
|
||||
seed = seed,
|
||||
birthday = BlockHeight.new(ZcashNetwork.Testnet, birthdayHeight)
|
||||
)
|
||||
synchronizer.start(classScope)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import cash.z.ecc.android.sdk.annotation.TestPurpose
|
|||
import cash.z.ecc.android.sdk.exception.LightWalletException.ChangeServerException.ChainInfoNotMatching
|
||||
import cash.z.ecc.android.sdk.exception.LightWalletException.ChangeServerException.StatusException
|
||||
import cash.z.ecc.android.sdk.internal.block.CompactBlockDownloader
|
||||
import cash.z.ecc.android.sdk.internal.block.CompactBlockStore
|
||||
import cash.z.ecc.android.sdk.internal.repository.CompactBlockRepository
|
||||
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
|
||||
import cash.z.ecc.android.sdk.internal.service.LightWalletService
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
|
@ -45,7 +45,7 @@ class ChangeServiceTest : ScopedTest() {
|
|||
private val eccEndpoint = LightWalletEndpoint("lightwalletd.electriccoin.co", 9087, true)
|
||||
|
||||
@Mock
|
||||
lateinit var mockBlockStore: CompactBlockStore
|
||||
lateinit var mockBlockStore: CompactBlockRepository
|
||||
var mockCloseable: AutoCloseable? = null
|
||||
|
||||
@Spy
|
||||
|
|
|
@ -4,25 +4,26 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||
import androidx.test.filters.SmallTest
|
||||
import cash.z.ecc.android.sdk.annotation.MaintainedTest
|
||||
import cash.z.ecc.android.sdk.annotation.TestPurpose
|
||||
import cash.z.ecc.android.sdk.db.entity.EncodedTransaction
|
||||
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
|
||||
import cash.z.ecc.android.sdk.db.entity.isCancelled
|
||||
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
|
||||
import cash.z.ecc.android.sdk.internal.Twig
|
||||
import cash.z.ecc.android.sdk.internal.db.commonDatabaseBuilder
|
||||
import cash.z.ecc.android.sdk.internal.db.pending.PendingTransactionDb
|
||||
import cash.z.ecc.android.sdk.internal.model.EncodedTransaction
|
||||
import cash.z.ecc.android.sdk.internal.service.LightWalletService
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.FirstClassByteArray
|
||||
import cash.z.ecc.android.sdk.model.PendingTransaction
|
||||
import cash.z.ecc.android.sdk.model.TransactionRecipient
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.test.ScopedTest
|
||||
import cash.z.ecc.android.sdk.test.getAppContext
|
||||
import cash.z.ecc.fixture.DatabaseNameFixture
|
||||
import cash.z.ecc.fixture.DatabasePathFixture
|
||||
import com.nhaarman.mockitokotlin2.any
|
||||
import com.nhaarman.mockitokotlin2.stub
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Before
|
||||
import org.junit.BeforeClass
|
||||
|
@ -31,7 +32,6 @@ import org.junit.runner.RunWith
|
|||
import org.mockito.Mock
|
||||
import org.mockito.MockitoAnnotations
|
||||
import java.io.File
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
@ -41,9 +41,11 @@ import kotlin.test.assertTrue
|
|||
@SmallTest
|
||||
class PersistentTransactionManagerTest : ScopedTest() {
|
||||
|
||||
@Mock lateinit var mockEncoder: TransactionEncoder
|
||||
@Mock
|
||||
internal lateinit var mockEncoder: TransactionEncoder
|
||||
|
||||
@Mock lateinit var mockService: LightWalletService
|
||||
@Mock
|
||||
lateinit var mockService: LightWalletService
|
||||
|
||||
private val pendingDbFile = File(
|
||||
DatabasePathFixture.new(),
|
||||
|
@ -61,7 +63,12 @@ class PersistentTransactionManagerTest : ScopedTest() {
|
|||
fun setup() {
|
||||
initMocks()
|
||||
deleteDb()
|
||||
manager = PersistentTransactionManager(context, mockEncoder, mockService, pendingDbFile)
|
||||
val db = commonDatabaseBuilder(
|
||||
getAppContext(),
|
||||
PendingTransactionDb::class.java,
|
||||
pendingDbFile
|
||||
).build()
|
||||
manager = PersistentTransactionManager(db, ZcashNetwork.Mainnet, mockEncoder, mockService)
|
||||
}
|
||||
|
||||
private fun deleteDb() {
|
||||
|
@ -72,52 +79,33 @@ class PersistentTransactionManagerTest : ScopedTest() {
|
|||
MockitoAnnotations.openMocks(this)
|
||||
mockEncoder.stub {
|
||||
onBlocking {
|
||||
createTransaction(any(), any(), any(), any(), any())
|
||||
createTransaction(any(), any(), any(), any())
|
||||
}.thenAnswer {
|
||||
runBlocking {
|
||||
delay(200)
|
||||
EncodedTransaction(byteArrayOf(1, 2, 3), byteArrayOf(8, 9), 5_000_000)
|
||||
EncodedTransaction(
|
||||
FirstClassByteArray(byteArrayOf(1, 2, 3)),
|
||||
FirstClassByteArray(
|
||||
byteArrayOf(
|
||||
8,
|
||||
9
|
||||
)
|
||||
),
|
||||
BlockHeight.new(ZcashNetwork.Mainnet, 5_000_000)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCancellation_RaceCondition() = runBlocking {
|
||||
val tx = manager.initSpend(Zatoshi(1234), "taddr", "memo-good", 0)
|
||||
val txFlow = manager.monitorById(tx.id)
|
||||
|
||||
// encode TX
|
||||
testScope.launch {
|
||||
twig("ENCODE: start"); manager.encode("fookey", tx); twig("ENCODE: end")
|
||||
}
|
||||
|
||||
// then cancel it before it is done encoding
|
||||
testScope.launch {
|
||||
delay(100)
|
||||
twig("CANCEL: start"); manager.cancel(tx.id); twig("CANCEL: end")
|
||||
}
|
||||
|
||||
txFlow.drop(2).onEach {
|
||||
twig("found tx: $it")
|
||||
assertTrue(it.isCancelled(), "Expected the encoded tx to be cancelled but it wasn't")
|
||||
twig("found it to be successfully cancelled")
|
||||
testScope.cancel()
|
||||
}.launchIn(testScope).join()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCancel() = runBlocking {
|
||||
var tx = manager.initSpend(Zatoshi(1234), "a", "b", 0)
|
||||
assertFalse(tx.isCancelled())
|
||||
manager.cancel(tx.id)
|
||||
tx = manager.findById(tx.id)!!
|
||||
assertTrue(tx.isCancelled(), "Transaction was not cancelled")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAbort() = runBlocking {
|
||||
var tx: PendingTransaction? = manager.initSpend(Zatoshi(1234), "a", "b", 0)
|
||||
var tx: PendingTransaction? = manager.initSpend(
|
||||
Zatoshi(1234),
|
||||
TransactionRecipient.Address("a"),
|
||||
"b",
|
||||
Account.DEFAULT
|
||||
)
|
||||
assertNotNull(tx)
|
||||
manager.abort(tx)
|
||||
tx = manager.findById(tx.id)
|
||||
|
|
|
@ -22,28 +22,14 @@ import org.junit.runners.Parameterized
|
|||
class TransparentTest(val expected: Expected, val network: ZcashNetwork) {
|
||||
|
||||
@Test
|
||||
fun deriveTransparentSecretKeyTest() = runBlocking {
|
||||
assertEquals(expected.tskCompressed, DerivationTool.deriveTransparentSecretKey(SEED, network = network))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deriveTransparentAddressTest() = runBlocking {
|
||||
assertEquals(expected.tAddr, DerivationTool.deriveTransparentAddress(SEED, network = network))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deriveTransparentAddressFromSecretKeyTest() = runBlocking {
|
||||
val pk = DerivationTool.deriveTransparentSecretKey(SEED, network = network)
|
||||
assertEquals(expected.tAddr, DerivationTool.deriveTransparentAddressFromPrivateKey(pk, network = network))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deriveUnifiedViewingKeysFromSeedTest() = runBlocking {
|
||||
val uvks = DerivationTool.deriveUnifiedViewingKeys(SEED, network = network)
|
||||
assertEquals(1, uvks.size)
|
||||
val uvk = uvks.first()
|
||||
assertEquals(expected.zAddr, DerivationTool.deriveShieldedAddress(uvk.extfvk, network = network))
|
||||
assertEquals(expected.tAddr, DerivationTool.deriveTransparentAddressFromPublicKey(uvk.extpub, network = network))
|
||||
fun deriveUnifiedFullViewingKeysFromSeedTest() = runBlocking {
|
||||
val ufvks = DerivationTool.deriveUnifiedFullViewingKeys(SEED, network = network)
|
||||
assertEquals(1, ufvks.size)
|
||||
val ufvk = ufvks.first()
|
||||
assertEquals(expected.uAddr, DerivationTool.deriveUnifiedAddress(ufvk.encoding, network = network))
|
||||
// TODO: If we need this, change DerivationTool to derive from the UFVK instead of the public key.
|
||||
// assertEquals(expected.tAddr, DerivationTool.deriveTransparentAddressFromPublicKey(ufvk.encoding,
|
||||
// network = network))
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -54,6 +40,8 @@ class TransparentTest(val expected: Expected, val network: ZcashNetwork) {
|
|||
object ExpectedMainnet : Expected {
|
||||
override val tAddr = "t1PKtYdJJHhc3Pxowmznkg7vdTwnhEsCvR4"
|
||||
override val zAddr = "zs1yc4sgtfwwzz6xfsy2xsradzr6m4aypgxhfw2vcn3hatrh5ryqsr08sgpemlg39vdh9kfupx20py"
|
||||
override val uAddr = "u1607xqhx72u8x94xcg6kyt9sd83aw8zvys2vwlr5n956e5jfytcaaeuzrk938c03jv4t0kdk73yxz9yd8rdksutw68ycpy6yt9vzhu28z58rh89gtt653cspr0c50ev4av0ddzj5vrrh"
|
||||
override val tAccountPrivKey = "xprv9z1aorRbyM5A6ok9QmdCUztMRRgthiNpus4u8Rgn9YeZEz1EVkLthFpJS1Y1FaXAvgNDPKTwxvshUMj7KJiGeNVhKL8RzDv14yHbUu3szy5"
|
||||
override val tskCompressed = "L4BvDC33yLjMRxipZvdiUmdYeRfZmR8viziwsVwe72zJdGbiJPv2"
|
||||
override val tpk = "03b1d7fb28d17c125b504d06b1530097e0a3c76ada184237e3bc0925041230a5af"
|
||||
}
|
||||
|
@ -61,6 +49,8 @@ class TransparentTest(val expected: Expected, val network: ZcashNetwork) {
|
|||
object ExpectedTestnet : Expected {
|
||||
override val tAddr = "tm9v3KTsjXK8XWSqiwFjic6Vda6eHY9Mjjq"
|
||||
override val zAddr = "ztestsapling1wn3tw9w5rs55x5yl586gtk72e8hcfdq8zsnjzcu8p7ghm8lrx54axc74mvm335q7lmy3g0sqje6"
|
||||
override val uAddr = "utest1cy80kzr6fj5vrrazldtcgmycs6rgu2x73pvwrjjmlwrwx343m06lxua5u36jdwyeckn4a6a0fkxm4y7t3lvhzscqrwg3gxpj4rgrgmf93m0cpm9ddkzn5qyzgadktuwza5d5kucewv3"
|
||||
override val tAccountPrivKey = "xprv9yUDoMsKVAQ8W8tf3VuPGyBKHuDPa4SkBXT7KHp4dfW7iBWKUEgAYG1g6ZpdotTWc4iMrj6vgaT8otHCWRj5SYtXkDcxkheFCp6QZEW9dPi"
|
||||
override val tskCompressed = "KzVugoXxR7AtTMdR5sdJtHxCNvMzQ4H196k7ATv4nnjoummsRC9G"
|
||||
override val tpk = "03b1d7fb28d17c125b504d06b1530097e0a3c76ada184237e3bc0925041230a5af"
|
||||
}
|
||||
|
@ -82,6 +72,8 @@ class TransparentTest(val expected: Expected, val network: ZcashNetwork) {
|
|||
interface Expected {
|
||||
val tAddr: String
|
||||
val zAddr: String
|
||||
val uAddr: String
|
||||
val tAccountPrivKey: String
|
||||
val tskCompressed: String
|
||||
val tpk: String
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
package cash.z.ecc.android.sdk.model
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import cash.z.ecc.android.sdk.fixture.WalletFixture
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class UnifiedSpendingKeyTest {
|
||||
@Test
|
||||
@SmallTest
|
||||
fun factory_copies_bytes() = runTest {
|
||||
val spendingKey = WalletFixture.getUnifiedSpendingKey()
|
||||
val expected = spendingKey.copyBytes().copyOf()
|
||||
|
||||
val bytes = spendingKey.copyBytes()
|
||||
val newSpendingKey = UnifiedSpendingKey.new(spendingKey.account, bytes)
|
||||
bytes.clear()
|
||||
|
||||
assertContentEquals(expected, newSpendingKey.getOrThrow().copyBytes())
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun get_copies_bytes() = runTest {
|
||||
val spendingKey = WalletFixture.getUnifiedSpendingKey()
|
||||
|
||||
val expected = spendingKey.copyBytes()
|
||||
val newSpendingKey = UnifiedSpendingKey.new(spendingKey.account, expected)
|
||||
|
||||
newSpendingKey.getOrThrow().copyBytes().clear()
|
||||
|
||||
assertContentEquals(expected, newSpendingKey.getOrThrow().copyBytes())
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun toString_does_not_leak() = runTest {
|
||||
assertEquals(
|
||||
"UnifiedSpendingKey(account=Account(value=0))",
|
||||
WalletFixture.getUnifiedSpendingKey().toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ByteArray.clear() {
|
||||
for (i in indices) {
|
||||
this[i] = 0
|
||||
}
|
||||
}
|
|
@ -30,7 +30,7 @@ class ShieldFundsSample {
|
|||
|
||||
val wallet = TestWallet(TestWallet.Backups.DEV_WALLET, ZcashNetwork.Mainnet)
|
||||
|
||||
Assert.assertEquals("foo", "${wallet.shieldedAddress} ${wallet.transparentAddress}")
|
||||
Assert.assertEquals("foo", "${wallet.unifiedAddress} ${wallet.transparentAddress}")
|
||||
// wallet.shieldFunds()
|
||||
|
||||
Twig.clip("ShieldFundsSample")
|
||||
|
|
|
@ -162,7 +162,7 @@ class TransparentRestoreSample {
|
|||
// private val context = InstrumentationRegistry.getInstrumentation().context
|
||||
// private val seed: ByteArray = Mnemonics.MnemonicCode(seedPhrase).toSeed()
|
||||
// private val shieldedSpendingKey = DerivationTool.deriveSpendingKeys(seed, Testnet)[0]
|
||||
// private val transparentSecretKey = DerivationTool.deriveTransparentSecretKey(seed, Testnet)
|
||||
// private val transparentAccountPrivateKey = DerivationTool.deriveTransparentAccountPrivateKey(seed, Testnet)
|
||||
// private val host = "lightwalletd.testnet.electriccoin.co"
|
||||
// private val initializer = Initializer(context) { config ->
|
||||
// config.importWallet(seed, startHeight)
|
||||
|
@ -219,7 +219,7 @@ class TransparentRestoreSample {
|
|||
// twig("FOUND utxo balance of total: ${walletBalance.totalZatoshi} available: ${walletBalance.availableZatoshi}")
|
||||
//
|
||||
// if (walletBalance.availableZatoshi > 0L) {
|
||||
// synchronizer.shieldFunds(shieldedSpendingKey, transparentSecretKey)
|
||||
// synchronizer.shieldFunds(shieldedSpendingKey, transparentAccountPrivateKey)
|
||||
// .onCompletion { twig("done shielding funds") }
|
||||
// .catch { twig("Failed with $it") }
|
||||
// .collect()
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package cash.z.ecc.android.sdk.tool
|
||||
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.sdk.fixture.WalletFixture
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
|
||||
class DerivationToolTest {
|
||||
@Test
|
||||
fun create_spending_key_does_not_mutate_passed_bytes() = runTest {
|
||||
val bytesOne = Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy()
|
||||
val bytesTwo = Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy()
|
||||
|
||||
DerivationTool.deriveUnifiedSpendingKey(bytesOne, WalletFixture.NETWORK, Account.DEFAULT)
|
||||
|
||||
assertContentEquals(bytesTwo, bytesOne)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package cash.z.ecc.android.sdk.util
|
||||
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.test.readFileLinesInFlow
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
|
@ -30,10 +31,10 @@ class AddressGeneratorUtil {
|
|||
.map { seedPhrase ->
|
||||
mnemonics.toSeed(seedPhrase.toCharArray())
|
||||
}.map { seed ->
|
||||
DerivationTool.deriveShieldedAddress(seed, ZcashNetwork.Mainnet)
|
||||
DerivationTool.deriveUnifiedAddress(seed, ZcashNetwork.Mainnet, Account.DEFAULT)
|
||||
}.collect { address ->
|
||||
println("xrxrx2\t$address")
|
||||
assertTrue(address.startsWith("zs1"))
|
||||
assertTrue(address.startsWith("u1"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package cash.z.ecc.android.sdk.util
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import cash.z.ecc.android.sdk.Initializer
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
|
||||
import cash.z.ecc.android.sdk.internal.Twig
|
||||
|
@ -80,11 +79,7 @@ class BalancePrinterUtil {
|
|||
mnemonics.toSeed(seedPhrase.toCharArray())
|
||||
}.collect { seed ->
|
||||
// TODO: clear the dataDb but leave the cacheDb
|
||||
val initializer = Initializer.new(context) { config ->
|
||||
val endpoint = LightWalletEndpoint.defaultForNetwork(network)
|
||||
runBlocking { config.importWallet(seed, birthdayHeight, network, endpoint) }
|
||||
config.alias = alias
|
||||
}
|
||||
|
||||
/*
|
||||
what I need to do right now
|
||||
- for each seed
|
||||
|
@ -100,7 +95,14 @@ class BalancePrinterUtil {
|
|||
- can we be more stateless and thereby improve the flexibility of this code?!!!
|
||||
*/
|
||||
synchronizer?.stop()
|
||||
synchronizer = Synchronizer.new(initializer).apply {
|
||||
synchronizer = Synchronizer.new(
|
||||
context,
|
||||
network,
|
||||
lightWalletEndpoint = LightWalletEndpoint
|
||||
.defaultForNetwork(network),
|
||||
seed = seed,
|
||||
birthday = birthdayHeight
|
||||
).apply {
|
||||
start()
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
package cash.z.ecc.android.sdk.util
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import cash.z.ecc.android.sdk.Initializer
|
||||
import cash.z.ecc.android.sdk.SdkSynchronizer
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
|
||||
import cash.z.ecc.android.sdk.internal.Twig
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
@ -65,27 +66,17 @@ class DataDbScannerUtil {
|
|||
@Test
|
||||
@Ignore("This test is broken")
|
||||
fun scanExistingDb() {
|
||||
synchronizer = run {
|
||||
val initializer = runBlocking {
|
||||
Initializer.new(context) {
|
||||
it.setBirthdayHeight(
|
||||
BlockHeight.new(
|
||||
ZcashNetwork.Mainnet,
|
||||
birthdayHeight
|
||||
),
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val synchronizer = runBlocking {
|
||||
Synchronizer.new(
|
||||
initializer
|
||||
)
|
||||
}
|
||||
|
||||
synchronizer
|
||||
}
|
||||
synchronizer = Synchronizer.newBlocking(
|
||||
context,
|
||||
ZcashNetwork.Mainnet,
|
||||
lightWalletEndpoint = LightWalletEndpoint
|
||||
.defaultForNetwork(ZcashNetwork.Mainnet),
|
||||
seed = byteArrayOf(),
|
||||
birthday = BlockHeight.new(
|
||||
ZcashNetwork.Mainnet,
|
||||
birthdayHeight
|
||||
)
|
||||
)
|
||||
|
||||
println("sync!")
|
||||
synchronizer.start()
|
||||
|
|
|
@ -3,19 +3,19 @@ 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.Account
|
||||
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.model.isPending
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
|
@ -63,23 +63,23 @@ class TestWallet(
|
|||
|
||||
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
|
||||
private val spendingKey =
|
||||
runBlocking { DerivationTool.deriveUnifiedSpendingKey(seed, network = network, Account.DEFAULT) }
|
||||
val synchronizer: SdkSynchronizer = Synchronizer.newBlocking(
|
||||
context,
|
||||
network,
|
||||
alias,
|
||||
lightWalletEndpoint = endpoint,
|
||||
seed = seed,
|
||||
startHeight
|
||||
) 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 unifiedAddress =
|
||||
runBlocking { synchronizer.getUnifiedAddress(Account.DEFAULT) }
|
||||
val transparentAddress =
|
||||
runBlocking { DerivationTool.deriveTransparentAddress(seed, network = network) }
|
||||
runBlocking { synchronizer.getTransparentAddress(Account.DEFAULT) }
|
||||
val birthdayHeight get() = synchronizer.latestBirthdayHeight
|
||||
val networkName get() = synchronizer.network.networkName
|
||||
|
||||
|
@ -109,9 +109,13 @@ class TestWallet(
|
|||
return this
|
||||
}
|
||||
|
||||
suspend fun send(address: String = transparentAddress, memo: String = "", amount: Zatoshi = Zatoshi(500L), fromAccountIndex: Int = 0): TestWallet {
|
||||
suspend fun send(
|
||||
address: String = transparentAddress,
|
||||
memo: String = "",
|
||||
amount: Zatoshi = Zatoshi(500L)
|
||||
): TestWallet {
|
||||
Twig.sprout("$alias sending")
|
||||
synchronizer.sendToAddress(shieldedSpendingKey, amount, address, memo, fromAccountIndex)
|
||||
synchronizer.sendToAddress(spendingKey, amount, address, memo)
|
||||
.takeWhile { it.isPending(null) }
|
||||
.collect {
|
||||
twig("Updated transaction: $it")
|
||||
|
@ -135,7 +139,7 @@ class TestWallet(
|
|||
twig("FOUND utxo balance of total: ${walletBalance.total} available: ${walletBalance.available}")
|
||||
|
||||
if (walletBalance.available.value > 0L) {
|
||||
synchronizer.shieldFunds(shieldedSpendingKey, transparentSecretKey)
|
||||
synchronizer.shieldFunds(spendingKey)
|
||||
.onCompletion { twig("done shielding funds") }
|
||||
.catch { twig("Failed with $it") }
|
||||
.collect()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package cash.z.ecc.fixture
|
||||
|
||||
import cash.z.ecc.android.sdk.db.DatabaseCoordinator
|
||||
import cash.z.ecc.android.sdk.internal.db.DatabaseCoordinator
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
|
||||
object DatabaseNameFixture {
|
||||
|
|
|
@ -1,437 +0,0 @@
|
|||
package cash.z.ecc.android.sdk
|
||||
|
||||
import android.content.Context
|
||||
import cash.z.ecc.android.sdk.db.DatabaseCoordinator
|
||||
import cash.z.ecc.android.sdk.exception.InitializerException
|
||||
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
||||
import cash.z.ecc.android.sdk.internal.SaplingParamTool
|
||||
import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
import cash.z.ecc.android.sdk.jni.RustBackend
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.tool.CheckpointTool
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
* Simplified Initializer focused on starting from a ViewingKey.
|
||||
*/
|
||||
@Suppress("LongParameterList", "unused")
|
||||
class Initializer private constructor(
|
||||
val context: Context,
|
||||
internal val rustBackend: RustBackend,
|
||||
val network: ZcashNetwork,
|
||||
val alias: String,
|
||||
val lightWalletEndpoint: LightWalletEndpoint,
|
||||
val viewingKeys: List<UnifiedViewingKey>,
|
||||
val overwriteVks: Boolean,
|
||||
internal val checkpoint: Checkpoint,
|
||||
internal val saplingParamTool: SaplingParamTool
|
||||
) {
|
||||
|
||||
suspend fun erase() = erase(context, network, alias)
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
class Config private constructor(
|
||||
val viewingKeys: MutableList<UnifiedViewingKey> = mutableListOf(),
|
||||
var alias: String = ZcashSdk.DEFAULT_ALIAS
|
||||
) {
|
||||
var birthdayHeight: BlockHeight? = null
|
||||
private set
|
||||
|
||||
lateinit var network: ZcashNetwork
|
||||
private set
|
||||
|
||||
lateinit var lightWalletEndpoint: LightWalletEndpoint
|
||||
private set
|
||||
|
||||
/**
|
||||
* Determines the default behavior for null birthdays. When null, nothing has been specified
|
||||
* so a null birthdayHeight value is an error. When false, null birthdays will be replaced
|
||||
* with the most recent checkpoint height available (typically, the latest `*.json` file in
|
||||
* `assets/co.electriccoin.zcash/checkpoint/`). When true, null birthdays will be replaced with the oldest
|
||||
* reasonable height where a transaction could exist (typically, sapling activation but
|
||||
* better approximations could be devised in the future, such as the date when the first
|
||||
* BIP-39 zcash wallets came online).
|
||||
*/
|
||||
var defaultToOldestHeight: Boolean? = null
|
||||
private set
|
||||
|
||||
var overwriteVks: Boolean = false
|
||||
private set
|
||||
|
||||
constructor(block: (Config) -> Unit) : this() {
|
||||
block(this)
|
||||
}
|
||||
|
||||
//
|
||||
// Birthday functions
|
||||
//
|
||||
|
||||
/**
|
||||
* Set the birthday height for this configuration. When the height is not known, the wallet
|
||||
* can either default to the latest known birthday (in order to sync new wallets faster) or
|
||||
* the oldest possible birthday (in order to import a wallet with an unknown birthday
|
||||
* without skipping old transactions).
|
||||
*
|
||||
* @param height nullable birthday height to use for this configuration.
|
||||
* @param defaultToOldestHeight determines how a null birthday height will be
|
||||
* interpreted. Typically, `false` for new wallets and `true` for restored wallets because
|
||||
* new wallets want to load quickly but restored wallets want to find all possible
|
||||
* transactions. Again, this value is only considered when [height] is null.
|
||||
*
|
||||
*/
|
||||
fun setBirthdayHeight(height: BlockHeight?, defaultToOldestHeight: Boolean): Config =
|
||||
apply {
|
||||
this.birthdayHeight = height
|
||||
this.defaultToOldestHeight = defaultToOldestHeight
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the most recent checkpoint available. This is useful for new wallets.
|
||||
*/
|
||||
fun newWalletBirthday(): Config = apply {
|
||||
birthdayHeight = null
|
||||
defaultToOldestHeight = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the birthday checkpoint closest to the given wallet birthday. This is useful when
|
||||
* importing a pre-existing wallet. It is the same as calling
|
||||
* `birthdayHeight = importedHeight`.
|
||||
*/
|
||||
fun importedWalletBirthday(importedHeight: BlockHeight?): Config = apply {
|
||||
birthdayHeight = importedHeight
|
||||
defaultToOldestHeight = true
|
||||
}
|
||||
|
||||
//
|
||||
// Viewing key functions
|
||||
//
|
||||
|
||||
/**
|
||||
* Add viewing keys to the set of accounts to monitor. Note: Using more than one viewing key
|
||||
* is not currently well supported. Consider it an alpha-preview feature that might work but
|
||||
* probably has serious bugs.
|
||||
*/
|
||||
fun setViewingKeys(
|
||||
vararg unifiedViewingKeys: UnifiedViewingKey,
|
||||
overwrite: Boolean = false
|
||||
): Config = apply {
|
||||
overwriteVks = overwrite
|
||||
viewingKeys.apply {
|
||||
clear()
|
||||
addAll(unifiedViewingKeys)
|
||||
}
|
||||
}
|
||||
|
||||
fun setOverwriteKeys(isOverwrite: Boolean) {
|
||||
overwriteVks = isOverwrite
|
||||
}
|
||||
|
||||
/**
|
||||
* Add viewing key to the set of accounts to monitor. Note: Using more than one viewing key
|
||||
* is not currently well supported. Consider it an alpha-preview feature that might work but
|
||||
* probably has serious bugs.
|
||||
*/
|
||||
fun addViewingKey(unifiedFullViewingKey: UnifiedViewingKey): Config = apply {
|
||||
viewingKeys.add(unifiedFullViewingKey)
|
||||
}
|
||||
|
||||
//
|
||||
// Convenience functions
|
||||
//
|
||||
|
||||
/**
|
||||
* Set the server and the network property at the same time to prevent them from getting out
|
||||
* of sync. Ultimately, this determines which host a synchronizer will use in order to
|
||||
* connect to lightwalletd.
|
||||
*
|
||||
* @param network the Zcash network to use. Either testnet or mainnet.
|
||||
* @param lightWalletEndpoint the light wallet endpoint to use.
|
||||
*/
|
||||
fun setNetwork(
|
||||
network: ZcashNetwork,
|
||||
lightWalletEndpoint: LightWalletEndpoint
|
||||
): Config = apply {
|
||||
this.network = network
|
||||
this.lightWalletEndpoint = lightWalletEndpoint
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a wallet using the first viewing key derived from the given seed.
|
||||
*/
|
||||
suspend fun importWallet(
|
||||
seed: ByteArray,
|
||||
birthday: BlockHeight?,
|
||||
network: ZcashNetwork,
|
||||
lightWalletEndpoint: LightWalletEndpoint,
|
||||
alias: String = ZcashSdk.DEFAULT_ALIAS
|
||||
): Config =
|
||||
importWallet(
|
||||
DerivationTool.deriveUnifiedViewingKeys(seed, network = network)[0],
|
||||
birthday,
|
||||
network,
|
||||
lightWalletEndpoint,
|
||||
alias
|
||||
)
|
||||
|
||||
/**
|
||||
* Default function for importing a wallet.
|
||||
*/
|
||||
fun importWallet(
|
||||
viewingKey: UnifiedViewingKey,
|
||||
birthday: BlockHeight?,
|
||||
network: ZcashNetwork,
|
||||
lightWalletEndpoint: LightWalletEndpoint,
|
||||
alias: String = ZcashSdk.DEFAULT_ALIAS
|
||||
): Config = apply {
|
||||
setViewingKeys(viewingKey)
|
||||
setNetwork(network, lightWalletEndpoint)
|
||||
importedWalletBirthday(birthday)
|
||||
this.alias = alias
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new wallet using the first viewing key derived from the given seed.
|
||||
*/
|
||||
suspend fun newWallet(
|
||||
seed: ByteArray,
|
||||
network: ZcashNetwork,
|
||||
lightWalletEndpoint: LightWalletEndpoint,
|
||||
alias: String = ZcashSdk.DEFAULT_ALIAS
|
||||
): Config = newWallet(
|
||||
DerivationTool.deriveUnifiedViewingKeys(seed, network)[0],
|
||||
network,
|
||||
lightWalletEndpoint,
|
||||
alias
|
||||
)
|
||||
|
||||
/**
|
||||
* Default function for creating a new wallet.
|
||||
*/
|
||||
fun newWallet(
|
||||
viewingKey: UnifiedViewingKey,
|
||||
network: ZcashNetwork,
|
||||
lightWalletEndpoint: LightWalletEndpoint,
|
||||
alias: String = ZcashSdk.DEFAULT_ALIAS
|
||||
): Config = apply {
|
||||
setViewingKeys(viewingKey)
|
||||
setNetwork(network, lightWalletEndpoint)
|
||||
newWalletBirthday()
|
||||
this.alias = alias
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for setting thew viewingKeys from a given seed. This is the same as
|
||||
* calling `setViewingKeys` with the keys that match this seed.
|
||||
*/
|
||||
suspend fun setSeed(
|
||||
seed: ByteArray,
|
||||
network: ZcashNetwork,
|
||||
numberOfAccounts: Int = 1
|
||||
): Config =
|
||||
apply {
|
||||
@Suppress("SpreadOperator")
|
||||
setViewingKeys(
|
||||
*DerivationTool.deriveUnifiedViewingKeys(
|
||||
seed,
|
||||
network,
|
||||
numberOfAccounts
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the network from a network id, throwing an exception if the id is not recognized.
|
||||
*
|
||||
* @param networkId the ID of the network corresponding to the [ZcashNetwork] enum.
|
||||
* Typically, it is 0 for testnet and 1 for mainnet.
|
||||
*/
|
||||
fun setNetworkId(networkId: Int): Config = apply {
|
||||
network = ZcashNetwork.from(networkId)
|
||||
}
|
||||
|
||||
//
|
||||
// Validation helpers
|
||||
//
|
||||
|
||||
fun validate(): Config = apply {
|
||||
validateAlias(alias)
|
||||
validateViewingKeys()
|
||||
validateBirthday()
|
||||
}
|
||||
|
||||
private fun validateBirthday() {
|
||||
// if birthday is missing then we need to know how to interpret it
|
||||
// so defaultToOldestHeight ought to be set, in that case
|
||||
if (birthdayHeight == null && defaultToOldestHeight == null) {
|
||||
throw InitializerException.MissingDefaultBirthdayException
|
||||
}
|
||||
// allow either null or a value greater than the activation height
|
||||
if (
|
||||
(birthdayHeight?.value ?: network.saplingActivationHeight.value)
|
||||
< network.saplingActivationHeight.value
|
||||
) {
|
||||
throw InitializerException.InvalidBirthdayHeightException(birthdayHeight, network)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateViewingKeys() {
|
||||
require(viewingKeys.isNotEmpty()) {
|
||||
"Unified Viewing keys are required. Ensure that the unified viewing keys or seed" +
|
||||
" have been set on this Initializer."
|
||||
}
|
||||
viewingKeys.forEach {
|
||||
DerivationTool.validateUnifiedViewingKey(it)
|
||||
}
|
||||
}
|
||||
|
||||
companion object
|
||||
}
|
||||
|
||||
companion object : SdkSynchronizer.Erasable {
|
||||
|
||||
suspend fun new(appContext: Context, config: Config) = new(appContext, null, config)
|
||||
|
||||
fun newBlocking(appContext: Context, config: Config) = runBlocking {
|
||||
new(
|
||||
appContext,
|
||||
null,
|
||||
config
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun new(
|
||||
appContext: Context,
|
||||
onCriticalErrorHandler: ((Throwable?) -> Boolean)? = null,
|
||||
block: (Config) -> Unit
|
||||
) = new(appContext, onCriticalErrorHandler, Config(block))
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
suspend fun new(
|
||||
context: Context,
|
||||
onCriticalErrorHandler: ((Throwable?) -> Boolean)?,
|
||||
config: Config
|
||||
): Initializer {
|
||||
config.validate()
|
||||
|
||||
val loadedCheckpoint = run {
|
||||
val height = config.birthdayHeight
|
||||
?: if (config.defaultToOldestHeight == true) {
|
||||
config.network.saplingActivationHeight
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
CheckpointTool.loadNearest(
|
||||
context,
|
||||
config.network,
|
||||
height
|
||||
)
|
||||
}
|
||||
|
||||
val saplingParamTool = SaplingParamTool.new(context.applicationContext)
|
||||
|
||||
val rustBackend =
|
||||
initRustBackend(context, config.network, config.alias, loadedCheckpoint.height, saplingParamTool)
|
||||
|
||||
return Initializer(
|
||||
context.applicationContext,
|
||||
rustBackend,
|
||||
config.network,
|
||||
config.alias,
|
||||
config.lightWalletEndpoint,
|
||||
config.viewingKeys,
|
||||
config.overwriteVks,
|
||||
loadedCheckpoint,
|
||||
saplingParamTool
|
||||
)
|
||||
}
|
||||
|
||||
private fun onCriticalError(onCriticalErrorHandler: ((Throwable?) -> Boolean)?, error: Throwable) {
|
||||
twig("********")
|
||||
twig("******** INITIALIZER ERROR: $error")
|
||||
if (error.cause != null) twig("******** caused by ${error.cause}")
|
||||
if (error.cause?.cause != null) twig("******** caused by ${error.cause?.cause}")
|
||||
twig("********")
|
||||
twig(error)
|
||||
|
||||
if (onCriticalErrorHandler == null) {
|
||||
twig(
|
||||
"WARNING: a critical error occurred on the Initializer but no callback is " +
|
||||
"registered to be notified of critical errors! THIS IS PROBABLY A MISTAKE. To " +
|
||||
"respond to these errors (perhaps to update the UI or alert the user) set " +
|
||||
"initializer.onCriticalErrorHandler to a non-null value or use the secondary " +
|
||||
"constructor: Initializer(context, handler) { ... }. Note that the synchronizer " +
|
||||
"and initializer BOTH have error handlers and since the initializer exists " +
|
||||
"before the synchronizer, it needs its error handler set separately."
|
||||
)
|
||||
}
|
||||
|
||||
onCriticalErrorHandler?.invoke(error)
|
||||
}
|
||||
|
||||
private suspend fun initRustBackend(
|
||||
context: Context,
|
||||
network: ZcashNetwork,
|
||||
alias: String,
|
||||
blockHeight: BlockHeight,
|
||||
saplingParamTool: SaplingParamTool
|
||||
): RustBackend {
|
||||
val coordinator = DatabaseCoordinator.getInstance(context)
|
||||
|
||||
return RustBackend.init(
|
||||
coordinator.cacheDbFile(network, alias),
|
||||
coordinator.dataDbFile(network, alias),
|
||||
saplingParamTool.properties.paramsDirectory,
|
||||
network,
|
||||
blockHeight
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the databases associated with this wallet. This removes all compact blocks and
|
||||
* data derived from those blocks. For most wallets, this should not result in a loss of
|
||||
* funds because the seed and spending keys are stored separately. This call just removes
|
||||
* the associated data but not the seed or spending key, themselves, because those are
|
||||
* managed separately by the wallet.
|
||||
*
|
||||
* @param appContext the application context.
|
||||
* @param network the network associated with the data to be erased.
|
||||
* @param alias the alias used to create the local data.
|
||||
*
|
||||
* @return true when one of the associated files was found. False most likely indicates
|
||||
* that the wrong alias was provided.
|
||||
*/
|
||||
override suspend fun erase(
|
||||
appContext: Context,
|
||||
network: ZcashNetwork,
|
||||
alias: String
|
||||
): Boolean = DatabaseCoordinator.getInstance(appContext).deleteDatabases(network, alias)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the alias doesn't contain malicious characters by enforcing simple rules which
|
||||
* permit the alias to be used as part of a file name for the preferences and databases. This
|
||||
* enables multiple wallets to exist on one device, which is also helpful for sweeping funds.
|
||||
*
|
||||
* @param alias the alias to validate.
|
||||
*
|
||||
* @throws IllegalArgumentException whenever the alias is not less than 100 characters or
|
||||
* contains something other than alphanumeric characters. Underscores are allowed but aliases
|
||||
* must start with a letter.
|
||||
*/
|
||||
internal fun validateAlias(alias: String) {
|
||||
require(
|
||||
alias.length in ZcashSdk.ALIAS_MIN_LENGTH..ZcashSdk.ALIAS_MAX_LENGTH && alias[0].isLetter() &&
|
||||
alias.all { it.isLetterOrDigit() || it == '_' }
|
||||
) {
|
||||
"ERROR: Invalid alias ($alias). For security, the alias must be shorter than 100 " +
|
||||
"characters and only contain letters, digits or underscores and start with a letter."
|
||||
}
|
||||
}
|
|
@ -17,47 +17,52 @@ import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Scanned
|
|||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Scanning
|
||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Stopped
|
||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Validating
|
||||
import cash.z.ecc.android.sdk.db.DatabaseCoordinator
|
||||
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
|
||||
import cash.z.ecc.android.sdk.db.entity.hasRawTransactionId
|
||||
import cash.z.ecc.android.sdk.db.entity.isCancelled
|
||||
import cash.z.ecc.android.sdk.db.entity.isExpired
|
||||
import cash.z.ecc.android.sdk.db.entity.isFailedSubmit
|
||||
import cash.z.ecc.android.sdk.db.entity.isLongExpired
|
||||
import cash.z.ecc.android.sdk.db.entity.isMarkedForDeletion
|
||||
import cash.z.ecc.android.sdk.db.entity.isMined
|
||||
import cash.z.ecc.android.sdk.db.entity.isSafeToDiscard
|
||||
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
|
||||
import cash.z.ecc.android.sdk.db.entity.isSubmitted
|
||||
import cash.z.ecc.android.sdk.exception.SynchronizerException
|
||||
import cash.z.ecc.android.sdk.ext.ConsensusBranchId
|
||||
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
||||
import cash.z.ecc.android.sdk.internal.SaplingParamTool
|
||||
import cash.z.ecc.android.sdk.internal.block.CompactBlockDbStore
|
||||
import cash.z.ecc.android.sdk.internal.block.CompactBlockDownloader
|
||||
import cash.z.ecc.android.sdk.internal.block.CompactBlockStore
|
||||
import cash.z.ecc.android.sdk.internal.db.DatabaseCoordinator
|
||||
import cash.z.ecc.android.sdk.internal.db.block.DbCompactBlockRepository
|
||||
import cash.z.ecc.android.sdk.internal.db.derived.DbDerivedDataRepository
|
||||
import cash.z.ecc.android.sdk.internal.db.derived.DerivedDataDb
|
||||
import cash.z.ecc.android.sdk.internal.ext.toHexReversed
|
||||
import cash.z.ecc.android.sdk.internal.ext.tryNull
|
||||
import cash.z.ecc.android.sdk.internal.isEmpty
|
||||
import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
||||
import cash.z.ecc.android.sdk.internal.repository.CompactBlockRepository
|
||||
import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository
|
||||
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
|
||||
import cash.z.ecc.android.sdk.internal.service.LightWalletService
|
||||
import cash.z.ecc.android.sdk.internal.transaction.OutboundTransactionManager
|
||||
import cash.z.ecc.android.sdk.internal.transaction.PagedTransactionRepository
|
||||
import cash.z.ecc.android.sdk.internal.transaction.PersistentTransactionManager
|
||||
import cash.z.ecc.android.sdk.internal.transaction.TransactionEncoder
|
||||
import cash.z.ecc.android.sdk.internal.transaction.TransactionRepository
|
||||
import cash.z.ecc.android.sdk.internal.transaction.WalletTransactionEncoder
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
import cash.z.ecc.android.sdk.internal.twigTask
|
||||
import cash.z.ecc.android.sdk.jni.RustBackend
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
||||
import cash.z.ecc.android.sdk.model.PendingTransaction
|
||||
import cash.z.ecc.android.sdk.model.TransactionOverview
|
||||
import cash.z.ecc.android.sdk.model.TransactionRecipient
|
||||
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
||||
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 cash.z.ecc.android.sdk.model.isExpired
|
||||
import cash.z.ecc.android.sdk.model.isLongExpired
|
||||
import cash.z.ecc.android.sdk.model.isMarkedForDeletion
|
||||
import cash.z.ecc.android.sdk.model.isMined
|
||||
import cash.z.ecc.android.sdk.model.isSafeToDiscard
|
||||
import cash.z.ecc.android.sdk.model.isSubmitSuccess
|
||||
import cash.z.ecc.android.sdk.type.AddressType
|
||||
import cash.z.ecc.android.sdk.type.AddressType.Shielded
|
||||
import cash.z.ecc.android.sdk.type.AddressType.Transparent
|
||||
import cash.z.ecc.android.sdk.type.AddressType.Unified
|
||||
import cash.z.ecc.android.sdk.type.ConsensusMatchType
|
||||
import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey
|
||||
import cash.z.wallet.sdk.rpc.Service
|
||||
import io.grpc.ManagedChannel
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
|
@ -71,13 +76,13 @@ import kotlinx.coroutines.flow.Flow
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
|
@ -97,9 +102,10 @@ import kotlin.coroutines.EmptyCoroutineContext
|
|||
@FlowPreview
|
||||
@Suppress("TooManyFunctions")
|
||||
class SdkSynchronizer internal constructor(
|
||||
private val storage: TransactionRepository,
|
||||
private val storage: DerivedDataRepository,
|
||||
private val txManager: OutboundTransactionManager,
|
||||
val processor: CompactBlockProcessor
|
||||
val processor: CompactBlockProcessor,
|
||||
private val rustBackend: RustBackend
|
||||
) : Synchronizer {
|
||||
|
||||
// pools
|
||||
|
@ -294,7 +300,7 @@ class SdkSynchronizer internal constructor(
|
|||
processor.stop()
|
||||
twig("Synchronizer::stop: coroutineScope.cancel()")
|
||||
coroutineScope.cancel()
|
||||
twig("Synchronizer::stop: _status.value = STOPPED")
|
||||
twig("Synchronizer::stop: _status.cancel()")
|
||||
_status.value = STOPPED
|
||||
twig("Synchronizer::stop: COMPLETE")
|
||||
}
|
||||
|
@ -318,6 +324,27 @@ class SdkSynchronizer internal constructor(
|
|||
processor.quickRewind()
|
||||
}
|
||||
|
||||
override fun getMemos(transactionOverview: TransactionOverview): Flow<String> {
|
||||
return when (transactionOverview.isSentTransaction) {
|
||||
true -> {
|
||||
val sentNoteIds = storage.getSentNoteIds(transactionOverview.id)
|
||||
|
||||
sentNoteIds.map { rustBackend.getSentMemoAsUtf8(it) }.filterNotNull()
|
||||
}
|
||||
false -> {
|
||||
val receivedNoteIds = storage.getReceivedNoteIds(transactionOverview.id)
|
||||
|
||||
receivedNoteIds.map { rustBackend.getReceivedMemoAsUtf8(it) }.filterNotNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRecipients(transactionOverview: TransactionOverview): Flow<TransactionRecipient> {
|
||||
require(transactionOverview.isSentTransaction) { "Recipients can only be queried for sent transactions" }
|
||||
|
||||
return storage.getRecipients(transactionOverview.id)
|
||||
}
|
||||
|
||||
//
|
||||
// Storage APIs
|
||||
//
|
||||
|
@ -327,7 +354,7 @@ class SdkSynchronizer internal constructor(
|
|||
// TODO [#682]: https://github.com/zcash/zcash-android-wallet-sdk/issues/682
|
||||
|
||||
suspend fun findBlockHash(height: BlockHeight): ByteArray? {
|
||||
return (storage as? PagedTransactionRepository)?.findBlockHash(height)
|
||||
return storage.findBlockHash(height)
|
||||
}
|
||||
|
||||
suspend fun findBlockHashAsHex(height: BlockHeight): String? {
|
||||
|
@ -335,7 +362,7 @@ class SdkSynchronizer internal constructor(
|
|||
}
|
||||
|
||||
suspend fun getTransactionCount(): Int {
|
||||
return (storage as? PagedTransactionRepository)?.getTransactionCount() ?: 0
|
||||
return storage.getTransactionCount().toInt()
|
||||
}
|
||||
|
||||
fun refreshTransactions() {
|
||||
|
@ -365,7 +392,7 @@ class SdkSynchronizer internal constructor(
|
|||
|
||||
suspend fun refreshSaplingBalance() {
|
||||
twig("refreshing sapling balance")
|
||||
_saplingBalances.value = processor.getBalanceInfo()
|
||||
_saplingBalances.value = processor.getBalanceInfo(Account.DEFAULT)
|
||||
}
|
||||
|
||||
suspend fun refreshTransparentBalance() {
|
||||
|
@ -527,7 +554,7 @@ class SdkSynchronizer internal constructor(
|
|||
.forEach { pendingTx ->
|
||||
twig("checking for updates on pendingTx id: ${pendingTx.id}")
|
||||
pendingTx.rawTransactionId?.let { rawId ->
|
||||
storage.findMinedHeight(rawId)?.let { minedHeight ->
|
||||
storage.findMinedHeight(rawId.byteArray)?.let { minedHeight ->
|
||||
twig(
|
||||
"found matching transaction for pending transaction with id" +
|
||||
" ${pendingTx.id} mined at height $minedHeight!"
|
||||
|
@ -537,31 +564,6 @@ class SdkSynchronizer internal constructor(
|
|||
}
|
||||
}
|
||||
|
||||
twig("[cleanup] beginning to cleanup cancelled transactions", -1)
|
||||
var hasCleaned = false
|
||||
// Experimental: cleanup cancelled transactions
|
||||
allPendingTxs.filter { it.isCancelled() && it.hasRawTransactionId() }.let { cancellable ->
|
||||
cancellable.forEachIndexed { index, pendingTx ->
|
||||
twig(
|
||||
"[cleanup] FOUND (${index + 1} of ${cancellable.size})" +
|
||||
" CANCELLED pendingTxId: ${pendingTx.id}"
|
||||
)
|
||||
hasCleaned = hasCleaned || cleanupCancelledTx(pendingTx)
|
||||
}
|
||||
}
|
||||
|
||||
// Experimental: cleanup failed transactions
|
||||
allPendingTxs.filter { it.isSubmitted() && it.isFailedSubmit() && !it.isMarkedForDeletion() }
|
||||
.let { failed ->
|
||||
failed.forEachIndexed { index, pendingTx ->
|
||||
twig(
|
||||
"[cleanup] FOUND (${index + 1} of ${failed.size})" +
|
||||
" FAILED pendingTxId: ${pendingTx.id}"
|
||||
)
|
||||
cleanupCancelledTx(pendingTx)
|
||||
}
|
||||
}
|
||||
|
||||
twig("[cleanup] beginning to cleanup expired transactions", -1)
|
||||
// Experimental: cleanup expired transactions
|
||||
// note: don't delete the pendingTx until the related data has been scrubbed, or else you
|
||||
|
@ -581,38 +583,25 @@ class SdkSynchronizer internal constructor(
|
|||
lastScannedHeight,
|
||||
network.saplingActivationHeight
|
||||
) || it.isSafeToDiscard()
|
||||
}.forEach {
|
||||
val result = txManager.abort(it)
|
||||
twig(
|
||||
"[cleanup] FOUND EXPIRED pendingTX (lastScanHeight: $lastScannedHeight " +
|
||||
" expiryHeight: ${it.expiryHeight}): and ${it.id} " +
|
||||
"${if (result > 0) "successfully removed" else "failed to remove"} it"
|
||||
)
|
||||
}
|
||||
.forEach {
|
||||
val result = txManager.abort(it)
|
||||
twig(
|
||||
"[cleanup] FOUND EXPIRED pendingTX (lastScanHeight: $lastScannedHeight " +
|
||||
" expiryHeight: ${it.expiryHeight}): and ${it.id} " +
|
||||
"${if (result > 0) "successfully removed" else "failed to remove"} it"
|
||||
)
|
||||
}
|
||||
|
||||
twig("[cleanup] deleting expired transactions from storage", -1)
|
||||
val expiredCount = storage.deleteExpired(lastScannedHeight)
|
||||
if (expiredCount > 0) {
|
||||
twig("[cleanup] deleted $expiredCount expired transaction(s)!")
|
||||
}
|
||||
hasCleaned = hasCleaned || (expiredCount > 0)
|
||||
|
||||
if (hasCleaned) {
|
||||
refreshAllBalances()
|
||||
}
|
||||
twig("[cleanup] done refreshing and cleaning up pending transactions", -1)
|
||||
}
|
||||
|
||||
private suspend fun cleanupCancelledTx(pendingTx: PendingTransaction): Boolean {
|
||||
return if (storage.cleanupCancelledTx(pendingTx.rawTransactionId!!)) {
|
||||
txManager.markForDeletion(pendingTx.id)
|
||||
true
|
||||
} else {
|
||||
twig("[cleanup] no matching tx was cleaned so the pendingTx will not be marked for deletion")
|
||||
false
|
||||
}
|
||||
}
|
||||
//
|
||||
// Account management
|
||||
//
|
||||
|
||||
// Not ready to be a public API; internal for testing only
|
||||
internal suspend fun createAccount(seed: ByteArray): UnifiedSpendingKey =
|
||||
processor.createAccount(seed)
|
||||
|
||||
//
|
||||
// Send / Receive
|
||||
|
@ -620,74 +609,60 @@ class SdkSynchronizer internal constructor(
|
|||
|
||||
override suspend fun cancelSpend(pendingId: Long) = txManager.cancel(pendingId)
|
||||
|
||||
override suspend fun getAddress(accountId: Int): String = getShieldedAddress(accountId)
|
||||
/**
|
||||
* Returns the current Unified Address for this account.
|
||||
*/
|
||||
override suspend fun getUnifiedAddress(account: Account): String =
|
||||
processor.getCurrentAddress(account)
|
||||
|
||||
override suspend fun getShieldedAddress(accountId: Int): String =
|
||||
processor.getShieldedAddress(accountId)
|
||||
/**
|
||||
* Returns the legacy Sapling address corresponding to the current Unified Address for this account.
|
||||
*/
|
||||
override suspend fun getSaplingAddress(account: Account): String =
|
||||
processor.getLegacySaplingAddress(account)
|
||||
|
||||
override suspend fun getTransparentAddress(accountId: Int): String =
|
||||
processor.getTransparentAddress(accountId)
|
||||
/**
|
||||
* Returns the legacy transparent address corresponding to the current Unified Address for this account.
|
||||
*/
|
||||
override suspend fun getTransparentAddress(account: Account): String =
|
||||
processor.getTransparentAddress(account)
|
||||
|
||||
override fun sendToAddress(
|
||||
spendingKey: String,
|
||||
override suspend fun sendToAddress(
|
||||
usk: UnifiedSpendingKey,
|
||||
amount: Zatoshi,
|
||||
toAddress: String,
|
||||
memo: String,
|
||||
fromAccountIndex: Int
|
||||
): Flow<PendingTransaction> = flow {
|
||||
twig("Initializing pending transaction")
|
||||
// Emit the placeholder transaction, then switch to monitoring the database
|
||||
txManager.initSpend(amount, toAddress, memo, fromAccountIndex).let { placeHolderTx ->
|
||||
emit(placeHolderTx)
|
||||
txManager.encode(spendingKey, placeHolderTx).let { encodedTx ->
|
||||
// only submit if it wasn't cancelled. Otherwise cleanup, immediately for best UX.
|
||||
if (encodedTx.isCancelled()) {
|
||||
twig("[cleanup] this tx has been cancelled so we will cleanup instead of submitting")
|
||||
if (cleanupCancelledTx(encodedTx)) {
|
||||
refreshAllBalances()
|
||||
}
|
||||
} else {
|
||||
txManager.submit(encodedTx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.flatMapLatest {
|
||||
// switch this flow over to monitoring the database for transactions
|
||||
// so we emit the placeholder TX above, then watch the database for all further updates
|
||||
twig("Monitoring pending transaction (id: ${it.id}) for updates...")
|
||||
txManager.monitorById(it.id)
|
||||
}.distinctUntilChanged()
|
||||
|
||||
override fun shieldFunds(
|
||||
spendingKey: String,
|
||||
transparentSecretKey: String,
|
||||
memo: String
|
||||
): Flow<PendingTransaction> = flow {
|
||||
): Flow<PendingTransaction> {
|
||||
// Emit the placeholder transaction, then switch to monitoring the database
|
||||
val placeHolderTx = txManager.initSpend(amount, TransactionRecipient.Address(toAddress), memo, usk.account)
|
||||
|
||||
txManager.encode(usk, placeHolderTx).let { encodedTx ->
|
||||
txManager.submit(encodedTx)
|
||||
}
|
||||
|
||||
return txManager.monitorById(placeHolderTx.id)
|
||||
}
|
||||
|
||||
override suspend fun shieldFunds(
|
||||
usk: UnifiedSpendingKey,
|
||||
memo: String
|
||||
): Flow<PendingTransaction> {
|
||||
twig("Initializing shielding transaction")
|
||||
val tAddr =
|
||||
DerivationTool.deriveTransparentAddressFromPrivateKey(transparentSecretKey, network)
|
||||
val tAddr = processor.getTransparentAddress(usk.account)
|
||||
val tBalance = processor.getUtxoCacheBalance(tAddr)
|
||||
val zAddr = getAddress(0)
|
||||
|
||||
// Emit the placeholder transaction, then switch to monitoring the database
|
||||
txManager.initSpend(tBalance.available, zAddr, memo, 0).let { placeHolderTx ->
|
||||
emit(placeHolderTx)
|
||||
txManager.encode(spendingKey, transparentSecretKey, placeHolderTx).let { encodedTx ->
|
||||
// only submit if it wasn't cancelled. Otherwise cleanup, immediately for best UX.
|
||||
if (encodedTx.isCancelled()) {
|
||||
twig("[cleanup] this shielding tx has been cancelled so we will cleanup instead of submitting")
|
||||
if (cleanupCancelledTx(encodedTx)) {
|
||||
refreshAllBalances()
|
||||
}
|
||||
} else {
|
||||
txManager.submit(encodedTx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.flatMapLatest {
|
||||
twig("Monitoring shielding transaction (id: ${it.id}) for updates...")
|
||||
txManager.monitorById(it.id)
|
||||
}.distinctUntilChanged()
|
||||
val placeHolderTx = txManager.initSpend(
|
||||
tBalance.available,
|
||||
TransactionRecipient.Account(usk.account),
|
||||
memo,
|
||||
usk.account
|
||||
)
|
||||
val encodedTx = txManager.encode("", usk, placeHolderTx)
|
||||
txManager.submit(encodedTx)
|
||||
|
||||
return txManager.monitorById(placeHolderTx.id)
|
||||
}
|
||||
|
||||
override suspend fun refreshUtxos(tAddr: String, since: BlockHeight): Int? {
|
||||
return processor.refreshUtxos(tAddr, since)
|
||||
|
@ -703,30 +678,23 @@ class SdkSynchronizer internal constructor(
|
|||
override suspend fun isValidTransparentAddr(address: String) =
|
||||
txManager.isValidTransparentAddress(address)
|
||||
|
||||
override suspend fun isValidUnifiedAddr(address: String) =
|
||||
txManager.isValidUnifiedAddress(address)
|
||||
|
||||
override suspend fun validateAddress(address: String): AddressType {
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
return try {
|
||||
if (isValidShieldedAddr(address)) {
|
||||
Shielded
|
||||
} else {
|
||||
} else if (isValidTransparentAddr(address)) {
|
||||
Transparent
|
||||
} else if (isValidUnifiedAddr(address)) {
|
||||
Unified
|
||||
} else {
|
||||
AddressType.Invalid("Not a Zcash address")
|
||||
}
|
||||
} catch (zError: Throwable) {
|
||||
val message = zError.message
|
||||
try {
|
||||
if (isValidTransparentAddr(address)) {
|
||||
Transparent
|
||||
} else {
|
||||
Shielded
|
||||
}
|
||||
} catch (tError: Throwable) {
|
||||
val reason = if (message != tError.message) {
|
||||
"$message and ${tError.message}"
|
||||
} else {
|
||||
message ?: "Invalid"
|
||||
}
|
||||
AddressType.Invalid(reason)
|
||||
}
|
||||
} catch (@Suppress("TooGenericExceptionCaught") error: Throwable) {
|
||||
AddressType.Invalid(error.message ?: "Invalid")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -766,86 +734,102 @@ class SdkSynchronizer internal constructor(
|
|||
*
|
||||
* See the helper methods for generating default values.
|
||||
*/
|
||||
object DefaultSynchronizerFactory {
|
||||
internal object DefaultSynchronizerFactory {
|
||||
|
||||
fun new(
|
||||
repository: TransactionRepository,
|
||||
repository: DerivedDataRepository,
|
||||
txManager: OutboundTransactionManager,
|
||||
processor: CompactBlockProcessor
|
||||
processor: CompactBlockProcessor,
|
||||
rustBackend: RustBackend
|
||||
): Synchronizer {
|
||||
// call the actual constructor now that all dependencies have been injected
|
||||
// alternatively, this entire object graph can be supplied by Dagger
|
||||
// This builder just makes that easier.
|
||||
return SdkSynchronizer(
|
||||
repository,
|
||||
txManager,
|
||||
processor
|
||||
processor,
|
||||
rustBackend
|
||||
)
|
||||
}
|
||||
|
||||
// TODO [#242]: Don't hard code page size. It is a workaround for Uncaught Exception:
|
||||
// android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy
|
||||
// can touch its views. and is probably related to FlowPagedList
|
||||
// TODO [#242]: https://github.com/zcash/zcash-android-wallet-sdk/issues/242
|
||||
private const val DEFAULT_PAGE_SIZE = 1000
|
||||
suspend fun defaultTransactionRepository(initializer: Initializer): TransactionRepository =
|
||||
PagedTransactionRepository.new(
|
||||
initializer.context,
|
||||
initializer.network,
|
||||
DEFAULT_PAGE_SIZE,
|
||||
initializer.rustBackend,
|
||||
initializer.checkpoint,
|
||||
initializer.viewingKeys,
|
||||
initializer.overwriteVks
|
||||
internal suspend fun defaultRustBackend(
|
||||
context: Context,
|
||||
network: ZcashNetwork,
|
||||
alias: String,
|
||||
blockHeight: BlockHeight,
|
||||
saplingParamTool: SaplingParamTool
|
||||
): RustBackend {
|
||||
val coordinator = DatabaseCoordinator.getInstance(context)
|
||||
|
||||
return RustBackend.init(
|
||||
coordinator.cacheDbFile(network, alias),
|
||||
coordinator.dataDbFile(network, alias),
|
||||
saplingParamTool.properties.paramsDirectory,
|
||||
network,
|
||||
blockHeight
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal suspend fun defaultDerivedDataRepository(
|
||||
context: Context,
|
||||
rustBackend: RustBackend,
|
||||
zcashNetwork: ZcashNetwork,
|
||||
checkpoint: Checkpoint,
|
||||
seed: ByteArray?,
|
||||
viewingKeys: List<UnifiedFullViewingKey>
|
||||
): DerivedDataRepository =
|
||||
DbDerivedDataRepository(DerivedDataDb.new(context, rustBackend, zcashNetwork, checkpoint, seed, viewingKeys))
|
||||
|
||||
internal fun defaultCompactBlockRepository(context: Context, cacheDbFile: File, zcashNetwork: ZcashNetwork):
|
||||
CompactBlockRepository =
|
||||
DbCompactBlockRepository.new(
|
||||
context,
|
||||
zcashNetwork,
|
||||
cacheDbFile
|
||||
)
|
||||
|
||||
fun defaultBlockStore(initializer: Initializer): CompactBlockStore =
|
||||
CompactBlockDbStore.new(
|
||||
initializer.context,
|
||||
initializer.network,
|
||||
initializer.rustBackend.cacheDbFile
|
||||
)
|
||||
|
||||
fun defaultService(initializer: Initializer): LightWalletService =
|
||||
LightWalletGrpcService.new(initializer.context, initializer.lightWalletEndpoint)
|
||||
fun defaultService(context: Context, lightWalletEndpoint: LightWalletEndpoint): LightWalletService =
|
||||
LightWalletGrpcService.new(context, lightWalletEndpoint)
|
||||
|
||||
internal fun defaultEncoder(
|
||||
initializer: Initializer,
|
||||
rustBackend: RustBackend,
|
||||
saplingParamTool: SaplingParamTool,
|
||||
repository: TransactionRepository
|
||||
): TransactionEncoder = WalletTransactionEncoder(initializer.rustBackend, saplingParamTool, repository)
|
||||
repository: DerivedDataRepository
|
||||
): TransactionEncoder = WalletTransactionEncoder(rustBackend, saplingParamTool, repository)
|
||||
|
||||
fun defaultDownloader(
|
||||
service: LightWalletService,
|
||||
blockStore: CompactBlockStore
|
||||
blockStore: CompactBlockRepository
|
||||
): CompactBlockDownloader = CompactBlockDownloader(service, blockStore)
|
||||
|
||||
suspend fun defaultTxManager(
|
||||
initializer: Initializer,
|
||||
internal suspend fun defaultTxManager(
|
||||
context: Context,
|
||||
zcashNetwork: ZcashNetwork,
|
||||
alias: String,
|
||||
encoder: TransactionEncoder,
|
||||
service: LightWalletService
|
||||
): OutboundTransactionManager {
|
||||
val databaseFile = DatabaseCoordinator.getInstance(initializer.context).pendingTransactionsDbFile(
|
||||
initializer.network,
|
||||
initializer.alias
|
||||
val databaseFile = DatabaseCoordinator.getInstance(context).pendingTransactionsDbFile(
|
||||
zcashNetwork,
|
||||
alias
|
||||
)
|
||||
|
||||
return PersistentTransactionManager(
|
||||
initializer.context,
|
||||
return PersistentTransactionManager.new(
|
||||
context,
|
||||
zcashNetwork,
|
||||
encoder,
|
||||
service,
|
||||
databaseFile
|
||||
)
|
||||
}
|
||||
|
||||
fun defaultProcessor(
|
||||
initializer: Initializer,
|
||||
internal fun defaultProcessor(
|
||||
rustBackend: RustBackend,
|
||||
downloader: CompactBlockDownloader,
|
||||
repository: TransactionRepository
|
||||
repository: DerivedDataRepository
|
||||
): CompactBlockProcessor = CompactBlockProcessor(
|
||||
downloader,
|
||||
repository,
|
||||
initializer.rustBackend,
|
||||
initializer.rustBackend.birthdayHeight
|
||||
rustBackend,
|
||||
rustBackend.birthdayHeight
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,14 +1,23 @@
|
|||
package cash.z.ecc.android.sdk
|
||||
|
||||
import android.content.Context
|
||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
||||
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
|
||||
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
|
||||
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
||||
import cash.z.ecc.android.sdk.internal.SaplingParamTool
|
||||
import cash.z.ecc.android.sdk.internal.db.DatabaseCoordinator
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
||||
import cash.z.ecc.android.sdk.model.PendingTransaction
|
||||
import cash.z.ecc.android.sdk.model.Transaction
|
||||
import cash.z.ecc.android.sdk.model.TransactionOverview
|
||||
import cash.z.ecc.android.sdk.model.TransactionRecipient
|
||||
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
||||
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.CheckpointTool
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import cash.z.ecc.android.sdk.type.AddressType
|
||||
import cash.z.ecc.android.sdk.type.ConsensusMatchType
|
||||
import cash.z.wallet.sdk.rpc.Service
|
||||
|
@ -129,17 +138,17 @@ interface Synchronizer {
|
|||
/**
|
||||
* A flow of all the transactions that are on the blockchain.
|
||||
*/
|
||||
val clearedTransactions: Flow<List<ConfirmedTransaction>>
|
||||
val clearedTransactions: Flow<List<TransactionOverview>>
|
||||
|
||||
/**
|
||||
* A flow of all transactions related to sending funds.
|
||||
*/
|
||||
val sentTransactions: Flow<List<ConfirmedTransaction>>
|
||||
val sentTransactions: Flow<List<Transaction.Sent>>
|
||||
|
||||
/**
|
||||
* A flow of all transactions related to receiving funds.
|
||||
*/
|
||||
val receivedTransactions: Flow<List<ConfirmedTransaction>>
|
||||
val receivedTransactions: Flow<List<Transaction.Received>>
|
||||
|
||||
//
|
||||
// Latest Properties
|
||||
|
@ -161,61 +170,84 @@ interface Synchronizer {
|
|||
//
|
||||
|
||||
/**
|
||||
* Gets the shielded address for the given account. This is syntactic sugar for
|
||||
* [getShieldedAddress] because we use z-addrs by default.
|
||||
* Adds the next available account-level spend authority, given the current set of
|
||||
* [ZIP 316](https://zips.z.cash/zip-0316) account identifiers known, to the wallet
|
||||
* database.
|
||||
*
|
||||
* @param accountId the optional accountId whose address is of interest. By default, the first
|
||||
* account is used.
|
||||
* The caller should store the byte encoding of the returned spending key in a secure
|
||||
* fashion. This encoding **MUST NOT** be exposed to users. It is an internal encoding
|
||||
* that is inherently unstable, and only intended to be passed between the SDK and the
|
||||
* storage backend. The caller **MUST NOT** allow this encoding to be exported or
|
||||
* imported.
|
||||
*
|
||||
* @return the shielded address for the given account.
|
||||
* If `seed` was imported from a backup and this method is being used to restore a
|
||||
* previous wallet state, you should use this method to add all of the desired
|
||||
* accounts before scanning the chain from the seed's birthday height.
|
||||
*
|
||||
* By convention, wallets should only allow a new account to be generated after funds
|
||||
* have been received by the currently-available account (in order to enable
|
||||
* automated account recovery).
|
||||
*
|
||||
* @param seed the wallet's seed phrase.
|
||||
*
|
||||
* @return the newly created ZIP 316 account identifier, along with the binary
|
||||
* encoding of the `UnifiedSpendingKey` for the newly created account.
|
||||
*/
|
||||
suspend fun getAddress(accountId: Int = 0) = getShieldedAddress(accountId)
|
||||
// This is not yet ready to be a public API
|
||||
// suspend fun createAccount(seed: ByteArray): UnifiedSpendingKey
|
||||
|
||||
/**
|
||||
* Gets the shielded address for the given account.
|
||||
* Gets the current unified address for the given account.
|
||||
*
|
||||
* @param accountId the optional accountId whose address is of interest. By default, the first
|
||||
* account is used.
|
||||
*
|
||||
* @return the shielded address for the given account.
|
||||
* @return the current unified address for the given account.
|
||||
*/
|
||||
suspend fun getShieldedAddress(accountId: Int = 0): String
|
||||
suspend fun getUnifiedAddress(account: Account = Account.DEFAULT): String
|
||||
|
||||
/**
|
||||
* Gets the transparent address for the given account.
|
||||
* Gets the legacy Sapling address corresponding to the current unified address for the given account.
|
||||
*
|
||||
* @param accountId the optional accountId whose address is of interest. By default, the first
|
||||
* @param account the optional accountId whose address is of interest. By default, the first
|
||||
* account is used.
|
||||
*
|
||||
* @return the address for the given account.
|
||||
* @return a legacy Sapling address for the given account.
|
||||
*/
|
||||
suspend fun getTransparentAddress(accountId: Int = 0): String
|
||||
suspend fun getSaplingAddress(account: Account = Account.DEFAULT): String
|
||||
|
||||
/**
|
||||
* Gets the legacy transparent address corresponding to the current unified address for the given account.
|
||||
*
|
||||
* @param account the optional accountId whose address is of interest. By default, the first
|
||||
* account is used.
|
||||
*
|
||||
* @return a legacy transparent address for the given account.
|
||||
*/
|
||||
suspend fun getTransparentAddress(account: Account = Account.DEFAULT): String
|
||||
|
||||
/**
|
||||
* Sends zatoshi.
|
||||
*
|
||||
* @param spendingKey the key associated with the notes that will be spent.
|
||||
* @param usk the unified spending key associated with the notes that will be spent.
|
||||
* @param zatoshi the amount of zatoshi to send.
|
||||
* @param toAddress the recipient's address.
|
||||
* @param memo the optional memo to include as part of the transaction.
|
||||
* @param fromAccountIndex the optional account id to use. By default, the first account is used.
|
||||
*
|
||||
* @return a flow of PendingTransaction objects representing changes to the state of the
|
||||
* transaction. Any time the state changes a new instance will be emitted by this flow. This is
|
||||
* useful for updating the UI without needing to poll. Of course, polling is always an option
|
||||
* for any wallet that wants to ignore this return value.
|
||||
*/
|
||||
fun sendToAddress(
|
||||
spendingKey: String,
|
||||
suspend fun sendToAddress(
|
||||
usk: UnifiedSpendingKey,
|
||||
amount: Zatoshi,
|
||||
toAddress: String,
|
||||
memo: String = "",
|
||||
fromAccountIndex: Int = 0
|
||||
memo: String = ""
|
||||
): Flow<PendingTransaction>
|
||||
|
||||
fun shieldFunds(
|
||||
spendingKey: String,
|
||||
transparentSecretKey: String,
|
||||
suspend fun shieldFunds(
|
||||
usk: UnifiedSpendingKey,
|
||||
memo: String = ZcashSdk.DEFAULT_SHIELD_FUNDS_MEMO_PREFIX
|
||||
): Flow<PendingTransaction>
|
||||
|
||||
|
@ -243,6 +275,20 @@ interface Synchronizer {
|
|||
*/
|
||||
suspend fun isValidTransparentAddr(address: String): Boolean
|
||||
|
||||
/**
|
||||
* Returns true when the given address is a valid ZIP 316 unified address.
|
||||
*
|
||||
* This method is intended for type checking (e.g. form validation). Invalid
|
||||
* addresses will throw an exception.
|
||||
*
|
||||
* @param address the address to validate.
|
||||
*
|
||||
* @return true when the given address is a valid unified address.
|
||||
*
|
||||
* @throws RuntimeException when the address is invalid.
|
||||
*/
|
||||
suspend fun isValidUnifiedAddr(address: String): Boolean
|
||||
|
||||
/**
|
||||
* Validate whether the server and this SDK share the same consensus branch. This is
|
||||
* particularly important to check around network updates so that any wallet that's connected to
|
||||
|
@ -258,10 +304,11 @@ interface Synchronizer {
|
|||
|
||||
/**
|
||||
* Validates the given address, returning information about why it is invalid. This is a
|
||||
* convenience method that combines the behavior of [isValidShieldedAddr] and
|
||||
* [isValidTransparentAddr] into one call so that the developer doesn't have to worry about
|
||||
* handling the exceptions that they throw. Rather, exceptions are converted to
|
||||
* [AddressType.Invalid] which has a `reason` property describing why it is invalid.
|
||||
* convenience method that combines the behavior of [isValidShieldedAddr],
|
||||
* [isValidTransparentAddr], and [isValidUnifiedAddr] into one call so that the developer
|
||||
* doesn't have to worry about handling the exceptions that they throw. Rather, exceptions
|
||||
* are converted to [AddressType.Invalid] which has a `reason` property describing why it is
|
||||
* invalid.
|
||||
*
|
||||
* @param address the address to validate.
|
||||
*
|
||||
|
@ -313,6 +360,16 @@ interface Synchronizer {
|
|||
|
||||
suspend fun quickRewind()
|
||||
|
||||
/**
|
||||
* Returns a list of memos for a transaction.
|
||||
*/
|
||||
fun getMemos(transactionOverview: TransactionOverview): Flow<String>
|
||||
|
||||
/**
|
||||
* Returns a list of recipients for a transaction.
|
||||
*/
|
||||
fun getRecipients(transactionOverview: TransactionOverview): Flow<TransactionRecipient>
|
||||
|
||||
//
|
||||
// Error Handling
|
||||
//
|
||||
|
@ -423,32 +480,97 @@ interface Synchronizer {
|
|||
/**
|
||||
* Primary method that SDK clients will use to construct a synchronizer.
|
||||
*
|
||||
* @param zcashNetwork the network to use.
|
||||
* @param alias A string used to segregate multiple wallets in the filesystem. This implies the string
|
||||
* should not contain characters unsuitable for the platform's filesystem. The default value is
|
||||
* generally used unless an SDK client needs to support multiple wallets.
|
||||
* @param lightWalletEndpoint Server endpoint. See [cash.z.ecc.android.sdk.model.defaultForNetwork]. If a
|
||||
* client wishes to change the server endpoint, the active synchronizer will need to be stopped and a new
|
||||
* instance created with a new value.
|
||||
* @param seed the wallet's seed phrase. This is required the first time a new wallet is set up. For
|
||||
* subsequent calls, seed is only needed if [InitializerException.SeedRequired] is thrown.
|
||||
* @param birthday Block height representing the "birthday" of the wallet. When creating a new wallet, see
|
||||
* [BlockHeight.ofLatestCheckpoint]. When restoring an existing wallet, use block height that was first used
|
||||
* to create the wallet. If that value is unknown, null is acceptable but will result in longer
|
||||
* sync times. After sync completes, the birthday can be determined from [Synchronizer.latestBirthdayHeight].
|
||||
* @throws InitializerException.SeedRequired Indicates clients need to call this method again, providing the
|
||||
* seed bytes.
|
||||
*/
|
||||
/*
|
||||
* If customized initialization is required (e.g. for dependency injection or testing), see
|
||||
* [DefaultSynchronizerFactory].
|
||||
*
|
||||
* @param initializer the helper that is leveraged for creating all the components that the
|
||||
* Synchronizer requires. It contains all information necessary to build a synchronizer and it is
|
||||
* mainly responsible for initializing the databases associated with this synchronizer and loading
|
||||
* the rust backend.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
suspend fun new(
|
||||
initializer: Initializer
|
||||
context: Context,
|
||||
zcashNetwork: ZcashNetwork,
|
||||
alias: String = ZcashSdk.DEFAULT_ALIAS,
|
||||
lightWalletEndpoint: LightWalletEndpoint,
|
||||
seed: ByteArray?,
|
||||
birthday: BlockHeight?
|
||||
): Synchronizer {
|
||||
val saplingParamTool = SaplingParamTool.new(initializer.context)
|
||||
val repository = DefaultSynchronizerFactory.defaultTransactionRepository(initializer)
|
||||
val blockStore = DefaultSynchronizerFactory.defaultBlockStore(initializer)
|
||||
val service = DefaultSynchronizerFactory.defaultService(initializer)
|
||||
val encoder = DefaultSynchronizerFactory.defaultEncoder(initializer, saplingParamTool, repository)
|
||||
val applicationContext = context.applicationContext
|
||||
|
||||
validateAlias(alias)
|
||||
|
||||
val saplingParamTool = SaplingParamTool.new(applicationContext)
|
||||
|
||||
val loadedCheckpoint = CheckpointTool.loadNearest(
|
||||
applicationContext,
|
||||
zcashNetwork,
|
||||
birthday ?: zcashNetwork.saplingActivationHeight
|
||||
)
|
||||
|
||||
val coordinator = DatabaseCoordinator.getInstance(context)
|
||||
|
||||
val rustBackend = DefaultSynchronizerFactory.defaultRustBackend(
|
||||
applicationContext,
|
||||
zcashNetwork,
|
||||
alias,
|
||||
loadedCheckpoint.height,
|
||||
saplingParamTool
|
||||
)
|
||||
|
||||
val blockStore = DefaultSynchronizerFactory.defaultCompactBlockRepository(
|
||||
applicationContext,
|
||||
coordinator.cacheDbFile(zcashNetwork, alias),
|
||||
zcashNetwork
|
||||
)
|
||||
|
||||
val viewingKeys = seed?.let {
|
||||
DerivationTool.deriveUnifiedFullViewingKeys(
|
||||
seed,
|
||||
zcashNetwork,
|
||||
1
|
||||
).toList()
|
||||
} ?: emptyList()
|
||||
|
||||
val repository = DefaultSynchronizerFactory.defaultDerivedDataRepository(
|
||||
applicationContext,
|
||||
rustBackend,
|
||||
zcashNetwork,
|
||||
loadedCheckpoint,
|
||||
seed,
|
||||
viewingKeys
|
||||
)
|
||||
|
||||
val service = DefaultSynchronizerFactory.defaultService(applicationContext, lightWalletEndpoint)
|
||||
val encoder = DefaultSynchronizerFactory.defaultEncoder(rustBackend, saplingParamTool, repository)
|
||||
val downloader = DefaultSynchronizerFactory.defaultDownloader(service, blockStore)
|
||||
val txManager =
|
||||
DefaultSynchronizerFactory.defaultTxManager(initializer, encoder, service)
|
||||
val processor =
|
||||
DefaultSynchronizerFactory.defaultProcessor(initializer, downloader, repository)
|
||||
val txManager = DefaultSynchronizerFactory.defaultTxManager(
|
||||
applicationContext,
|
||||
zcashNetwork,
|
||||
alias,
|
||||
encoder,
|
||||
service
|
||||
)
|
||||
val processor = DefaultSynchronizerFactory.defaultProcessor(rustBackend, downloader, repository)
|
||||
|
||||
return SdkSynchronizer(
|
||||
repository,
|
||||
txManager,
|
||||
processor
|
||||
processor,
|
||||
rustBackend
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -459,6 +581,57 @@ interface Synchronizer {
|
|||
* This is a blocking call, so it should not be called from the main thread.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun newBlocking(initializer: Initializer): Synchronizer = runBlocking { new(initializer) }
|
||||
@Suppress("LongParameterList")
|
||||
fun newBlocking(
|
||||
context: Context,
|
||||
zcashNetwork: ZcashNetwork,
|
||||
alias: String = ZcashSdk.DEFAULT_ALIAS,
|
||||
lightWalletEndpoint: LightWalletEndpoint,
|
||||
seed: ByteArray?,
|
||||
birthday: BlockHeight?
|
||||
): Synchronizer = runBlocking {
|
||||
new(context, zcashNetwork, alias, lightWalletEndpoint, seed, birthday)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the databases associated with this wallet. This removes all compact blocks and
|
||||
* data derived from those blocks. Although most data can be regenerated by setting up a new
|
||||
* Synchronizer instance with the seed, there are two special cases where data is not retained:
|
||||
* 1. Outputs created with a `null` OVK
|
||||
* 2. The UA to which a transaction was sent (recovery from seed will only reveal the receiver, not the full UA)
|
||||
*
|
||||
* @param appContext the application context.
|
||||
* @param network the network associated with the data to be erased.
|
||||
* @param alias the alias used to create the local data.
|
||||
*
|
||||
* @return true when one of the associated files was found. False most likely indicates
|
||||
* that the wrong alias was provided.
|
||||
*/
|
||||
suspend fun erase(
|
||||
appContext: Context,
|
||||
network: ZcashNetwork,
|
||||
alias: String = ZcashSdk.DEFAULT_ALIAS
|
||||
): Boolean = DatabaseCoordinator.getInstance(appContext).deleteDatabases(network, alias)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the alias doesn't contain malicious characters by enforcing simple rules which
|
||||
* permit the alias to be used as part of a file name for the preferences and databases. This
|
||||
* enables multiple wallets to exist on one device, which is also helpful for sweeping funds.
|
||||
*
|
||||
* @param alias the alias to validate.
|
||||
*
|
||||
* @throws IllegalArgumentException whenever the alias is not less than 100 characters or
|
||||
* contains something other than alphanumeric characters. Underscores are allowed but aliases
|
||||
* must start with a letter.
|
||||
*/
|
||||
private fun validateAlias(alias: String) {
|
||||
require(
|
||||
alias.length in ZcashSdk.ALIAS_MIN_LENGTH..ZcashSdk.ALIAS_MAX_LENGTH && alias[0].isLetter() &&
|
||||
alias.all { it.isLetterOrDigit() || it == '_' }
|
||||
) {
|
||||
"ERROR: Invalid alias ($alias). For security, the alias must be shorter than 100 " +
|
||||
"characters and only contain letters, digits or underscores and start with a letter."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,13 +11,12 @@ import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Scanned
|
|||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Scanning
|
||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Stopped
|
||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Validating
|
||||
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
|
||||
import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException
|
||||
import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException.EnhanceTransactionError.EnhanceTxDecryptError
|
||||
import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException.EnhanceTransactionError.EnhanceTxDownloadError
|
||||
import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException.MismatchedBranch
|
||||
import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException.MismatchedNetwork
|
||||
import cash.z.ecc.android.sdk.exception.InitializerException
|
||||
import cash.z.ecc.android.sdk.exception.InitializeException
|
||||
import cash.z.ecc.android.sdk.exception.RustLayerException
|
||||
import cash.z.ecc.android.sdk.ext.BatchMetrics
|
||||
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
||||
|
@ -34,13 +33,15 @@ import cash.z.ecc.android.sdk.internal.ext.retryUpTo
|
|||
import cash.z.ecc.android.sdk.internal.ext.retryWithBackoff
|
||||
import cash.z.ecc.android.sdk.internal.ext.toHexReversed
|
||||
import cash.z.ecc.android.sdk.internal.isEmpty
|
||||
import cash.z.ecc.android.sdk.internal.transaction.PagedTransactionRepository
|
||||
import cash.z.ecc.android.sdk.internal.transaction.TransactionRepository
|
||||
import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
import cash.z.ecc.android.sdk.internal.twigTask
|
||||
import cash.z.ecc.android.sdk.jni.RustBackend
|
||||
import cash.z.ecc.android.sdk.jni.RustBackendWelding
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.TransactionOverview
|
||||
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||
import cash.z.wallet.sdk.rpc.Service
|
||||
import io.grpc.StatusRuntimeException
|
||||
|
@ -49,7 +50,6 @@ import kotlinx.coroutines.Dispatchers.IO
|
|||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
@ -79,7 +79,7 @@ import kotlin.time.Duration.Companion.days
|
|||
@Suppress("TooManyFunctions", "LargeClass")
|
||||
class CompactBlockProcessor internal constructor(
|
||||
val downloader: CompactBlockDownloader,
|
||||
private val repository: TransactionRepository,
|
||||
private val repository: DerivedDataRepository,
|
||||
private val rustBackend: RustBackendWelding,
|
||||
minimumHeight: BlockHeight = rustBackend.network.saplingActivationHeight
|
||||
) {
|
||||
|
@ -190,6 +190,7 @@ class CompactBlockProcessor internal constructor(
|
|||
/**
|
||||
* Download compact blocks, verify and scan them until [stop] is called.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
suspend fun start() = withContext(IO) {
|
||||
verifySetup()
|
||||
updateBirthdayHeight()
|
||||
|
@ -223,13 +224,15 @@ class CompactBlockProcessor internal constructor(
|
|||
consecutiveChainErrors.set(0)
|
||||
val napTime = calculatePollInterval()
|
||||
twig(
|
||||
"$summary${if (result == BlockProcessingResult.FailedEnhance) {
|
||||
"$summary${
|
||||
if (result == BlockProcessingResult.FailedEnhance) {
|
||||
" (but there were" +
|
||||
" enhancement errors! We ignore those, for now. Memos in this block range are" +
|
||||
" probably missing! This will be improved in a future release.)"
|
||||
} else {
|
||||
""
|
||||
}}! Sleeping" +
|
||||
}
|
||||
}! Sleeping" +
|
||||
" for ${napTime}ms (latest height: ${currentInfo.networkBlockHeight})."
|
||||
)
|
||||
delay(napTime)
|
||||
|
@ -403,7 +406,7 @@ class CompactBlockProcessor internal constructor(
|
|||
} else {
|
||||
twig("enhancing ${newTxs.size} transaction(s)!")
|
||||
// if the first transaction has been added
|
||||
if (newTxs.size == repository.count()) {
|
||||
if (newTxs.size.toLong() == repository.getTransactionCount()) {
|
||||
twig("Encountered the first transaction. This changes the birthday height!")
|
||||
updateBirthdayHeight()
|
||||
}
|
||||
|
@ -421,30 +424,31 @@ class CompactBlockProcessor internal constructor(
|
|||
Twig.clip("enhancing")
|
||||
}
|
||||
}
|
||||
|
||||
// TODO [#683]: we still need a way to identify those transactions that failed to be enhanced
|
||||
// TODO [#683]: https://github.com/zcash/zcash-android-wallet-sdk/issues/683
|
||||
|
||||
private suspend fun enhance(transaction: ConfirmedTransaction) = withContext(Dispatchers.IO) {
|
||||
var downloaded = false
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
try {
|
||||
twig("START: enhancing transaction (id:${transaction.id} block:${transaction.minedHeight})")
|
||||
downloader.fetchTransaction(transaction.rawTransactionId)?.let { tx ->
|
||||
downloaded = true
|
||||
twig("decrypting and storing transaction (id:${transaction.id} block:${transaction.minedHeight})")
|
||||
rustBackend.decryptAndStoreTransaction(tx.data.toByteArray())
|
||||
} ?: twig("no transaction found. Nothing to enhance. This probably shouldn't happen.")
|
||||
twig("DONE: enhancing transaction (id:${transaction.id} block:${transaction.minedHeight})")
|
||||
} catch (t: Throwable) {
|
||||
twig("Warning: failure on transaction: error: $t\ttransaction: $transaction")
|
||||
onProcessorError(
|
||||
if (downloaded) {
|
||||
EnhanceTxDecryptError(transaction.minedBlockHeight, t)
|
||||
} else {
|
||||
EnhanceTxDownloadError(transaction.minedBlockHeight, t)
|
||||
private suspend fun enhance(transaction: TransactionOverview) = withContext(Dispatchers.IO) {
|
||||
enhanceHelper(transaction.id, transaction.rawId.byteArray, transaction.minedHeight)
|
||||
}
|
||||
|
||||
private suspend fun enhanceHelper(id: Long, rawTransactionId: ByteArray, minedHeight: BlockHeight) {
|
||||
twig("START: enhancing transaction (id:$id block:$minedHeight)")
|
||||
|
||||
runCatching {
|
||||
downloader.fetchTransaction(rawTransactionId)
|
||||
}.onSuccess { tx ->
|
||||
tx?.let {
|
||||
runCatching {
|
||||
twig("decrypting and storing transaction (id:$id block:$minedHeight)")
|
||||
rustBackend.decryptAndStoreTransaction(it.data.toByteArray())
|
||||
}.onSuccess {
|
||||
twig("DONE: enhancing transaction (id:$id block:$minedHeight)")
|
||||
}.onFailure { error ->
|
||||
onProcessorError(EnhanceTxDecryptError(minedHeight, error))
|
||||
}
|
||||
)
|
||||
} ?: twig("no transaction found. Nothing to enhance. This probably shouldn't happen.")
|
||||
}.onFailure { error ->
|
||||
onProcessorError(EnhanceTxDownloadError(minedHeight, error))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -546,8 +550,15 @@ class CompactBlockProcessor internal constructor(
|
|||
): Int = withContext(IO) {
|
||||
var skipped = 0
|
||||
val aboveHeight = startHeight
|
||||
twig("Clearing utxos above height $aboveHeight", -1)
|
||||
rustBackend.clearUtxos(tAddress, aboveHeight)
|
||||
// TODO(str4d): We no longer clear UTXOs here, as rustBackend.putUtxo now uses an upsert instead of an insert.
|
||||
// This means that now-spent UTXOs would previously have been deleted, but now are left in the database (like
|
||||
// shielded notes). Due to the fact that the lightwalletd query only returns _current_ UTXOs, we don't learn
|
||||
// about recently-spent UTXOs here, so the transparent balance does not get updated here. Instead, when a
|
||||
// received shielded note is "enhanced" by downloading the full transaction, we mark any UTXOs spent in that
|
||||
// transaction as spent in the database. This relies on two current properties: UTXOs are only ever spent in
|
||||
// shielding transactions, and at least one shielded note from each shielding transaction is always enhanced.
|
||||
// However, for greater reliability, we may want to alter the Data Access API to support "inferring spentness"
|
||||
// from what is _not_ returned as a UTXO, or alternatively fetch TXOs from lightwalletd instead of just UTXOs.
|
||||
twig("Checking for UTXOs above height $aboveHeight")
|
||||
result.forEach { utxo: Service.GetAddressUtxosReply ->
|
||||
twig("Found UTXO at height ${utxo.height.toInt()} with ${utxo.valueZat} zatoshi")
|
||||
|
@ -899,11 +910,11 @@ class CompactBlockProcessor internal constructor(
|
|||
twig("=================== BLOCKS [$errorHeight..${errorHeight.value + count - 1}]: START ========")
|
||||
repeat(count) { i ->
|
||||
val height = errorHeight + i
|
||||
val block = downloader.compactBlockStore.findCompactBlock(height)
|
||||
val block = downloader.compactBlockRepository.findCompactBlock(height)
|
||||
// sometimes the initial block was inserted via checkpoint and will not appear in the cache. We can get
|
||||
// the hash another way but prevHash is correctly null.
|
||||
val hash = block?.hash?.toByteArray()
|
||||
?: (repository as PagedTransactionRepository).findBlockHash(height)
|
||||
?: repository.findBlockHash(height)
|
||||
twig(
|
||||
"block: $height\thash=${hash?.toHexReversed()} \tprevHash=${
|
||||
block?.prevHash?.toByteArray()?.toHexReversed()
|
||||
|
@ -914,11 +925,11 @@ class CompactBlockProcessor internal constructor(
|
|||
}
|
||||
|
||||
private suspend fun fetchValidationErrorInfo(errorHeight: BlockHeight): ValidationErrorInfo {
|
||||
val hash = (repository as PagedTransactionRepository).findBlockHash(errorHeight + 1)
|
||||
val hash = repository.findBlockHash(errorHeight + 1)
|
||||
?.toHexReversed()
|
||||
val prevHash = repository.findBlockHash(errorHeight)?.toHexReversed()
|
||||
|
||||
val compactBlock = downloader.compactBlockStore.findCompactBlock(errorHeight + 1)
|
||||
val compactBlock = downloader.compactBlockRepository.findCompactBlock(errorHeight + 1)
|
||||
val expectedPrevHash = compactBlock?.prevHash?.toByteArray()?.toHexReversed()
|
||||
return ValidationErrorInfo(errorHeight, hash, expectedPrevHash, prevHash)
|
||||
}
|
||||
|
@ -966,10 +977,7 @@ class CompactBlockProcessor internal constructor(
|
|||
var oldestTransactionHeight: BlockHeight? = null
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
try {
|
||||
val tempOldestTransactionHeight = repository.receivedTransactions
|
||||
.first()
|
||||
.lastOrNull()
|
||||
?.minedBlockHeight
|
||||
val tempOldestTransactionHeight = repository.getOldestTransaction()?.minedHeight
|
||||
?: lowerBoundHeight
|
||||
// to be safe adjust for reorgs (and generally a little cushion is good for privacy)
|
||||
// so we round down to the nearest 100 and then subtract 100 to ensure that the result is always at least
|
||||
|
@ -1005,33 +1013,55 @@ class CompactBlockProcessor internal constructor(
|
|||
suspend fun getLastScannedHeight() =
|
||||
repository.lastScannedHeight()
|
||||
|
||||
/**
|
||||
* Get address corresponding to the given account for this wallet.
|
||||
*
|
||||
* @return the address of this wallet.
|
||||
*/
|
||||
suspend fun getShieldedAddress(accountId: Int = 0) =
|
||||
repository.getAccount(accountId)?.rawShieldedAddress
|
||||
?: throw InitializerException.MissingAddressException("shielded")
|
||||
// CompactBlockProcessor is the wrong place for this, but it's where all the other APIs that need
|
||||
// access to the RustBackend live. This should be refactored.
|
||||
internal suspend fun createAccount(seed: ByteArray): UnifiedSpendingKey =
|
||||
rustBackend.createAccount(seed)
|
||||
|
||||
suspend fun getTransparentAddress(accountId: Int = 0) =
|
||||
repository.getAccount(accountId)?.rawTransparentAddress
|
||||
?: throw InitializerException.MissingAddressException("transparent")
|
||||
/**
|
||||
* Get the current unified address for the given wallet account.
|
||||
*
|
||||
* @return the current unified address of this account.
|
||||
*/
|
||||
suspend fun getCurrentAddress(account: Account) =
|
||||
rustBackend.getCurrentAddress(account.value)
|
||||
|
||||
/**
|
||||
* Get the legacy Sapling address corresponding to the current unified address for the given wallet account.
|
||||
*
|
||||
* @return a Sapling address.
|
||||
*/
|
||||
suspend fun getLegacySaplingAddress(account: Account) =
|
||||
rustBackend.getSaplingReceiver(
|
||||
rustBackend.getCurrentAddress(account.value)
|
||||
)
|
||||
?: throw InitializeException.MissingAddressException("legacy Sapling")
|
||||
|
||||
/**
|
||||
* Get the legacy transparent address corresponding to the current unified address for the given wallet account.
|
||||
*
|
||||
* @return a transparent address.
|
||||
*/
|
||||
suspend fun getTransparentAddress(account: Account) =
|
||||
rustBackend.getTransparentReceiver(
|
||||
rustBackend.getCurrentAddress(account.value)
|
||||
)
|
||||
?: throw InitializeException.MissingAddressException("legacy transparent")
|
||||
|
||||
/**
|
||||
* Calculates the latest balance info. Defaults to the first account.
|
||||
*
|
||||
* @param accountIndex the account to check for balance info.
|
||||
* @param account the account to check for balance info.
|
||||
*
|
||||
* @return an instance of WalletBalance containing information about available and total funds.
|
||||
*/
|
||||
suspend fun getBalanceInfo(accountIndex: Int = 0): WalletBalance =
|
||||
suspend fun getBalanceInfo(account: Account): WalletBalance =
|
||||
twigTask("checking balance info", -1) {
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
try {
|
||||
val balanceTotal = rustBackend.getBalance(accountIndex)
|
||||
val balanceTotal = rustBackend.getBalance(account.value)
|
||||
twig("found total balance: $balanceTotal")
|
||||
val balanceAvailable = rustBackend.getVerifiedBalance(accountIndex)
|
||||
val balanceAvailable = rustBackend.getVerifiedBalance(account.value)
|
||||
twig("found available balance: $balanceAvailable")
|
||||
WalletBalance(balanceTotal, balanceAvailable)
|
||||
} catch (t: Throwable) {
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
package cash.z.ecc.android.sdk.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
|
||||
@Entity(
|
||||
tableName = "accounts",
|
||||
primaryKeys = ["account"]
|
||||
)
|
||||
data class Account(
|
||||
|
||||
val account: Int? = 0,
|
||||
|
||||
@ColumnInfo(name = "extfvk")
|
||||
val extendedFullViewingKey: String = "",
|
||||
|
||||
val address: String = "",
|
||||
|
||||
@ColumnInfo(name = "transparent_address")
|
||||
val transparentAddress: String = ""
|
||||
)
|
|
@ -1,34 +0,0 @@
|
|||
package cash.z.ecc.android.sdk.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
|
||||
@Entity(primaryKeys = ["height"], tableName = "blocks")
|
||||
data class Block(
|
||||
val height: Int?,
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB, name = "hash")
|
||||
val hash: ByteArray,
|
||||
val time: Int,
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB, name = "sapling_tree")
|
||||
val saplingTree: ByteArray
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Block) return false
|
||||
|
||||
if (height != other.height) return false
|
||||
if (!hash.contentEquals(other.hash)) return false
|
||||
if (time != other.time) return false
|
||||
if (!saplingTree.contentEquals(other.saplingTree)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = height ?: 0
|
||||
result = 31 * result + hash.contentHashCode()
|
||||
result = 31 * result + time
|
||||
result = 31 * result + saplingTree.contentHashCode()
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
package cash.z.ecc.android.sdk.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.RoomWarnings
|
||||
|
||||
@Entity(
|
||||
tableName = "received_notes",
|
||||
primaryKeys = ["id_note"],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = TransactionEntity::class,
|
||||
parentColumns = ["id_tx"],
|
||||
childColumns = ["tx"]
|
||||
),
|
||||
ForeignKey(
|
||||
entity = Account::class,
|
||||
parentColumns = ["account"],
|
||||
childColumns = ["account"]
|
||||
),
|
||||
ForeignKey(
|
||||
entity = TransactionEntity::class,
|
||||
parentColumns = ["id_tx"],
|
||||
childColumns = ["spent"]
|
||||
)
|
||||
]
|
||||
)
|
||||
@SuppressWarnings(RoomWarnings.MISSING_INDEX_ON_FOREIGN_KEY_CHILD)
|
||||
data class Received(
|
||||
@ColumnInfo(name = "id_note")
|
||||
val id: Int? = 0,
|
||||
|
||||
/**
|
||||
* A reference to the transaction this note was received in
|
||||
*/
|
||||
@ColumnInfo(name = "tx")
|
||||
val transactionId: Int = 0,
|
||||
|
||||
@ColumnInfo(name = "output_index")
|
||||
val outputIndex: Int = 0,
|
||||
|
||||
val account: Int = 0,
|
||||
val value: Long = 0,
|
||||
|
||||
/**
|
||||
* A reference to the transaction this note was later spent in
|
||||
*/
|
||||
val spent: Int? = 0,
|
||||
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
||||
val diversifier: ByteArray = byteArrayOf(),
|
||||
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
||||
val rcm: ByteArray = byteArrayOf(),
|
||||
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
||||
val nf: ByteArray = byteArrayOf(),
|
||||
|
||||
@ColumnInfo(name = "is_change")
|
||||
val isChange: Boolean = false,
|
||||
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
||||
val memo: ByteArray? = byteArrayOf()
|
||||
)
|
|
@ -1,73 +0,0 @@
|
|||
package cash.z.ecc.android.sdk.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.RoomWarnings
|
||||
|
||||
@Entity(
|
||||
tableName = "sent_notes",
|
||||
primaryKeys = ["id_note"],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = TransactionEntity::class,
|
||||
parentColumns = ["id_tx"],
|
||||
childColumns = ["tx"]
|
||||
), ForeignKey(
|
||||
entity = Account::class,
|
||||
parentColumns = ["account"],
|
||||
childColumns = ["from_account"]
|
||||
)
|
||||
]
|
||||
)
|
||||
@SuppressWarnings(RoomWarnings.MISSING_INDEX_ON_FOREIGN_KEY_CHILD)
|
||||
data class Sent(
|
||||
@ColumnInfo(name = "id_note")
|
||||
val id: Int? = 0,
|
||||
|
||||
@ColumnInfo(name = "tx")
|
||||
val transactionId: Long = 0,
|
||||
|
||||
@ColumnInfo(name = "output_index")
|
||||
val outputIndex: Int = 0,
|
||||
|
||||
@ColumnInfo(name = "from_account")
|
||||
val account: Int = 0,
|
||||
|
||||
val address: String = "",
|
||||
|
||||
val value: Long = 0,
|
||||
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
||||
val memo: ByteArray? = byteArrayOf()
|
||||
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Sent) return false
|
||||
|
||||
if (id != other.id) return false
|
||||
if (transactionId != other.transactionId) return false
|
||||
if (outputIndex != other.outputIndex) return false
|
||||
if (account != other.account) return false
|
||||
if (address != other.address) return false
|
||||
if (value != other.value) return false
|
||||
if (memo != null) {
|
||||
if (other.memo == null) return false
|
||||
if (!memo.contentEquals(other.memo)) return false
|
||||
} else if (other.memo != null) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id ?: 0
|
||||
result = 31 * result + transactionId.hashCode()
|
||||
result = 31 * result + outputIndex
|
||||
result = 31 * result + account
|
||||
result = 31 * result + address.hashCode()
|
||||
result = 31 * result + value.hashCode()
|
||||
result = 31 * result + (memo?.contentHashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -1,417 +0,0 @@
|
|||
@file:Suppress("TooManyFunctions")
|
||||
|
||||
package cash.z.ecc.android.sdk.db.entity
|
||||
|
||||
import android.text.format.DateUtils
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.RoomWarnings
|
||||
import cash.z.ecc.android.sdk.internal.transaction.PersistentTransactionManager
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
|
||||
//
|
||||
// Entities
|
||||
//
|
||||
|
||||
@Entity(
|
||||
primaryKeys = ["id_tx"],
|
||||
tableName = "transactions",
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = Block::class,
|
||||
parentColumns = ["height"],
|
||||
childColumns = ["block"]
|
||||
)
|
||||
]
|
||||
)
|
||||
@SuppressWarnings(RoomWarnings.MISSING_INDEX_ON_FOREIGN_KEY_CHILD)
|
||||
data class TransactionEntity(
|
||||
@ColumnInfo(name = "id_tx")
|
||||
val id: Long?,
|
||||
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB, name = "txid")
|
||||
val transactionId: ByteArray,
|
||||
|
||||
@ColumnInfo(name = "tx_index")
|
||||
val transactionIndex: Int?,
|
||||
|
||||
val created: String?,
|
||||
|
||||
@ColumnInfo(name = "expiry_height")
|
||||
val expiryHeight: Int?,
|
||||
|
||||
@ColumnInfo(name = "block")
|
||||
val minedHeight: Int?,
|
||||
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
||||
val raw: ByteArray?
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is TransactionEntity) return false
|
||||
|
||||
if (id != other.id) return false
|
||||
if (!transactionId.contentEquals(other.transactionId)) return false
|
||||
if (transactionIndex != other.transactionIndex) return false
|
||||
if (created != other.created) return false
|
||||
if (expiryHeight != other.expiryHeight) return false
|
||||
if (minedHeight != other.minedHeight) return false
|
||||
if (raw != null) {
|
||||
if (other.raw == null) return false
|
||||
if (!raw.contentEquals(other.raw)) return false
|
||||
} else if (other.raw != null) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + transactionId.contentHashCode()
|
||||
result = 31 * result + (transactionIndex ?: 0)
|
||||
result = 31 * result + (created?.hashCode() ?: 0)
|
||||
result = 31 * result + (expiryHeight ?: 0)
|
||||
result = 31 * result + (minedHeight ?: 0)
|
||||
result = 31 * result + (raw?.contentHashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@Entity(tableName = "pending_transactions")
|
||||
data class PendingTransactionEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
override val id: Long = 0,
|
||||
override val toAddress: String = "",
|
||||
override val value: Long = -1,
|
||||
override val memo: ByteArray? = byteArrayOf(),
|
||||
override val accountIndex: Int,
|
||||
override val minedHeight: Long = -1,
|
||||
override val expiryHeight: Long = -1,
|
||||
|
||||
override val cancelled: Int = 0,
|
||||
override val encodeAttempts: Int = -1,
|
||||
override val submitAttempts: Int = -1,
|
||||
override val errorMessage: String? = null,
|
||||
override val errorCode: Int? = null,
|
||||
override val createTime: Long = System.currentTimeMillis(),
|
||||
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
||||
override val raw: ByteArray = byteArrayOf(),
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
||||
override val rawTransactionId: ByteArray? = byteArrayOf()
|
||||
) : PendingTransaction {
|
||||
|
||||
val valueZatoshi: Zatoshi
|
||||
get() = Zatoshi(value)
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is PendingTransactionEntity) return false
|
||||
|
||||
if (id != other.id) return false
|
||||
if (toAddress != other.toAddress) return false
|
||||
if (value != other.value) return false
|
||||
if (memo != null) {
|
||||
if (other.memo == null) return false
|
||||
if (!memo.contentEquals(other.memo)) return false
|
||||
} else if (other.memo != null) return false
|
||||
if (accountIndex != other.accountIndex) return false
|
||||
if (minedHeight != other.minedHeight) return false
|
||||
if (expiryHeight != other.expiryHeight) return false
|
||||
if (cancelled != other.cancelled) return false
|
||||
if (encodeAttempts != other.encodeAttempts) return false
|
||||
if (submitAttempts != other.submitAttempts) return false
|
||||
if (errorMessage != other.errorMessage) return false
|
||||
if (errorCode != other.errorCode) return false
|
||||
if (createTime != other.createTime) return false
|
||||
if (!raw.contentEquals(other.raw)) return false
|
||||
if (rawTransactionId != null) {
|
||||
if (other.rawTransactionId == null) return false
|
||||
if (!rawTransactionId.contentEquals(other.rawTransactionId)) return false
|
||||
} else if (other.rawTransactionId != null) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + toAddress.hashCode()
|
||||
result = 31 * result + value.hashCode()
|
||||
result = 31 * result + (memo?.contentHashCode() ?: 0)
|
||||
result = 31 * result + accountIndex
|
||||
result = 31 * result + minedHeight.hashCode()
|
||||
result = 31 * result + expiryHeight.hashCode()
|
||||
result = 31 * result + cancelled
|
||||
result = 31 * result + encodeAttempts
|
||||
result = 31 * result + submitAttempts
|
||||
result = 31 * result + (errorMessage?.hashCode() ?: 0)
|
||||
result = 31 * result + (errorCode ?: 0)
|
||||
result = 31 * result + createTime.hashCode()
|
||||
result = 31 * result + raw.contentHashCode()
|
||||
result = 31 * result + (rawTransactionId?.contentHashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Query Objects
|
||||
//
|
||||
|
||||
/**
|
||||
* A mined, shielded transaction. Since this is a [MinedTransaction], it represents data
|
||||
* on the blockchain.
|
||||
*/
|
||||
data class ConfirmedTransaction(
|
||||
override val id: Long = 0L,
|
||||
override val value: Long = 0L,
|
||||
override val memo: ByteArray? = ByteArray(0),
|
||||
override val noteId: Long = 0L,
|
||||
override val blockTimeInSeconds: Long = 0L,
|
||||
override val minedHeight: Long = -1,
|
||||
override val transactionIndex: Int,
|
||||
override val rawTransactionId: ByteArray = ByteArray(0),
|
||||
|
||||
// properties that differ from received transactions
|
||||
val toAddress: String? = null,
|
||||
val expiryHeight: Int? = null,
|
||||
override val raw: ByteArray? = byteArrayOf()
|
||||
) : MinedTransaction, SignedTransaction {
|
||||
|
||||
val minedBlockHeight
|
||||
get() = if (minedHeight == -1L) {
|
||||
null
|
||||
} else {
|
||||
BlockHeight(minedHeight)
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is ConfirmedTransaction) return false
|
||||
|
||||
if (id != other.id) return false
|
||||
if (value != other.value) return false
|
||||
if (memo != null) {
|
||||
if (other.memo == null) return false
|
||||
if (!memo.contentEquals(other.memo)) return false
|
||||
} else if (other.memo != null) return false
|
||||
if (noteId != other.noteId) return false
|
||||
if (blockTimeInSeconds != other.blockTimeInSeconds) return false
|
||||
if (minedHeight != other.minedHeight) return false
|
||||
if (transactionIndex != other.transactionIndex) return false
|
||||
if (!rawTransactionId.contentEquals(other.rawTransactionId)) return false
|
||||
if (toAddress != other.toAddress) return false
|
||||
if (expiryHeight != other.expiryHeight) return false
|
||||
if (raw != null) {
|
||||
if (other.raw == null) return false
|
||||
if (!raw.contentEquals(other.raw)) return false
|
||||
} else if (other.raw != null) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + value.hashCode()
|
||||
result = 31 * result + (memo?.contentHashCode() ?: 0)
|
||||
result = 31 * result + noteId.hashCode()
|
||||
result = 31 * result + blockTimeInSeconds.hashCode()
|
||||
result = 31 * result + minedHeight.hashCode()
|
||||
result = 31 * result + transactionIndex
|
||||
result = 31 * result + rawTransactionId.contentHashCode()
|
||||
result = 31 * result + (toAddress?.hashCode() ?: 0)
|
||||
result = 31 * result + (expiryHeight ?: 0)
|
||||
result = 31 * result + (raw?.contentHashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
val ConfirmedTransaction.valueInZatoshi
|
||||
get() = Zatoshi(value)
|
||||
|
||||
data class EncodedTransaction(val txId: ByteArray, override val raw: ByteArray, val expiryHeight: Long?) :
|
||||
SignedTransaction {
|
||||
|
||||
val expiryBlockHeight
|
||||
get() = expiryHeight?.let { BlockHeight(it) }
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is EncodedTransaction) return false
|
||||
|
||||
if (!txId.contentEquals(other.txId)) return false
|
||||
if (!raw.contentEquals(other.raw)) return false
|
||||
if (expiryHeight != other.expiryHeight) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = txId.contentHashCode()
|
||||
result = 31 * result + raw.contentHashCode()
|
||||
result = 31 * result + (expiryHeight?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Transaction Interfaces
|
||||
//
|
||||
|
||||
/**
|
||||
* Common interface between confirmed transactions on the blockchain and pending transactions being
|
||||
* constructed.
|
||||
*/
|
||||
interface Transaction {
|
||||
val id: Long
|
||||
val value: Long
|
||||
val memo: ByteArray?
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for anything that's able to provide signed transaction bytes.
|
||||
*/
|
||||
interface SignedTransaction {
|
||||
val raw: ByteArray?
|
||||
}
|
||||
|
||||
/**
|
||||
* Parent type for transactions that have been mined. This is useful for putting all transactions in
|
||||
* one list for things like history. A mined tx should have all properties, except possibly a memo.
|
||||
*/
|
||||
interface MinedTransaction : Transaction {
|
||||
val minedHeight: Long
|
||||
val noteId: Long
|
||||
val blockTimeInSeconds: Long
|
||||
val transactionIndex: Int
|
||||
val rawTransactionId: ByteArray
|
||||
}
|
||||
|
||||
interface PendingTransaction : SignedTransaction, Transaction {
|
||||
override val id: Long
|
||||
override val value: Long
|
||||
override val memo: ByteArray?
|
||||
val toAddress: String
|
||||
val accountIndex: Int
|
||||
val minedHeight: Long // apparently this can be -1 as an uninitialized value
|
||||
val expiryHeight: Long // apparently this can be -1 as an uninitialized value
|
||||
val cancelled: Int
|
||||
val encodeAttempts: Int
|
||||
val submitAttempts: Int
|
||||
val errorMessage: String?
|
||||
val errorCode: Int?
|
||||
val createTime: Long
|
||||
val rawTransactionId: ByteArray?
|
||||
}
|
||||
|
||||
//
|
||||
// Extension-oriented design
|
||||
//
|
||||
|
||||
fun PendingTransaction.isSameTxId(other: MinedTransaction): Boolean {
|
||||
return rawTransactionId != null && rawTransactionId!!.contentEquals(other.rawTransactionId)
|
||||
}
|
||||
|
||||
fun PendingTransaction.isSameTxId(other: PendingTransaction): Boolean {
|
||||
return rawTransactionId != null && other.rawTransactionId != null &&
|
||||
rawTransactionId!!.contentEquals(other.rawTransactionId!!)
|
||||
}
|
||||
|
||||
fun PendingTransaction.hasRawTransactionId(): Boolean {
|
||||
return rawTransactionId != null && (rawTransactionId?.isNotEmpty() == true)
|
||||
}
|
||||
|
||||
fun PendingTransaction.isCreating(): Boolean {
|
||||
return (raw?.isEmpty() != false) && submitAttempts <= 0 && !isFailedSubmit() && !isFailedEncoding()
|
||||
}
|
||||
|
||||
fun PendingTransaction.isCreated(): Boolean {
|
||||
return (raw?.isEmpty() == false) && submitAttempts <= 0 && !isFailedSubmit() && !isFailedEncoding()
|
||||
}
|
||||
|
||||
fun PendingTransaction.isFailedEncoding(): Boolean {
|
||||
return (raw?.isEmpty() != false) && encodeAttempts > 0
|
||||
}
|
||||
|
||||
fun PendingTransaction.isFailedSubmit(): Boolean {
|
||||
return errorMessage != null || (errorCode != null && errorCode!! < 0)
|
||||
}
|
||||
|
||||
fun PendingTransaction.isFailure(): Boolean {
|
||||
return isFailedEncoding() || isFailedSubmit()
|
||||
}
|
||||
|
||||
fun PendingTransaction.isCancelled(): Boolean {
|
||||
return cancelled > 0
|
||||
}
|
||||
|
||||
fun PendingTransaction.isMined(): Boolean {
|
||||
return minedHeight > 0
|
||||
}
|
||||
|
||||
fun PendingTransaction.isSubmitted(): Boolean {
|
||||
return submitAttempts > 0
|
||||
}
|
||||
|
||||
fun PendingTransaction.isExpired(latestHeight: BlockHeight?, saplingActivationHeight: BlockHeight): Boolean {
|
||||
// TODO [#687]: test for off-by-one error here. Should we use <= or <
|
||||
// TODO [#687]: https://github.com/zcash/zcash-android-wallet-sdk/issues/687
|
||||
if (latestHeight == null ||
|
||||
latestHeight.value < saplingActivationHeight.value ||
|
||||
expiryHeight < saplingActivationHeight.value
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return expiryHeight < latestHeight.value
|
||||
}
|
||||
|
||||
// if we don't have info on a pendingtx after 100 blocks then it's probably safe to stop polling!
|
||||
@Suppress("MagicNumber")
|
||||
fun PendingTransaction.isLongExpired(latestHeight: BlockHeight?, saplingActivationHeight: BlockHeight): Boolean {
|
||||
if (latestHeight == null ||
|
||||
latestHeight.value < saplingActivationHeight.value ||
|
||||
expiryHeight < saplingActivationHeight.value
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return (latestHeight.value - expiryHeight) > 100
|
||||
}
|
||||
|
||||
fun PendingTransaction.isMarkedForDeletion(): Boolean {
|
||||
return rawTransactionId == null &&
|
||||
(errorCode ?: 0) == PersistentTransactionManager.SAFE_TO_DELETE_ERROR_CODE
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun PendingTransaction.isSafeToDiscard(): Boolean {
|
||||
// invalid dates shouldn't happen or should be temporary
|
||||
if (createTime < 0) return false
|
||||
|
||||
val age = System.currentTimeMillis() - createTime
|
||||
val smallThreshold = 30 * DateUtils.MINUTE_IN_MILLIS
|
||||
val hugeThreshold = 30 * DateUtils.DAY_IN_MILLIS
|
||||
return when {
|
||||
// if it is mined, then it is not pending so it can be deleted fairly quickly from this db
|
||||
isMined() && age > smallThreshold -> true
|
||||
// if a tx fails to encode, then there's not much we can do with it
|
||||
isFailedEncoding() && age > smallThreshold -> true
|
||||
// don't delete failed submissions until they've been cleaned up, properly, or else we lose
|
||||
// the ability to remove them in librustzcash prior to expiration
|
||||
isFailedSubmit() && isMarkedForDeletion() -> true
|
||||
!isMined() && age > hugeThreshold -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun PendingTransaction.isPending(currentHeight: BlockHeight?): Boolean {
|
||||
// not mined and not expired and successfully created
|
||||
return !isSubmitSuccess() && minedHeight == -1L &&
|
||||
(expiryHeight == -1L || expiryHeight > (currentHeight?.value ?: 0L)) && raw != null
|
||||
}
|
||||
|
||||
fun PendingTransaction.isSubmitSuccess(): Boolean {
|
||||
return submitAttempts > 0 && (errorCode != null && errorCode!! >= 0) && errorMessage == null
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
package cash.z.ecc.android.sdk.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.RoomWarnings
|
||||
|
||||
@Entity(
|
||||
tableName = "utxos",
|
||||
primaryKeys = ["id_utxo"],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = TransactionEntity::class,
|
||||
parentColumns = ["id_tx"],
|
||||
childColumns = ["spent_in_tx"]
|
||||
)
|
||||
]
|
||||
)
|
||||
@SuppressWarnings(RoomWarnings.MISSING_INDEX_ON_FOREIGN_KEY_CHILD)
|
||||
data class Utxo(
|
||||
@ColumnInfo(name = "id_utxo")
|
||||
val id: Long? = 0L,
|
||||
|
||||
val address: String = "",
|
||||
|
||||
@ColumnInfo(name = "prevout_txid", typeAffinity = ColumnInfo.BLOB)
|
||||
val txid: ByteArray = byteArrayOf(),
|
||||
|
||||
@ColumnInfo(name = "prevout_idx")
|
||||
val transactionIndex: Int = -1,
|
||||
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
||||
val script: ByteArray = byteArrayOf(),
|
||||
|
||||
@ColumnInfo(name = "value_zat")
|
||||
val value: Long = 0L,
|
||||
|
||||
val height: Int = -1,
|
||||
|
||||
/**
|
||||
* A reference to the transaction this note was later spent in
|
||||
*/
|
||||
@ColumnInfo(name = "spent_in_tx")
|
||||
val spent: Int? = 0
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Utxo) return false
|
||||
|
||||
if (id != other.id) return false
|
||||
if (address != other.address) return false
|
||||
if (!txid.contentEquals(other.txid)) return false
|
||||
if (transactionIndex != other.transactionIndex) return false
|
||||
if (!script.contentEquals(other.script)) return false
|
||||
if (value != other.value) return false
|
||||
if (height != other.height) return false
|
||||
if (spent != other.spent) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + address.hashCode()
|
||||
result = 31 * result + txid.contentHashCode()
|
||||
result = 31 * result + transactionIndex
|
||||
result = 31 * result + script.contentHashCode()
|
||||
result = 31 * result + value.hashCode()
|
||||
result = 31 * result + height
|
||||
result = 31 * result + (spent ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -177,26 +177,30 @@ sealed class BirthdayException(message: String, cause: Throwable? = null) : SdkE
|
|||
/**
|
||||
* Exceptions thrown by the initializer.
|
||||
*/
|
||||
sealed class InitializerException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
||||
class FalseStart(cause: Throwable?) : InitializerException("Failed to initialize accounts due to: $cause", cause)
|
||||
class AlreadyInitializedException(cause: Throwable, dbPath: String) : InitializerException(
|
||||
sealed class InitializeException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
||||
object SeedRequired : InitializeException(
|
||||
"A pending database migration requires the wallet's seed. Call this initialization " +
|
||||
"method again with the seed."
|
||||
)
|
||||
class FalseStart(cause: Throwable?) : InitializeException("Failed to initialize accounts due to: $cause", cause)
|
||||
class AlreadyInitializedException(cause: Throwable, dbPath: String) : InitializeException(
|
||||
"Failed to initialize the blocks table" +
|
||||
" because it already exists in $dbPath",
|
||||
cause
|
||||
)
|
||||
object MissingBirthdayException : InitializerException(
|
||||
object MissingBirthdayException : InitializeException(
|
||||
"Expected a birthday for this wallet but failed to find one. This usually means that " +
|
||||
"wallet setup did not happen correctly. A workaround might be to interpret the " +
|
||||
"birthday, based on the contents of the wallet data but it is probably better " +
|
||||
"not to mask this error because the root issue should be addressed."
|
||||
)
|
||||
object MissingViewingKeyException : InitializerException(
|
||||
object MissingViewingKeyException : InitializeException(
|
||||
"Expected a unified viewingKey for this wallet but failed to find one. This usually means" +
|
||||
" that wallet setup happened incorrectly. A workaround might be to derive the" +
|
||||
" unified viewingKey from the seed or seedPhrase, if they exist, but it is probably" +
|
||||
" better not to mask this error because the root issue should be addressed."
|
||||
)
|
||||
class MissingAddressException(description: String, cause: Throwable? = null) : InitializerException(
|
||||
class MissingAddressException(description: String, cause: Throwable? = null) : InitializeException(
|
||||
"Expected a $description address for this wallet but failed to find one. This usually" +
|
||||
" means that wallet setup happened incorrectly. If this problem persists, a" +
|
||||
" workaround might be to go to settings and WIPE the wallet and rescan. Doing so" +
|
||||
|
@ -205,18 +209,18 @@ sealed class InitializerException(message: String, cause: Throwable? = null) : S
|
|||
if (cause != null) "\nCaused by: $cause" else ""
|
||||
)
|
||||
object DatabasePathException :
|
||||
InitializerException(
|
||||
InitializeException(
|
||||
"Critical failure to locate path for storing databases. Perhaps this device prevents" +
|
||||
" apps from storing data? We cannot initialize the wallet unless we can store" +
|
||||
" data."
|
||||
)
|
||||
|
||||
class InvalidBirthdayHeightException(birthday: BlockHeight?, network: ZcashNetwork) : InitializerException(
|
||||
class InvalidBirthdayHeightException(birthday: BlockHeight?, network: ZcashNetwork) : InitializeException(
|
||||
"Invalid birthday height of ${birthday?.value}. The birthday height must be at least the height of" +
|
||||
" Sapling activation on ${network.networkName} (${network.saplingActivationHeight})."
|
||||
)
|
||||
|
||||
object MissingDefaultBirthdayException : InitializerException(
|
||||
object MissingDefaultBirthdayException : InitializeException(
|
||||
"The birthday height is missing and it is unclear which value to use as a default."
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,8 +2,6 @@ package cash.z.ecc.android.sdk.internal
|
|||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
|
@ -18,7 +16,6 @@ import java.io.File
|
|||
* @param parentDir The directory in which is the database file placed.
|
||||
* @return Wrapped context class.
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.O_MR1)
|
||||
internal class NoBackupContextWrapper(
|
||||
context: Context,
|
||||
private val parentDir: File
|
||||
|
|
|
@ -2,6 +2,7 @@ package cash.z.ecc.android.sdk.internal.block
|
|||
import cash.z.ecc.android.sdk.exception.LightWalletException
|
||||
import cash.z.ecc.android.sdk.internal.ext.retryUpTo
|
||||
import cash.z.ecc.android.sdk.internal.ext.tryWarn
|
||||
import cash.z.ecc.android.sdk.internal.repository.CompactBlockRepository
|
||||
import cash.z.ecc.android.sdk.internal.service.LightWalletService
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
|
@ -21,17 +22,17 @@ import kotlinx.coroutines.withContext
|
|||
* data; although, by default the SDK uses gRPC and SQL.
|
||||
*
|
||||
* @property lightWalletService the service used for requesting compact blocks
|
||||
* @property compactBlockStore responsible for persisting the compact blocks that are received
|
||||
* @property compactBlockRepository responsible for persisting the compact blocks that are received
|
||||
*/
|
||||
open class CompactBlockDownloader private constructor(val compactBlockStore: CompactBlockStore) {
|
||||
open class CompactBlockDownloader private constructor(val compactBlockRepository: CompactBlockRepository) {
|
||||
|
||||
lateinit var lightWalletService: LightWalletService
|
||||
private set
|
||||
|
||||
constructor(
|
||||
lightWalletService: LightWalletService,
|
||||
compactBlockStore: CompactBlockStore
|
||||
) : this(compactBlockStore) {
|
||||
compactBlockRepository: CompactBlockRepository
|
||||
) : this(compactBlockRepository) {
|
||||
this.lightWalletService = lightWalletService
|
||||
}
|
||||
|
||||
|
@ -46,7 +47,7 @@ open class CompactBlockDownloader private constructor(val compactBlockStore: Com
|
|||
*/
|
||||
suspend fun downloadBlockRange(heightRange: ClosedRange<BlockHeight>): Int = withContext(IO) {
|
||||
val result = lightWalletService.getBlockRange(heightRange)
|
||||
compactBlockStore.write(result)
|
||||
compactBlockRepository.write(result)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -58,7 +59,7 @@ open class CompactBlockDownloader private constructor(val compactBlockStore: Com
|
|||
suspend fun rewindToHeight(height: BlockHeight) =
|
||||
// TODO [#685]: cancel anything in flight
|
||||
// TODO [#685]: https://github.com/zcash/zcash-android-wallet-sdk/issues/685
|
||||
compactBlockStore.rewindTo(height)
|
||||
compactBlockRepository.rewindTo(height)
|
||||
|
||||
/**
|
||||
* Return the latest block height known by the lightwalletService.
|
||||
|
@ -69,12 +70,12 @@ open class CompactBlockDownloader private constructor(val compactBlockStore: Com
|
|||
lightWalletService.getLatestBlockHeight()
|
||||
|
||||
/**
|
||||
* Return the latest block height that has been persisted into the [CompactBlockStore].
|
||||
* Return the latest block height that has been persisted into the [CompactBlockRepository].
|
||||
*
|
||||
* @return the latest block height that has been persisted.
|
||||
*/
|
||||
suspend fun getLastDownloadedHeight() =
|
||||
compactBlockStore.getLatestHeight()
|
||||
compactBlockRepository.getLatestHeight()
|
||||
|
||||
suspend fun getServerInfo(): Service.LightdInfo = withContext<Service.LightdInfo>(IO) {
|
||||
lateinit var result: Service.LightdInfo
|
||||
|
@ -126,7 +127,7 @@ open class CompactBlockDownloader private constructor(val compactBlockStore: Com
|
|||
withContext(Dispatchers.IO) {
|
||||
lightWalletService.shutdown()
|
||||
}
|
||||
compactBlockStore.close()
|
||||
compactBlockRepository.close()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
@file:Suppress("ktlint:filename")
|
||||
|
||||
package cash.z.ecc.android.sdk.internal.db
|
||||
|
||||
import android.database.Cursor
|
||||
|
||||
internal fun Cursor.optLong(columnIndex: Int): Long? =
|
||||
if (isNull(columnIndex)) {
|
||||
null
|
||||
} else {
|
||||
getLong(columnIndex)
|
||||
}
|
||||
|
||||
internal fun Cursor.optBlobOrThrow(index: Int): ByteArray? {
|
||||
return if (isNull(index)) {
|
||||
null
|
||||
} else {
|
||||
getBlob(index)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package cash.z.ecc.android.sdk.internal.db
|
||||
|
||||
import android.database.Cursor
|
||||
|
||||
fun interface CursorParser<T> {
|
||||
/**
|
||||
* Extracts an object from a Cursor. This method assumes that the Cursor contains all the needed columns and
|
||||
* that the Cursor is positioned to a row that is ready to be read. This method, in turn, will not mutate
|
||||
* the Cursor or move the Cursor position.
|
||||
*
|
||||
* @param cursor Cursor from a query to a contract this parser can handle.
|
||||
* @return a new Object.
|
||||
* @throws AssertionError If the cursor is closed or the cursor is out of range.
|
||||
*/
|
||||
fun newObject(cursor: Cursor): T
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
package cash.z.ecc.android.sdk.db
|
||||
package cash.z.ecc.android.sdk.internal.db
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import cash.z.ecc.android.sdk.exception.InitializerException
|
||||
import cash.z.ecc.android.sdk.exception.InitializeException
|
||||
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
||||
import cash.z.ecc.android.sdk.internal.AndroidApiVersion
|
||||
import cash.z.ecc.android.sdk.internal.Files
|
||||
|
@ -325,7 +325,7 @@ internal class DatabaseCoordinator private constructor(context: Context) {
|
|||
*/
|
||||
private suspend fun getDatabaseParentDir(appContext: Context): File {
|
||||
return appContext.getDatabasePathSuspend("unused.db").parentFile
|
||||
?: throw InitializerException.DatabasePathException
|
||||
?: throw InitializeException.DatabasePathException
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -401,7 +401,7 @@ internal fun <T : RoomDatabase?> commonDatabaseBuilder(
|
|||
Room.databaseBuilder(
|
||||
NoBackupContextWrapper(
|
||||
context,
|
||||
databaseFile.parentFile ?: throw InitializerException.DatabasePathException
|
||||
databaseFile.parentFile ?: throw InitializeException.DatabasePathException
|
||||
),
|
||||
klass,
|
||||
databaseFile.name
|
|
@ -1,553 +0,0 @@
|
|||
package cash.z.ecc.android.sdk.internal.db
|
||||
|
||||
import androidx.paging.DataSource
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Database
|
||||
import androidx.room.Query
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.RoomWarnings
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import cash.z.ecc.android.sdk.db.entity.Account
|
||||
import cash.z.ecc.android.sdk.db.entity.Block
|
||||
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
|
||||
import cash.z.ecc.android.sdk.db.entity.EncodedTransaction
|
||||
import cash.z.ecc.android.sdk.db.entity.Received
|
||||
import cash.z.ecc.android.sdk.db.entity.Sent
|
||||
import cash.z.ecc.android.sdk.db.entity.TransactionEntity
|
||||
import cash.z.ecc.android.sdk.db.entity.Utxo
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
import cash.z.ecc.android.sdk.type.UnifiedAddressAccount
|
||||
|
||||
//
|
||||
// Database
|
||||
//
|
||||
|
||||
/**
|
||||
* The "Data DB," where all data derived from the compact blocks is stored. Most importantly, this
|
||||
* database contains transaction information and can be queried for the current balance. The
|
||||
* "blocks" table contains a copy of everything that has been scanned. In the future, that table can
|
||||
* be truncated up to the last scanned block, for storage efficiency. Wallets should only read from,
|
||||
* but never write to, this database.
|
||||
*/
|
||||
@Database(
|
||||
entities = [
|
||||
TransactionEntity::class,
|
||||
Block::class,
|
||||
Received::class,
|
||||
Account::class,
|
||||
Sent::class,
|
||||
Utxo::class
|
||||
],
|
||||
version = 7,
|
||||
exportSchema = true
|
||||
)
|
||||
abstract class DerivedDataDb : RoomDatabase() {
|
||||
abstract fun transactionDao(): TransactionDao
|
||||
abstract fun blockDao(): BlockDao
|
||||
abstract fun receivedDao(): ReceivedDao
|
||||
abstract fun sentDao(): SentDao
|
||||
abstract fun accountDao(): AccountDao
|
||||
|
||||
//
|
||||
// Migrations
|
||||
//
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
companion object {
|
||||
|
||||
val MIGRATION_3_4 = object : Migration(3, 4) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("PRAGMA foreign_keys = OFF;")
|
||||
database.execSQL(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS received_notes_new (
|
||||
id_note INTEGER PRIMARY KEY, tx INTEGER NOT NULL,
|
||||
output_index INTEGER NOT NULL, account INTEGER NOT NULL,
|
||||
diversifier BLOB NOT NULL, value INTEGER NOT NULL,
|
||||
rcm BLOB NOT NULL, nf BLOB NOT NULL UNIQUE,
|
||||
is_change INTEGER NOT NULL, memo BLOB,
|
||||
spent INTEGER,
|
||||
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
|
||||
FOREIGN KEY (account) REFERENCES accounts(account),
|
||||
FOREIGN KEY (spent) REFERENCES transactions(id_tx),
|
||||
CONSTRAINT tx_output UNIQUE (tx, output_index)
|
||||
);
|
||||
""".trimIndent()
|
||||
)
|
||||
database.execSQL("INSERT INTO received_notes_new SELECT * FROM received_notes;")
|
||||
database.execSQL("DROP TABLE received_notes;")
|
||||
database.execSQL("ALTER TABLE received_notes_new RENAME TO received_notes;")
|
||||
database.execSQL("PRAGMA foreign_keys = ON;")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATION_4_3 = object : Migration(4, 3) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("PRAGMA foreign_keys = OFF;")
|
||||
database.execSQL(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS received_notes_new (
|
||||
id_note INTEGER PRIMARY KEY,
|
||||
tx INTEGER NOT NULL,
|
||||
output_index INTEGER NOT NULL,
|
||||
account INTEGER NOT NULL,
|
||||
diversifier BLOB NOT NULL,
|
||||
value INTEGER NOT NULL,
|
||||
rcm BLOB NOT NULL,
|
||||
nf BLOB NOT NULL UNIQUE,
|
||||
is_change INTEGER NOT NULL,
|
||||
memo BLOB,
|
||||
spent INTEGER,
|
||||
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
|
||||
FOREIGN KEY (account) REFERENCES accounts(account),
|
||||
FOREIGN KEY (spent) REFERENCES transactions(id_tx),
|
||||
CONSTRAINT tx_output UNIQUE (tx, output_index)
|
||||
);
|
||||
""".trimIndent()
|
||||
)
|
||||
database.execSQL("INSERT INTO received_notes_new SELECT * FROM received_notes;")
|
||||
database.execSQL("DROP TABLE received_notes;")
|
||||
database.execSQL("ALTER TABLE received_notes_new RENAME TO received_notes;")
|
||||
database.execSQL("PRAGMA foreign_keys = ON;")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATION_4_5 = object : Migration(4, 5) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("PRAGMA foreign_keys = OFF;")
|
||||
database.execSQL(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS received_notes_new (
|
||||
id_note INTEGER PRIMARY KEY,
|
||||
tx INTEGER NOT NULL,
|
||||
output_index INTEGER NOT NULL,
|
||||
account INTEGER NOT NULL,
|
||||
diversifier BLOB NOT NULL,
|
||||
value INTEGER NOT NULL,
|
||||
rcm BLOB NOT NULL,
|
||||
nf BLOB NOT NULL UNIQUE,
|
||||
is_change INTEGER NOT NULL,
|
||||
memo BLOB,
|
||||
spent INTEGER,
|
||||
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
|
||||
FOREIGN KEY (account) REFERENCES accounts(account),
|
||||
FOREIGN KEY (spent) REFERENCES transactions(id_tx),
|
||||
CONSTRAINT tx_output UNIQUE (tx, output_index)
|
||||
);
|
||||
""".trimIndent()
|
||||
)
|
||||
database.execSQL("INSERT INTO received_notes_new SELECT * FROM received_notes;")
|
||||
database.execSQL("DROP TABLE received_notes;")
|
||||
database.execSQL("ALTER TABLE received_notes_new RENAME TO received_notes;")
|
||||
database.execSQL("PRAGMA foreign_keys = ON;")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATION_5_6 = object : Migration(5, 6) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS utxos (
|
||||
id_utxo INTEGER PRIMARY KEY,
|
||||
address TEXT NOT NULL,
|
||||
prevout_txid BLOB NOT NULL,
|
||||
prevout_idx INTEGER NOT NULL,
|
||||
script BLOB NOT NULL,
|
||||
value_zat INTEGER NOT NULL,
|
||||
height INTEGER NOT NULL,
|
||||
spent_in_tx INTEGER,
|
||||
FOREIGN KEY (spent_in_tx) REFERENCES transactions(id_tx),
|
||||
CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx)
|
||||
);
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATION_6_7 = object : Migration(6, 7) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("PRAGMA foreign_keys = OFF;")
|
||||
database.execSQL(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS accounts_new (
|
||||
account INTEGER PRIMARY KEY,
|
||||
extfvk TEXT NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
transparent_address TEXT NOT NULL
|
||||
);
|
||||
""".trimIndent()
|
||||
)
|
||||
database.execSQL("DROP TABLE accounts;")
|
||||
database.execSQL("ALTER TABLE accounts_new RENAME TO accounts;")
|
||||
database.execSQL("PRAGMA foreign_keys = ON;")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Data Access Objects
|
||||
//
|
||||
|
||||
/**
|
||||
* The data access object for blocks, used for determining the last scanned height.
|
||||
*/
|
||||
@Dao
|
||||
interface BlockDao {
|
||||
@Query("SELECT COUNT(height) FROM blocks")
|
||||
suspend fun count(): Int
|
||||
|
||||
@Query("SELECT MAX(height) FROM blocks")
|
||||
suspend fun lastScannedHeight(): Long
|
||||
|
||||
@Query("SELECT MIN(height) FROM blocks")
|
||||
suspend fun firstScannedHeight(): Long
|
||||
|
||||
@Query("SELECT hash FROM BLOCKS WHERE height = :height")
|
||||
suspend fun findHashByHeight(height: Long): ByteArray?
|
||||
}
|
||||
|
||||
/**
|
||||
* The data access object for notes, used for determining whether transactions exist.
|
||||
*/
|
||||
@Dao
|
||||
interface ReceivedDao {
|
||||
@Query("SELECT COUNT(tx) FROM received_notes")
|
||||
suspend fun count(): Int
|
||||
}
|
||||
|
||||
/**
|
||||
* The data access object for sent notes, used for determining whether outbound transactions exist.
|
||||
*/
|
||||
@Dao
|
||||
interface SentDao {
|
||||
@Query("SELECT COUNT(tx) FROM sent_notes")
|
||||
suspend fun count(): Int
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface AccountDao {
|
||||
@Query("SELECT COUNT(account) FROM accounts")
|
||||
suspend fun count(): Int
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT account AS accountId,
|
||||
transparent_address AS rawTransparentAddress,
|
||||
address AS rawShieldedAddress
|
||||
FROM accounts
|
||||
WHERE account = :id
|
||||
"""
|
||||
)
|
||||
suspend fun findAccountById(id: Int): UnifiedAddressAccount?
|
||||
}
|
||||
|
||||
/**
|
||||
* The data access object for transactions, used for querying all transaction information, including
|
||||
* whether transactions are mined.
|
||||
*/
|
||||
@Dao
|
||||
@Suppress("TooManyFunctions")
|
||||
interface TransactionDao {
|
||||
@Query("SELECT COUNT(id_tx) FROM transactions")
|
||||
suspend fun count(): Int
|
||||
|
||||
@Query("SELECT COUNT(block) FROM transactions WHERE block IS NULL")
|
||||
suspend fun countUnmined(): Int
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT transactions.txid AS txId,
|
||||
transactions.raw AS raw,
|
||||
transactions.expiry_height AS expiryHeight
|
||||
FROM transactions
|
||||
WHERE id_tx = :id AND raw is not null
|
||||
"""
|
||||
)
|
||||
suspend fun findEncodedTransactionById(id: Long): EncodedTransaction?
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT transactions.block
|
||||
FROM transactions
|
||||
WHERE txid = :rawTransactionId
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
suspend fun findMinedHeight(rawTransactionId: ByteArray): Long?
|
||||
|
||||
/**
|
||||
* Query sent transactions that have been mined, sorted so the newest data is at the top.
|
||||
*/
|
||||
@Query(
|
||||
"""
|
||||
SELECT transactions.id_tx AS id,
|
||||
transactions.block AS minedHeight,
|
||||
transactions.tx_index AS transactionIndex,
|
||||
transactions.txid AS rawTransactionId,
|
||||
transactions.expiry_height AS expiryHeight,
|
||||
transactions.raw AS raw,
|
||||
sent_notes.address AS toAddress,
|
||||
sent_notes.value AS value,
|
||||
sent_notes.memo AS memo,
|
||||
sent_notes.id_note AS noteId,
|
||||
blocks.time AS blockTimeInSeconds
|
||||
FROM transactions
|
||||
LEFT JOIN sent_notes
|
||||
ON transactions.id_tx = sent_notes.tx
|
||||
LEFT JOIN blocks
|
||||
ON transactions.block = blocks.height
|
||||
WHERE transactions.raw IS NOT NULL
|
||||
AND minedheight > 0
|
||||
ORDER BY block IS NOT NULL, height DESC, time DESC, txid DESC
|
||||
LIMIT :limit
|
||||
"""
|
||||
)
|
||||
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
|
||||
fun getSentTransactions(limit: Int = Int.MAX_VALUE): DataSource.Factory<Int, ConfirmedTransaction>
|
||||
|
||||
/**
|
||||
* Query transactions, aggregating information on send/receive, sorted carefully so the newest
|
||||
* data is at the top and the oldest transactions are at the bottom.
|
||||
*/
|
||||
@Query(
|
||||
"""
|
||||
SELECT transactions.id_tx AS id,
|
||||
transactions.block AS minedHeight,
|
||||
transactions.tx_index AS transactionIndex,
|
||||
transactions.txid AS rawTransactionId,
|
||||
received_notes.value AS value,
|
||||
received_notes.memo AS memo,
|
||||
received_notes.id_note AS noteId,
|
||||
blocks.time AS blockTimeInSeconds
|
||||
FROM transactions
|
||||
LEFT JOIN received_notes
|
||||
ON transactions.id_tx = received_notes.tx
|
||||
LEFT JOIN blocks
|
||||
ON transactions.block = blocks.height
|
||||
WHERE received_notes.is_change != 1
|
||||
ORDER BY minedheight DESC, blocktimeinseconds DESC, id DESC
|
||||
LIMIT :limit
|
||||
"""
|
||||
)
|
||||
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
|
||||
fun getReceivedTransactions(limit: Int = Int.MAX_VALUE): DataSource.Factory<Int, ConfirmedTransaction>
|
||||
|
||||
/**
|
||||
* Query all transactions, joining outbound and inbound transactions into the same table.
|
||||
*/
|
||||
@Query(
|
||||
"""
|
||||
SELECT transactions.id_tx AS id,
|
||||
transactions.block AS minedHeight,
|
||||
transactions.tx_index AS transactionIndex,
|
||||
transactions.txid AS rawTransactionId,
|
||||
transactions.expiry_height AS expiryHeight,
|
||||
transactions.raw AS raw,
|
||||
sent_notes.address AS toAddress,
|
||||
CASE
|
||||
WHEN sent_notes.value IS NOT NULL THEN sent_notes.value
|
||||
ELSE received_notes.value
|
||||
end AS value,
|
||||
CASE
|
||||
WHEN sent_notes.memo IS NOT NULL THEN sent_notes.memo
|
||||
ELSE received_notes.memo
|
||||
end AS memo,
|
||||
CASE
|
||||
WHEN sent_notes.id_note IS NOT NULL THEN sent_notes.id_note
|
||||
ELSE received_notes.id_note
|
||||
end AS noteId,
|
||||
blocks.time AS blockTimeInSeconds
|
||||
FROM transactions
|
||||
LEFT JOIN received_notes
|
||||
ON transactions.id_tx = received_notes.tx
|
||||
LEFT JOIN sent_notes
|
||||
ON transactions.id_tx = sent_notes.tx
|
||||
LEFT JOIN blocks
|
||||
ON transactions.block = blocks.height
|
||||
/* we want all received txs except those that are change and all sent transactions (even those that haven't been
|
||||
mined yet). Note: every entry in the 'send_notes' table has a non-null value for 'address' */
|
||||
WHERE ( sent_notes.address IS NULL
|
||||
AND received_notes.is_change != 1 )
|
||||
OR sent_notes.address IS NOT NULL
|
||||
ORDER BY ( minedheight IS NOT NULL ),
|
||||
minedheight DESC,
|
||||
blocktimeinseconds DESC,
|
||||
id DESC
|
||||
LIMIT :limit
|
||||
"""
|
||||
)
|
||||
fun getAllTransactions(limit: Int = Int.MAX_VALUE): DataSource.Factory<Int, ConfirmedTransaction>
|
||||
|
||||
/**
|
||||
* Query the transactions table over the given block range, this includes transactions that
|
||||
* should not show up in most UIs. The intended purpose of this request is to find new
|
||||
* transactions that need to be enhanced via follow-up requests to the server.
|
||||
*/
|
||||
@Query(
|
||||
"""
|
||||
SELECT transactions.id_tx AS id,
|
||||
transactions.block AS minedHeight,
|
||||
transactions.tx_index AS transactionIndex,
|
||||
transactions.txid AS rawTransactionId,
|
||||
transactions.expiry_height AS expiryHeight,
|
||||
transactions.raw AS raw,
|
||||
sent_notes.address AS toAddress,
|
||||
CASE
|
||||
WHEN sent_notes.value IS NOT NULL THEN sent_notes.value
|
||||
ELSE received_notes.value
|
||||
end AS value,
|
||||
CASE
|
||||
WHEN sent_notes.memo IS NOT NULL THEN sent_notes.memo
|
||||
ELSE received_notes.memo
|
||||
end AS memo,
|
||||
CASE
|
||||
WHEN sent_notes.id_note IS NOT NULL THEN sent_notes.id_note
|
||||
ELSE received_notes.id_note
|
||||
end AS noteId,
|
||||
blocks.time AS blockTimeInSeconds
|
||||
FROM transactions
|
||||
LEFT JOIN received_notes
|
||||
ON transactions.id_tx = received_notes.tx
|
||||
LEFT JOIN sent_notes
|
||||
ON transactions.id_tx = sent_notes.tx
|
||||
LEFT JOIN blocks
|
||||
ON transactions.block = blocks.height
|
||||
WHERE :blockRangeStart <= minedheight
|
||||
AND minedheight <= :blockRangeEnd
|
||||
ORDER BY ( minedheight IS NOT NULL ),
|
||||
minedheight ASC,
|
||||
blocktimeinseconds DESC,
|
||||
id DESC
|
||||
LIMIT :limit
|
||||
"""
|
||||
)
|
||||
suspend fun findAllTransactionsByRange(
|
||||
blockRangeStart: Long,
|
||||
blockRangeEnd: Long = blockRangeStart,
|
||||
limit: Int = Int.MAX_VALUE
|
||||
): List<ConfirmedTransaction>
|
||||
|
||||
// Experimental: cleanup cancelled transactions
|
||||
// This should probably be a rust call but there's not a lot of bandwidth for this
|
||||
// work to happen in librustzcash. So prove the concept on our side, first
|
||||
// then move the logic to the right place. Especially since the data access API is
|
||||
// coming soon
|
||||
@Transaction
|
||||
suspend fun cleanupCancelledTx(rawTransactionId: ByteArray): Boolean {
|
||||
var success = false
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
try {
|
||||
var hasInitialMatch = false
|
||||
twig("[cleanup] cleanupCancelledTx starting...")
|
||||
findUnminedTransactionIds(rawTransactionId).also {
|
||||
twig("[cleanup] cleanupCancelledTx found ${it.size} matching transactions to cleanup")
|
||||
}.forEach { transactionId ->
|
||||
hasInitialMatch = true
|
||||
removeInvalidOutboundTransaction(transactionId)
|
||||
}
|
||||
val hasFinalMatch = findMatchingTransactionId(rawTransactionId) != null
|
||||
success = hasInitialMatch && !hasFinalMatch
|
||||
twig("[cleanup] cleanupCancelledTx Done. success? $success")
|
||||
} catch (t: Throwable) {
|
||||
twig("[cleanup] failed to cleanup transaction due to: $t")
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
@Transaction
|
||||
suspend fun removeInvalidOutboundTransaction(transactionId: Long): Boolean {
|
||||
var success = false
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
try {
|
||||
twig("[cleanup] removing invalid transactionId:$transactionId")
|
||||
val result = unspendTransactionNotes(transactionId)
|
||||
twig("[cleanup] unspent ($result) notes matching transaction $transactionId")
|
||||
findSentNoteIds(transactionId)?.forEach { noteId ->
|
||||
twig("[cleanup] WARNING: deleting invalid sent noteId:$noteId")
|
||||
deleteSentNote(noteId)
|
||||
}
|
||||
|
||||
// delete the UTXOs because these are effectively cached and we don't have a good way of knowing whether
|
||||
// they're spent
|
||||
deleteUtxos(transactionId).let { count ->
|
||||
twig("[cleanup] removed $count UTXOs matching transactionId $transactionId")
|
||||
}
|
||||
|
||||
twig("[cleanup] WARNING: deleting invalid transactionId $transactionId")
|
||||
success = deleteTransaction(transactionId) != 0
|
||||
twig("[cleanup] removeInvalidTransaction Done. success? $success")
|
||||
} catch (t: Throwable) {
|
||||
twig("[cleanup] failed to remove Invalid Transaction due to: $t")
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
@Transaction
|
||||
suspend fun deleteExpired(lastHeight: Long): Int {
|
||||
var count = 0
|
||||
findExpiredTxs(lastHeight).forEach { transactionId ->
|
||||
if (removeInvalidOutboundTransaction(transactionId)) count++
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
//
|
||||
// Private-ish functions (these will move to rust, or the data access API eventually)
|
||||
//
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT transactions.id_tx AS id
|
||||
FROM transactions
|
||||
WHERE txid = :rawTransactionId
|
||||
AND block IS NULL
|
||||
"""
|
||||
)
|
||||
suspend fun findUnminedTransactionIds(rawTransactionId: ByteArray): List<Long>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT transactions.id_tx AS id
|
||||
FROM transactions
|
||||
WHERE txid = :rawTransactionId
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
suspend fun findMatchingTransactionId(rawTransactionId: ByteArray): Long?
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT sent_notes.id_note AS id
|
||||
FROM sent_notes
|
||||
WHERE tx = :transactionId
|
||||
"""
|
||||
)
|
||||
suspend fun findSentNoteIds(transactionId: Long): List<Int>?
|
||||
|
||||
@Query("DELETE FROM sent_notes WHERE id_note = :id")
|
||||
suspend fun deleteSentNote(id: Int): Int
|
||||
|
||||
@Query("DELETE FROM transactions WHERE id_tx = :id")
|
||||
suspend fun deleteTransaction(id: Long): Int
|
||||
|
||||
@Query("UPDATE received_notes SET spent = null WHERE spent = :transactionId")
|
||||
suspend fun unspendTransactionNotes(transactionId: Long): Int
|
||||
|
||||
@Query("DELETE FROM utxos WHERE spent_in_tx = :utxoId")
|
||||
suspend fun deleteUtxos(utxoId: Long): Int
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT transactions.id_tx
|
||||
FROM transactions
|
||||
WHERE created IS NOT NULL
|
||||
AND block IS NULL
|
||||
AND tx_index IS NULL
|
||||
AND expiry_height < :lastheight
|
||||
"""
|
||||
)
|
||||
suspend fun findExpiredTxs(lastheight: Long): List<Long>
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
package cash.z.ecc.android.sdk.internal.db
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Database
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.Update
|
||||
import cash.z.ecc.android.sdk.db.entity.PendingTransactionEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
//
|
||||
// Database
|
||||
//
|
||||
|
||||
/**
|
||||
* Database for pending transaction information. Unlike with the "Data DB," the wallet is free to
|
||||
* write to this database. In a way, this almost serves as a local mempool for all transactions
|
||||
* initiated by this wallet. Currently, the data necessary to support expired transactions is there
|
||||
* but it is not being leveraged.
|
||||
*/
|
||||
@Database(
|
||||
entities = [
|
||||
PendingTransactionEntity::class
|
||||
],
|
||||
version = 1,
|
||||
exportSchema = true
|
||||
)
|
||||
abstract class PendingTransactionDb : RoomDatabase() {
|
||||
abstract fun pendingTransactionDao(): PendingTransactionDao
|
||||
}
|
||||
|
||||
//
|
||||
// Data Access Objects
|
||||
//
|
||||
|
||||
/**
|
||||
* Data access object providing crud for pending transactions.
|
||||
*/
|
||||
@Dao
|
||||
@Suppress("TooManyFunctions")
|
||||
interface PendingTransactionDao {
|
||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||
suspend fun create(transaction: PendingTransactionEntity): Long
|
||||
|
||||
@Update(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun update(transaction: PendingTransactionEntity)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(transaction: PendingTransactionEntity): Int
|
||||
|
||||
@Query("UPDATE pending_transactions SET cancelled = 1 WHERE id = :id")
|
||||
suspend fun cancel(id: Long)
|
||||
|
||||
@Query("SELECT * FROM pending_transactions WHERE id = :id")
|
||||
suspend fun findById(id: Long): PendingTransactionEntity?
|
||||
|
||||
@Query("SELECT * FROM pending_transactions ORDER BY createTime")
|
||||
fun getAll(): Flow<List<PendingTransactionEntity>>
|
||||
|
||||
@Query("SELECT * FROM pending_transactions WHERE id = :id")
|
||||
fun monitorById(id: Long): Flow<PendingTransactionEntity>
|
||||
|
||||
//
|
||||
// Update helper functions
|
||||
//
|
||||
|
||||
@Query("UPDATE pending_transactions SET rawTransactionId = null WHERE id = :id")
|
||||
suspend fun removeRawTransactionId(id: Long)
|
||||
|
||||
@Query("UPDATE pending_transactions SET minedHeight = :minedHeight WHERE id = :id")
|
||||
suspend fun updateMinedHeight(id: Long, minedHeight: Long)
|
||||
|
||||
@Query(
|
||||
"UPDATE pending_transactions SET raw = :raw, rawTransactionId = :rawTransactionId," +
|
||||
" expiryHeight = :expiryHeight WHERE id = :id"
|
||||
)
|
||||
suspend fun updateEncoding(id: Long, raw: ByteArray, rawTransactionId: ByteArray, expiryHeight: Long?)
|
||||
|
||||
@Query("UPDATE pending_transactions SET errorMessage = :errorMessage, errorCode = :errorCode WHERE id = :id")
|
||||
suspend fun updateError(id: Long, errorMessage: String?, errorCode: Int?)
|
||||
|
||||
@Query("UPDATE pending_transactions SET encodeAttempts = :attempts WHERE id = :id")
|
||||
suspend fun updateEncodeAttempts(id: Long, attempts: Int)
|
||||
|
||||
@Query("UPDATE pending_transactions SET submitAttempts = :attempts WHERE id = :id")
|
||||
suspend fun updateSubmitAttempts(id: Long, attempts: Int)
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package cash.z.ecc.android.sdk.internal.db
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class ReadOnlySqliteOpenHelper(
|
||||
context: Context,
|
||||
name: String,
|
||||
version: Int
|
||||
) : SQLiteOpenHelper(context, name, null, version) {
|
||||
|
||||
override fun onCreate(db: SQLiteDatabase?) {
|
||||
error("Database should be created by Rust libraries")
|
||||
}
|
||||
|
||||
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
|
||||
error("Database should be upgraded by Rust libraries")
|
||||
}
|
||||
|
||||
internal companion object {
|
||||
/**
|
||||
* Opens a database that has already been initialized by something else.
|
||||
*
|
||||
* @param context Application context.
|
||||
* @param name Database file name.
|
||||
* @param databaseVersion Version of the database as set in https://sqlite.org/pragma.html#pragma_user_version
|
||||
* This is required to bypass database creation/migration logic in Android.
|
||||
*/
|
||||
suspend fun openExistingDatabaseAsReadOnly(
|
||||
context: Context,
|
||||
name: String,
|
||||
databaseVersion: Int
|
||||
): SQLiteDatabase {
|
||||
return withContext(Dispatchers.IO) {
|
||||
ReadOnlySqliteOpenHelper(
|
||||
context,
|
||||
name,
|
||||
databaseVersion
|
||||
).readableDatabase
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package cash.z.ecc.android.sdk.internal.db
|
||||
|
||||
import android.content.Context
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
||||
import cash.z.ecc.android.sdk.exception.InitializeException
|
||||
import cash.z.ecc.android.sdk.internal.AndroidApiVersion
|
||||
import cash.z.ecc.android.sdk.internal.NoBackupContextWrapper
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
object ReadOnlySupportSqliteOpenHelper {
|
||||
|
||||
/**
|
||||
* Opens a database that has already been initialized by something else.
|
||||
*
|
||||
* @param context Application context.
|
||||
* @param file Database file name.
|
||||
* @param databaseVersion Version of the database as set in https://sqlite.org/pragma.html#pragma_user_version
|
||||
* This is required to bypass database creation/migration logic in Android.
|
||||
*/
|
||||
suspend fun openExistingDatabaseAsReadOnly(
|
||||
context: Context,
|
||||
file: File,
|
||||
databaseVersion: Int
|
||||
): SupportSQLiteDatabase {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val config = if (AndroidApiVersion.isAtLeastO_MR1) {
|
||||
val contextWrapper = NoBackupContextWrapper(
|
||||
context,
|
||||
file.parentFile ?: throw InitializeException.DatabasePathException
|
||||
)
|
||||
SupportSQLiteOpenHelper.Configuration.builder(contextWrapper)
|
||||
.apply {
|
||||
name(file.name)
|
||||
callback(ReadOnlyCallback(databaseVersion))
|
||||
}.build()
|
||||
} else {
|
||||
SupportSQLiteOpenHelper.Configuration.builder(context)
|
||||
.apply {
|
||||
name(file.absolutePath)
|
||||
callback(ReadOnlyCallback(databaseVersion))
|
||||
}.build()
|
||||
}
|
||||
|
||||
FrameworkSQLiteOpenHelperFactory().create(config).readableDatabase
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ReadOnlyCallback(version: Int) : SupportSQLiteOpenHelper.Callback(version) {
|
||||
override fun onCreate(db: SupportSQLiteDatabase) {
|
||||
error("Database ${db.path} should be created by Rust libraries")
|
||||
}
|
||||
|
||||
override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
error("Database ${db.path} should be upgraded by Rust libraries")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
@file:Suppress("ktlint:filename")
|
||||
|
||||
package cash.z.ecc.android.sdk.internal.db
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteQueryBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import java.util.Locale
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* Performs a query on a background thread.
|
||||
*
|
||||
* Note that this method is best for small queries, as Cursor has an in-memory window of cached data. If iterating
|
||||
* through a large number of items that exceeds the window, the Cursor may perform additional IO.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
internal fun <T> SQLiteDatabase.queryAndMap(
|
||||
table: String,
|
||||
columns: Array<String>? = null,
|
||||
selection: String? = null,
|
||||
selectionArgs: Array<Any>? = null,
|
||||
groupBy: String? = null,
|
||||
having: String? = null,
|
||||
orderBy: String? = null,
|
||||
limit: String? = null,
|
||||
offset: String? = null,
|
||||
coroutineContext: CoroutineContext = Dispatchers.IO,
|
||||
cursorParser: CursorParser<T>
|
||||
) = flow<T> {
|
||||
// TODO [#703]: Support blobs for argument binding
|
||||
// https://github.com/zcash/zcash-android-wallet-sdk/issues/703
|
||||
val mappedSelectionArgs = selectionArgs?.onEach {
|
||||
if (it is ByteArray) {
|
||||
throw IllegalArgumentException("ByteArray is not supported")
|
||||
}
|
||||
}?.map { it.toString() }?.toTypedArray()
|
||||
|
||||
// Counterintuitive but correct. When using the comma syntax, offset comes first.
|
||||
// When using the keyword syntax, "LIMIT 1 OFFSET 2" then the offset comes second.
|
||||
val limitAndOffset = if (null == offset) {
|
||||
limit
|
||||
} else {
|
||||
String.format(Locale.ROOT, "%s,%s", offset, limit) // NON-NLS
|
||||
}
|
||||
|
||||
query(
|
||||
table,
|
||||
columns,
|
||||
selection,
|
||||
mappedSelectionArgs,
|
||||
groupBy,
|
||||
having,
|
||||
orderBy,
|
||||
limitAndOffset
|
||||
).use {
|
||||
it.moveToPosition(-1)
|
||||
while (it.moveToNext()) {
|
||||
emit(cursorParser.newObject(it))
|
||||
}
|
||||
}
|
||||
}.flowOn(coroutineContext)
|
||||
|
||||
/**
|
||||
* Performs a query on a background thread.
|
||||
*
|
||||
* Note that this method is best for small queries, as Cursor has an in-memory window of cached data. If iterating
|
||||
* through a large number of items that exceeds the window, the Cursor may perform additional IO.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
internal fun <T> SupportSQLiteDatabase.queryAndMap(
|
||||
table: String,
|
||||
columns: Array<String>? = null,
|
||||
selection: String? = null,
|
||||
selectionArgs: Array<Any>? = null,
|
||||
groupBy: String? = null,
|
||||
having: String? = null,
|
||||
orderBy: String? = null,
|
||||
limit: String? = null,
|
||||
offset: String? = null,
|
||||
coroutineContext: CoroutineContext = Dispatchers.IO,
|
||||
cursorParser: CursorParser<T>
|
||||
) = flow<T> {
|
||||
val qb = SupportSQLiteQueryBuilder.builder(table).apply {
|
||||
columns(columns)
|
||||
selection(selection, selectionArgs)
|
||||
having(having)
|
||||
groupBy(groupBy)
|
||||
orderBy(orderBy)
|
||||
|
||||
// Counterintuitive but correct. When using the comma syntax, offset comes first.
|
||||
// When using the keyword syntax, "LIMIT 1 OFFSET 2" then the offset comes second.
|
||||
if (null == offset) {
|
||||
limit(limit)
|
||||
} else {
|
||||
limit(String.format(Locale.ROOT, "%s,%s", offset, limit)) // NON-NLS
|
||||
}
|
||||
}
|
||||
|
||||
query(qb.create()).use {
|
||||
it.moveToPosition(-1)
|
||||
while (it.moveToNext()) {
|
||||
emit(cursorParser.newObject(it))
|
||||
}
|
||||
}
|
||||
}.flowOn(coroutineContext)
|
|
@ -1,4 +1,4 @@
|
|||
package cash.z.ecc.android.sdk.internal.db
|
||||
package cash.z.ecc.android.sdk.internal.db.block
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Database
|
||||
|
@ -7,7 +7,6 @@ import androidx.room.OnConflictStrategy
|
|||
import androidx.room.Query
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.Transaction
|
||||
import cash.z.ecc.android.sdk.db.entity.CompactBlockEntity
|
||||
|
||||
//
|
||||
// Database
|
|
@ -1,7 +1,11 @@
|
|||
package cash.z.ecc.android.sdk.db.entity
|
||||
package cash.z.ecc.android.sdk.internal.db.block
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import cash.z.ecc.android.sdk.internal.model.CompactBlock
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.FirstClassByteArray
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
|
||||
@Entity(primaryKeys = ["height"], tableName = "compactblocks")
|
||||
data class CompactBlockEntity(
|
||||
|
@ -24,4 +28,14 @@ data class CompactBlockEntity(
|
|||
result = 31 * result + data.contentHashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
internal fun toCompactBlock(zcashNetwork: ZcashNetwork) = CompactBlock(
|
||||
BlockHeight.new(zcashNetwork, height),
|
||||
FirstClassByteArray(data)
|
||||
)
|
||||
|
||||
companion object {
|
||||
internal fun fromCompactBlock(compactBlock: CompactBlock) =
|
||||
CompactBlockEntity(compactBlock.height.value, compactBlock.data.byteArray)
|
||||
}
|
||||
}
|
|
@ -1,12 +1,11 @@
|
|||
package cash.z.ecc.android.sdk.internal.block
|
||||
package cash.z.ecc.android.sdk.internal.db.block
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.RoomDatabase
|
||||
import cash.z.ecc.android.sdk.db.commonDatabaseBuilder
|
||||
import cash.z.ecc.android.sdk.db.entity.CompactBlockEntity
|
||||
import cash.z.ecc.android.sdk.internal.SdkDispatchers
|
||||
import cash.z.ecc.android.sdk.internal.SdkExecutors
|
||||
import cash.z.ecc.android.sdk.internal.db.CompactBlockDb
|
||||
import cash.z.ecc.android.sdk.internal.db.commonDatabaseBuilder
|
||||
import cash.z.ecc.android.sdk.internal.repository.CompactBlockRepository
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.wallet.sdk.rpc.CompactFormats
|
||||
|
@ -17,10 +16,10 @@ import java.io.File
|
|||
* An implementation of CompactBlockStore that persists information to a database in the given
|
||||
* path. This represents the "cache db" or local cache of compact blocks waiting to be scanned.
|
||||
*/
|
||||
class CompactBlockDbStore private constructor(
|
||||
class DbCompactBlockRepository private constructor(
|
||||
private val network: ZcashNetwork,
|
||||
private val cacheDb: CompactBlockDb
|
||||
) : CompactBlockStore {
|
||||
) : CompactBlockRepository {
|
||||
|
||||
private val cacheDao = cacheDb.compactBlockDao()
|
||||
|
||||
|
@ -52,10 +51,10 @@ class CompactBlockDbStore private constructor(
|
|||
appContext: Context,
|
||||
zcashNetwork: ZcashNetwork,
|
||||
databaseFile: File
|
||||
): CompactBlockDbStore {
|
||||
): DbCompactBlockRepository {
|
||||
val cacheDb = createCompactBlockCacheDb(appContext.applicationContext, databaseFile)
|
||||
|
||||
return CompactBlockDbStore(zcashNetwork, cacheDb)
|
||||
return DbCompactBlockRepository(zcashNetwork, cacheDb)
|
||||
}
|
||||
|
||||
private fun createCompactBlockCacheDb(
|
|
@ -0,0 +1,22 @@
|
|||
package cash.z.ecc.android.sdk.internal.db.derived
|
||||
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import cash.z.ecc.android.sdk.internal.db.queryAndMap
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
internal class AccountTable(private val sqliteDatabase: SupportSQLiteDatabase) {
|
||||
companion object {
|
||||
|
||||
private val PROJECTION_COUNT = arrayOf("COUNT(*)") // $NON-NLS
|
||||
}
|
||||
|
||||
suspend fun count() = sqliteDatabase.queryAndMap(
|
||||
AccountTableDefinition.TABLE_NAME,
|
||||
columns = PROJECTION_COUNT,
|
||||
cursorParser = { it.getLong(0) }
|
||||
).first()
|
||||
}
|
||||
|
||||
object AccountTableDefinition {
|
||||
const val TABLE_NAME = "accounts" // $NON-NLS
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
package cash.z.ecc.android.sdk.internal.db.derived
|
||||
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import cash.z.ecc.android.sdk.internal.db.CursorParser
|
||||
import cash.z.ecc.android.sdk.internal.db.optBlobOrThrow
|
||||
import cash.z.ecc.android.sdk.internal.db.queryAndMap
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.FirstClassByteArray
|
||||
import cash.z.ecc.android.sdk.model.TransactionOverview
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import java.util.Locale
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
internal class AllTransactionView(
|
||||
private val zcashNetwork: ZcashNetwork,
|
||||
private val sqliteDatabase: SupportSQLiteDatabase
|
||||
) {
|
||||
companion object {
|
||||
|
||||
private val ORDER_BY = String.format(
|
||||
Locale.ROOT,
|
||||
"%s DESC, %s DESC", // $NON-NLS
|
||||
AllTransactionViewDefinition.COLUMN_INTEGER_MINED_HEIGHT,
|
||||
AllTransactionViewDefinition.COLUMN_INTEGER_ID
|
||||
)
|
||||
|
||||
private val SELECTION_BLOCK_RANGE = String.format(
|
||||
Locale.ROOT,
|
||||
"%s >= ? AND %s <= ?", // $NON-NLS
|
||||
AllTransactionViewDefinition.COLUMN_INTEGER_MINED_HEIGHT,
|
||||
AllTransactionViewDefinition.COLUMN_INTEGER_MINED_HEIGHT
|
||||
)
|
||||
|
||||
private val PROJECTION_COUNT = arrayOf("COUNT(*)") // $NON-NLS
|
||||
}
|
||||
|
||||
private val cursorParser: CursorParser<TransactionOverview> = CursorParser { cursor ->
|
||||
val idColumnIndex = cursor.getColumnIndex(AllTransactionViewDefinition.COLUMN_INTEGER_ID)
|
||||
val minedHeightColumnIndex =
|
||||
cursor.getColumnIndex(AllTransactionViewDefinition.COLUMN_INTEGER_MINED_HEIGHT)
|
||||
val transactionIndexColumnIndex = cursor.getColumnIndex(
|
||||
AllTransactionViewDefinition.COLUMN_INTEGER_TRANSACTION_INDEX
|
||||
)
|
||||
val rawTransactionIdIndex =
|
||||
cursor.getColumnIndex(AllTransactionViewDefinition.COLUMN_BLOB_RAW_TRANSACTION_ID)
|
||||
val expiryHeightIndex = cursor.getColumnIndex(AllTransactionViewDefinition.COLUMN_INTEGER_EXPIRY_HEIGHT)
|
||||
val rawIndex = cursor.getColumnIndex(AllTransactionViewDefinition.COLUMN_BLOB_RAW)
|
||||
val netValueIndex = cursor.getColumnIndex(AllTransactionViewDefinition.COLUMN_LONG_VALUE)
|
||||
val feePaidIndex = cursor.getColumnIndex(AllTransactionViewDefinition.COLUMN_LONG_FEE_PAID)
|
||||
val isChangeIndex = cursor.getColumnIndex(AllTransactionViewDefinition.COLUMN_BOOLEAN_IS_CHANGE)
|
||||
val isWalletInternalIndex = cursor.getColumnIndex(
|
||||
AllTransactionViewDefinition.COLUMN_BOOLEAN_IS_WALLET_INTERNAL
|
||||
)
|
||||
val receivedNoteCountIndex = cursor.getColumnIndex(
|
||||
AllTransactionViewDefinition.COLUMN_INTEGER_RECEIVED_NOTE_COUNT
|
||||
)
|
||||
val sentNoteCountIndex = cursor.getColumnIndex(AllTransactionViewDefinition.COLUMN_INTEGER_SENT_NOTE_COUNT)
|
||||
val memoCountIndex = cursor.getColumnIndex(AllTransactionViewDefinition.COLUMN_INTEGER_MEMO_COUNT)
|
||||
val blockTimeIndex = cursor.getColumnIndex(AllTransactionViewDefinition.COLUMN_INTEGER_BLOCK_TIME)
|
||||
|
||||
val netValueLong = cursor.getLong(netValueIndex)
|
||||
val isSent = netValueLong < 0
|
||||
|
||||
val expiryHeightLong = cursor.getLong(expiryHeightIndex)
|
||||
|
||||
TransactionOverview(
|
||||
id = cursor.getLong(idColumnIndex),
|
||||
rawId = FirstClassByteArray(cursor.getBlob(rawTransactionIdIndex)),
|
||||
minedHeight = BlockHeight.new(zcashNetwork, cursor.getLong(minedHeightColumnIndex)),
|
||||
expiryHeight = if (0L == expiryHeightLong) {
|
||||
null
|
||||
} else {
|
||||
BlockHeight.new(zcashNetwork, expiryHeightLong)
|
||||
},
|
||||
index = cursor.getLong(transactionIndexColumnIndex),
|
||||
raw = cursor.optBlobOrThrow(rawIndex)?.let { FirstClassByteArray(it) },
|
||||
isSentTransaction = isSent,
|
||||
netValue = Zatoshi(netValueLong.absoluteValue),
|
||||
feePaid = Zatoshi(cursor.getLong(feePaidIndex)),
|
||||
isChange = cursor.getInt(isChangeIndex) != 0,
|
||||
isWalletInternal = cursor.getInt(isWalletInternalIndex) != 0,
|
||||
receivedNoteCount = cursor.getInt(receivedNoteCountIndex),
|
||||
sentNoteCount = cursor.getInt(sentNoteCountIndex),
|
||||
memoCount = cursor.getInt(memoCountIndex),
|
||||
blockTimeEpochSeconds = cursor.getLong(blockTimeIndex)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun count() = sqliteDatabase.queryAndMap(
|
||||
AllTransactionViewDefinition.VIEW_NAME,
|
||||
columns = PROJECTION_COUNT,
|
||||
cursorParser = { it.getLong(0) }
|
||||
).first()
|
||||
|
||||
fun getAllTransactions() =
|
||||
sqliteDatabase.queryAndMap(
|
||||
table = AllTransactionViewDefinition.VIEW_NAME,
|
||||
orderBy = ORDER_BY,
|
||||
cursorParser = cursorParser
|
||||
)
|
||||
|
||||
fun getTransactionRange(blockHeightRange: ClosedRange<BlockHeight>) =
|
||||
sqliteDatabase.queryAndMap(
|
||||
table = AllTransactionViewDefinition.VIEW_NAME,
|
||||
orderBy = ORDER_BY,
|
||||
selection = SELECTION_BLOCK_RANGE,
|
||||
selectionArgs = arrayOf(blockHeightRange.start.value, blockHeightRange.endInclusive.value),
|
||||
cursorParser = cursorParser
|
||||
)
|
||||
|
||||
suspend fun getOldestTransaction() =
|
||||
sqliteDatabase.queryAndMap(
|
||||
table = AllTransactionViewDefinition.VIEW_NAME,
|
||||
orderBy = ORDER_BY,
|
||||
limit = "1",
|
||||
cursorParser = cursorParser
|
||||
).firstOrNull()
|
||||
}
|
||||
|
||||
internal object AllTransactionViewDefinition {
|
||||
const val VIEW_NAME = "v_transactions" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_ID = "id_tx" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_MINED_HEIGHT = "mined_height" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_TRANSACTION_INDEX = "tx_index" // $NON-NLS
|
||||
|
||||
const val COLUMN_BLOB_RAW_TRANSACTION_ID = "txid" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_EXPIRY_HEIGHT = "expiry_height" // $NON-NLS
|
||||
|
||||
const val COLUMN_BLOB_RAW = "raw" // $NON-NLS
|
||||
|
||||
const val COLUMN_LONG_VALUE = "net_value" // $NON-NLS
|
||||
|
||||
const val COLUMN_LONG_FEE_PAID = "fee_paid" // $NON-NLS
|
||||
|
||||
const val COLUMN_BOOLEAN_IS_WALLET_INTERNAL = "is_wallet_internal" // $NON-NLS
|
||||
|
||||
const val COLUMN_BOOLEAN_IS_CHANGE = "has_change" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_SENT_NOTE_COUNT = "sent_note_count" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_RECEIVED_NOTE_COUNT = "received_note_count" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_MEMO_COUNT = "memo_count" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_BLOCK_TIME = "block_time" // $NON-NLS
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
package cash.z.ecc.android.sdk.internal.db.derived
|
||||
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import cash.z.ecc.android.sdk.internal.db.queryAndMap
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import java.util.Locale
|
||||
|
||||
internal class BlockTable(private val zcashNetwork: ZcashNetwork, private val sqliteDatabase: SupportSQLiteDatabase) {
|
||||
companion object {
|
||||
|
||||
private val SELECTION_MIN_HEIGHT = arrayOf(
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
"MIN(%s)", // $NON-NLS
|
||||
BlockTableDefinition.COLUMN_LONG_HEIGHT
|
||||
)
|
||||
)
|
||||
|
||||
private val SELECTION_MAX_HEIGHT = arrayOf(
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
"MAX(%s)", // $NON-NLS
|
||||
BlockTableDefinition.COLUMN_LONG_HEIGHT
|
||||
)
|
||||
)
|
||||
|
||||
private val SELECTION_BLOCK_HEIGHT = String.format(
|
||||
Locale.ROOT,
|
||||
"%s = ?", // $NON-NLS
|
||||
BlockTableDefinition.COLUMN_LONG_HEIGHT
|
||||
)
|
||||
|
||||
private val PROJECTION_COUNT = arrayOf("COUNT(*)") // $NON-NLS
|
||||
|
||||
private val PROJECTION_HASH = arrayOf(BlockTableDefinition.COLUMN_BLOB_HASH)
|
||||
}
|
||||
|
||||
suspend fun count() = sqliteDatabase.queryAndMap(
|
||||
BlockTableDefinition.TABLE_NAME,
|
||||
columns = PROJECTION_COUNT,
|
||||
cursorParser = { it.getLong(0) }
|
||||
).first()
|
||||
|
||||
suspend fun firstScannedHeight(): BlockHeight {
|
||||
// Note that we assume the Rust layer will add the birthday height as the first block
|
||||
val heightLong =
|
||||
sqliteDatabase.queryAndMap(
|
||||
table = BlockTableDefinition.TABLE_NAME,
|
||||
columns = SELECTION_MIN_HEIGHT,
|
||||
cursorParser = { it.getLong(0) }
|
||||
).first()
|
||||
|
||||
return BlockHeight.new(zcashNetwork, heightLong)
|
||||
}
|
||||
|
||||
suspend fun lastScannedHeight(): BlockHeight {
|
||||
// Note that we assume the Rust layer will add the birthday height as the first block
|
||||
val heightLong =
|
||||
sqliteDatabase.queryAndMap(
|
||||
table = BlockTableDefinition.TABLE_NAME,
|
||||
columns = SELECTION_MAX_HEIGHT,
|
||||
cursorParser = { it.getLong(0) }
|
||||
).first()
|
||||
|
||||
return BlockHeight.new(zcashNetwork, heightLong)
|
||||
}
|
||||
|
||||
suspend fun findBlockHash(blockHeight: BlockHeight): ByteArray? {
|
||||
return sqliteDatabase.queryAndMap(
|
||||
table = BlockTableDefinition.TABLE_NAME,
|
||||
columns = PROJECTION_HASH,
|
||||
selection = SELECTION_BLOCK_HEIGHT,
|
||||
selectionArgs = arrayOf(blockHeight.value),
|
||||
cursorParser = { it.getBlob(0) }
|
||||
).firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
object BlockTableDefinition {
|
||||
const val TABLE_NAME = "blocks" // $NON-NLS
|
||||
|
||||
const val COLUMN_LONG_HEIGHT = "height" // $NON-NLS
|
||||
|
||||
const val COLUMN_BLOB_HASH = "hash" // $NON-NLS
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package cash.z.ecc.android.sdk.internal.db.derived
|
||||
|
||||
import cash.z.ecc.android.sdk.internal.model.EncodedTransaction
|
||||
import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.Transaction
|
||||
import cash.z.ecc.android.sdk.model.TransactionOverview
|
||||
import cash.z.ecc.android.sdk.model.TransactionRecipient
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
internal class DbDerivedDataRepository(
|
||||
private val derivedDataDb: DerivedDataDb
|
||||
) : DerivedDataRepository {
|
||||
private val invalidatingFlow = MutableStateFlow(UUID.randomUUID())
|
||||
|
||||
override suspend fun lastScannedHeight(): BlockHeight {
|
||||
return derivedDataDb.blockTable.lastScannedHeight()
|
||||
}
|
||||
|
||||
override suspend fun firstScannedHeight(): BlockHeight {
|
||||
return derivedDataDb.blockTable.firstScannedHeight()
|
||||
}
|
||||
|
||||
override suspend fun isInitialized(): Boolean {
|
||||
return derivedDataDb.blockTable.count() > 0
|
||||
}
|
||||
|
||||
override suspend fun findEncodedTransactionById(txId: Long): EncodedTransaction? {
|
||||
return derivedDataDb.transactionTable.findEncodedTransactionById(txId)
|
||||
}
|
||||
|
||||
override suspend fun findNewTransactions(blockHeightRange: ClosedRange<BlockHeight>): List<TransactionOverview> =
|
||||
derivedDataDb.allTransactionView.getTransactionRange(blockHeightRange).toList()
|
||||
|
||||
override suspend fun getOldestTransaction() = derivedDataDb.allTransactionView.getOldestTransaction()
|
||||
|
||||
override suspend fun findMinedHeight(rawTransactionId: ByteArray) = derivedDataDb.transactionTable
|
||||
.findMinedHeight(rawTransactionId)
|
||||
|
||||
override suspend fun findMatchingTransactionId(rawTransactionId: ByteArray) = derivedDataDb.transactionTable
|
||||
.findDatabaseId(rawTransactionId)
|
||||
|
||||
override suspend fun findBlockHash(height: BlockHeight) = derivedDataDb.blockTable.findBlockHash(height)
|
||||
|
||||
override suspend fun getTransactionCount() = derivedDataDb.transactionTable.count()
|
||||
|
||||
override fun invalidate() {
|
||||
invalidatingFlow.value = UUID.randomUUID()
|
||||
}
|
||||
|
||||
override suspend fun getAccountCount() = derivedDataDb.accountTable.count()
|
||||
// toInt() should be safe because we expect very few accounts
|
||||
.toInt()
|
||||
|
||||
override val receivedTransactions: Flow<List<Transaction.Received>>
|
||||
get() = invalidatingFlow.map { derivedDataDb.receivedTransactionView.getReceivedTransactions().toList() }
|
||||
override val sentTransactions: Flow<List<Transaction.Sent>>
|
||||
get() = invalidatingFlow.map { derivedDataDb.sentTransactionView.getSentTransactions().toList() }
|
||||
override val allTransactions: Flow<List<TransactionOverview>>
|
||||
get() = invalidatingFlow.map { derivedDataDb.allTransactionView.getAllTransactions().toList() }
|
||||
|
||||
override fun getSentNoteIds(transactionId: Long) = derivedDataDb.sentNotesTable.getSentNoteIds(transactionId)
|
||||
override fun getRecipients(transactionId: Long): Flow<TransactionRecipient> {
|
||||
return derivedDataDb.sentNotesTable.getRecipients(transactionId)
|
||||
}
|
||||
|
||||
override fun getReceivedNoteIds(transactionId: Long) =
|
||||
derivedDataDb.receivedNotesTable.getReceivedNoteIds(transactionId)
|
||||
|
||||
override suspend fun close() {
|
||||
derivedDataDb.close()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package cash.z.ecc.android.sdk.internal.db.derived
|
||||
|
||||
import android.content.Context
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import cash.z.ecc.android.sdk.internal.NoBackupContextWrapper
|
||||
import cash.z.ecc.android.sdk.internal.db.ReadOnlySupportSqliteOpenHelper
|
||||
import cash.z.ecc.android.sdk.internal.ext.tryWarn
|
||||
import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
||||
import cash.z.ecc.android.sdk.jni.RustBackend
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class DerivedDataDb private constructor(
|
||||
zcashNetwork: ZcashNetwork,
|
||||
private val sqliteDatabase: SupportSQLiteDatabase
|
||||
) {
|
||||
val accountTable = AccountTable(sqliteDatabase)
|
||||
|
||||
val blockTable = BlockTable(zcashNetwork, sqliteDatabase)
|
||||
|
||||
val transactionTable = TransactionTable(zcashNetwork, sqliteDatabase)
|
||||
|
||||
val allTransactionView = AllTransactionView(zcashNetwork, sqliteDatabase)
|
||||
|
||||
val sentTransactionView = SentTransactionView(zcashNetwork, sqliteDatabase)
|
||||
|
||||
val receivedTransactionView = ReceivedTransactionView(zcashNetwork, sqliteDatabase)
|
||||
|
||||
val sentNotesTable = SentNoteTable(zcashNetwork, sqliteDatabase)
|
||||
|
||||
val receivedNotesTable = ReceivedNoteTable(zcashNetwork, sqliteDatabase)
|
||||
|
||||
suspend fun close() {
|
||||
withContext(Dispatchers.IO) {
|
||||
sqliteDatabase.close()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Database migrations are managed by librustzcash. This is a hard-coded value to ensure that Android's
|
||||
// SqliteOpenHelper is happy
|
||||
private const val DATABASE_VERSION = 8
|
||||
|
||||
@Suppress("LongParameterList", "SpreadOperator")
|
||||
suspend fun new(
|
||||
context: Context,
|
||||
rustBackend: RustBackend,
|
||||
zcashNetwork: ZcashNetwork,
|
||||
checkpoint: Checkpoint,
|
||||
seed: ByteArray?,
|
||||
viewingKeys: List<UnifiedFullViewingKey>
|
||||
): DerivedDataDb {
|
||||
rustBackend.initDataDb(seed)
|
||||
|
||||
// TODO [#681]: consider converting these to typed exceptions in the welding layer
|
||||
// TODO [#681]: https://github.com/zcash/zcash-android-wallet-sdk/issues/681
|
||||
tryWarn(
|
||||
"Did not initialize the blocks table. It probably was already initialized.",
|
||||
ifContains = "table is not empty"
|
||||
) {
|
||||
rustBackend.initBlocksTable(checkpoint)
|
||||
}
|
||||
|
||||
tryWarn(
|
||||
"Did not initialize the accounts table. It probably was already initialized.",
|
||||
ifContains = "table is not empty"
|
||||
) {
|
||||
rustBackend.initAccountsTable(*viewingKeys.toTypedArray())
|
||||
}
|
||||
|
||||
val database = ReadOnlySupportSqliteOpenHelper.openExistingDatabaseAsReadOnly(
|
||||
NoBackupContextWrapper(
|
||||
context,
|
||||
rustBackend.dataDbFile.parentFile!!
|
||||
),
|
||||
rustBackend.dataDbFile,
|
||||
DATABASE_VERSION
|
||||
)
|
||||
|
||||
return DerivedDataDb(zcashNetwork, database)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package cash.z.ecc.android.sdk.internal.db.derived
|
||||
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import cash.z.ecc.android.sdk.internal.db.queryAndMap
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import java.util.Locale
|
||||
|
||||
internal class ReceivedNoteTable(
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private val zcashNetwork: ZcashNetwork,
|
||||
private val sqliteDatabase: SupportSQLiteDatabase
|
||||
) {
|
||||
companion object {
|
||||
|
||||
private val ORDER_BY = String.format(
|
||||
Locale.ROOT,
|
||||
"%s ASC", // $NON-NLS
|
||||
ReceivedNoteTableDefinition.COLUMN_INTEGER_ID
|
||||
)
|
||||
|
||||
private val PROJECTION_ID = arrayOf(ReceivedNoteTableDefinition.COLUMN_INTEGER_ID)
|
||||
|
||||
private val SELECT_BY_TRANSACTION_ID = String.format(
|
||||
Locale.ROOT,
|
||||
"%s = ?", // $NON-NLS
|
||||
ReceivedNoteTableDefinition.COLUMN_INTEGER_TRANSACTION_ID
|
||||
)
|
||||
}
|
||||
|
||||
fun getReceivedNoteIds(transactionId: Long) =
|
||||
sqliteDatabase.queryAndMap(
|
||||
table = ReceivedNoteTableDefinition.TABLE_NAME,
|
||||
columns = PROJECTION_ID,
|
||||
selection = SELECT_BY_TRANSACTION_ID,
|
||||
selectionArgs = arrayOf(transactionId),
|
||||
orderBy = ORDER_BY,
|
||||
cursorParser = {
|
||||
val idColumnIndex = it.getColumnIndex(ReceivedNoteTableDefinition.COLUMN_INTEGER_ID)
|
||||
|
||||
it.getLong(idColumnIndex)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// https://github.com/zcash/librustzcash/blob/277d07c79c7a08907b05a6b29730b74cdb238b97/zcash_client_sqlite/src/wallet/init.rs#L364
|
||||
internal object ReceivedNoteTableDefinition {
|
||||
const val TABLE_NAME = "received_notes" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_ID = "id_note" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_TRANSACTION_ID = "tx" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_OUTPUT_INDEX = "output_index" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_ACCOUNT = "account" // $NON-NLS
|
||||
|
||||
const val COLUMN_BLOB_DIVERSIFIER = "diversifier" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_VALUE = "value" // $NON-NLS
|
||||
|
||||
const val COLUMN_BLOB_RCM = "rcm" // $NON-NLS
|
||||
|
||||
const val COLUMN_BLOB_NF = "nf" // $NON-NLS
|
||||
|
||||
const val COLUMN_BLOB_MEMO = "memo" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_SPENT = "spent" // $NON-NLS
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
package cash.z.ecc.android.sdk.internal.db.derived
|
||||
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import cash.z.ecc.android.sdk.internal.db.optBlobOrThrow
|
||||
import cash.z.ecc.android.sdk.internal.db.queryAndMap
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.FirstClassByteArray
|
||||
import cash.z.ecc.android.sdk.model.Transaction
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import kotlinx.coroutines.flow.first
|
||||
import java.util.Locale
|
||||
|
||||
internal class ReceivedTransactionView(
|
||||
private val zcashNetwork: ZcashNetwork,
|
||||
private val sqliteDatabase: SupportSQLiteDatabase
|
||||
) {
|
||||
companion object {
|
||||
|
||||
private val ORDER_BY = String.format(
|
||||
Locale.ROOT,
|
||||
"%s DESC, %s DESC", // $NON-NLS
|
||||
ReceivedTransactionViewDefinition.COLUMN_INTEGER_MINED_HEIGHT,
|
||||
ReceivedTransactionViewDefinition.COLUMN_INTEGER_ID
|
||||
)
|
||||
|
||||
private val PROJECTION_COUNT = arrayOf("COUNT(*)") // $NON-NLS
|
||||
}
|
||||
|
||||
suspend fun count() = sqliteDatabase.queryAndMap(
|
||||
ReceivedTransactionViewDefinition.VIEW_NAME,
|
||||
columns = PROJECTION_COUNT,
|
||||
cursorParser = { it.getLong(0) }
|
||||
).first()
|
||||
|
||||
fun getReceivedTransactions() =
|
||||
sqliteDatabase.queryAndMap(
|
||||
table = ReceivedTransactionViewDefinition.VIEW_NAME,
|
||||
orderBy = ORDER_BY,
|
||||
cursorParser = { cursor ->
|
||||
val idColumnIndex = cursor.getColumnIndex(ReceivedTransactionViewDefinition.COLUMN_INTEGER_ID)
|
||||
val minedHeightColumnIndex =
|
||||
cursor.getColumnIndex(ReceivedTransactionViewDefinition.COLUMN_INTEGER_MINED_HEIGHT)
|
||||
val transactionIndexColumnIndex = cursor.getColumnIndex(
|
||||
ReceivedTransactionViewDefinition
|
||||
.COLUMN_INTEGER_TRANSACTION_INDEX
|
||||
)
|
||||
val rawTransactionIdIndex =
|
||||
cursor.getColumnIndex(ReceivedTransactionViewDefinition.COLUMN_BLOB_RAW_TRANSACTION_ID)
|
||||
val expiryHeightIndex = cursor.getColumnIndex(
|
||||
ReceivedTransactionViewDefinition.COLUMN_INTEGER_EXPIRY_HEIGHT
|
||||
)
|
||||
val rawIndex = cursor.getColumnIndex(ReceivedTransactionViewDefinition.COLUMN_BLOB_RAW)
|
||||
val receivedAccountIndex = cursor.getColumnIndex(
|
||||
ReceivedTransactionViewDefinition.COLUMN_INTEGER_RECEIVED_BY_ACCOUNT
|
||||
)
|
||||
val receivedTotalIndex =
|
||||
cursor.getColumnIndex(ReceivedTransactionViewDefinition.COLUMN_INTEGER_RECEIVED_TOTAL)
|
||||
val receivedNoteCountIndex =
|
||||
cursor.getColumnIndex(ReceivedTransactionViewDefinition.COLUMN_INTEGER_RECEIVED_NOTE_COUNT)
|
||||
val memoCountIndex = cursor.getColumnIndex(ReceivedTransactionViewDefinition.COLUMN_INTEGER_MEMO_COUNT)
|
||||
val blockTimeIndex = cursor.getColumnIndex(ReceivedTransactionViewDefinition.COLUMN_INTEGER_BLOCK_TIME)
|
||||
|
||||
val expiryHeightLong = cursor.getLong(expiryHeightIndex)
|
||||
|
||||
Transaction.Received(
|
||||
id = cursor.getLong(idColumnIndex),
|
||||
rawId = FirstClassByteArray(cursor.getBlob(rawTransactionIdIndex)),
|
||||
minedHeight = BlockHeight.new(zcashNetwork, cursor.getLong(minedHeightColumnIndex)),
|
||||
expiryHeight = if (0L == expiryHeightLong) {
|
||||
null
|
||||
} else {
|
||||
BlockHeight.new(zcashNetwork, expiryHeightLong)
|
||||
},
|
||||
index = cursor.getLong(transactionIndexColumnIndex),
|
||||
raw = cursor.optBlobOrThrow(rawIndex)?.let { FirstClassByteArray(it) },
|
||||
receivedByAccount = Account(cursor.getInt(receivedAccountIndex)),
|
||||
receivedTotal = Zatoshi(cursor.getLong(receivedTotalIndex)),
|
||||
receivedNoteCount = cursor.getInt(receivedNoteCountIndex),
|
||||
memoCount = cursor.getInt(memoCountIndex),
|
||||
blockTimeEpochSeconds = cursor.getLong(blockTimeIndex)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
internal object ReceivedTransactionViewDefinition {
|
||||
const val VIEW_NAME = "v_tx_received" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_ID = "id_tx" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_MINED_HEIGHT = "mined_height" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_TRANSACTION_INDEX = "tx_index" // $NON-NLS
|
||||
|
||||
const val COLUMN_BLOB_RAW_TRANSACTION_ID = "txid" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_EXPIRY_HEIGHT = "expiry_height" // $NON-NLS
|
||||
|
||||
const val COLUMN_BLOB_RAW = "raw" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_RECEIVED_BY_ACCOUNT = "received_by_account" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_RECEIVED_TOTAL = "received_total" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_RECEIVED_NOTE_COUNT = "received_note_count" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_MEMO_COUNT = "memo_count" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_BLOCK_TIME = "block_time" // $NON-NLS
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package cash.z.ecc.android.sdk.internal.db.derived
|
||||
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import cash.z.ecc.android.sdk.internal.db.queryAndMap
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.TransactionRecipient
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import java.util.Locale
|
||||
|
||||
internal class SentNoteTable(
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private val zcashNetwork: ZcashNetwork,
|
||||
private val sqliteDatabase: SupportSQLiteDatabase
|
||||
) {
|
||||
companion object {
|
||||
|
||||
private val ORDER_BY = String.format(
|
||||
Locale.ROOT,
|
||||
"%s ASC", // $NON-NLS
|
||||
SentNoteTableDefinition.COLUMN_INTEGER_ID
|
||||
)
|
||||
|
||||
private val PROJECTION_ID = arrayOf(SentNoteTableDefinition.COLUMN_INTEGER_ID)
|
||||
|
||||
private val PROJECTION_RECIPIENT = arrayOf(
|
||||
SentNoteTableDefinition.COLUMN_STRING_TO_ADDRESS,
|
||||
SentNoteTableDefinition.COLUMN_INTEGER_TO_ACCOUNT
|
||||
)
|
||||
|
||||
private val SELECT_BY_TRANSACTION_ID = String.format(
|
||||
Locale.ROOT,
|
||||
"%s = ?", // $NON-NLS
|
||||
SentNoteTableDefinition.COLUMN_INTEGER_TRANSACTION_ID
|
||||
)
|
||||
}
|
||||
|
||||
fun getSentNoteIds(transactionId: Long) =
|
||||
sqliteDatabase.queryAndMap(
|
||||
table = SentNoteTableDefinition.TABLE_NAME,
|
||||
columns = PROJECTION_ID,
|
||||
selection = SELECT_BY_TRANSACTION_ID,
|
||||
selectionArgs = arrayOf(transactionId),
|
||||
orderBy = ORDER_BY,
|
||||
cursorParser = {
|
||||
val idColumnIndex = it.getColumnIndex(SentNoteTableDefinition.COLUMN_INTEGER_ID)
|
||||
|
||||
it.getLong(idColumnIndex)
|
||||
}
|
||||
)
|
||||
|
||||
fun getRecipients(transactionId: Long) =
|
||||
sqliteDatabase.queryAndMap(
|
||||
table = SentNoteTableDefinition.TABLE_NAME,
|
||||
columns = PROJECTION_RECIPIENT,
|
||||
selection = SELECT_BY_TRANSACTION_ID,
|
||||
selectionArgs = arrayOf(transactionId),
|
||||
orderBy = ORDER_BY,
|
||||
cursorParser = {
|
||||
val toAccountIndex = it.getColumnIndex(SentNoteTableDefinition.COLUMN_INTEGER_TO_ACCOUNT)
|
||||
val toAddressIndex = it.getColumnIndex(SentNoteTableDefinition.COLUMN_STRING_TO_ADDRESS)
|
||||
|
||||
if (!it.isNull(toAccountIndex)) {
|
||||
TransactionRecipient.Account(Account(it.getInt(toAccountIndex)))
|
||||
} else {
|
||||
TransactionRecipient.Address(it.getString(toAddressIndex))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// https://github.com/zcash/librustzcash/blob/277d07c79c7a08907b05a6b29730b74cdb238b97/zcash_client_sqlite/src/wallet/init.rs#L393
|
||||
internal object SentNoteTableDefinition {
|
||||
const val TABLE_NAME = "sent_notes" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_ID = "id_note" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_TRANSACTION_ID = "tx" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_OUTPUT_POOL = "output_pool" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_OUTPUT_INDEX = "output_index" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_FROM_ACCOUNT = "from_account" // $NON-NLS
|
||||
|
||||
const val COLUMN_STRING_TO_ADDRESS = "to_address" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_TO_ACCOUNT = "to_account" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_VALUE = "value" // $NON-NLS
|
||||
|
||||
const val COLUMN_BLOB_MEMO = "memo" // $NON-NLS
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
package cash.z.ecc.android.sdk.internal.db.derived
|
||||
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import cash.z.ecc.android.sdk.internal.db.optBlobOrThrow
|
||||
import cash.z.ecc.android.sdk.internal.db.queryAndMap
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.FirstClassByteArray
|
||||
import cash.z.ecc.android.sdk.model.Transaction
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import kotlinx.coroutines.flow.first
|
||||
import java.util.Locale
|
||||
|
||||
internal class SentTransactionView(
|
||||
private val zcashNetwork: ZcashNetwork,
|
||||
private val sqliteDatabase: SupportSQLiteDatabase
|
||||
) {
|
||||
companion object {
|
||||
|
||||
private val ORDER_BY = String.format(
|
||||
Locale.ROOT,
|
||||
"%s DESC, %s DESC", // $NON-NLS
|
||||
SentTransactionViewDefinition.COLUMN_INTEGER_MINED_HEIGHT,
|
||||
SentTransactionViewDefinition.COLUMN_INTEGER_ID
|
||||
)
|
||||
|
||||
private val PROJECTION_COUNT = arrayOf("COUNT(*)") // $NON-NLS
|
||||
}
|
||||
|
||||
suspend fun count() = sqliteDatabase.queryAndMap(
|
||||
SentTransactionViewDefinition.VIEW_NAME,
|
||||
columns = PROJECTION_COUNT,
|
||||
cursorParser = { it.getLong(0) }
|
||||
).first()
|
||||
|
||||
fun getSentTransactions() =
|
||||
sqliteDatabase.queryAndMap(
|
||||
table = SentTransactionViewDefinition.VIEW_NAME,
|
||||
orderBy = ORDER_BY,
|
||||
cursorParser = { cursor ->
|
||||
val idColumnIndex = cursor.getColumnIndex(SentTransactionViewDefinition.COLUMN_INTEGER_ID)
|
||||
val minedHeightColumnIndex =
|
||||
cursor.getColumnIndex(SentTransactionViewDefinition.COLUMN_INTEGER_MINED_HEIGHT)
|
||||
val transactionIndexColumnIndex = cursor.getColumnIndex(
|
||||
SentTransactionViewDefinition
|
||||
.COLUMN_INTEGER_TRANSACTION_INDEX
|
||||
)
|
||||
val rawTransactionIdIndex =
|
||||
cursor.getColumnIndex(SentTransactionViewDefinition.COLUMN_BLOB_RAW_TRANSACTION_ID)
|
||||
val expiryHeightIndex = cursor.getColumnIndex(
|
||||
SentTransactionViewDefinition.COLUMN_INTEGER_EXPIRY_HEIGHT
|
||||
)
|
||||
val rawIndex = cursor.getColumnIndex(SentTransactionViewDefinition.COLUMN_BLOB_RAW)
|
||||
val sentFromAccount = cursor.getColumnIndex(
|
||||
SentTransactionViewDefinition.COLUMN_INTEGER_SENT_FROM_ACCOUNT
|
||||
)
|
||||
val sentTotalIndex = cursor.getColumnIndex(SentTransactionViewDefinition.COLUMN_INTEGER_SENT_TOTAL)
|
||||
val sentNoteCountIndex = cursor.getColumnIndex(
|
||||
SentTransactionViewDefinition.COLUMN_INTEGER_SENT_NOTE_COUNT
|
||||
)
|
||||
val memoCountIndex = cursor.getColumnIndex(SentTransactionViewDefinition.COLUMN_INTEGER_MEMO_COUNT)
|
||||
val blockTimeIndex = cursor.getColumnIndex(SentTransactionViewDefinition.COLUMN_INTEGER_BLOCK_TIME)
|
||||
|
||||
val expiryHeightLong = cursor.getLong(expiryHeightIndex)
|
||||
|
||||
Transaction.Sent(
|
||||
id = cursor.getLong(idColumnIndex),
|
||||
rawId = FirstClassByteArray(cursor.getBlob(rawTransactionIdIndex)),
|
||||
minedHeight = BlockHeight.new(zcashNetwork, cursor.getLong(minedHeightColumnIndex)),
|
||||
expiryHeight = if (0L == expiryHeightLong) {
|
||||
null
|
||||
} else {
|
||||
BlockHeight.new(zcashNetwork, expiryHeightLong)
|
||||
},
|
||||
index = cursor.getLong(transactionIndexColumnIndex),
|
||||
raw = cursor.optBlobOrThrow(rawIndex)?.let { FirstClassByteArray(it) },
|
||||
sentFromAccount = Account(cursor.getInt(sentFromAccount)),
|
||||
sentTotal = Zatoshi(cursor.getLong(sentTotalIndex)),
|
||||
sentNoteCount = cursor.getInt(sentNoteCountIndex),
|
||||
memoCount = cursor.getInt(memoCountIndex),
|
||||
blockTimeEpochSeconds = cursor.getLong(blockTimeIndex)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
internal object SentTransactionViewDefinition {
|
||||
const val VIEW_NAME = "v_tx_sent" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_ID = "id_tx" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_MINED_HEIGHT = "mined_height" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_TRANSACTION_INDEX = "tx_index" // $NON-NLS
|
||||
|
||||
const val COLUMN_BLOB_RAW_TRANSACTION_ID = "txid" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_EXPIRY_HEIGHT = "expiry_height" // $NON-NLS
|
||||
|
||||
const val COLUMN_BLOB_RAW = "raw" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_SENT_FROM_ACCOUNT = "sent_from_account" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_SENT_TOTAL = "sent_total" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_SENT_NOTE_COUNT = "sent_note_count" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_MEMO_COUNT = "memo_count" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_BLOCK_TIME = "block_time" // $NON-NLS
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
package cash.z.ecc.android.sdk.internal.db.derived
|
||||
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import cash.z.ecc.android.sdk.internal.db.queryAndMap
|
||||
import cash.z.ecc.android.sdk.internal.model.EncodedTransaction
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.FirstClassByteArray
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Locale
|
||||
|
||||
internal class TransactionTable(
|
||||
private val zcashNetwork: ZcashNetwork,
|
||||
private val sqliteDatabase: SupportSQLiteDatabase
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private val SELECTION_BLOCK_IS_NULL = String.format(
|
||||
Locale.ROOT,
|
||||
"%s IS NULL", // $NON-NLS
|
||||
TransactionTableDefinition.COLUMN_INTEGER_BLOCK
|
||||
)
|
||||
|
||||
private val PROJECTION_COUNT = arrayOf("COUNT(*)") // $NON-NLS
|
||||
|
||||
private val PROJECTION_BLOCK = arrayOf(TransactionTableDefinition.COLUMN_INTEGER_BLOCK)
|
||||
|
||||
private val PROJECTION_PRIMARY_KEY_ID = arrayOf(TransactionTableDefinition.COLUMN_INTEGER_ID)
|
||||
|
||||
private val PROJECTION_ENCODED_TRANSACTION = arrayOf(
|
||||
TransactionTableDefinition.COLUMN_BLOB_TRANSACTION_ID,
|
||||
TransactionTableDefinition.COLUMN_BLOB_RAW,
|
||||
TransactionTableDefinition.COLUMN_INTEGER_EXPIRY_HEIGHT
|
||||
)
|
||||
|
||||
private val SELECTION_RAW_TRANSACTION_ID = String.format(
|
||||
Locale.ROOT,
|
||||
"%s = ?", // $NON-NLS
|
||||
TransactionTableDefinition.COLUMN_BLOB_TRANSACTION_ID
|
||||
)
|
||||
|
||||
private val SELECTION_TRANSACTION_ID_AND_RAW_NOT_NULL = String.format(
|
||||
Locale.ROOT,
|
||||
"%s = ? AND %s IS NOT NULL", // $NON-NLS
|
||||
TransactionTableDefinition.COLUMN_INTEGER_ID,
|
||||
TransactionTableDefinition.COLUMN_BLOB_RAW
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun count() = withContext(Dispatchers.IO) {
|
||||
sqliteDatabase.queryAndMap(
|
||||
table = TransactionTableDefinition.TABLE_NAME,
|
||||
columns = PROJECTION_COUNT,
|
||||
cursorParser = { it.getLong(0) }
|
||||
).first()
|
||||
}
|
||||
|
||||
suspend fun countUnmined() =
|
||||
sqliteDatabase.queryAndMap(
|
||||
table = TransactionTableDefinition.TABLE_NAME,
|
||||
columns = PROJECTION_COUNT,
|
||||
selection = SELECTION_BLOCK_IS_NULL,
|
||||
cursorParser = { it.getLong(0) }
|
||||
).first()
|
||||
|
||||
suspend fun findEncodedTransactionById(id: Long): EncodedTransaction? {
|
||||
return sqliteDatabase.queryAndMap(
|
||||
table = TransactionTableDefinition.TABLE_NAME,
|
||||
columns = PROJECTION_ENCODED_TRANSACTION,
|
||||
selection = SELECTION_TRANSACTION_ID_AND_RAW_NOT_NULL,
|
||||
selectionArgs = arrayOf(id)
|
||||
) {
|
||||
val txIdIndex = it.getColumnIndexOrThrow(TransactionTableDefinition.COLUMN_BLOB_TRANSACTION_ID)
|
||||
val rawIndex = it.getColumnIndexOrThrow(TransactionTableDefinition.COLUMN_BLOB_RAW)
|
||||
val heightIndex = it.getColumnIndexOrThrow(TransactionTableDefinition.COLUMN_INTEGER_EXPIRY_HEIGHT)
|
||||
|
||||
val txid = it.getBlob(txIdIndex)
|
||||
val raw = it.getBlob(rawIndex)
|
||||
val expiryHeight = if (it.isNull(heightIndex)) {
|
||||
null
|
||||
} else {
|
||||
BlockHeight.new(zcashNetwork, it.getLong(heightIndex))
|
||||
}
|
||||
|
||||
EncodedTransaction(
|
||||
FirstClassByteArray(txid),
|
||||
FirstClassByteArray(raw),
|
||||
expiryHeight
|
||||
)
|
||||
}.firstOrNull()
|
||||
}
|
||||
|
||||
suspend fun findMinedHeight(rawTransactionId: ByteArray): BlockHeight? {
|
||||
return sqliteDatabase.queryAndMap(
|
||||
table = TransactionTableDefinition.TABLE_NAME,
|
||||
columns = PROJECTION_BLOCK,
|
||||
selection = SELECTION_RAW_TRANSACTION_ID,
|
||||
selectionArgs = arrayOf(rawTransactionId)
|
||||
) {
|
||||
val blockIndex = it.getColumnIndexOrThrow(TransactionTableDefinition.COLUMN_INTEGER_BLOCK)
|
||||
BlockHeight.new(zcashNetwork, it.getLong(blockIndex))
|
||||
}.firstOrNull()
|
||||
}
|
||||
|
||||
suspend fun findDatabaseId(rawTransactionId: ByteArray): Long? {
|
||||
return sqliteDatabase.queryAndMap(
|
||||
table = TransactionTableDefinition.TABLE_NAME,
|
||||
columns = PROJECTION_PRIMARY_KEY_ID,
|
||||
selection = SELECTION_RAW_TRANSACTION_ID,
|
||||
selectionArgs = arrayOf(rawTransactionId)
|
||||
) {
|
||||
val idIndex = it.getColumnIndexOrThrow(TransactionTableDefinition.COLUMN_INTEGER_ID)
|
||||
it.getLong(idIndex)
|
||||
}.firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
object TransactionTableDefinition {
|
||||
const val TABLE_NAME = "transactions" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_ID = "id_tx" // $NON-NLS
|
||||
|
||||
const val COLUMN_BLOB_TRANSACTION_ID = "txid" // $NON-NLS
|
||||
|
||||
const val COLUMN_TEXT_CREATED = "created" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_BLOCK = "block" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_TX_INDEX = "tx_index" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_EXPIRY_HEIGHT = "expiry_height" // $NON-NLS
|
||||
|
||||
const val COLUMN_BLOB_RAW = "raw" // $NON-NLS
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
package cash.z.ecc.android.sdk.internal.db.pending
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Database
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.Update
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
//
|
||||
// Database
|
||||
//
|
||||
|
||||
/**
|
||||
* Database for pending transaction information. Unlike with the "Data DB," the wallet is free to
|
||||
* write to this database. In a way, this almost serves as a local mempool for all transactions
|
||||
* initiated by this wallet. Currently, the data necessary to support expired transactions is there
|
||||
* but it is not being leveraged.
|
||||
*/
|
||||
@Database(
|
||||
entities = [
|
||||
PendingTransactionEntity::class
|
||||
],
|
||||
version = 2,
|
||||
exportSchema = true
|
||||
)
|
||||
internal abstract class PendingTransactionDb : RoomDatabase() {
|
||||
abstract fun pendingTransactionDao(): PendingTransactionDao
|
||||
|
||||
companion object {
|
||||
|
||||
/*
|
||||
* Non-automatic migration required because to_address became nullable.
|
||||
*/
|
||||
internal val MIGRATION_1_2 = object : Migration(1, 2) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL(
|
||||
"""
|
||||
ALTER TABLE pending_transactions RENAME TO pending_transactions_old;
|
||||
CREATE TABLE pending_transactions(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
to_address TEXT,
|
||||
to_internal_account_index INTEGER,
|
||||
sent_from_account_index INTEGER NOT NULL,
|
||||
mined_height INTEGER,
|
||||
expiry_height INTEGER,
|
||||
cancelled INTEGER,
|
||||
encode_attempts INTEGER DEFAULT (0),
|
||||
error_message TEXT,
|
||||
error_code INTEGER,
|
||||
submit_attempts INTEGER DEFAULT (0),
|
||||
create_time INTEGER,
|
||||
txid BLOB,
|
||||
value INTEGER NOT NULL,
|
||||
raw BLOB,
|
||||
memo BLOB,
|
||||
fee INTEGER
|
||||
);
|
||||
INSERT INTO pending_transactions
|
||||
SELECT
|
||||
id,
|
||||
toAddress,
|
||||
NULL,
|
||||
accountIndex,
|
||||
minedHeight,
|
||||
expiryHeight,
|
||||
cancelled,
|
||||
encodeAttempts,
|
||||
errorMessage,
|
||||
errorCode,
|
||||
submitAttempts,
|
||||
createTime,
|
||||
txid,
|
||||
value,
|
||||
raw,
|
||||
memo,
|
||||
NULL
|
||||
FROM pending_transactions_old;
|
||||
DROP TABLE pending_transactions_old
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Data Access Objects
|
||||
//
|
||||
|
||||
/**
|
||||
* Data access object providing crud for pending transactions.
|
||||
*/
|
||||
@Dao
|
||||
@Suppress("TooManyFunctions")
|
||||
internal interface PendingTransactionDao {
|
||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||
suspend fun create(transaction: PendingTransactionEntity): Long
|
||||
|
||||
@Update(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun update(transaction: PendingTransactionEntity)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(transaction: PendingTransactionEntity): Int
|
||||
|
||||
@Query("UPDATE pending_transactions SET cancelled = 1 WHERE id = :id")
|
||||
suspend fun cancel(id: Long)
|
||||
|
||||
@Query("SELECT * FROM pending_transactions WHERE id = :id")
|
||||
suspend fun findById(id: Long): PendingTransactionEntity?
|
||||
|
||||
@Query("SELECT * FROM pending_transactions ORDER BY create_time")
|
||||
fun getAll(): Flow<List<PendingTransactionEntity>>
|
||||
|
||||
@Query("SELECT * FROM pending_transactions WHERE id = :id")
|
||||
fun monitorById(id: Long): Flow<PendingTransactionEntity>
|
||||
|
||||
//
|
||||
// Update helper functions
|
||||
//
|
||||
|
||||
@Query("UPDATE pending_transactions SET raw_transaction_id = null WHERE id = :id")
|
||||
suspend fun removeRawTransactionId(id: Long)
|
||||
|
||||
@Query("UPDATE pending_transactions SET mined_height = :minedHeight WHERE id = :id")
|
||||
suspend fun updateMinedHeight(id: Long, minedHeight: Long)
|
||||
|
||||
@Query(
|
||||
"UPDATE pending_transactions SET raw = :raw, raw_transaction_id = :rawTransactionId," +
|
||||
" expiry_height = :expiryHeight WHERE id = :id"
|
||||
)
|
||||
suspend fun updateEncoding(id: Long, raw: ByteArray, rawTransactionId: ByteArray, expiryHeight: Long?)
|
||||
|
||||
@Query("UPDATE pending_transactions SET error_message = :errorMessage, error_code = :errorCode WHERE id = :id")
|
||||
suspend fun updateError(id: Long, errorMessage: String?, errorCode: Int?)
|
||||
|
||||
@Query("UPDATE pending_transactions SET encode_attempts = :attempts WHERE id = :id")
|
||||
suspend fun updateEncodeAttempts(id: Long, attempts: Int)
|
||||
|
||||
@Query("UPDATE pending_transactions SET submit_attempts = :attempts WHERE id = :id")
|
||||
suspend fun updateSubmitAttempts(id: Long, attempts: Int)
|
||||
}
|
|
@ -0,0 +1,213 @@
|
|||
package cash.z.ecc.android.sdk.internal.db.pending
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.FirstClassByteArray
|
||||
import cash.z.ecc.android.sdk.model.PendingTransaction
|
||||
import cash.z.ecc.android.sdk.model.TransactionRecipient
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
|
||||
@Entity(tableName = "pending_transactions")
|
||||
internal data class PendingTransactionEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Long = 0,
|
||||
@ColumnInfo(name = "to_address")
|
||||
val toAddress: String?,
|
||||
@ColumnInfo(name = "to_internal_account_index")
|
||||
val toInternalAccountIndex: Int?,
|
||||
val value: Long,
|
||||
val fee: Long?,
|
||||
val memo: ByteArray?,
|
||||
@ColumnInfo(name = "sent_from_account_index")
|
||||
val sentFromAccountIndex: Int,
|
||||
@ColumnInfo(name = "mined_height")
|
||||
val minedHeight: Long = NO_BLOCK_HEIGHT,
|
||||
@ColumnInfo(name = "expiry_height")
|
||||
val expiryHeight: Long = NO_BLOCK_HEIGHT,
|
||||
|
||||
val cancelled: Int = 0,
|
||||
@ColumnInfo(name = "encode_attempts")
|
||||
val encodeAttempts: Int = -1,
|
||||
@ColumnInfo(name = "submit_attempts")
|
||||
val submitAttempts: Int = -1,
|
||||
@ColumnInfo(name = "error_message")
|
||||
val errorMessage: String? = null,
|
||||
@ColumnInfo(name = "error_code")
|
||||
val errorCode: Int? = null,
|
||||
@ColumnInfo(name = "create_time")
|
||||
val createTime: Long = System.currentTimeMillis(),
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
||||
val raw: ByteArray = byteArrayOf(),
|
||||
@ColumnInfo(name = "raw_transaction_id", typeAffinity = ColumnInfo.BLOB)
|
||||
val rawTransactionId: ByteArray? = byteArrayOf()
|
||||
) {
|
||||
init {
|
||||
require(
|
||||
(null != toAddress && null == toInternalAccountIndex) ||
|
||||
(null == toAddress && null != toInternalAccountIndex)
|
||||
) {
|
||||
"PendingTransaction cannot contain both a toAddress and internal account"
|
||||
}
|
||||
}
|
||||
|
||||
fun toPendingTransaction(zcashNetwork: ZcashNetwork) = PendingTransaction(
|
||||
id = id,
|
||||
value = Zatoshi(value),
|
||||
fee = fee?.let { Zatoshi(it) },
|
||||
memo = memo?.let { FirstClassByteArray(it) },
|
||||
raw = FirstClassByteArray(raw),
|
||||
recipient = TransactionRecipient.new(
|
||||
toAddress,
|
||||
toInternalAccountIndex?.let { Account(toInternalAccountIndex) }
|
||||
),
|
||||
sentFromAccount = Account(sentFromAccountIndex),
|
||||
minedHeight = if (minedHeight == NO_BLOCK_HEIGHT) {
|
||||
null
|
||||
} else {
|
||||
BlockHeight.new(zcashNetwork, minedHeight)
|
||||
},
|
||||
expiryHeight = if (expiryHeight == NO_BLOCK_HEIGHT) {
|
||||
null
|
||||
} else {
|
||||
BlockHeight.new(zcashNetwork, expiryHeight)
|
||||
},
|
||||
cancelled = cancelled,
|
||||
encodeAttempts = encodeAttempts,
|
||||
submitAttempts = submitAttempts,
|
||||
errorMessage = errorMessage,
|
||||
errorCode = errorCode,
|
||||
createTime = createTime,
|
||||
rawTransactionId = rawTransactionId?.let { FirstClassByteArray(it) }
|
||||
)
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as PendingTransactionEntity
|
||||
|
||||
if (id != other.id) return false
|
||||
if (toAddress != other.toAddress) return false
|
||||
if (toInternalAccountIndex != other.toInternalAccountIndex) return false
|
||||
if (value != other.value) return false
|
||||
if (fee != other.fee) return false
|
||||
if (memo != null) {
|
||||
if (other.memo == null) return false
|
||||
if (!memo.contentEquals(other.memo)) return false
|
||||
} else if (other.memo != null) return false
|
||||
if (sentFromAccountIndex != other.sentFromAccountIndex) return false
|
||||
if (minedHeight != other.minedHeight) return false
|
||||
if (expiryHeight != other.expiryHeight) return false
|
||||
if (cancelled != other.cancelled) return false
|
||||
if (encodeAttempts != other.encodeAttempts) return false
|
||||
if (submitAttempts != other.submitAttempts) return false
|
||||
if (errorMessage != other.errorMessage) return false
|
||||
if (errorCode != other.errorCode) return false
|
||||
if (createTime != other.createTime) return false
|
||||
if (!raw.contentEquals(other.raw)) return false
|
||||
if (rawTransactionId != null) {
|
||||
if (other.rawTransactionId == null) return false
|
||||
if (!rawTransactionId.contentEquals(other.rawTransactionId)) return false
|
||||
} else if (other.rawTransactionId != null) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + (toAddress?.hashCode() ?: 0)
|
||||
result = 31 * result + (toInternalAccountIndex ?: 0)
|
||||
result = 31 * result + value.hashCode()
|
||||
result = 31 * result + fee.hashCode()
|
||||
result = 31 * result + (memo?.contentHashCode() ?: 0)
|
||||
result = 31 * result + sentFromAccountIndex
|
||||
result = 31 * result + minedHeight.hashCode()
|
||||
result = 31 * result + expiryHeight.hashCode()
|
||||
result = 31 * result + cancelled
|
||||
result = 31 * result + encodeAttempts
|
||||
result = 31 * result + submitAttempts
|
||||
result = 31 * result + (errorMessage?.hashCode() ?: 0)
|
||||
result = 31 * result + (errorCode ?: 0)
|
||||
result = 31 * result + createTime.hashCode()
|
||||
result = 31 * result + raw.contentHashCode()
|
||||
result = 31 * result + (rawTransactionId?.contentHashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val NO_BLOCK_HEIGHT = -1L
|
||||
|
||||
fun from(pendingTransaction: PendingTransaction): PendingTransactionEntity {
|
||||
val toAddress = if (pendingTransaction.recipient is TransactionRecipient.Address) {
|
||||
pendingTransaction.recipient.addressValue
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val toInternal = if (pendingTransaction.recipient is TransactionRecipient.Account) {
|
||||
pendingTransaction.recipient.accountValue
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return PendingTransactionEntity(
|
||||
id = pendingTransaction.id,
|
||||
value = pendingTransaction.value.value,
|
||||
fee = pendingTransaction.fee?.value,
|
||||
memo = pendingTransaction.memo?.byteArray,
|
||||
raw = pendingTransaction.raw.byteArray,
|
||||
toAddress = toAddress,
|
||||
toInternalAccountIndex = toInternal?.value,
|
||||
sentFromAccountIndex = pendingTransaction.sentFromAccount.value,
|
||||
minedHeight = pendingTransaction.minedHeight?.value ?: NO_BLOCK_HEIGHT,
|
||||
expiryHeight = pendingTransaction.expiryHeight?.value ?: NO_BLOCK_HEIGHT,
|
||||
cancelled = pendingTransaction.cancelled,
|
||||
encodeAttempts = pendingTransaction.encodeAttempts,
|
||||
submitAttempts = pendingTransaction.submitAttempts,
|
||||
errorMessage = pendingTransaction.errorMessage,
|
||||
errorCode = pendingTransaction.errorCode,
|
||||
createTime = pendingTransaction.createTime,
|
||||
rawTransactionId = pendingTransaction.rawTransactionId?.byteArray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal val PendingTransactionEntity.recipient: TransactionRecipient
|
||||
get() {
|
||||
return TransactionRecipient.new(toAddress, toInternalAccountIndex?.let { Account(it) })
|
||||
}
|
||||
|
||||
internal fun PendingTransactionEntity.isSubmitted(): Boolean {
|
||||
return submitAttempts > 0
|
||||
}
|
||||
|
||||
internal fun PendingTransactionEntity.isFailedEncoding() = raw.isNotEmpty() && encodeAttempts > 0
|
||||
|
||||
internal fun PendingTransactionEntity.isCancelled(): Boolean {
|
||||
return cancelled > 0
|
||||
}
|
||||
|
||||
private fun TransactionRecipient.Companion.new(
|
||||
toAddress: String?,
|
||||
toInternalAccountIndex: Account?
|
||||
): TransactionRecipient {
|
||||
require(
|
||||
(null != toAddress && null == toInternalAccountIndex) ||
|
||||
(null == toAddress && null != toInternalAccountIndex)
|
||||
) {
|
||||
"Pending transaction cannot contain both a toAddress and internal account"
|
||||
}
|
||||
|
||||
if (null != toAddress) {
|
||||
return TransactionRecipient.Address(toAddress)
|
||||
} else if (null != toInternalAccountIndex) {
|
||||
return TransactionRecipient.Account(toInternalAccountIndex)
|
||||
}
|
||||
|
||||
error("Pending transaction recipient require a toAddress or an internal account")
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package cash.z.ecc.android.sdk.internal.model
|
||||
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.FirstClassByteArray
|
||||
|
||||
internal data class Block(
|
||||
val height: BlockHeight,
|
||||
val hash: FirstClassByteArray,
|
||||
val time: Int,
|
||||
val saplingTree: FirstClassByteArray
|
||||
)
|
|
@ -0,0 +1,9 @@
|
|||
package cash.z.ecc.android.sdk.internal.model
|
||||
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.FirstClassByteArray
|
||||
|
||||
internal data class CompactBlock(
|
||||
val height: BlockHeight,
|
||||
val data: FirstClassByteArray
|
||||
)
|
|
@ -0,0 +1,10 @@
|
|||
package cash.z.ecc.android.sdk.internal.model
|
||||
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.FirstClassByteArray
|
||||
|
||||
internal data class EncodedTransaction(
|
||||
val txId: FirstClassByteArray,
|
||||
val raw: FirstClassByteArray,
|
||||
val expiryHeight: BlockHeight?
|
||||
)
|
|
@ -1,4 +1,4 @@
|
|||
package cash.z.ecc.android.sdk.internal.block
|
||||
package cash.z.ecc.android.sdk.internal.repository
|
||||
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.wallet.sdk.rpc.CompactFormats
|
||||
|
@ -6,7 +6,7 @@ import cash.z.wallet.sdk.rpc.CompactFormats
|
|||
/**
|
||||
* Interface for storing compact blocks.
|
||||
*/
|
||||
interface CompactBlockStore {
|
||||
interface CompactBlockRepository {
|
||||
/**
|
||||
* Gets the highest block that is currently stored.
|
||||
*
|
|
@ -1,16 +1,17 @@
|
|||
package cash.z.ecc.android.sdk.internal.transaction
|
||||
package cash.z.ecc.android.sdk.internal.repository
|
||||
|
||||
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
|
||||
import cash.z.ecc.android.sdk.db.entity.EncodedTransaction
|
||||
import cash.z.ecc.android.sdk.internal.model.EncodedTransaction
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.type.UnifiedAddressAccount
|
||||
import cash.z.ecc.android.sdk.model.Transaction
|
||||
import cash.z.ecc.android.sdk.model.TransactionOverview
|
||||
import cash.z.ecc.android.sdk.model.TransactionRecipient
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Repository of wallet transactions, providing an agnostic interface to the underlying information.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface TransactionRepository {
|
||||
internal interface DerivedDataRepository {
|
||||
|
||||
/**
|
||||
* The last height scanned by this repository.
|
||||
|
@ -27,8 +28,6 @@ interface TransactionRepository {
|
|||
suspend fun firstScannedHeight(): BlockHeight
|
||||
|
||||
/**
|
||||
* Returns true when this repository has been initialized and seeded with the initial checkpoint.
|
||||
*
|
||||
* @return true when this repository has been initialized and seeded with the initial checkpoint.
|
||||
*/
|
||||
suspend fun isInitialized(): Boolean
|
||||
|
@ -53,7 +52,9 @@ interface TransactionRepository {
|
|||
*
|
||||
* @return a list of transactions that were mined in the given range, inclusive.
|
||||
*/
|
||||
suspend fun findNewTransactions(blockHeightRange: ClosedRange<BlockHeight>): List<ConfirmedTransaction>
|
||||
suspend fun findNewTransactions(blockHeightRange: ClosedRange<BlockHeight>): List<TransactionOverview>
|
||||
|
||||
suspend fun getOldestTransaction(): TransactionOverview?
|
||||
|
||||
/**
|
||||
* Find the mined height that matches the given raw tx_id in bytes. This is useful for matching
|
||||
|
@ -67,38 +68,48 @@ interface TransactionRepository {
|
|||
|
||||
suspend fun findMatchingTransactionId(rawTransactionId: ByteArray): Long?
|
||||
|
||||
// TODO [#681]: begin converting these into Data Access API. For now, just collect the desired
|
||||
// operations and iterate/refactor, later
|
||||
// TODO [#681]: https://github.com/zcash/zcash-android-wallet-sdk/issues/681
|
||||
suspend fun findBlockHash(height: BlockHeight): ByteArray?
|
||||
|
||||
suspend fun getTransactionCount(): Long
|
||||
|
||||
/**
|
||||
* Provides a way for other components to signal that the underlying data has been modified.
|
||||
*/
|
||||
fun invalidate()
|
||||
|
||||
/**
|
||||
* When a transaction has been cancelled by the user, we need a bridge to clean it up from the
|
||||
* dataDb. This function will safely remove everything related to that transaction in the right
|
||||
* order to satisfy foreign key constraints, even if cascading isn't setup in the DB.
|
||||
*
|
||||
* @return true when an unmined transaction was found and then successfully removed
|
||||
*/
|
||||
suspend fun cleanupCancelledTx(rawTransactionId: ByteArray): Boolean
|
||||
|
||||
suspend fun deleteExpired(lastScannedHeight: BlockHeight): Int
|
||||
|
||||
suspend fun count(): Int
|
||||
|
||||
suspend fun getAccount(accountId: Int): UnifiedAddressAccount?
|
||||
|
||||
suspend fun getAccountCount(): Int
|
||||
|
||||
//
|
||||
// Transactions
|
||||
//
|
||||
|
||||
/*
|
||||
* Note there are two big limitations with this implementation:
|
||||
* 1. Clients don't receive notification if the underlying data changes. A flow of flows could help there.
|
||||
* 2. Pagination isn't supported. Although flow does a good job of allowing the data to be processed as a stream,
|
||||
* that doesn't work so well in UI when users might scroll forwards/backwards.
|
||||
*
|
||||
* We'll come back to this and improve it in the future. This implementation is already an improvement over
|
||||
* prior versions.
|
||||
*/
|
||||
|
||||
/** A flow of all the inbound confirmed transactions */
|
||||
val receivedTransactions: Flow<List<ConfirmedTransaction>>
|
||||
val receivedTransactions: Flow<List<Transaction.Received>>
|
||||
|
||||
/** A flow of all the outbound confirmed transactions */
|
||||
val sentTransactions: Flow<List<ConfirmedTransaction>>
|
||||
val sentTransactions: Flow<List<Transaction.Sent>>
|
||||
|
||||
/** A flow of all the inbound and outbound confirmed transactions */
|
||||
val allTransactions: Flow<List<ConfirmedTransaction>>
|
||||
val allTransactions: Flow<List<TransactionOverview>>
|
||||
|
||||
fun getSentNoteIds(transactionId: Long): Flow<Long>
|
||||
|
||||
fun getRecipients(transactionId: Long): Flow<TransactionRecipient>
|
||||
|
||||
fun getReceivedNoteIds(transactionId: Long): Flow<Long>
|
||||
|
||||
suspend fun close()
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue