SDK changes to support new demo app.
- Convert status flags into stream of statuses, instead. - Improve logging during transaction submission - Database corrections. Aparently Room has gotten more strict with schema parsing and this required lots of corrections mainly around get nullability correct for DB fields. - Simplify Synchronizer creation via constructor functions. Created one function for maximum simplicity and another for maximum flexibility. - Changed logic for Wallet initialization to simplify dependencies and allow for optional access to private keys for wallet apps - Created TransactionRepository that leverages the paging library for Room - Provided sample implementation of bridging to a key manager in a way where wallet apps do not have to modify their existing code. - Made it easier to clear the wallet data that can be repopulated from the blockchain - Allowed for better cleanup of heavy-weight lightwalletd services by adding a shutdown API call
This commit is contained in:
parent
190f7f5548
commit
f89d2be250
16
build.gradle
16
build.gradle
|
@ -6,12 +6,13 @@ buildscript {
|
|||
]
|
||||
ext.versions = [
|
||||
'architectureComponents': [
|
||||
'lifecycle': '2.2.0-alpha04',
|
||||
'room': '2.1.0-rc01'
|
||||
'lifecycle': '2.2.0-alpha05',
|
||||
'room': '2.2.0',
|
||||
'paging': '2.1.0'
|
||||
],
|
||||
'grpc':'1.21.0',
|
||||
'kotlin': '1.3.50',
|
||||
'coroutines': '1.3.0-M1',
|
||||
'coroutines': '1.3.2',
|
||||
'junitJupiter': '5.5.0-M1'
|
||||
]
|
||||
repositories {
|
||||
|
@ -22,7 +23,7 @@ buildscript {
|
|||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.6.0-alpha11'
|
||||
classpath 'com.android.tools.build:gradle:3.6.0-beta01'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
|
||||
classpath "org.jetbrains.kotlin:kotlin-allopen:${versions.kotlin}"
|
||||
classpath "org.jetbrains.dokka:dokka-gradle-plugin:0.9.18"
|
||||
|
@ -50,7 +51,7 @@ apply plugin: 'org.mozilla.rust-android-gradle.rust-android'
|
|||
apply plugin: 'org.owasp.dependencycheck'
|
||||
|
||||
group = 'cash.z.android.wallet'
|
||||
version = '1.0.0-alpha02'
|
||||
version = '1.0.0-alpha03'
|
||||
|
||||
repositories {
|
||||
google()
|
||||
|
@ -65,7 +66,7 @@ android {
|
|||
defaultConfig {
|
||||
minSdkVersion buildConfig.minSdkVersion
|
||||
targetSdkVersion buildConfig.targetSdkVersion
|
||||
versionCode = 1_00_00_002 // last digits are alpha(0XX) beta(2XX) rc(4XX) release(8XX). Ex: 1_08_04_401 is an release candidate build of version 1.8.4 and 1_08_04_800 would be the final release.
|
||||
versionCode = 1_00_00_003 // last digits are alpha(0XX) beta(2XX) rc(4XX) release(8XX). Ex: 1_08_04_401 is an release candidate build of version 1.8.4 and 1_08_04_800 would be the final release.
|
||||
versionName = "$version"
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
|
@ -195,6 +196,7 @@ dependencies {
|
|||
// Architecture Components: Room
|
||||
implementation "androidx.room:room-runtime:${versions.architectureComponents.room}"
|
||||
implementation "androidx.room:room-common:${versions.architectureComponents.room}"
|
||||
implementation "androidx.paging:paging-runtime-ktx:${versions.architectureComponents.paging}"
|
||||
kapt "androidx.room:room-compiler:${versions.architectureComponents.room}"
|
||||
|
||||
// Kotlin
|
||||
|
@ -233,7 +235,7 @@ dependencies {
|
|||
androidTestImplementation 'org.mockito:mockito-android:2.25.1'
|
||||
androidTestImplementation "androidx.test:runner:1.2.0"
|
||||
androidTestImplementation "androidx.test:core:1.2.0"
|
||||
androidTestImplementation "androidx.arch.core:core-testing:2.1.0-rc01"
|
||||
androidTestImplementation "androidx.arch.core:core-testing:2.1.0"
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
}
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
package cash.z.wallet.sdk.demoapp
|
||||
|
||||
import android.app.Application
|
||||
import cash.z.wallet.sdk.demoapp.util.DemoConfig
|
||||
|
||||
class App : Application() {
|
||||
|
||||
var defaultConfig = DemoConfig()
|
||||
|
||||
override fun onCreate() {
|
||||
instance = this
|
||||
super.onCreate()
|
||||
|
|
|
@ -39,7 +39,10 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
|
|||
|
||||
override fun onClear() {
|
||||
ledger.close()
|
||||
synchronizer.stop()
|
||||
(synchronizer as SdkSynchronizer).apply {
|
||||
stop()
|
||||
clearData()
|
||||
}
|
||||
}
|
||||
|
||||
private fun monitorStatus() {
|
||||
|
|
|
@ -2,6 +2,7 @@ package cash.z.wallet.sdk.block
|
|||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import cash.z.wallet.sdk.annotation.OpenForTesting
|
||||
import cash.z.wallet.sdk.block.CompactBlockProcessor.State.*
|
||||
import cash.z.wallet.sdk.data.TransactionRepository
|
||||
import cash.z.wallet.sdk.data.Twig
|
||||
import cash.z.wallet.sdk.data.twig
|
||||
|
@ -15,12 +16,12 @@ import cash.z.wallet.sdk.ext.ZcashSdk.REWIND_DISTANCE
|
|||
import cash.z.wallet.sdk.ext.ZcashSdk.SAPLING_ACTIVATION_HEIGHT
|
||||
import cash.z.wallet.sdk.ext.retryUpTo
|
||||
import cash.z.wallet.sdk.ext.retryWithBackoff
|
||||
import cash.z.wallet.sdk.jni.RustBackend
|
||||
import cash.z.wallet.sdk.jni.RustBackendWelding
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
@ -37,15 +38,17 @@ import kotlin.math.roundToInt
|
|||
class CompactBlockProcessor(
|
||||
internal val downloader: CompactBlockDownloader,
|
||||
private val repository: TransactionRepository,
|
||||
private val rustBackend: RustBackendWelding
|
||||
private val rustBackend: RustBackendWelding,
|
||||
minimumHeight: Int = SAPLING_ACTIVATION_HEIGHT
|
||||
) {
|
||||
var onErrorListener: ((Throwable) -> Boolean)? = null
|
||||
var isConnected: Boolean = false
|
||||
var isSyncing: Boolean = false
|
||||
var isScanning: Boolean = false
|
||||
|
||||
private val progressChannel = ConflatedBroadcastChannel(0)
|
||||
private var isStopped = false
|
||||
private val consecutiveChainErrors = AtomicInteger(0)
|
||||
private val lowerBoundHeight: Int = max(SAPLING_ACTIVATION_HEIGHT, minimumHeight - MAX_REORG_SIZE)
|
||||
|
||||
private val _state: ConflatedBroadcastChannel<State> = ConflatedBroadcastChannel(Initialized)
|
||||
val state = _state.asFlow()
|
||||
|
||||
fun progress(): ReceiveChannel<Int> = progressChannel.openSubscription()
|
||||
|
||||
|
@ -62,8 +65,6 @@ class CompactBlockProcessor(
|
|||
val result = processNewBlocks()
|
||||
// immediately process again after failures in order to download new blocks right away
|
||||
if (result < 0) {
|
||||
isSyncing = false
|
||||
isScanning = false
|
||||
consecutiveChainErrors.set(0)
|
||||
twig("Successfully processed new blocks. Sleeping for ${POLL_INTERVAL}ms")
|
||||
delay(POLL_INTERVAL)
|
||||
|
@ -77,16 +78,16 @@ class CompactBlockProcessor(
|
|||
consecutiveChainErrors.getAndIncrement()
|
||||
}
|
||||
}
|
||||
} while (isActive && !isStopped)
|
||||
} while (isActive && _state.value !is Stopped)
|
||||
twig("processor complete")
|
||||
stop()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
isStopped = true
|
||||
suspend fun stop() {
|
||||
setState(Stopped)
|
||||
}
|
||||
|
||||
fun fail(error: Throwable) {
|
||||
private suspend fun fail(error: Throwable) {
|
||||
stop()
|
||||
twig("${error.message}")
|
||||
throw error
|
||||
|
@ -103,27 +104,33 @@ class CompactBlockProcessor(
|
|||
|
||||
// define ranges
|
||||
val latestBlockHeight = downloader.getLatestBlockHeight()
|
||||
isConnected = true // no exception on downloader call
|
||||
isSyncing = true
|
||||
val lastDownloadedHeight = max(getLastDownloadedHeight(), SAPLING_ACTIVATION_HEIGHT - 1)
|
||||
val lastDownloadedHeight = getLastDownloadedHeight()
|
||||
val lastScannedHeight = getLastScannedHeight()
|
||||
val boundedLastDownloadedHeight = max(lastDownloadedHeight, lowerBoundHeight - 1)
|
||||
|
||||
twig("latestBlockHeight: $latestBlockHeight\tlastDownloadedHeight: $lastDownloadedHeight" +
|
||||
"\tlastScannedHeight: $lastScannedHeight")
|
||||
twig(
|
||||
"latestBlockHeight: $latestBlockHeight\tlastDownloadedHeight: $lastDownloadedHeight" +
|
||||
"\tlastScannedHeight: $lastScannedHeight\tlowerBoundHeight: $lowerBoundHeight"
|
||||
)
|
||||
|
||||
// as long as the database has the sapling tree (like when it's initialized from a checkpoint) we can avoid
|
||||
// downloading earlier blocks so take the larger of these two numbers
|
||||
val rangeToDownload = (max(lastDownloadedHeight, lastScannedHeight) + 1)..latestBlockHeight
|
||||
val rangeToDownload = (max(boundedLastDownloadedHeight, lastScannedHeight) + 1)..latestBlockHeight
|
||||
val rangeToScan = (lastScannedHeight + 1)..latestBlockHeight
|
||||
|
||||
setState(Downloading)
|
||||
downloadNewBlocks(rangeToDownload)
|
||||
val error = validateNewBlocks(rangeToScan)
|
||||
|
||||
setState(Validating)
|
||||
var error = validateNewBlocks(rangeToScan)
|
||||
if (error < 0) {
|
||||
// in theory, a scan should not fail after validation succeeds but maybe consider
|
||||
// changing the rust layer to return the failed block height whenever scan does fail
|
||||
// rather than a boolean
|
||||
setState(Scanning)
|
||||
val success = scanNewBlocks(rangeToScan)
|
||||
if (!success) throw CompactBlockProcessorException.FailedScan
|
||||
else setState(Synced)
|
||||
-1
|
||||
} else {
|
||||
error
|
||||
|
@ -136,6 +143,7 @@ class CompactBlockProcessor(
|
|||
if (range.isEmpty()) {
|
||||
twig("no blocks to download")
|
||||
} else {
|
||||
_state.send(Downloading)
|
||||
Twig.sprout("downloading")
|
||||
twig("downloading blocks in range $range")
|
||||
|
||||
|
@ -181,9 +189,7 @@ class CompactBlockProcessor(
|
|||
}
|
||||
Twig.sprout("scanning")
|
||||
twig("scanning blocks in range $range")
|
||||
isScanning = true
|
||||
val result = rustBackend.scanBlocks()
|
||||
isScanning = false
|
||||
Twig.clip("scanning")
|
||||
return result
|
||||
}
|
||||
|
@ -196,15 +202,13 @@ class CompactBlockProcessor(
|
|||
}
|
||||
|
||||
private fun onConnectionError(throwable: Throwable): Boolean {
|
||||
isConnected = false
|
||||
isSyncing = false
|
||||
isScanning = false
|
||||
_state.offer(Disconnected)
|
||||
return onErrorListener?.invoke(throwable) ?: true
|
||||
}
|
||||
|
||||
private fun determineLowerBound(errorHeight: Int): Int {
|
||||
val offset = Math.min(MAX_REORG_SIZE, REWIND_DISTANCE * (consecutiveChainErrors.get() + 1))
|
||||
return Math.max(errorHeight - offset, SAPLING_ACTIVATION_HEIGHT)
|
||||
return Math.max(errorHeight - offset, lowerBoundHeight)
|
||||
}
|
||||
|
||||
suspend fun getLastDownloadedHeight() = withContext(IO) {
|
||||
|
@ -214,4 +218,19 @@ class CompactBlockProcessor(
|
|||
suspend fun getLastScannedHeight() = withContext(IO) {
|
||||
repository.lastScannedHeight()
|
||||
}
|
||||
|
||||
suspend fun setState(newState: State) {
|
||||
_state.send(newState)
|
||||
}
|
||||
|
||||
sealed class State {
|
||||
interface Connected
|
||||
object Downloading : Connected, State()
|
||||
object Validating : Connected, State()
|
||||
object Scanning : Connected, State()
|
||||
object Synced : Connected, State()
|
||||
object Disconnected : State()
|
||||
object Stopped : State()
|
||||
object Initialized : State()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package cash.z.wallet.sdk.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.paging.PagedList
|
||||
import androidx.paging.toLiveData
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import cash.z.wallet.sdk.db.BlockDao
|
||||
import cash.z.wallet.sdk.db.DerivedDataDb
|
||||
import cash.z.wallet.sdk.db.TransactionDao
|
||||
import cash.z.wallet.sdk.entity.ClearedTransaction
|
||||
import cash.z.wallet.sdk.entity.Transaction
|
||||
import cash.z.wallet.sdk.ext.ZcashSdk
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Example of a repository that leverages the Room paging library to return a [PagedList] of
|
||||
* transactions. Consumers can register as a page listener and receive an interface that allows for
|
||||
* efficiently paging data.
|
||||
*
|
||||
* @param pageSize transactions per page. This influences pre-fetch and memory configuration.
|
||||
*/
|
||||
open class PagedTransactionRepository(
|
||||
private val derivedDataDb: DerivedDataDb,
|
||||
private val pageSize: Int = 10
|
||||
) : TransactionRepository {
|
||||
|
||||
/**
|
||||
* Constructor that creates the database.
|
||||
*/
|
||||
constructor(
|
||||
context: Context,
|
||||
pageSize: Int = 10,
|
||||
dataDbName: String = ZcashSdk.DB_DATA_NAME
|
||||
) : this(
|
||||
Room.databaseBuilder(context, DerivedDataDb::class.java, dataDbName)
|
||||
.setJournalMode(RoomDatabase.JournalMode.TRUNCATE)
|
||||
.build(),
|
||||
pageSize
|
||||
)
|
||||
|
||||
private val blocks: BlockDao = derivedDataDb.blockDao()
|
||||
private val transactions: TransactionDao = derivedDataDb.transactionDao()
|
||||
private val transactionLivePagedList =
|
||||
transactions.getReceivedTransactions().toLiveData(pageSize)
|
||||
|
||||
/**
|
||||
* The primary function of this repository. Callers to this method receive a [PagedList]
|
||||
* snapshot of the current data source that can then be queried by page, via the normal [List]
|
||||
* API. Meaning, the details of the paging behavior, including caching and pre-fetch are handled
|
||||
* automatically. This integrates directly with the RecyclerView for seamless display of a large
|
||||
* number of transactions.
|
||||
*/
|
||||
fun setTransactionPageListener(
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
listener: (PagedList<out ClearedTransaction>) -> Unit
|
||||
) {
|
||||
transactionLivePagedList.removeObservers(lifecycleOwner)
|
||||
transactionLivePagedList.observe(lifecycleOwner, Observer { transactions ->
|
||||
listener(transactions)
|
||||
})
|
||||
}
|
||||
|
||||
override fun lastScannedHeight(): Int {
|
||||
return blocks.lastScannedHeight()
|
||||
}
|
||||
|
||||
override fun isInitialized(): Boolean {
|
||||
return blocks.count() > 0
|
||||
}
|
||||
|
||||
override suspend fun findTransactionById(txId: Long): Transaction? = withContext(IO) {
|
||||
twig("finding transaction with id $txId on thread ${Thread.currentThread().name}")
|
||||
val transaction = transactions.findById(txId)
|
||||
twig("found ${transaction?.id}")
|
||||
transaction
|
||||
}
|
||||
|
||||
override suspend fun findTransactionByRawId(rawTxId: ByteArray): Transaction? = withContext(IO) {
|
||||
transactions.findByRawId(rawTxId)
|
||||
}
|
||||
|
||||
override suspend fun deleteTransactionById(txId: Long) = withContext(IO) {
|
||||
twigTask("deleting transaction with id $txId") {
|
||||
transactions.deleteById(txId)
|
||||
}
|
||||
}
|
||||
|
||||
fun close() {
|
||||
derivedDataDb.close()
|
||||
}
|
||||
|
||||
}
|
|
@ -105,14 +105,11 @@ class PersistentTransactionManager(private val db: PendingTransactionDb) : Trans
|
|||
override suspend fun manageSubmission(service: LightWalletService, pendingTransaction: SignedTransaction) {
|
||||
var tx = pendingTransaction as PendingTransaction
|
||||
try {
|
||||
twig("managing the preparation to submit transaction memo: ${tx.memo} amount: ${tx.value}")
|
||||
twig("submitting transaction to lightwalletd - memo: ${tx.memo} amount: ${tx.value}")
|
||||
val response = service.submitTransaction(pendingTransaction.raw!!)
|
||||
twig("management of submit transaction completed with response: ${response.errorCode}: ${response.errorMessage}")
|
||||
tx = if (response.errorCode < 0) {
|
||||
tx.copy(errorMessage = response.errorMessage, errorCode = response.errorCode)
|
||||
} else {
|
||||
tx.copy(errorMessage = null, errorCode = response.errorCode)
|
||||
}
|
||||
val error = response.errorCode < 0
|
||||
twig("${if (error) "FAILURE! " else "SUCCESS!"} submit transaction completed with response: ${response.errorCode}: ${response.errorMessage}")
|
||||
tx = tx.copy(errorMessage = if (error) response.errorMessage else null, errorCode = response.errorCode)
|
||||
} catch (t: Throwable) {
|
||||
twig("error while managing submitting transaction: ${t.message} caused by: ${t.cause}")
|
||||
} finally {
|
||||
|
@ -141,7 +138,7 @@ class PersistentTransactionManager(private val db: PendingTransactionDb) : Trans
|
|||
|
||||
suspend fun manageMined(pendingTx: PendingTransaction, matchingMinedTx: Transaction) = withContext(IO) {
|
||||
twig("a pending transaction has been mined!")
|
||||
val tx = pendingTx.copy(minedHeight = matchingMinedTx.minedHeight)
|
||||
val tx = pendingTx.copy(minedHeight = matchingMinedTx.minedHeight!!)
|
||||
dao.insert(tx)
|
||||
}
|
||||
|
||||
|
|
|
@ -241,14 +241,17 @@ class PersistentTransactionSender (
|
|||
tx.rawTransactionId?.let {
|
||||
ledger.findTransactionByRawId(tx.rawTransactionId)
|
||||
}?.let {
|
||||
twig("matching transaction found! $tx")
|
||||
(manager as PersistentTransactionManager).manageMined(tx, it)
|
||||
refreshSentTransactions()
|
||||
if (it.minedHeight != null) {
|
||||
twig("matching mined transaction found! $tx")
|
||||
(manager as PersistentTransactionManager).manageMined(tx, it)
|
||||
refreshSentTransactions()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
twig("given current height $currentHeight, we found $pendingCount pending txs to submit")
|
||||
} catch (t: Throwable) {
|
||||
t.printStackTrace()
|
||||
twig("Error during updatePendingTransactions: $t caused by ${t.cause}")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,130 +0,0 @@
|
|||
package cash.z.wallet.sdk.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import cash.z.wallet.sdk.db.*
|
||||
import cash.z.wallet.sdk.entity.ClearedTransaction
|
||||
import cash.z.wallet.sdk.entity.Transaction
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
|
||||
/**
|
||||
* Repository that does polling for simplicity. We will implement an alternative version that uses live data as well as
|
||||
* one that creates triggers and then reference them here. For now this is the most basic example of keeping track of
|
||||
* changes.
|
||||
*
|
||||
* @param limit the max number of transactions to return when polling for changes. After a wallet has been a round for a
|
||||
* long time, returning ALL transactions might be overkill. Instead, it may be more efficient to only return a limited
|
||||
* number of transactions, like the most recent transaction that has changed.
|
||||
*/
|
||||
open class PollingTransactionRepository(
|
||||
private val derivedDataDb: DerivedDataDb,
|
||||
private val pollFrequencyMillis: Long = DEFAULT_TX_POLL_INTERVAL_MS,
|
||||
private val limit: Int = Int.MAX_VALUE
|
||||
) : TransactionRepository {
|
||||
|
||||
/**
|
||||
* Constructor that creates the database.
|
||||
*/
|
||||
constructor(
|
||||
context: Context,
|
||||
dataDbName: String,
|
||||
pollFrequencyMillis: Long = DEFAULT_TX_POLL_INTERVAL_MS
|
||||
) : this(
|
||||
Room.databaseBuilder(context, DerivedDataDb::class.java, dataDbName)
|
||||
.setJournalMode(RoomDatabase.JournalMode.TRUNCATE)
|
||||
.build(),
|
||||
pollFrequencyMillis
|
||||
)
|
||||
|
||||
private val blocks: BlockDao = derivedDataDb.blockDao()
|
||||
private val receivedNotes: ReceivedDao = derivedDataDb.receivedDao()
|
||||
private val sentNotes: SentDao = derivedDataDb.sentDao()
|
||||
private val transactions: TransactionDao = derivedDataDb.transactionDao()
|
||||
protected var pollingJob: Job? = null
|
||||
|
||||
override fun lastScannedHeight(): Int {
|
||||
return blocks.lastScannedHeight()
|
||||
}
|
||||
|
||||
override fun isInitialized(): Boolean {
|
||||
return blocks.count() > 0
|
||||
}
|
||||
|
||||
override suspend fun findTransactionById(txId: Long): Transaction? = withContext(IO) {
|
||||
twig("finding transaction with id $txId on thread ${Thread.currentThread().name}")
|
||||
val transaction = transactions.findById(txId)
|
||||
twig("found ${transaction?.id}")
|
||||
transaction
|
||||
}
|
||||
|
||||
override suspend fun findTransactionByRawId(rawTxId: ByteArray): Transaction? = withContext(IO) {
|
||||
transactions.findByRawId(rawTxId)
|
||||
}
|
||||
|
||||
override suspend fun deleteTransactionById(txId: Long) = withContext(IO) {
|
||||
twigTask("deleting transaction with id $txId") {
|
||||
transactions.deleteById(txId)
|
||||
}
|
||||
}
|
||||
override suspend fun getClearedTransactions(): List<ClearedTransaction> = withContext(IO) {
|
||||
transactions.getSentTransactions(limit) + transactions.getReceivedTransactions(limit)
|
||||
}
|
||||
|
||||
override suspend fun monitorChanges(listener: () -> Unit) = withContext(IO) {
|
||||
// since the only thing mutable is unmined transactions, we can simply check for new data rows rather than doing any deep comparisons
|
||||
// in the future we can leverage triggers instead
|
||||
pollingJob?.cancel()
|
||||
pollingJob = launch {
|
||||
val txCount = ValueHolder(-1, "Transaction Count")
|
||||
val unminedCount = ValueHolder(-1, "Unmined Transaction Count")
|
||||
val sentCount = ValueHolder(-1, "Sent Transaction Count")
|
||||
val receivedCount = ValueHolder(-1, "Received Transaction Count")
|
||||
|
||||
while (coroutineContext.isActive) {
|
||||
// we check all conditions to avoid duplicate notifications whenever a change impacts multiple tables
|
||||
// if counting becomes slower than the blocktime (highly unlikely) then this could be optimized to call the listener early and continue counting afterward but there's no need for that complexity now
|
||||
if (txCount.changed(transactions.count())
|
||||
|| unminedCount.changed(transactions.countUnmined())
|
||||
|| sentCount.changed(sentNotes.count())
|
||||
|| receivedCount.changed(receivedNotes.count())
|
||||
) {
|
||||
twig("Notifying listener that changes have been detected in transactions!")
|
||||
listener.invoke()
|
||||
} else {
|
||||
//twig("No changes detected in transactions.")
|
||||
}
|
||||
delay(pollFrequencyMillis)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
pollingJob?.cancel().also { pollingJob = null }
|
||||
derivedDataDb.close()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_TX_POLL_INTERVAL_MS = 5_000L
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces some of the boilerplate of checking a value for changes.
|
||||
*/
|
||||
internal class ValueHolder<T>(var value: T, val description: String = "Value") {
|
||||
|
||||
/**
|
||||
* Hold the new value and report whether it has changed.
|
||||
*/
|
||||
fun changed(newValue: T): Boolean {
|
||||
return if (newValue == value) {
|
||||
false
|
||||
} else {
|
||||
twig("$description changed from $value to $newValue")
|
||||
value = newValue
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +1,82 @@
|
|||
package cash.z.wallet.sdk.data
|
||||
|
||||
import android.content.Context
|
||||
import cash.z.wallet.sdk.block.CompactBlockDbStore
|
||||
import cash.z.wallet.sdk.block.CompactBlockDownloader
|
||||
import cash.z.wallet.sdk.block.CompactBlockProcessor
|
||||
import cash.z.wallet.sdk.block.CompactBlockProcessor.State.*
|
||||
import cash.z.wallet.sdk.block.CompactBlockStore
|
||||
import cash.z.wallet.sdk.data.Synchronizer.Status.*
|
||||
import cash.z.wallet.sdk.entity.ClearedTransaction
|
||||
import cash.z.wallet.sdk.entity.PendingTransaction
|
||||
import cash.z.wallet.sdk.entity.SentTransaction
|
||||
import cash.z.wallet.sdk.exception.SynchronizerException
|
||||
import cash.z.wallet.sdk.exception.WalletException
|
||||
import cash.z.wallet.sdk.secure.Wallet
|
||||
import cash.z.wallet.sdk.service.LightWalletGrpcService
|
||||
import cash.z.wallet.sdk.service.LightWalletService
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
|
||||
/**
|
||||
* Constructor function for building a Synchronizer, given the bare minimum amount of information
|
||||
* necessary to do so.
|
||||
*/
|
||||
@Suppress("FunctionName")
|
||||
fun Synchronizer(
|
||||
appContext: Context,
|
||||
lightwalletdHost: String,
|
||||
keyManager: Wallet.KeyManager,
|
||||
birthdayHeight: Int? = null
|
||||
): Synchronizer {
|
||||
val wallet = Wallet().also {
|
||||
val privateKeyMaybe = it.initialize(appContext, keyManager.seed, birthdayHeight)
|
||||
if (privateKeyMaybe != null) keyManager.key = privateKeyMaybe[0]
|
||||
}
|
||||
return Synchronizer(
|
||||
appContext,
|
||||
wallet,
|
||||
lightwalletdHost,
|
||||
keyManager
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor function for building a Synchronizer in the most flexible way possible. This allows
|
||||
* a wallet maker to customize any subcomponent of the Synchronzier.
|
||||
*/
|
||||
@Suppress("FunctionName")
|
||||
fun Synchronizer(
|
||||
appContext: Context,
|
||||
wallet: Wallet,
|
||||
lightwalletdHost: String,
|
||||
keyManager: Wallet.KeyManager,
|
||||
ledger: TransactionRepository = PagedTransactionRepository(appContext),
|
||||
manager: TransactionManager = PersistentTransactionManager(appContext),
|
||||
service: LightWalletService = LightWalletGrpcService(appContext, lightwalletdHost),
|
||||
sender: TransactionSender = PersistentTransactionSender(manager, service, ledger),
|
||||
blockStore: CompactBlockStore = CompactBlockDbStore(appContext),
|
||||
downloader: CompactBlockDownloader = CompactBlockDownloader(service, blockStore),
|
||||
processor: CompactBlockProcessor = CompactBlockProcessor(downloader, ledger, wallet.rustBackend),
|
||||
encoder: TransactionEncoder = WalletTransactionEncoder(wallet, ledger, keyManager)
|
||||
): Synchronizer {
|
||||
// ties everything together
|
||||
return SdkSynchronizer(
|
||||
wallet,
|
||||
ledger,
|
||||
sender,
|
||||
processor,
|
||||
encoder
|
||||
)
|
||||
}
|
||||
/**
|
||||
* A synchronizer that attempts to remain operational, despite any number of errors that can occur. It acts as the glue
|
||||
* that ties all the pieces of the SDK together. Each component of the SDK is designed for the potential of stand-alone
|
||||
|
@ -20,7 +84,7 @@ import kotlin.coroutines.CoroutineContext
|
|||
* that demonstrates how all the pieces can be tied together. Its goal is to allow a developer to focus on their app
|
||||
* rather than the nuances of how Zcash works.
|
||||
*
|
||||
* @param wallet the component that wraps the JNI layer that interacts with the rust backend and manages wallet config.
|
||||
* @param wallet An initialized wallet. This component wraps the rust backend and manages wallet config.
|
||||
* @param repository the component that exposes streams of wallet transaction information.
|
||||
* @param sender the component responsible for sending transactions to lightwalletd in order to spend funds.
|
||||
* @param processor the component that saves the downloaded compact blocks to the cache and then scans those blocks for
|
||||
|
@ -51,19 +115,17 @@ class SdkSynchronizer (
|
|||
//
|
||||
|
||||
/**
|
||||
* A property that is true while a connection to the lightwalletd server exists.
|
||||
* Indicates the status of this Synchronizer. This implementation basically simplifies the
|
||||
* status of the processor to focus only on the high level states that matter most.
|
||||
*/
|
||||
override val isConnected: Boolean get() = processor.isConnected
|
||||
|
||||
/**
|
||||
* A property that is true while actively downloading blocks from lightwalletd.
|
||||
*/
|
||||
override val isSyncing: Boolean get() = processor.isSyncing
|
||||
|
||||
/**
|
||||
* A property that is true while actively scanning the cache of compact blocks for transactions.
|
||||
*/
|
||||
override val isScanning: Boolean get() = processor.isScanning
|
||||
override val status = processor.state.map {
|
||||
when (it) {
|
||||
is Synced -> SYNCED
|
||||
is Stopped -> STOPPED
|
||||
is Disconnected -> DISCONNECTED
|
||||
else -> SYNCING
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
|
@ -99,7 +161,7 @@ class SdkSynchronizer (
|
|||
override var onCriticalErrorHandler: ((Throwable?) -> Boolean)? = null
|
||||
|
||||
/**
|
||||
* A callback to invoke whenver a processor error is encountered. Returning true signals that the error was handled
|
||||
* A callback to invoke whenever a processor error is encountered. Returning true signals that the error was handled
|
||||
* and a retry attempt should be made, if possible. This callback is not called on the main thread so any UI work
|
||||
* would need to switch context to the main thread.
|
||||
*/
|
||||
|
@ -132,12 +194,10 @@ class SdkSynchronizer (
|
|||
|
||||
// TODO: this doesn't work as intended. Refactor to improve the cancellation behavior (i.e. what happens when one job fails) by making launchTransactionMonitor throw an exception
|
||||
coroutineScope.launch {
|
||||
initWallet()
|
||||
startSender(this)
|
||||
|
||||
launchProgressMonitor()
|
||||
launchPendingMonitor()
|
||||
launchTransactionMonitor()
|
||||
onReady()
|
||||
}
|
||||
return this
|
||||
|
@ -151,24 +211,6 @@ class SdkSynchronizer (
|
|||
sender.start(parentScope)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the wallet, which involves populating data tables based on the latest state of the wallet.
|
||||
*/
|
||||
private suspend fun initWallet() = withContext(IO) {
|
||||
try {
|
||||
wallet.initialize()
|
||||
} catch (e: WalletException.AlreadyInitializedException) {
|
||||
twig("Warning: wallet already initialized but this is safe to ignore " +
|
||||
"because the SDK automatically detects where to start downloading.")
|
||||
} catch (f: WalletException.FalseStart) {
|
||||
if (recoverFrom(f)) {
|
||||
twig("Warning: had a wallet init error but we recovered!")
|
||||
} else {
|
||||
twig("Error: false start while initializing wallet!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this is a work in progress. We could take drastic measures to automatically recover from certain critical
|
||||
// errors and alert the user but this might be better to do at the app level, rather than SDK level.
|
||||
private fun recoverFrom(error: WalletException.FalseStart): Boolean {
|
||||
|
@ -186,9 +228,15 @@ class SdkSynchronizer (
|
|||
* was never previously started.
|
||||
*/
|
||||
override fun stop() {
|
||||
coroutineScope.cancel()
|
||||
coroutineScope.launch {
|
||||
processor.stop()
|
||||
coroutineScope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearData() {
|
||||
wallet.clear()
|
||||
}
|
||||
|
||||
//
|
||||
// Monitors
|
||||
|
@ -223,19 +271,6 @@ class SdkSynchronizer (
|
|||
twig("done monitoring for pending changes and balance changes")
|
||||
}
|
||||
|
||||
private fun CoroutineScope.launchTransactionMonitor(): Job = launch {
|
||||
ledger.monitorChanges(::onTransactionsChanged)
|
||||
}
|
||||
|
||||
fun onTransactionsChanged() {
|
||||
coroutineScope.launch {
|
||||
twig("triggering a balance update because transactions have changed")
|
||||
refreshBalance()
|
||||
clearedChannel.send(ledger.getClearedTransactions())
|
||||
}
|
||||
twig("done handling changed transactions")
|
||||
}
|
||||
|
||||
suspend fun refreshBalance() = withContext(IO) {
|
||||
if (!balanceChannel.isClosedForSend) {
|
||||
balanceChannel.send(wallet.getBalanceInfo())
|
||||
|
@ -247,6 +282,10 @@ class SdkSynchronizer (
|
|||
private fun CoroutineScope.onReady() = launch(CoroutineExceptionHandler(::onCriticalError)) {
|
||||
twig("Synchronizer Ready. Starting processor!")
|
||||
processor.onErrorListener = ::onProcessorError
|
||||
status.filter { it == SYNCED }.onEach {
|
||||
twig("Triggering an automatic balance refresh since the processor is synced!")
|
||||
refreshBalance()
|
||||
}.launchIn(this)
|
||||
processor.start()
|
||||
twig("Synchronizer onReady complete. Processor start has exited!")
|
||||
}
|
||||
|
@ -270,8 +309,15 @@ class SdkSynchronizer (
|
|||
|
||||
private fun onProcessorError(error: Throwable): Boolean {
|
||||
twig("ERROR while processing data: $error")
|
||||
if (onProcessorErrorHandler == null) {
|
||||
twig("WARNING: falling back to the default behavior for processor errors. To add" +
|
||||
" custom behavior, set synchronizer.onProcessorErrorHandler to" +
|
||||
" a non-null value")
|
||||
return true
|
||||
}
|
||||
return onProcessorErrorHandler?.invoke(error)?.also {
|
||||
if (it) twig("processor error handler signaled that we should try again!")
|
||||
twig("processor error handler signaled that we should " +
|
||||
"${if (it) "try again" else "abort"}!")
|
||||
} == true
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import cash.z.wallet.sdk.entity.SentTransaction
|
|||
import cash.z.wallet.sdk.secure.Wallet
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Primary interface for interacting with the SDK. Defines the contract that specific implementations like
|
||||
|
@ -84,23 +85,10 @@ interface Synchronizer {
|
|||
//
|
||||
|
||||
/**
|
||||
* A flag indicating whether this Synchronizer is connected to its lightwalletd server. When false, a UI element
|
||||
* may want to turn red.
|
||||
* A flow of values representing the [Status] of this Synchronizer. As the status changes, a new
|
||||
* value will be emitted by this flow.
|
||||
*/
|
||||
val isConnected: Boolean
|
||||
|
||||
|
||||
/**
|
||||
* A flag indicating whether this Synchronizer is actively downloading compact blocks. When true, a UI element
|
||||
* may want to turn yellow.
|
||||
*/
|
||||
val isSyncing: Boolean
|
||||
|
||||
/**
|
||||
* A flag indicating whether this Synchronizer is actively decrypting compact blocks, searching for transactions.
|
||||
* When true, a UI element may want to turn yellow.
|
||||
*/
|
||||
val isScanning: Boolean
|
||||
val status: Flow<Status>
|
||||
|
||||
|
||||
//
|
||||
|
@ -168,4 +156,30 @@ interface Synchronizer {
|
|||
* error is unrecoverable and the sender should [stop].
|
||||
*/
|
||||
var onSubmissionErrorHandler: ((Throwable?) -> Boolean)?
|
||||
|
||||
enum class Status {
|
||||
/**
|
||||
* Indicates that [stop] has been called on this Synchronizer and it will no longer be used.
|
||||
*/
|
||||
STOPPED,
|
||||
|
||||
/**
|
||||
* Indicates that this Synchronizer is disconnected from its lightwalletd server.
|
||||
* When set, a UI element may want to turn red.
|
||||
*/
|
||||
DISCONNECTED,
|
||||
|
||||
/**
|
||||
* Indicates that this Synchronizer is not yet synced and therefore should not broadcast
|
||||
* transactions because it does not have the latest data. When set, a UI element may want
|
||||
* to turn yellow.
|
||||
*/
|
||||
SYNCING,
|
||||
|
||||
/**
|
||||
* Indicates that this Synchronizer is fully up to date and ready for all wallet functions.
|
||||
* When set, a UI element may want to turn green.
|
||||
*/
|
||||
SYNCED
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package cash.z.wallet.sdk.data
|
||||
|
||||
import cash.z.wallet.sdk.entity.ClearedTransaction
|
||||
import cash.z.wallet.sdk.entity.Transaction
|
||||
|
||||
interface TransactionRepository {
|
||||
|
@ -9,6 +8,4 @@ interface TransactionRepository {
|
|||
suspend fun findTransactionById(txId: Long): Transaction?
|
||||
suspend fun findTransactionByRawId(rawTransactionId: ByteArray): Transaction?
|
||||
suspend fun deleteTransactionById(txId: Long)
|
||||
suspend fun getClearedTransactions(): List<ClearedTransaction>
|
||||
suspend fun monitorChanges(listener: () -> Unit)
|
||||
}
|
|
@ -9,7 +9,8 @@ import kotlinx.coroutines.withContext
|
|||
|
||||
class WalletTransactionEncoder(
|
||||
private val wallet: Wallet,
|
||||
private val repository: TransactionRepository
|
||||
private val repository: TransactionRepository,
|
||||
private val spendingKeyProvider: Wallet.SpendingKeyProvider
|
||||
) : TransactionEncoder {
|
||||
|
||||
/**
|
||||
|
@ -18,7 +19,7 @@ class WalletTransactionEncoder(
|
|||
* double-bangs for things).
|
||||
*/
|
||||
override suspend fun create(zatoshi: Long, toAddress: String, memo: String): EncodedTransaction = withContext(IO) {
|
||||
val transactionId = wallet.createSpend(zatoshi, toAddress, memo)
|
||||
val transactionId = wallet.createSpend(spendingKeyProvider.key, zatoshi, toAddress, memo)
|
||||
val transaction = repository.findTransactionById(transactionId)
|
||||
?: throw TransactionNotFoundException(transactionId)
|
||||
EncodedTransaction(transaction.transactionId, transaction.raw
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package cash.z.wallet.sdk.db
|
||||
|
||||
import androidx.paging.DataSource
|
||||
import androidx.room.*
|
||||
import cash.z.wallet.sdk.entity.*
|
||||
import cash.z.wallet.sdk.entity.Transaction
|
||||
|
@ -97,7 +98,7 @@ interface TransactionDao {
|
|||
ORDER BY block IS NOT NULL, height DESC, time DESC, txid DESC
|
||||
LIMIT :limit
|
||||
""")
|
||||
fun getSentTransactions(limit: Int = Int.MAX_VALUE): List<SentTransaction>
|
||||
fun getSentTransactions(limit: Int = Int.MAX_VALUE): DataSource.Factory<Int, SentTransaction>
|
||||
|
||||
|
||||
/**
|
||||
|
@ -122,6 +123,6 @@ interface TransactionDao {
|
|||
ORDER BY minedheight DESC, blocktimeinseconds DESC, id DESC
|
||||
LIMIT :limit
|
||||
""")
|
||||
fun getReceivedTransactions(limit: Int = Int.MAX_VALUE): List<ReceivedTransaction>
|
||||
fun getReceivedTransactions(limit: Int = Int.MAX_VALUE): DataSource.Factory<Int, ReceivedTransaction>
|
||||
|
||||
}
|
|
@ -10,7 +10,7 @@ import androidx.room.Ignore
|
|||
primaryKeys = ["account"]
|
||||
)
|
||||
data class Account(
|
||||
val account: Int = 0,
|
||||
val account: Int? = 0,
|
||||
|
||||
@ColumnInfo(name = "extfvk")
|
||||
val extendedFullViewingKey: String = "",
|
||||
|
|
|
@ -2,28 +2,37 @@ package cash.z.wallet.sdk.entity
|
|||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import org.jetbrains.annotations.NotNull
|
||||
|
||||
@Entity(primaryKeys = ["height"], tableName = "blocks")
|
||||
data class Block(
|
||||
val height: Int,
|
||||
val time: Int?,
|
||||
val height: Int?,
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB, name = "hash")
|
||||
@NotNull
|
||||
val hash: ByteArray,
|
||||
@NotNull
|
||||
val time: Int,
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB, name = "sapling_tree")
|
||||
@NotNull
|
||||
val saplingTree: ByteArray
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
return (other is Block)
|
||||
&& height == other.height
|
||||
&& time == other.time
|
||||
&& saplingTree.contentEquals(other.saplingTree)
|
||||
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
|
||||
result = 31 * result + (time ?: 0)
|
||||
var result = height ?: 0
|
||||
result = 31 * result + hash.contentHashCode()
|
||||
result = 31 * result + time
|
||||
result = 31 * result + saplingTree.contentHashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -10,26 +10,20 @@ import androidx.room.ForeignKey
|
|||
foreignKeys = [ForeignKey(
|
||||
entity = Transaction::class,
|
||||
parentColumns = ["id_tx"],
|
||||
childColumns = ["tx"],
|
||||
onUpdate = ForeignKey.CASCADE,
|
||||
onDelete = ForeignKey.CASCADE
|
||||
), ForeignKey(
|
||||
entity = Transaction::class,
|
||||
parentColumns = ["id_tx"],
|
||||
childColumns = ["spent"],
|
||||
onUpdate = ForeignKey.CASCADE,
|
||||
onDelete = ForeignKey.SET_NULL
|
||||
childColumns = ["tx"]
|
||||
), ForeignKey(
|
||||
entity = Account::class,
|
||||
parentColumns = ["account"],
|
||||
childColumns = ["account"],
|
||||
onUpdate = ForeignKey.CASCADE,
|
||||
onDelete = ForeignKey.CASCADE
|
||||
childColumns = ["account"]
|
||||
), ForeignKey(
|
||||
entity = Transaction::class,
|
||||
parentColumns = ["id_tx"],
|
||||
childColumns = ["spent"]
|
||||
)]
|
||||
)
|
||||
data class Received(
|
||||
@ColumnInfo(name = "id_note")
|
||||
val id: Int = 0,
|
||||
val id: Int? = 0,
|
||||
|
||||
/**
|
||||
* A reference to the transaction this note was received in
|
||||
|
@ -48,9 +42,6 @@ data class Received(
|
|||
*/
|
||||
val spent: Int? = 0,
|
||||
|
||||
@ColumnInfo(name = "is_change")
|
||||
val isChange: Boolean = false,
|
||||
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
||||
val diversifier: ByteArray = byteArrayOf(),
|
||||
|
||||
|
@ -60,40 +51,11 @@ data class Received(
|
|||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
||||
val nf: ByteArray = byteArrayOf(),
|
||||
|
||||
@ColumnInfo(name = "is_change")
|
||||
val isChange: Boolean = false,
|
||||
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
||||
val memo: ByteArray? = byteArrayOf()
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
|
||||
return (other is Received)
|
||||
&& id == other.id
|
||||
&& transactionId == other.transactionId
|
||||
&& outputIndex == other.outputIndex
|
||||
&& account == other.account
|
||||
&& value == other.value
|
||||
&& spent == other.spent
|
||||
&& diversifier.contentEquals(other.diversifier)
|
||||
&& rcm.contentEquals(other.rcm)
|
||||
&& nf.contentEquals(other.nf)
|
||||
&& isChange == other.isChange
|
||||
&& ((memo == null && other.memo == null)
|
||||
|| (memo != null && other.memo != null && memo.contentEquals(other.memo)))
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id
|
||||
result = 31 * result + transactionId
|
||||
result = 31 * result + outputIndex
|
||||
result = 31 * result + account
|
||||
result = 31 * result + value.toInt()
|
||||
result = 31 * result + (if (isChange) 1 else 0)
|
||||
result = 31 * result + (spent ?: 0)
|
||||
result = 31 * result + diversifier.contentHashCode()
|
||||
result = 31 * result + rcm.contentHashCode()
|
||||
result = 31 * result + nf.contentHashCode()
|
||||
result = 31 * result + (memo?.contentHashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -10,20 +10,16 @@ import androidx.room.ForeignKey
|
|||
foreignKeys = [ForeignKey(
|
||||
entity = Transaction::class,
|
||||
parentColumns = ["id_tx"],
|
||||
childColumns = ["tx"],
|
||||
onUpdate = ForeignKey.CASCADE,
|
||||
onDelete = ForeignKey.CASCADE
|
||||
childColumns = ["tx"]
|
||||
), ForeignKey(
|
||||
entity = Account::class,
|
||||
parentColumns = ["account"],
|
||||
childColumns = ["from_account"],
|
||||
onUpdate = ForeignKey.CASCADE,
|
||||
onDelete = ForeignKey.SET_NULL
|
||||
childColumns = ["from_account"]
|
||||
)]
|
||||
)
|
||||
data class Sent(
|
||||
@ColumnInfo(name = "id_note")
|
||||
val id: Int = 0,
|
||||
val id: Int? = 0,
|
||||
|
||||
@ColumnInfo(name = "tx")
|
||||
val transactionId: Int = 0,
|
||||
|
@ -34,7 +30,7 @@ data class Sent(
|
|||
@ColumnInfo(name = "from_account")
|
||||
val account: Int = 0,
|
||||
|
||||
val address: String,
|
||||
val address: String = "",
|
||||
|
||||
val value: Long = 0,
|
||||
|
||||
|
@ -44,22 +40,29 @@ data class Sent(
|
|||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Sent) return false
|
||||
|
||||
return (other is Sent)
|
||||
&& id == other.id
|
||||
&& transactionId == other.transactionId
|
||||
&& outputIndex == other.outputIndex
|
||||
&& account == other.account
|
||||
&& value == other.value
|
||||
&& ((memo == null && other.memo == null) || (memo != null && other.memo != null && memo.contentEquals(other.memo)))
|
||||
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
|
||||
var result = id ?: 0
|
||||
result = 31 * result + transactionId
|
||||
result = 31 * result + outputIndex
|
||||
result = 31 * result + account
|
||||
result = 31 * result + value.toInt()
|
||||
result = 31 * result + address.hashCode()
|
||||
result = 31 * result + value.hashCode()
|
||||
result = 31 * result + (memo?.contentHashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -17,26 +17,26 @@ import org.jetbrains.annotations.NotNull
|
|||
foreignKeys = [ForeignKey(
|
||||
entity = Block::class,
|
||||
parentColumns = ["height"],
|
||||
childColumns = ["block"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
childColumns = ["block"]
|
||||
)]
|
||||
)
|
||||
data class Transaction(
|
||||
@ColumnInfo(name = "id_tx")
|
||||
val id: Long,
|
||||
val id: Long?,
|
||||
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB, name = "txid")
|
||||
@NotNull
|
||||
val transactionId: ByteArray,
|
||||
|
||||
@ColumnInfo(name = "tx_index")
|
||||
val transactionIndex: Int,
|
||||
val transactionIndex: Int?,
|
||||
|
||||
val created: String?,
|
||||
|
||||
@ColumnInfo(name = "expiry_height")
|
||||
val expiryHeight: Int,
|
||||
val expiryHeight: Int?,
|
||||
|
||||
@ColumnInfo(name = "block")
|
||||
val minedHeight: Int,
|
||||
val minedHeight: Int?,
|
||||
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
||||
val raw: ByteArray?
|
||||
|
@ -48,6 +48,7 @@ data class Transaction(
|
|||
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) {
|
||||
|
@ -61,12 +62,14 @@ data class Transaction(
|
|||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + transactionId.contentHashCode()
|
||||
result = 31 * result + transactionIndex
|
||||
result = 31 * result + expiryHeight
|
||||
result = 31 * result + minedHeight
|
||||
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")
|
||||
|
|
|
@ -48,6 +48,9 @@ sealed class WalletException(message: String, cause: Throwable? = null) : Runtim
|
|||
object MissingParamsException : WalletException(
|
||||
"Cannot send funds due to missing spend or output params and attempting to download them failed."
|
||||
)
|
||||
class BirthdayNotFoundException(directory: String, height: Int?) : WalletException(
|
||||
"Unable to find birthday file for $height verify that $directory/$height.json exists."
|
||||
)
|
||||
class MalformattedBirthdayFilesException(directory: String, file: String) : WalletException(
|
||||
"Failed to parse file $directory/$file verify that it is formatted as #####.json, " +
|
||||
"where the first portion is an Int representing the height of the tree contained in the file"
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
|||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import cash.z.wallet.sdk.secure.Wallet
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
@ -23,9 +24,19 @@ class SampleSpendingKeyProvider(private val seedValue: String) : ReadWriteProper
|
|||
}
|
||||
|
||||
@Deprecated(message = InsecureWarning.message)
|
||||
class SampleSeedProvider(val seedValue: String) : ReadOnlyProperty<Any?, ByteArray> {
|
||||
class SampleSeedProvider(val seed: ByteArray) : ReadOnlyProperty<Any?, ByteArray> {
|
||||
constructor(seedValue: String) : this(seedValue.toByteArray())
|
||||
override fun getValue(thisRef: Any?, property: KProperty<*>): ByteArray {
|
||||
return seedValue.toByteArray()
|
||||
return seed
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated(message = InsecureWarning.message)
|
||||
class BlockingSeedProvider(val seed: ByteArray, val delay: Long = 5000L) : ReadOnlyProperty<Any?, ByteArray> {
|
||||
constructor(seedValue: String, delayMillis: Long = 5000L) : this(seedValue.toByteArray(), delayMillis)
|
||||
override fun getValue(thisRef: Any?, property: KProperty<*>): ByteArray {
|
||||
Thread.sleep(delay)
|
||||
return seed
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,6 +51,26 @@ class SimpleProvider<T>(var value: T) : ReadWriteProperty<Any?, T> {
|
|||
}
|
||||
}
|
||||
|
||||
@Deprecated(message = InsecureWarning.message)
|
||||
class BlockingProvider<T>(var value: T, val delay: Long = 5000L) : ReadWriteProperty<Any?, T> {
|
||||
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
|
||||
Thread.sleep(delay)
|
||||
return value
|
||||
}
|
||||
|
||||
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
|
||||
Thread.sleep(delay)
|
||||
this.value = value
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated(message = InsecureWarning.message)
|
||||
class SampleKeyManager(val sampleSeed: ByteArray) : Wallet.KeyManager {
|
||||
override lateinit var key: String
|
||||
override val seed: ByteArray get() = sampleSeed
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This is intentionally insecure. Wallet makers have told us storing keys is their specialty so we don't put a lot of
|
||||
* energy here. A true implementation would create a key using user interaction, perhaps with a password they know that
|
||||
|
|
|
@ -4,25 +4,30 @@ import android.content.Context
|
|||
import cash.z.wallet.sdk.data.twig
|
||||
import cash.z.wallet.sdk.ext.ZcashSdk.OUTPUT_PARAM_FILE_NAME
|
||||
import cash.z.wallet.sdk.ext.ZcashSdk.SPEND_PARAM_FILE_NAME
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Serves as the JNI boundary between the Kotlin and Rust layers. Functions in this class should
|
||||
* not be called directly by code outside of the SDK. Instead, one of the higher-level components
|
||||
* should be used such as Wallet.kt or CompactBlockProcessor.kt.
|
||||
*/
|
||||
internal object RustBackend : RustBackendWelding {
|
||||
object RustBackend : RustBackendWelding {
|
||||
private var loaded = false
|
||||
private lateinit var dbDataPath: String
|
||||
private lateinit var dbCachePath: String
|
||||
lateinit var paramDestinationDir: String
|
||||
|
||||
/**
|
||||
* Loads the library and initializes path variables. Although it is best to only call this
|
||||
* function once, it is idempotent.
|
||||
*/
|
||||
override fun create(appContext: Context, dbCacheName: String, dbDataName: String): RustBackend {
|
||||
twig("Creating RustBackend") {
|
||||
// It is safe to call these things twice but not efficient. So we add a loose check and
|
||||
// ignore the fact that it's not thread-safe.
|
||||
if (!loaded) {
|
||||
initLogs()
|
||||
loadRustLibrary()
|
||||
initLogs()
|
||||
}
|
||||
dbCachePath = appContext.getDatabasePath(dbCacheName).absolutePath
|
||||
dbDataPath = appContext.getDatabasePath(dbDataName).absolutePath
|
||||
|
@ -31,6 +36,12 @@ internal object RustBackend : RustBackendWelding {
|
|||
return this
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
twig("Deleting databases")
|
||||
File(dbCachePath).delete()
|
||||
File(dbDataPath).delete()
|
||||
}
|
||||
|
||||
/**
|
||||
* The first call made to this object in order to load the Rust backend library. All other calls
|
||||
* will fail if this function has not been invoked.
|
||||
|
@ -44,7 +55,6 @@ internal object RustBackend : RustBackendWelding {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Wrapper Functions
|
||||
//
|
||||
|
|
|
@ -6,8 +6,9 @@ import cash.z.wallet.sdk.data.twig
|
|||
import cash.z.wallet.sdk.data.twigTask
|
||||
import cash.z.wallet.sdk.exception.RustLayerException
|
||||
import cash.z.wallet.sdk.exception.WalletException
|
||||
import cash.z.wallet.sdk.exception.WalletException.MalformattedBirthdayFilesException
|
||||
import cash.z.wallet.sdk.exception.WalletException.MissingBirthdayFilesException
|
||||
import cash.z.wallet.sdk.exception.WalletException.*
|
||||
import cash.z.wallet.sdk.ext.ZcashSdk.DB_CACHE_NAME
|
||||
import cash.z.wallet.sdk.ext.ZcashSdk.DB_DATA_NAME
|
||||
import cash.z.wallet.sdk.ext.ZcashSdk.OUTPUT_PARAM_FILE_NAME
|
||||
import cash.z.wallet.sdk.ext.ZcashSdk.SAPLING_ACTIVATION_HEIGHT
|
||||
import cash.z.wallet.sdk.ext.ZcashSdk.SPEND_PARAM_FILE_NAME
|
||||
|
@ -23,8 +24,6 @@ import kotlinx.coroutines.withContext
|
|||
import okio.Okio
|
||||
import java.io.File
|
||||
import java.io.InputStreamReader
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
|
||||
|
||||
/**
|
||||
|
@ -33,61 +32,54 @@ import kotlin.properties.ReadWriteProperty
|
|||
* The [cash.z.wallet.sdk.block.CompactBlockProcessor] handles all the remaining Rust backend
|
||||
* functionality, related to processing blocks.
|
||||
*/
|
||||
class Wallet private constructor(
|
||||
private val birthday: WalletBirthday,
|
||||
private val rustBackend: RustBackendWelding,
|
||||
private val paramDestinationDir: String,
|
||||
private val seedProvider: ReadOnlyProperty<Any?, ByteArray>,
|
||||
spendingKeyProvider: ReadWriteProperty<Any?, String>,
|
||||
/** indexes of accounts ids. In the reference wallet, we only work with account 0 */
|
||||
private val accountIds: Array<Int> = arrayOf(0)
|
||||
) {
|
||||
class Wallet {
|
||||
|
||||
lateinit var rustBackend: RustBackendWelding
|
||||
var lowerBoundHeight: Int = SAPLING_ACTIVATION_HEIGHT
|
||||
|
||||
fun clear() {
|
||||
if (::rustBackend.isInitialized) {
|
||||
(rustBackend as RustBackend).clear()
|
||||
} else {
|
||||
twig("WARNING: attempted to clear an uninitialized wallet. No action was taken since " +
|
||||
"the database paths have not yet been set.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a wallet from the given birthday. It will take care of downloading params, on first
|
||||
* use, to a private cache directory as well as initializing the RustBackend. The file paths for
|
||||
* these components are not configurable
|
||||
* per github issue https://github.com/zcash/zcash-android-wallet-sdk/issues/30
|
||||
* Initialize the wallet with the given seed and return the related private keys for each
|
||||
* account specified or null if the wallet was previously initialized and block data exists on
|
||||
* disk. When this method returns null, that signals that the wallet will need to retrieve the
|
||||
* private keys from its own secure storage. In other words, the private keys are only given out
|
||||
* once for each set of database files. Subsequent calls to [initialize] will only load the Rust
|
||||
* library and return null.
|
||||
*
|
||||
* @param appContext the application context. This is used for loading a checkpoint
|
||||
* corresponding to the wallet's birthday from the SDK assets directory and also for setting
|
||||
* the path for storing the sapling params.
|
||||
* @param birthday the date this wallet's seed was created.
|
||||
* @param rustBackend the RustBackend that the rest of the SDK will use. It is initialized by
|
||||
* by this class.
|
||||
* @param seedProvider a read-only property that provides the seed.
|
||||
* @param spendingKeyProvider a read-only property that provides the spending key.
|
||||
* @param accountIds the ids of the accounts to use. Defaults to a array of 0.
|
||||
*/
|
||||
constructor(
|
||||
appContext: Context,
|
||||
rustBackend: RustBackendWelding,
|
||||
seedProvider: ReadOnlyProperty<Any?, ByteArray>,
|
||||
spendingKeyProvider: ReadWriteProperty<Any?, String>,
|
||||
birthday: WalletBirthday = loadBirthdayFromAssets(appContext),
|
||||
accountIds: Array<Int> = arrayOf(0)
|
||||
) : this(
|
||||
birthday = birthday,
|
||||
rustBackend = rustBackend,
|
||||
paramDestinationDir = (rustBackend as RustBackend).paramDestinationDir,
|
||||
seedProvider = seedProvider,
|
||||
spendingKeyProvider = spendingKeyProvider,
|
||||
accountIds = accountIds
|
||||
)
|
||||
|
||||
/**
|
||||
* Delegate for storing spending keys. This will be used when the spending keys are created
|
||||
* during initialization.
|
||||
*/
|
||||
private var spendingKeyStore by spendingKeyProvider
|
||||
|
||||
/**
|
||||
* Initializes the wallet by creating the DataDb and pre-populating it with data corresponding
|
||||
* to the birthday for this wallet.
|
||||
* 'compactBlockCache.db' and 'transactionData.db' files are created by this function (if they
|
||||
* do not already exist). These files can be given a prefix for scenarios where multiple wallets
|
||||
* operate in one app--for instance, when sweeping funds from another wallet seed.
|
||||
*
|
||||
* @param appContext the application context.
|
||||
* @param seed the seed to use for initializing this wallet.
|
||||
* @param birthdayHeight the height corresponding to when the wallet seed was created. If null,
|
||||
* this signals that the wallet is being born.
|
||||
* @param numberOfAccounts the number of accounts to create from this seed.
|
||||
* @param dbFileNamePrefix the optional prefix to add to the names of the database files.
|
||||
*/
|
||||
fun initialize(
|
||||
firstRunStartHeight: Int = SAPLING_ACTIVATION_HEIGHT
|
||||
): Int {
|
||||
appContext: Context,
|
||||
seed: ByteArray,
|
||||
birthdayHeight: Int? = null,
|
||||
numberOfAccounts: Int = 1,
|
||||
dbFileNamePrefix: String = ""
|
||||
): Array<String>? {
|
||||
rustBackend = RustBackend.create(
|
||||
appContext,
|
||||
"${dbFileNamePrefix}$DB_CACHE_NAME",
|
||||
"${dbFileNamePrefix}$DB_DATA_NAME"
|
||||
)
|
||||
|
||||
try {
|
||||
// only creates tables, if they don't exist
|
||||
rustBackend.initDataDb()
|
||||
twig("Initialized wallet for first run")
|
||||
} catch (e: Throwable) {
|
||||
|
@ -95,30 +87,27 @@ class Wallet private constructor(
|
|||
}
|
||||
|
||||
try {
|
||||
val birthday = loadBirthdayFromAssets(appContext, birthdayHeight)
|
||||
lowerBoundHeight = birthday.height
|
||||
rustBackend.initBlocksTable(
|
||||
birthday.height,
|
||||
birthday.hash,
|
||||
birthday.time,
|
||||
birthday.tree
|
||||
)
|
||||
twig("seeded the database with sapling tree at height ${birthday.height}")
|
||||
twig("seeded the database with sapling tree at height ${birthday.height} (expected $birthdayHeight)")
|
||||
} catch (t: Throwable) {
|
||||
if (t.message?.contains("is not empty") == true) {
|
||||
throw WalletException.AlreadyInitializedException(t)
|
||||
return null
|
||||
} else {
|
||||
throw WalletException.FalseStart(t)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// store the spendingkey by leveraging the utilities provided during construction
|
||||
val seed by seedProvider
|
||||
val accountSpendingKeys =
|
||||
rustBackend.initAccountsTable(seed, 1)
|
||||
spendingKeyStore = accountSpendingKeys[0]
|
||||
|
||||
twig("Initialized the accounts table")
|
||||
return Math.max(firstRunStartHeight, birthday.height)
|
||||
return rustBackend.initAccountsTable(seed, numberOfAccounts).also {
|
||||
twig("Initialized the accounts table with $numberOfAccounts account(s)")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
throw WalletException.FalseStart(e)
|
||||
}
|
||||
|
@ -127,32 +116,32 @@ class Wallet private constructor(
|
|||
/**
|
||||
* Gets the address for this wallet, defaulting to the first account.
|
||||
*/
|
||||
fun getAddress(accountId: Int = accountIds[0]): String {
|
||||
return rustBackend.getAddress(accountId)
|
||||
fun getAddress(accountIndex: Int = 0): String {
|
||||
return rustBackend.getAddress(accountIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a quick snapshot of the available balance. In most cases, the stream of balances
|
||||
* provided by [balances] should be used instead of this funciton.
|
||||
*
|
||||
* @param accountId the account to check for balance info. Defaults to zero.
|
||||
* @param accountIndex the account to check for balance info. Defaults to zero.
|
||||
*/
|
||||
fun availableBalanceSnapshot(accountId: Int = accountIds[0]): Long {
|
||||
return rustBackend.getVerifiedBalance(accountId)
|
||||
fun availableBalanceSnapshot(accountIndex: Int = 0): Long {
|
||||
return rustBackend.getVerifiedBalance(accountIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the latest balance info and emits it into the balance channel. Defaults to the
|
||||
* first account.
|
||||
*
|
||||
* @param accountId the account to check for balance info.
|
||||
* @param accountIndex the account to check for balance info.
|
||||
*/
|
||||
suspend fun getBalanceInfo(accountId: Int = accountIds[0]): WalletBalance = withContext(IO) {
|
||||
suspend fun getBalanceInfo(accountIndex: Int = 0): WalletBalance = withContext(IO) {
|
||||
twigTask("checking balance info") {
|
||||
try {
|
||||
val balanceTotal = rustBackend.getBalance(accountId)
|
||||
val balanceTotal = rustBackend.getBalance(accountIndex)
|
||||
twig("found total balance of: $balanceTotal")
|
||||
val balanceAvailable = rustBackend.getVerifiedBalance(accountId)
|
||||
val balanceAvailable = rustBackend.getVerifiedBalance(accountIndex)
|
||||
twig("found available balance of: $balanceAvailable")
|
||||
WalletBalance(balanceTotal, balanceAvailable)
|
||||
} catch (t: Throwable) {
|
||||
|
@ -174,19 +163,22 @@ class Wallet private constructor(
|
|||
* or -1 if it failed
|
||||
*/
|
||||
suspend fun createSpend(
|
||||
spendingKey: String,
|
||||
value: Long,
|
||||
toAddress: String,
|
||||
memo: String = "",
|
||||
fromAccountId: Int = accountIds[0]
|
||||
fromAccountIndex: Int = 0
|
||||
): Long = withContext(IO) {
|
||||
twigTask("creating transaction to spend $value zatoshi to" +
|
||||
" ${toAddress.masked()} with memo $memo") {
|
||||
twigTask(
|
||||
"creating transaction to spend $value zatoshi to" +
|
||||
" ${toAddress.masked()} with memo $memo"
|
||||
) {
|
||||
try {
|
||||
ensureParams(paramDestinationDir)
|
||||
twig("params exist at $paramDestinationDir! attempting to send...")
|
||||
ensureParams((rustBackend as RustBackend).paramDestinationDir)
|
||||
twig("params exist! attempting to send...")
|
||||
rustBackend.createToAddress(
|
||||
fromAccountId,
|
||||
spendingKeyStore,
|
||||
fromAccountIndex,
|
||||
spendingKey,
|
||||
toAddress,
|
||||
value,
|
||||
memo
|
||||
|
@ -277,9 +269,6 @@ class Wallet private constructor(
|
|||
return OkHttpClient()
|
||||
}
|
||||
|
||||
|
||||
private fun String.toPath(): String = "$paramDestinationDir/$this"
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The Url that is used by default in zcashd.
|
||||
|
@ -306,13 +295,18 @@ class Wallet private constructor(
|
|||
*/
|
||||
fun loadBirthdayFromAssets(context: Context, birthdayHeight: Int? = null): WalletBirthday {
|
||||
val treeFiles =
|
||||
context.assets.list(Wallet.BIRTHDAY_DIRECTORY)?.apply { sortDescending() }
|
||||
context.assets.list(BIRTHDAY_DIRECTORY)?.apply { sortDescending() }
|
||||
if (treeFiles.isNullOrEmpty()) throw MissingBirthdayFilesException(BIRTHDAY_DIRECTORY)
|
||||
val file: String
|
||||
try {
|
||||
val file = treeFiles.first {
|
||||
file = treeFiles.first() {
|
||||
if (birthdayHeight == null) true
|
||||
else it.contains(birthdayHeight.toString())
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
throw BirthdayNotFoundException(BIRTHDAY_DIRECTORY, birthdayHeight)
|
||||
}
|
||||
try {
|
||||
val reader = JsonReader(
|
||||
InputStreamReader(context.assets.open("$BIRTHDAY_DIRECTORY/$file"))
|
||||
)
|
||||
|
@ -374,4 +368,19 @@ class Wallet private constructor(
|
|||
val available: Long = -1
|
||||
)
|
||||
|
||||
//
|
||||
// Key Management Interfaces
|
||||
//
|
||||
|
||||
interface KeyManager: SeedProvider, SpendingKeyStore, SpendingKeyProvider
|
||||
|
||||
interface SeedProvider {
|
||||
val seed: ByteArray
|
||||
}
|
||||
interface SpendingKeyStore {
|
||||
var key: String
|
||||
}
|
||||
interface SpendingKeyProvider {
|
||||
val key: String
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,6 +59,9 @@ class LightWalletGrpcService private constructor(
|
|||
return channel.createStub().sendTransaction(request)
|
||||
}
|
||||
|
||||
override fun shutdown() {
|
||||
channel.shutdownNow()
|
||||
}
|
||||
|
||||
//
|
||||
// Utilities
|
||||
|
|
|
@ -25,4 +25,9 @@ interface LightWalletService {
|
|||
* Submit a raw transaction.
|
||||
*/
|
||||
fun submitTransaction(spendTransaction: ByteArray): Service.SendResponse
|
||||
|
||||
/**
|
||||
* Cleanup any connections when the service is shutting down and not going to be used again.
|
||||
*/
|
||||
fun shutdown()
|
||||
}
|
Loading…
Reference in New Issue