New: Additional APIs and functionality.
Added new functions that made things easier while testing, making the SDK a bit more usable.
This commit is contained in:
parent
c585ed93ff
commit
5cc8a38a5f
|
@ -1,8 +1,9 @@
|
|||
package cash.z.wallet.sdk
|
||||
|
||||
import android.content.Context
|
||||
import cash.z.wallet.sdk.Synchronizer.AddressType
|
||||
import cash.z.wallet.sdk.Synchronizer.AddressType.Shielded
|
||||
import cash.z.wallet.sdk.Synchronizer.AddressType.Transparent
|
||||
import cash.z.wallet.sdk.Synchronizer.ConsensusMatchType
|
||||
import cash.z.wallet.sdk.Synchronizer.Status.*
|
||||
import cash.z.wallet.sdk.block.CompactBlockDbStore
|
||||
import cash.z.wallet.sdk.block.CompactBlockDownloader
|
||||
|
@ -12,17 +13,17 @@ import cash.z.wallet.sdk.block.CompactBlockProcessor.WalletBalance
|
|||
import cash.z.wallet.sdk.block.CompactBlockStore
|
||||
import cash.z.wallet.sdk.entity.*
|
||||
import cash.z.wallet.sdk.exception.SynchronizerException
|
||||
import cash.z.wallet.sdk.ext.ZcashSdk
|
||||
import cash.z.wallet.sdk.ext.twig
|
||||
import cash.z.wallet.sdk.ext.twigTask
|
||||
import cash.z.wallet.sdk.jni.RustBackend
|
||||
import cash.z.wallet.sdk.ext.*
|
||||
import cash.z.wallet.sdk.rpc.Service
|
||||
import cash.z.wallet.sdk.service.LightWalletGrpcService
|
||||
import cash.z.wallet.sdk.service.LightWalletService
|
||||
import cash.z.wallet.sdk.transaction.*
|
||||
import io.grpc.ManagedChannel
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
/**
|
||||
* A Synchronizer that attempts to remain operational, despite any number of errors that can occur.
|
||||
|
@ -50,10 +51,32 @@ class SdkSynchronizer internal constructor(
|
|||
* The lifespan of this Synchronizer. This scope is initialized once the Synchronizer starts
|
||||
* because it will be a child of the parentScope that gets passed into the [start] function.
|
||||
* Everything launched by this Synchronizer will be cancelled once the Synchronizer or its
|
||||
* parentScope stops. This is a lateinit rather than nullable property so that it fails early
|
||||
* parentScope stops. This coordinates with [isStarted] so that it fails early
|
||||
* rather than silently, whenever the scope is used before the Synchronizer has been started.
|
||||
*/
|
||||
lateinit var coroutineScope: CoroutineScope
|
||||
var coroutineScope: CoroutineScope = CoroutineScope(EmptyCoroutineContext)
|
||||
get() {
|
||||
if (!isStarted) {
|
||||
throw SynchronizerException.NotYetStarted
|
||||
} else {
|
||||
return field
|
||||
}
|
||||
}
|
||||
set(value) {
|
||||
field = value
|
||||
if (value.coroutineContext !is EmptyCoroutineContext) isStarted = true
|
||||
}
|
||||
|
||||
/**
|
||||
* The channel that this Synchronizer uses to communicate with lightwalletd. In most cases, this
|
||||
* should not be needed or used. Instead, APIs should be added to the synchronizer to
|
||||
* enable the desired behavior. In the rare case, such as testing, it can be helpful to share
|
||||
* the underlying channel to connect to the same service, and use other APIs
|
||||
* (such as darksidewalletd) because channels are heavyweight.
|
||||
*/
|
||||
val channel: ManagedChannel get() = (processor.downloader.lightwalletService as LightWalletGrpcService).channel
|
||||
|
||||
var isStarted = false
|
||||
|
||||
|
||||
//
|
||||
|
@ -93,6 +116,7 @@ class SdkSynchronizer internal constructor(
|
|||
*/
|
||||
override val processorInfo: Flow<CompactBlockProcessor.ProcessorInfo> = processor.processorInfo
|
||||
|
||||
|
||||
//
|
||||
// Error Handling
|
||||
//
|
||||
|
@ -125,13 +149,27 @@ class SdkSynchronizer internal constructor(
|
|||
* A callback to invoke whenever a chain error is encountered. These occur whenever the
|
||||
* processor detects a missing or non-chain-sequential block (i.e. a reorg).
|
||||
*/
|
||||
override var onChainErrorHandler: ((Int, Int) -> Any)? = null
|
||||
override var onChainErrorHandler: ((errorHeight: Int, rewindHeight: Int) -> Any)? = null
|
||||
|
||||
|
||||
//
|
||||
// Public API
|
||||
//
|
||||
|
||||
/**
|
||||
* Convenience function for the latest balance. Instead of using this, a wallet will more likely
|
||||
* want to consume the flow of balances using [balances].
|
||||
*/
|
||||
override val latestBalance: WalletBalance get() = _balances.value
|
||||
|
||||
/**
|
||||
* Convenience function for the latest height. Specifically, this value represents the last
|
||||
* height that the synchronizer has observed from the lightwalletd server. Instead of using
|
||||
* this, a wallet will more likely want to consume the flow of processor info using
|
||||
* [processorInfo].
|
||||
*/
|
||||
override val latestHeight: Int get() = processor.currentInfo.networkBlockHeight
|
||||
|
||||
/**
|
||||
* Starts this synchronizer within the given scope. For simplicity, attempting to start an
|
||||
* instance that has already been started will throw a [SynchronizerException.FalseStart]
|
||||
|
@ -148,15 +186,15 @@ class SdkSynchronizer internal constructor(
|
|||
* @return an instance of this class so that this function can be used fluidly.
|
||||
*/
|
||||
override fun start(parentScope: CoroutineScope?): Synchronizer {
|
||||
if (::coroutineScope.isInitialized) throw SynchronizerException.FalseStart
|
||||
|
||||
if (isStarted) throw SynchronizerException.FalseStart
|
||||
// base this scope on the parent so that when the parent's job cancels, everything here
|
||||
// cancels as well also use a supervisor job so that one failure doesn't bring down the
|
||||
// whole synchronizer
|
||||
val supervisorJob = SupervisorJob(parentScope?.coroutineContext?.get(Job))
|
||||
coroutineScope =
|
||||
CoroutineScope(supervisorJob + Dispatchers.Main)
|
||||
coroutineScope.onReady()
|
||||
CoroutineScope(supervisorJob + Dispatchers.Main).let { scope ->
|
||||
coroutineScope = scope
|
||||
scope.onReady()
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -174,6 +212,32 @@ class SdkSynchronizer internal constructor(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function that exposes the underlying server information, like its name and
|
||||
* consensus branch id. Most wallets should already have a different source of truth for the
|
||||
* server(s) with which they operate.
|
||||
*/
|
||||
override suspend fun getServerInfo(): Service.LightdInfo = processor.downloader.getServerInfo()
|
||||
|
||||
|
||||
//
|
||||
// Storage APIs
|
||||
//
|
||||
|
||||
// TODO: turn this section into the data access API. For now, just aggregate all the things that we want to do with the underlying data
|
||||
|
||||
fun findBlockHash(height: Int): ByteArray? {
|
||||
return (storage as? PagedTransactionRepository)?.findBlockHash(height)
|
||||
}
|
||||
|
||||
fun findBlockHashAsHex(height: Int): String? {
|
||||
return findBlockHash(height)?.toHexReversed()
|
||||
}
|
||||
|
||||
fun getTransactionCount(): Int {
|
||||
return (storage as? PagedTransactionRepository)?.getTransactionCount() ?: 0
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Private API
|
||||
|
|
|
@ -3,7 +3,9 @@ package cash.z.wallet.sdk
|
|||
import androidx.paging.PagedList
|
||||
import cash.z.wallet.sdk.block.CompactBlockProcessor
|
||||
import cash.z.wallet.sdk.block.CompactBlockProcessor.WalletBalance
|
||||
import cash.z.wallet.sdk.entity.*
|
||||
import cash.z.wallet.sdk.entity.ConfirmedTransaction
|
||||
import cash.z.wallet.sdk.entity.PendingTransaction
|
||||
import cash.z.wallet.sdk.ext.ConsensusBranchId
|
||||
import cash.z.wallet.sdk.rpc.Service
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
@ -34,6 +36,12 @@ interface Synchronizer {
|
|||
/**
|
||||
* Stop this synchronizer. Implementations should ensure that calling this method cancels all
|
||||
* jobs that were created by this instance.
|
||||
*
|
||||
* Note that in most cases, there is no need to call [stop] because the Synchronizer will
|
||||
* automatically stop whenever the parentScope is cancelled. For instance, if that scope is
|
||||
* bound to the lifecycle of the activity, the Synchronizer will stop when the activity stops.
|
||||
* However, if no scope is provided to the start method, then the Synchronizer must be stopped
|
||||
* with this function.
|
||||
*/
|
||||
fun stop()
|
||||
|
||||
|
@ -93,6 +101,21 @@ interface Synchronizer {
|
|||
val receivedTransactions: Flow<PagedList<ConfirmedTransaction>>
|
||||
|
||||
|
||||
//
|
||||
// Latest Properties
|
||||
//
|
||||
|
||||
/**
|
||||
* An in-memory reference to the latest height seen on the network.
|
||||
*/
|
||||
val latestHeight: Int
|
||||
|
||||
/**
|
||||
* An in-memory reference to the most recently calculated balance.
|
||||
*/
|
||||
val latestBalance: WalletBalance
|
||||
|
||||
|
||||
//
|
||||
// Operations
|
||||
//
|
||||
|
@ -154,6 +177,19 @@ interface Synchronizer {
|
|||
*/
|
||||
suspend fun isValidTransparentAddr(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
|
||||
* an incompatible server can surface that information effectively. For the SDK, the consensus
|
||||
* branch is used when creating transactions as each one needs to target a specific branch. This
|
||||
* function compares the server's branch id to this SDK's and returns information that helps
|
||||
* determine whether they match.
|
||||
*
|
||||
* @return an instance of [ConsensusMatchType] that is essentially a wrapper for both branch ids
|
||||
* and provides helper functions for communicating detailed errors to the user.
|
||||
*/
|
||||
suspend fun validateConsensusBranch(): ConsensusMatchType
|
||||
|
||||
/**
|
||||
* Validates the given address, returning information about why it is invalid. This is a
|
||||
* convenience method that combines the behavior of [isValidShieldedAddr] and
|
||||
|
@ -177,6 +213,13 @@ interface Synchronizer {
|
|||
*/
|
||||
suspend fun cancelSpend(transaction: PendingTransaction): Boolean
|
||||
|
||||
/**
|
||||
* Convenience function that exposes the underlying server information, like its name and
|
||||
* consensus branch id. Most wallets should already have a different source of truth for the
|
||||
* server(s) with which they operate and thereby not need this function.
|
||||
*/
|
||||
suspend fun getServerInfo(): Service.LightdInfo
|
||||
|
||||
|
||||
//
|
||||
// Error Handling
|
||||
|
@ -294,4 +337,38 @@ interface Synchronizer {
|
|||
val isNotValid get() = this !is Valid
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class that provides consensus branch information for this SDK and the server to which
|
||||
* it is connected and whether they are aligned. Essentially a wrapper for both branch ids with
|
||||
* helper functions for communicating detailed error information to the end-user.
|
||||
*/
|
||||
class ConsensusMatchType(val sdkBranch: ConsensusBranchId?, val serverBranch: ConsensusBranchId?) {
|
||||
val hasServerBranch = serverBranch != null
|
||||
val hasSdkBranch = sdkBranch != null
|
||||
val isValid = hasServerBranch && sdkBranch == serverBranch
|
||||
val hasBoth = hasServerBranch && hasSdkBranch
|
||||
val hasNeither = !hasServerBranch && !hasSdkBranch
|
||||
val isServerNewer = hasBoth && serverBranch!!.ordinal > sdkBranch!!.ordinal
|
||||
val isSdkNewer = hasBoth && sdkBranch!!.ordinal > serverBranch!!.ordinal
|
||||
|
||||
val errorMessage
|
||||
get() = when {
|
||||
isValid -> null
|
||||
hasNeither -> "Our branch is unknown and the server branch is unknown. Verify" +
|
||||
" that they are both using the latest consensus branch ID."
|
||||
hasServerBranch -> "The server is on $serverBranch but our branch is unknown." +
|
||||
" Verify that we are fully synced."
|
||||
hasSdkBranch -> "We are on $sdkBranch but the server branch is unknown. Verify" +
|
||||
" the network connection."
|
||||
else -> {
|
||||
val newerBranch = if (isServerNewer) serverBranch else sdkBranch
|
||||
val olderBranch = if (isSdkNewer) serverBranch else sdkBranch
|
||||
val newerDevice = if (isServerNewer) "the server has" else "we have"
|
||||
val olderDevice = if (isSdkNewer) "the server has" else "we have"
|
||||
"Incompatible consensus: $newerDevice upgraded to $newerBranch but" +
|
||||
" $olderDevice $olderBranch."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -44,6 +44,10 @@ class CompactBlockDbStore(
|
|||
if (lastBlock < SAPLING_ACTIVATION_HEIGHT) -1 else lastBlock
|
||||
}
|
||||
|
||||
override suspend fun findCompactBlock(height: Int): CompactFormats.CompactBlock? {
|
||||
return cacheDao.findCompactBlock(height)?.let { CompactFormats.CompactBlock.parseFrom(it) }
|
||||
}
|
||||
|
||||
override suspend fun write(result: List<CompactFormats.CompactBlock>) = withContext(IO) {
|
||||
cacheDao.insert(result.map { CompactBlockEntity(it.height.toInt(), it.toByteArray()) })
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package cash.z.wallet.sdk.block
|
||||
|
||||
import cash.z.wallet.sdk.rpc.Service
|
||||
import cash.z.wallet.sdk.service.LightWalletService
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -61,6 +62,10 @@ open class CompactBlockDownloader(
|
|||
compactBlockStore.getLatestHeight()
|
||||
}
|
||||
|
||||
suspend fun getServerInfo(): Service.LightdInfo = withContext(IO) {
|
||||
lightwalletService.getServerInfo()
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop this downloader and cleanup any resources being used.
|
||||
*/
|
||||
|
|
|
@ -13,6 +13,13 @@ interface CompactBlockStore {
|
|||
*/
|
||||
suspend fun getLatestHeight(): Int
|
||||
|
||||
/**
|
||||
* Fetch the compact block for the given height, if it exists.
|
||||
*
|
||||
* @return the compact block or null when it did not exist.
|
||||
*/
|
||||
suspend fun findCompactBlock(height: Int): CompactFormats.CompactBlock?
|
||||
|
||||
/**
|
||||
* Write the given blocks to this store, which may be anything from an in-memory cache to a DB.
|
||||
*
|
||||
|
|
|
@ -44,4 +44,7 @@ interface CompactBlockDao {
|
|||
|
||||
@Query("SELECT MAX(height) FROM compactblocks")
|
||||
fun latestBlockHeight(): Int
|
||||
|
||||
@Query("SELECT data FROM compactblocks WHERE height = :height")
|
||||
fun findCompactBlock(height: Int): ByteArray?
|
||||
}
|
|
@ -5,6 +5,8 @@ import androidx.room.Dao
|
|||
import androidx.room.Database
|
||||
import androidx.room.Query
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import cash.z.wallet.sdk.entity.*
|
||||
|
||||
//
|
||||
|
@ -34,6 +36,99 @@ abstract class DerivedDataDb : RoomDatabase() {
|
|||
abstract fun blockDao(): BlockDao
|
||||
abstract fun receivedDao(): ReceivedDao
|
||||
abstract fun sentDao(): SentDao
|
||||
|
||||
|
||||
//
|
||||
// Migrations
|
||||
//
|
||||
|
||||
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;")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -51,6 +146,9 @@ interface BlockDao {
|
|||
|
||||
@Query("SELECT MAX(height) FROM blocks")
|
||||
fun lastScannedHeight(): Int
|
||||
|
||||
@Query( "SELECT hash FROM BLOCKS WHERE height = :height")
|
||||
fun findHashByHeight(height: Int): ByteArray?
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -237,4 +335,3 @@ interface TransactionDao {
|
|||
suspend fun findAllTransactionsByRange(blockRangeStart: Int, blockRangeEnd: Int = blockRangeStart, limit: Int = Int.MAX_VALUE): List<ConfirmedTransaction>
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
package cash.z.wallet.sdk.ext
|
||||
|
||||
fun ByteArray.toHex(): String {
|
||||
val sb = StringBuilder(size * 2)
|
||||
for (b in this)
|
||||
sb.append(String.format("%02x", b))
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
fun ByteArray.toHexReversed(): String {
|
||||
val sb = StringBuilder(size * 2)
|
||||
var i = size - 1
|
||||
while (i >= 0)
|
||||
sb.append(String.format("%02x", this[i--]))
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
fun String.fromHex(): ByteArray {
|
||||
val len = length
|
||||
val data = ByteArray(len / 2)
|
||||
var i = 0
|
||||
while (i < len) {
|
||||
data[i / 2] =
|
||||
((Character.digit(this[i], 16) shl 4) + Character.digit(this[i + 1], 16)).toByte()
|
||||
i += 2
|
||||
}
|
||||
return data
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package cash.z.wallet.sdk.ext
|
||||
|
||||
internal inline fun <R> tryNull(block: () -> R): R? {
|
||||
return try {
|
||||
block()
|
||||
} catch (t: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
|
@ -24,7 +24,7 @@ import java.util.concurrent.TimeUnit
|
|||
* created for streaming requests, it will use a deadline that is after the given duration from now.
|
||||
*/
|
||||
class LightWalletGrpcService private constructor(
|
||||
private var channel: ManagedChannel,
|
||||
var channel: ManagedChannel,
|
||||
private val singleRequestTimeoutSec: Long = 10L,
|
||||
private val streamingRequestTimeoutSec: Long = 90L
|
||||
) : LightWalletService {
|
||||
|
@ -63,6 +63,10 @@ class LightWalletGrpcService private constructor(
|
|||
return channel.createStub(singleRequestTimeoutSec).getLatestBlock(Service.ChainSpec.newBuilder().build()).height.toInt()
|
||||
}
|
||||
|
||||
override fun getServerInfo(): Service.LightdInfo {
|
||||
channel.resetConnectBackoff()
|
||||
return channel.createStub(singleRequestTimeoutSec).getLightdInfo(Service.Empty.newBuilder().build())
|
||||
}
|
||||
override fun submitTransaction(spendTransaction: ByteArray): Service.SendResponse {
|
||||
channel.resetConnectBackoff()
|
||||
val request = Service.RawTransaction.newBuilder().setData(ByteString.copyFrom(spendTransaction)).build()
|
||||
|
@ -79,6 +83,8 @@ class LightWalletGrpcService private constructor(
|
|||
}
|
||||
|
||||
override fun reconnect() {
|
||||
twig("closing existing channel and then reconnecting to" +
|
||||
" ${connectionInfo.host}:${connectionInfo.port}?usePlaintext=${connectionInfo.usePlaintext}")
|
||||
channel.shutdown()
|
||||
channel = createDefaultChannel(
|
||||
connectionInfo.appContext,
|
||||
|
@ -132,7 +138,7 @@ class LightWalletGrpcService private constructor(
|
|||
port: Int,
|
||||
usePlaintext: Boolean
|
||||
): ManagedChannel {
|
||||
twig("Creating channel that will connect to $host:$port")
|
||||
twig("Creating channel that will connect to $host:$port?usePlaintext=$usePlaintext")
|
||||
return AndroidChannelBuilder
|
||||
.forAddress(host, port)
|
||||
.context(appContext)
|
||||
|
|
|
@ -26,6 +26,25 @@ interface LightWalletService {
|
|||
*/
|
||||
fun getLatestBlockHeight(): Int
|
||||
|
||||
/**
|
||||
* Return basic information about the server such as:
|
||||
*
|
||||
* ```
|
||||
* {
|
||||
* "version": "0.2.1",
|
||||
* "vendor": "ECC LightWalletD",
|
||||
* "taddrSupport": true,
|
||||
* "chainName": "main",
|
||||
* "saplingActivationHeight": 419200,
|
||||
* "consensusBranchId": "2bb40e60",
|
||||
* "blockHeight": 861272
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @return useful server details.
|
||||
*/
|
||||
fun getServerInfo(): Service.LightdInfo
|
||||
|
||||
/**
|
||||
* Submit a raw transaction.
|
||||
*
|
||||
|
|
|
@ -38,9 +38,9 @@ open class PagedTransactionRepository(
|
|||
) : this(
|
||||
Room.databaseBuilder(context, DerivedDataDb::class.java, dataDbName)
|
||||
.setJournalMode(RoomDatabase.JournalMode.TRUNCATE)
|
||||
.addMigrations(MIGRATION_3_4)
|
||||
.addMigrations(MIGRATION_4_3)
|
||||
.addMigrations(MIGRATION_4_5)
|
||||
.addMigrations(DerivedDataDb.MIGRATION_3_4)
|
||||
.addMigrations(DerivedDataDb.MIGRATION_4_3)
|
||||
.addMigrations(DerivedDataDb.MIGRATION_4_5)
|
||||
.build(),
|
||||
pageSize
|
||||
)
|
||||
|
@ -86,6 +86,7 @@ open class PagedTransactionRepository(
|
|||
transactions.findMinedHeight(rawTransactionId)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Close the underlying database.
|
||||
*/
|
||||
|
@ -93,96 +94,7 @@ open class PagedTransactionRepository(
|
|||
derivedDataDb.close()
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Migrations
|
||||
//
|
||||
|
||||
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;")
|
||||
}
|
||||
}
|
||||
|
||||
private 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;")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private 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;")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// TODO: begin converting these into Data Access API. For now, just collect the desired operations and iterate/refactor, later
|
||||
fun findBlockHash(height: Int): ByteArray? = blocks.findHashByHeight(height)
|
||||
fun getTransactionCount(): Int = transactions.count()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue