Documented everything public-facing.
This should bring documentation coverage to 100%
This commit is contained in:
parent
de0d85c20d
commit
a43070cd6a
|
@ -4,7 +4,8 @@ import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import cash.z.wallet.sdk.exception.BirthdayException
|
import cash.z.wallet.sdk.exception.BirthdayException
|
||||||
import cash.z.wallet.sdk.exception.InitializerException
|
import cash.z.wallet.sdk.exception.InitializerException
|
||||||
import cash.z.wallet.sdk.ext.*
|
import cash.z.wallet.sdk.ext.ZcashSdk
|
||||||
|
import cash.z.wallet.sdk.ext.twig
|
||||||
import cash.z.wallet.sdk.jni.RustBackend
|
import cash.z.wallet.sdk.jni.RustBackend
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.stream.JsonReader
|
import com.google.gson.stream.JsonReader
|
||||||
|
@ -628,6 +629,52 @@ class Initializer(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Helper functions for using SharedPreferences
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenient constructor function for SharedPreferences used by this class.
|
||||||
|
*/
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
private fun SharedPrefs(context: Context, name: String = "prefs"): SharedPreferences {
|
||||||
|
val fileName = "${BuildConfig.FLAVOR}.${BuildConfig.BUILD_TYPE}.$name".toLowerCase()
|
||||||
|
return context.getSharedPreferences(fileName, Context.MODE_PRIVATE)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun SharedPreferences.edit(block: (SharedPreferences.Editor) -> Unit) {
|
||||||
|
edit().run {
|
||||||
|
block(this)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private operator fun SharedPreferences.set(key: String, value: Any?) {
|
||||||
|
when (value) {
|
||||||
|
is String? -> edit { it.putString(key, value) }
|
||||||
|
is Int -> edit { it.putInt(key, value) }
|
||||||
|
is Boolean -> edit { it.putBoolean(key, value) }
|
||||||
|
is Float -> edit { it.putFloat(key, value) }
|
||||||
|
is Long -> edit { it.putLong(key, value) }
|
||||||
|
else -> throw UnsupportedOperationException("Not yet implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline operator fun <reified T : Any> SharedPreferences.get(
|
||||||
|
key: String,
|
||||||
|
defaultValue: T? = null
|
||||||
|
): T? {
|
||||||
|
return when (T::class) {
|
||||||
|
String::class -> getString(key, defaultValue as? String) as T?
|
||||||
|
Int::class -> getInt(key, defaultValue as? Int ?: -1) as T?
|
||||||
|
Boolean::class -> getBoolean(key, defaultValue as? Boolean ?: false) as T?
|
||||||
|
Float::class -> getFloat(key, defaultValue as? Float ?: -1f) as T?
|
||||||
|
Long::class -> getLong(key, defaultValue as? Long ?: -1) as T?
|
||||||
|
else -> throw UnsupportedOperationException("Not yet implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,11 +109,11 @@ interface Synchronizer {
|
||||||
/**
|
/**
|
||||||
* Sends zatoshi.
|
* Sends zatoshi.
|
||||||
*
|
*
|
||||||
* @param spendingKey the key that allows spends to occur.
|
* @param spendingKey the key associated with the notes that will be spent.
|
||||||
* @param zatoshi the amount of zatoshi to send.
|
* @param zatoshi the amount of zatoshi to send.
|
||||||
* @param toAddress the recipient's address.
|
* @param toAddress the recipient's address.
|
||||||
* @param memo the optional memo to include as part of the transaction.
|
* @param memo the optional memo to include as part of the transaction.
|
||||||
* @param fromAccountId the optional account id to use. By default, the first account is used.
|
* @param fromAccountIndex the optional account id to use. By default, the first account is used.
|
||||||
*
|
*
|
||||||
* @return a flow of PendingTransaction objects representing changes to the state of the
|
* @return a flow of PendingTransaction objects representing changes to the state of the
|
||||||
* transaction. Any time the state changes a new instance will be emitted by this flow. This is
|
* transaction. Any time the state changes a new instance will be emitted by this flow. This is
|
||||||
|
|
|
@ -8,6 +8,12 @@ import cash.z.wallet.sdk.entity.CompactBlockEntity
|
||||||
// Database
|
// Database
|
||||||
//
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The "Cache DB", serving as a cache of compact blocks, waiting to be processed. This will contain
|
||||||
|
* the entire blockchain, from the birthdate of the wallet, forward. The [CompactBlockProcessor]
|
||||||
|
* will copy blocks from this database, as they are scanned. In the future, those blocks can be
|
||||||
|
* deleted because they are no longer needed. Currently, this efficiency has not been implemented.
|
||||||
|
*/
|
||||||
@Database(
|
@Database(
|
||||||
entities = [CompactBlockEntity::class],
|
entities = [CompactBlockEntity::class],
|
||||||
version = 1,
|
version = 1,
|
||||||
|
@ -22,6 +28,9 @@ abstract class CompactBlockDb : RoomDatabase() {
|
||||||
// Data Access Objects
|
// Data Access Objects
|
||||||
//
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data access object for compact blocks in the "Cache DB."
|
||||||
|
*/
|
||||||
@Dao
|
@Dao
|
||||||
interface CompactBlockDao {
|
interface CompactBlockDao {
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
|
|
@ -11,6 +11,13 @@ import cash.z.wallet.sdk.entity.*
|
||||||
// Database
|
// Database
|
||||||
//
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The "Data DB," where all data derived from the compact blocks is stored. Most importantly, this
|
||||||
|
* database contains transaction information and can be queried for the current balance. The
|
||||||
|
* "blocks" table contains a copy of everything that has been scanned. In the future, that table can
|
||||||
|
* be truncated up to the last scanned block, for storage efficiency. Wallets should only read from,
|
||||||
|
* but never write to, this database.
|
||||||
|
*/
|
||||||
@Database(
|
@Database(
|
||||||
entities = [
|
entities = [
|
||||||
TransactionEntity::class,
|
TransactionEntity::class,
|
||||||
|
@ -34,6 +41,9 @@ abstract class DerivedDataDb : RoomDatabase() {
|
||||||
// Data Access Objects
|
// Data Access Objects
|
||||||
//
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The data access object for blocks, used for determining the last scanned height.
|
||||||
|
*/
|
||||||
@Dao
|
@Dao
|
||||||
interface BlockDao {
|
interface BlockDao {
|
||||||
@Query("SELECT COUNT(height) FROM blocks")
|
@Query("SELECT COUNT(height) FROM blocks")
|
||||||
|
@ -43,18 +53,28 @@ interface BlockDao {
|
||||||
fun lastScannedHeight(): Int
|
fun lastScannedHeight(): Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The data access object for notes, used for determining whether transactions exist.
|
||||||
|
*/
|
||||||
@Dao
|
@Dao
|
||||||
interface ReceivedDao {
|
interface ReceivedDao {
|
||||||
@Query("SELECT COUNT(tx) FROM received_notes")
|
@Query("SELECT COUNT(tx) FROM received_notes")
|
||||||
fun count(): Int
|
fun count(): Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The data access object for sent notes, used for determining whether outbound transactions exist.
|
||||||
|
*/
|
||||||
@Dao
|
@Dao
|
||||||
interface SentDao {
|
interface SentDao {
|
||||||
@Query("SELECT COUNT(tx) FROM sent_notes")
|
@Query("SELECT COUNT(tx) FROM sent_notes")
|
||||||
fun count(): Int
|
fun count(): Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The data access object for transactions, used for querying all transaction information, including
|
||||||
|
* whether transactions are mined.
|
||||||
|
*/
|
||||||
@Dao
|
@Dao
|
||||||
interface TransactionDao {
|
interface TransactionDao {
|
||||||
@Query("SELECT COUNT(id_tx) FROM transactions")
|
@Query("SELECT COUNT(id_tx) FROM transactions")
|
||||||
|
@ -79,12 +99,6 @@ interface TransactionDao {
|
||||||
""")
|
""")
|
||||||
fun findMinedHeight(rawTransactionId: ByteArray): Int?
|
fun findMinedHeight(rawTransactionId: ByteArray): Int?
|
||||||
|
|
||||||
// @Delete
|
|
||||||
// fun delete(transaction: Transaction)
|
|
||||||
//
|
|
||||||
// @Query("DELETE FROM transactions WHERE id_tx = :id")
|
|
||||||
// fun deleteById(id: Long)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query sent transactions that have been mined, sorted so the newest data is at the top.
|
* Query sent transactions that have been mined, sorted so the newest data is at the top.
|
||||||
*/
|
*/
|
||||||
|
@ -137,6 +151,9 @@ interface TransactionDao {
|
||||||
""")
|
""")
|
||||||
fun getReceivedTransactions(limit: Int = Int.MAX_VALUE): DataSource.Factory<Int, ConfirmedTransaction>
|
fun getReceivedTransactions(limit: Int = Int.MAX_VALUE): DataSource.Factory<Int, ConfirmedTransaction>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query all transactions, joining outbound and inbound transactions into the same table.
|
||||||
|
*/
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT transactions.id_tx AS id,
|
SELECT transactions.id_tx AS id,
|
||||||
transactions.block AS minedHeight,
|
transactions.block AS minedHeight,
|
||||||
|
|
|
@ -9,6 +9,12 @@ import kotlinx.coroutines.flow.Flow
|
||||||
// Database
|
// Database
|
||||||
//
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database for pending transaction information. Unlike with the "Data DB," the wallet is free to
|
||||||
|
* write to this database. In a way, this almost serves as a local mempool for all transactions
|
||||||
|
* initiated by this wallet. Currently, the data necessary to support expired transactions is there
|
||||||
|
* but it is not being leveraged.
|
||||||
|
*/
|
||||||
@Database(
|
@Database(
|
||||||
entities = [
|
entities = [
|
||||||
PendingTransactionEntity::class
|
PendingTransactionEntity::class
|
||||||
|
@ -25,6 +31,9 @@ abstract class PendingTransactionDb : RoomDatabase() {
|
||||||
// Data Access Objects
|
// Data Access Objects
|
||||||
//
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data access object providing crud for pending transactions.
|
||||||
|
*/
|
||||||
@Dao
|
@Dao
|
||||||
interface PendingTransactionDao {
|
interface PendingTransactionDao {
|
||||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||||
|
|
|
@ -19,17 +19,27 @@ sealed class RustLayerException(message: String, cause: Throwable? = null) : Sdk
|
||||||
"blocks are not missing or have not been scanned out of order.", cause)
|
"blocks are not missing or have not been scanned out of order.", cause)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User-facing exceptions thrown by the transaction repository.
|
||||||
|
*/
|
||||||
sealed class RepositoryException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
sealed class RepositoryException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
||||||
object FalseStart: RepositoryException( "The channel is closed. Note that once a repository has stopped it " +
|
object FalseStart: RepositoryException( "The channel is closed. Note that once a repository has stopped it " +
|
||||||
"cannot be restarted. Verify that the repository is not being restarted.")
|
"cannot be restarted. Verify that the repository is not being restarted.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* High-level exceptions thrown by the synchronizer, which do not fall within the umbrealla of a
|
||||||
|
* child component.
|
||||||
|
*/
|
||||||
sealed class SynchronizerException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
sealed class SynchronizerException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
||||||
object FalseStart: SynchronizerException("This synchronizer was already started. Multiple calls to start are not" +
|
object FalseStart: SynchronizerException("This synchronizer was already started. Multiple calls to start are not" +
|
||||||
"allowed and once a synchronizer has stopped it cannot be restarted."
|
"allowed and once a synchronizer has stopped it cannot be restarted."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Potentially user-facing exceptions that occur while processing compact blocks.
|
||||||
|
*/
|
||||||
sealed class CompactBlockProcessorException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
sealed class CompactBlockProcessorException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
||||||
class DataDbMissing(path: String): CompactBlockProcessorException("No data db file found at path $path. Verify " +
|
class DataDbMissing(path: String): CompactBlockProcessorException("No data db file found at path $path. Verify " +
|
||||||
"that the data DB has been initialized via `rustBackend.initDataDb(path)`")
|
"that the data DB has been initialized via `rustBackend.initDataDb(path)`")
|
||||||
|
@ -48,13 +58,9 @@ sealed class CompactBlockProcessorException(message: String, cause: Throwable? =
|
||||||
" can be fixed by re-importing the wallet.")
|
" can be fixed by re-importing the wallet.")
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class CompactBlockStreamException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
/**
|
||||||
object ConnectionClosed: CompactBlockStreamException("Cannot start stream when connection is closed.")
|
* Exceptions related to the wallet's birthday.
|
||||||
class FalseStart(cause: Throwable?): CompactBlockStreamException("Failed to start compact block stream due to " +
|
*/
|
||||||
"$cause caused by ${cause?.cause}")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
sealed class BirthdayException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
sealed class BirthdayException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
||||||
object UninitializedBirthdayException : BirthdayException("Error the birthday cannot be" +
|
object UninitializedBirthdayException : BirthdayException("Error the birthday cannot be" +
|
||||||
" accessed before it is initialized. Verify that the new, import or open functions" +
|
" accessed before it is initialized. Verify that the new, import or open functions" +
|
||||||
|
@ -76,6 +82,9 @@ sealed class BirthdayException(message: String, cause: Throwable? = null) : SdkE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exceptions thrown by the initializer.
|
||||||
|
*/
|
||||||
sealed class InitializerException(message: String, cause: Throwable? = null) : SdkException(message, cause){
|
sealed class InitializerException(message: String, cause: Throwable? = null) : SdkException(message, cause){
|
||||||
class FalseStart(cause: Throwable?) : InitializerException("Failed to initialize accounts due to: $cause", cause)
|
class FalseStart(cause: Throwable?) : InitializerException("Failed to initialize accounts due to: $cause", cause)
|
||||||
class AlreadyInitializedException(cause: Throwable, dbPath: String) : InitializerException("Failed to initialize the blocks table" +
|
class AlreadyInitializedException(cause: Throwable, dbPath: String) : InitializerException("Failed to initialize the blocks table" +
|
||||||
|
@ -86,6 +95,9 @@ sealed class InitializerException(message: String, cause: Throwable? = null) :
|
||||||
" we can store data.")
|
" we can store data.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exceptions thrown while interacting with lightwalletd.
|
||||||
|
*/
|
||||||
sealed class LightwalletException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
sealed class LightwalletException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
||||||
object InsecureConnection : LightwalletException("Error: attempted to connect to lightwalletd" +
|
object InsecureConnection : LightwalletException("Error: attempted to connect to lightwalletd" +
|
||||||
" with an insecure connection! Plaintext connections are only allowed when the" +
|
" with an insecure connection! Plaintext connections are only allowed when the" +
|
||||||
|
@ -93,6 +105,9 @@ sealed class LightwalletException(message: String, cause: Throwable? = null) : S
|
||||||
" because this choice should be explicit.")
|
" because this choice should be explicit.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Potentially user-facing exceptions thrown while encoding transactions.
|
||||||
|
*/
|
||||||
sealed class TransactionEncoderException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
sealed class TransactionEncoderException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
||||||
class FetchParamsException(message: String) : TransactionEncoderException("Failed to fetch params due to: $message")
|
class FetchParamsException(message: String) : TransactionEncoderException("Failed to fetch params due to: $message")
|
||||||
object MissingParamsException : TransactionEncoderException(
|
object MissingParamsException : TransactionEncoderException(
|
||||||
|
|
|
@ -9,6 +9,14 @@ import java.math.RoundingMode
|
||||||
import java.text.NumberFormat
|
import java.text.NumberFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Convenience functions for converting currency values for display in user interfaces. The
|
||||||
|
* calculations done here are not intended for financial purposes, because all such transactions
|
||||||
|
* are done using Zatoshis in the Rust layer. Instead, these functions are focused on displaying
|
||||||
|
* accurately rounded values to the user.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
//TODO: provide a dynamic way to configure this globally for the SDK
|
//TODO: provide a dynamic way to configure this globally for the SDK
|
||||||
// For now, just make these vars so at least they could be modified in one place
|
// For now, just make these vars so at least they could be modified in one place
|
||||||
object Conversions {
|
object Conversions {
|
||||||
|
@ -29,11 +37,15 @@ object Conversions {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a Zatoshi value into Zec with the given number of digits, represented as a string.
|
* Format a Zatoshi value into ZEC with the given number of digits, represented as a string.
|
||||||
* Start with Zatoshi -> End with Zec.
|
* Start with Zatoshi -> End with ZEC.
|
||||||
*
|
*
|
||||||
* @param maxDecimals the number of decimal places to use in the format. Default is 6 because Zec is better than Usd.
|
* @param maxDecimals the number of decimal places to use in the format. Default is 6 because ZEC is
|
||||||
|
* better than USD.
|
||||||
* @param minDecimals the minimum number of digits to allow to the right of the decimal.
|
* @param minDecimals the minimum number of digits to allow to the right of the decimal.
|
||||||
|
*
|
||||||
|
* @return this Zatoshi value represented as ZEC, in a string with at least [minDecimals] and at
|
||||||
|
* most [maxDecimals]
|
||||||
*/
|
*/
|
||||||
inline fun Long?.convertZatoshiToZecString(
|
inline fun Long?.convertZatoshiToZecString(
|
||||||
maxDecimals: Int = ZEC_FORMATTER.maximumFractionDigits,
|
maxDecimals: Int = ZEC_FORMATTER.maximumFractionDigits,
|
||||||
|
@ -43,11 +55,15 @@ inline fun Long?.convertZatoshiToZecString(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a Zec value into Zec with the given number of digits, represented as a string.
|
* Format a ZEC value into ZEC with the given number of digits, represented as a string.
|
||||||
* Start with ZeC -> End with Zec.
|
* Start with ZEC -> End with ZEC.
|
||||||
*
|
*
|
||||||
* @param maxDecimals the number of decimal places to use in the format. Default is 6 because Zec is better when right.
|
* @param maxDecimals the number of decimal places to use in the format. Default is 6 because ZEC is
|
||||||
|
* better when right.
|
||||||
* @param minDecimals the minimum number of digits to allow to the right of the decimal.
|
* @param minDecimals the minimum number of digits to allow to the right of the decimal.
|
||||||
|
*
|
||||||
|
* @return this Double ZEC value represented as a string with at least [minDecimals] and at most
|
||||||
|
* [maxDecimals].
|
||||||
*/
|
*/
|
||||||
inline fun Double?.toZecString(
|
inline fun Double?.toZecString(
|
||||||
maxDecimals: Int = ZEC_FORMATTER.maximumFractionDigits,
|
maxDecimals: Int = ZEC_FORMATTER.maximumFractionDigits,
|
||||||
|
@ -57,11 +73,15 @@ inline fun Double?.toZecString(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a Zatoshi value into Zec with the given number of decimal places, represented as a string.
|
* Format a Zatoshi value into ZEC with the given number of decimal places, represented as a string.
|
||||||
* Start with ZeC -> End with Zec.
|
* Start with ZeC -> End with ZEC.
|
||||||
*
|
*
|
||||||
* @param maxDecimals the number of decimal places to use in the format. Default is 6 because Zec is better than bread.
|
* @param maxDecimals the number of decimal places to use in the format. Default is 6 because ZEC is
|
||||||
|
* better than bread.
|
||||||
* @param minDecimals the minimum number of digits to allow to the right of the decimal.
|
* @param minDecimals the minimum number of digits to allow to the right of the decimal.
|
||||||
|
*
|
||||||
|
* @return this BigDecimal ZEC value represented as a string with at least [minDecimals] and at most
|
||||||
|
* [maxDecimals].
|
||||||
*/
|
*/
|
||||||
inline fun BigDecimal?.toZecString(
|
inline fun BigDecimal?.toZecString(
|
||||||
maxDecimals: Int = ZEC_FORMATTER.maximumFractionDigits,
|
maxDecimals: Int = ZEC_FORMATTER.maximumFractionDigits,
|
||||||
|
@ -71,10 +91,15 @@ inline fun BigDecimal?.toZecString(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a Usd value into Usd with the given number of digits, represented as a string.
|
* Format a USD value into USD with the given number of digits, represented as a string.
|
||||||
|
* Start with USD -> end with USD.
|
||||||
*
|
*
|
||||||
* @param maxDecimals the number of decimal places to use in the format. Default is 6 because Zec is better than pennies
|
* @param maxDecimals the number of decimal places to use in the format. Default is 6 because
|
||||||
|
* ZEC is glorious.
|
||||||
* @param minDecimals the minimum number of digits to allow to the right of the decimal.
|
* @param minDecimals the minimum number of digits to allow to the right of the decimal.
|
||||||
|
*
|
||||||
|
* @return this Double ZEC value represented as a string with at least [minDecimals] and at most
|
||||||
|
* [maxDecimals], which is 2 by default. Zero is always represented without any decimals.
|
||||||
*/
|
*/
|
||||||
inline fun Double?.toUsdString(
|
inline fun Double?.toUsdString(
|
||||||
maxDecimals: Int = USD_FORMATTER.maximumFractionDigits,
|
maxDecimals: Int = USD_FORMATTER.maximumFractionDigits,
|
||||||
|
@ -88,9 +113,15 @@ inline fun Double?.toUsdString(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a Zatoshi value into Usd with the given number of decimal places, represented as a string.
|
* Format a USD value into USD with the given number of decimal places, represented as a string.
|
||||||
* @param maxDecimals the number of decimal places to use in the format. Default is 6 because Zec is glorious.
|
* Start with USD -> end with USD.
|
||||||
|
*
|
||||||
|
* @param maxDecimals the number of decimal places to use in the format. Default is 6 because ZEC is
|
||||||
|
* glorious.
|
||||||
* @param minDecimals the minimum number of digits to allow to the right of the decimal.
|
* @param minDecimals the minimum number of digits to allow to the right of the decimal.
|
||||||
|
*
|
||||||
|
* @return this BigDecimal USD value represented as a string with at least [minDecimals] and at most
|
||||||
|
* [maxDecimals], which is 2 by default.
|
||||||
*/
|
*/
|
||||||
inline fun BigDecimal?.toUsdString(
|
inline fun BigDecimal?.toUsdString(
|
||||||
maxDecimals: Int = USD_FORMATTER.maximumFractionDigits,
|
maxDecimals: Int = USD_FORMATTER.maximumFractionDigits,
|
||||||
|
@ -100,9 +131,15 @@ inline fun BigDecimal?.toUsdString(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a number formatter for use with converting currency to strings. This probably isn't needed externally since
|
* Create a number formatter for use with converting currency to strings. This probably isn't needed
|
||||||
* the other formatting functions leverage this, instead. Leverages the default rounding mode for zec found in
|
* externally since the other formatting functions leverage this, instead. Leverages the default
|
||||||
* ZEC_FORMATTER.
|
* rounding mode for zec found in ZEC_FORMATTER.
|
||||||
|
*
|
||||||
|
* @param maxDecimals the number of decimal places to use in the format. Default is 6 because ZEC is
|
||||||
|
* glorious.
|
||||||
|
* @param minDecimals the minimum number of digits to allow to the right of the decimal.
|
||||||
|
*
|
||||||
|
* @return a currency formatter, appropriate for the default locale.
|
||||||
*/
|
*/
|
||||||
inline fun currencyFormatter(maxDecimals: Int, minDecimals: Int): NumberFormat {
|
inline fun currencyFormatter(maxDecimals: Int, minDecimals: Int): NumberFormat {
|
||||||
return NumberFormat.getInstance(Locale.getDefault()).apply {
|
return NumberFormat.getInstance(Locale.getDefault()).apply {
|
||||||
|
@ -114,59 +151,108 @@ inline fun currencyFormatter(maxDecimals: Int, minDecimals: Int): NumberFormat {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a Zatoshi value into Zec, right-padded to the given number of fraction digits, represented as a BigDecimal in
|
* Convert a Zatoshi value into ZEC, right-padded to the given number of fraction digits,
|
||||||
* order to preserve rounding that minimizes cumulative error when applied repeatedly over a sequence of calculations.
|
* represented as a BigDecimal in order to preserve rounding that minimizes cumulative error when
|
||||||
* Start with Zatoshi -> End with Zec.
|
* applied repeatedly over a sequence of calculations.
|
||||||
|
* Start with Zatoshi -> End with ZEC.
|
||||||
*
|
*
|
||||||
* @param scale the number of digits to the right of the decimal place. Right-padding will be added, if necessary.
|
* @param scale the number of digits to the right of the decimal place. Right-padding will be added,
|
||||||
|
* if necessary.
|
||||||
|
*
|
||||||
|
* @return this Long Zatoshi value represented as ZEC using a BigDecimal with the given scale,
|
||||||
|
* rounded accurately out to 128 digits.
|
||||||
*/
|
*/
|
||||||
inline fun Long?.convertZatoshiToZec(scale: Int = ZEC_FORMATTER.maximumFractionDigits): BigDecimal {
|
inline fun Long?.convertZatoshiToZec(scale: Int = ZEC_FORMATTER.maximumFractionDigits): BigDecimal {
|
||||||
return BigDecimal(this ?: 0L, MathContext.DECIMAL128).divide(Conversions.ONE_ZEC_IN_ZATOSHI, MathContext.DECIMAL128).setScale(scale, ZEC_FORMATTER.roundingMode)
|
return BigDecimal(this ?: 0L, MathContext.DECIMAL128).divide(
|
||||||
|
Conversions.ONE_ZEC_IN_ZATOSHI,
|
||||||
|
MathContext.DECIMAL128
|
||||||
|
).setScale(scale, ZEC_FORMATTER.roundingMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a Zec value into Zatoshi.
|
* Convert a ZEC value into Zatoshi.
|
||||||
|
* Start with ZEC -> End with Zatoshi.
|
||||||
|
*
|
||||||
|
* @return this ZEC value represented as Zatoshi, rounded accurately out to 128 digits, in order to
|
||||||
|
* minimize cumulative errors when applied repeatedly over a sequence of calculations.
|
||||||
*/
|
*/
|
||||||
inline fun BigDecimal?.convertZecToZatoshi(): Long {
|
inline fun BigDecimal?.convertZecToZatoshi(): Long {
|
||||||
if (this == null) return 0L
|
if (this == null) return 0L
|
||||||
if (this < BigDecimal.ZERO) throw IllegalArgumentException("Invalid ZEC value: $this. ZEC is represented by notes and cannot be negative")
|
if (this < BigDecimal.ZERO) {
|
||||||
|
throw IllegalArgumentException("Invalid ZEC value: $this. ZEC is represented by notes and" +
|
||||||
|
" cannot be negative")
|
||||||
|
}
|
||||||
return this.multiply(Conversions.ONE_ZEC_IN_ZATOSHI, MathContext.DECIMAL128).toLong()
|
return this.multiply(Conversions.ONE_ZEC_IN_ZATOSHI, MathContext.DECIMAL128).toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a Double Zec value as a BigDecimal Zec value, right-padded to the given number of fraction digits.
|
* Format a Double ZEC value as a BigDecimal ZEC value, right-padded to the given number of fraction
|
||||||
* Start with Zec -> End with Zec.
|
* digits.
|
||||||
|
* Start with ZEC -> End with ZEC.
|
||||||
|
*
|
||||||
|
* @param decimals the scale to use for the resulting BigDecimal.
|
||||||
|
*
|
||||||
|
* @return this Double ZEC value converted into a BigDecimal, with the proper rounding mode for use
|
||||||
|
* with other formatting functions.
|
||||||
*/
|
*/
|
||||||
inline fun Double?.toZec(decimals: Int = ZEC_FORMATTER.maximumFractionDigits): BigDecimal {
|
inline fun Double?.toZec(decimals: Int = ZEC_FORMATTER.maximumFractionDigits): BigDecimal {
|
||||||
return BigDecimal(this?.toString() ?: "0.0", MathContext.DECIMAL128).setScale(decimals, ZEC_FORMATTER.roundingMode)
|
return BigDecimal(this?.toString() ?: "0.0", MathContext.DECIMAL128).setScale(
|
||||||
|
decimals,
|
||||||
|
ZEC_FORMATTER.roundingMode
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a Double Zec value as a Long Zatoshi value, by first converting to Zec with the given
|
* Format a Double ZEC value as a Long Zatoshi value, by first converting to ZEC with the given
|
||||||
* precision.
|
* precision.
|
||||||
* Start with Zec -> End with Zatoshi.
|
* Start with ZEC -> End with Zatoshi.
|
||||||
|
*
|
||||||
|
* @param decimals the scale to use for the intermediate BigDecimal.
|
||||||
|
*
|
||||||
|
* @return this Double ZEC value converted into Zatoshi, with proper rounding and precision by
|
||||||
|
* leveraging an intermediate BigDecimal object.
|
||||||
*/
|
*/
|
||||||
inline fun Double?.convertZecToZatoshi(decimals: Int = ZEC_FORMATTER.maximumFractionDigits): Long {
|
inline fun Double?.convertZecToZatoshi(decimals: Int = ZEC_FORMATTER.maximumFractionDigits): Long {
|
||||||
return this.toZec(decimals).convertZecToZatoshi()
|
return this.toZec(decimals).convertZecToZatoshi()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a BigDecimal Zec value as a BigDecimal Zec value, right-padded to the given number of fraction digits.
|
* Format a BigDecimal ZEC value as a BigDecimal ZEC value, right-padded to the given number of
|
||||||
* Start with Zec -> End with Zec.
|
* fraction digits.
|
||||||
|
* Start with ZEC -> End with ZEC.
|
||||||
|
*
|
||||||
|
* @param decimals the scale to use for the resulting BigDecimal.
|
||||||
|
*
|
||||||
|
* @return this BigDecimal ZEC adjusted to the default scale and rounding mode.
|
||||||
*/
|
*/
|
||||||
inline fun BigDecimal?.toZec(decimals: Int = ZEC_FORMATTER.maximumFractionDigits): BigDecimal {
|
inline fun BigDecimal?.toZec(decimals: Int = ZEC_FORMATTER.maximumFractionDigits): BigDecimal {
|
||||||
return (this ?: BigDecimal.ZERO).setScale(decimals, ZEC_FORMATTER.roundingMode)
|
return (this ?: BigDecimal.ZERO).setScale(decimals, ZEC_FORMATTER.roundingMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a Double Usd value as a BigDecimal Usd value, right-padded to the given number of fraction digits.
|
* Format a Double USD value as a BigDecimal USD value, right-padded to the given number of fraction
|
||||||
|
* digits.
|
||||||
|
* Start with USD -> End with USD.
|
||||||
|
*
|
||||||
|
* @param decimals the scale to use for the resulting BigDecimal.
|
||||||
|
*
|
||||||
|
* @return this Double USD value converted into a BigDecimal, with proper rounding and precision.
|
||||||
*/
|
*/
|
||||||
inline fun Double?.toUsd(decimals: Int = USD_FORMATTER.maximumFractionDigits): BigDecimal {
|
inline fun Double?.toUsd(decimals: Int = USD_FORMATTER.maximumFractionDigits): BigDecimal {
|
||||||
return BigDecimal(this?.toString() ?: "0.0", MathContext.DECIMAL128).setScale(decimals, USD_FORMATTER.roundingMode)
|
return BigDecimal(this?.toString() ?: "0.0", MathContext.DECIMAL128).setScale(
|
||||||
|
decimals,
|
||||||
|
USD_FORMATTER.roundingMode
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a BigDecimal Usd value as a BigDecimal Usd value, right-padded to the given number of fraction digits.
|
* Format a BigDecimal USD value as a BigDecimal USD value, right-padded to the given number of
|
||||||
|
* fraction digits.
|
||||||
|
* Start with USD -> End with USD.
|
||||||
|
*
|
||||||
|
* @param decimals the scale to use for the resulting BigDecimal.
|
||||||
|
*
|
||||||
|
* @return this BigDecimal USD value converted into USD, with proper rounding and precision.
|
||||||
*/
|
*/
|
||||||
inline fun BigDecimal?.toUsd(decimals: Int = USD_FORMATTER.maximumFractionDigits): BigDecimal {
|
inline fun BigDecimal?.toUsd(decimals: Int = USD_FORMATTER.maximumFractionDigits): BigDecimal {
|
||||||
return (this ?: BigDecimal.ZERO).setScale(decimals, USD_FORMATTER.roundingMode)
|
return (this ?: BigDecimal.ZERO).setScale(decimals, USD_FORMATTER.roundingMode)
|
||||||
|
@ -174,30 +260,49 @@ inline fun BigDecimal?.toUsd(decimals: Int = USD_FORMATTER.maximumFractionDigits
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert this ZEC value to USD, using the given price per ZEC.
|
* Convert this ZEC value to USD, using the given price per ZEC.
|
||||||
|
* Start with ZEC -> End with USD.
|
||||||
*
|
*
|
||||||
* @param zecPrice the current price of ZEC represented as USD per ZEC
|
* @param zecPrice the current price of ZEC represented as USD per ZEC
|
||||||
|
*
|
||||||
|
* @return this BigDecimal USD value converted into USD, with proper rounding and precision.
|
||||||
*/
|
*/
|
||||||
inline fun BigDecimal?.convertZecToUsd(zecPrice: BigDecimal): BigDecimal {
|
inline fun BigDecimal?.convertZecToUsd(zecPrice: BigDecimal): BigDecimal {
|
||||||
if(this == null) return BigDecimal.ZERO
|
if(this == null) return BigDecimal.ZERO
|
||||||
if(this < BigDecimal.ZERO) throw IllegalArgumentException("Invalid ZEC value: ${zecPrice.toDouble()}. ZEC is represented by notes and cannot be negative")
|
if(this < BigDecimal.ZERO) {
|
||||||
|
throw IllegalArgumentException("Invalid ZEC value: ${zecPrice.toDouble()}. ZEC is" +
|
||||||
|
" represented by notes and cannot be negative")
|
||||||
|
}
|
||||||
return this.multiply(zecPrice, MathContext.DECIMAL128)
|
return this.multiply(zecPrice, MathContext.DECIMAL128)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert this USD value to ZEC, using the given price per ZEC.
|
* Convert this USD value to ZEC, using the given price per ZEC.
|
||||||
|
* Start with USD -> End with ZEC.
|
||||||
*
|
*
|
||||||
* @param zecPrice the current price of ZEC represented as USD per ZEC
|
* @param zecPrice the current price of ZEC represented as USD per ZEC.
|
||||||
|
*
|
||||||
|
* @return this BigDecimal USD value converted into ZEC, with proper rounding and precision.
|
||||||
*/
|
*/
|
||||||
inline fun BigDecimal?.convertUsdToZec(zecPrice: BigDecimal): BigDecimal {
|
inline fun BigDecimal?.convertUsdToZec(zecPrice: BigDecimal): BigDecimal {
|
||||||
if(this == null) return BigDecimal.ZERO
|
if(this == null) return BigDecimal.ZERO
|
||||||
if(this < BigDecimal.ZERO) throw IllegalArgumentException("Invalid USD value: ${zecPrice.toDouble()}. Converting this would result in negative ZEC and ZEC is represented by notes and cannot be negative")
|
if(this < BigDecimal.ZERO) {
|
||||||
|
throw IllegalArgumentException("Invalid USD value: ${zecPrice.toDouble()}. Converting" +
|
||||||
|
" this would result in negative ZEC and ZEC is represented by notes and cannot be" +
|
||||||
|
" negative")
|
||||||
|
}
|
||||||
return this.divide(zecPrice, MathContext.DECIMAL128)
|
return this.divide(zecPrice, MathContext.DECIMAL128)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert this value from one currency to the other, based on given price and whether this value is USD.
|
* Convert this value from one currency to the other, based on given price and whether this value is
|
||||||
|
* USD.
|
||||||
|
* If starting with USD -> End with ZEC.
|
||||||
|
* If starting with ZEC -> End with USD.
|
||||||
*
|
*
|
||||||
* @param isUsd whether this value represents USD or not (ZEC)
|
* @param isUSD whether this value represents USD or not (ZEC)
|
||||||
|
*
|
||||||
|
* @return this BigDecimal value converted from one currency into the other, based on the given
|
||||||
|
* price.
|
||||||
*/
|
*/
|
||||||
inline fun BigDecimal.convertCurrency(zecPrice: BigDecimal, isUsd: Boolean): BigDecimal {
|
inline fun BigDecimal.convertCurrency(zecPrice: BigDecimal, isUsd: Boolean): BigDecimal {
|
||||||
return if (isUsd) {
|
return if (isUsd) {
|
||||||
|
@ -210,7 +315,7 @@ inline fun BigDecimal.convertCurrency(zecPrice: BigDecimal, isUsd: Boolean): Big
|
||||||
/**
|
/**
|
||||||
* Parse this string into a BigDecimal, ignoring all non numeric characters.
|
* Parse this string into a BigDecimal, ignoring all non numeric characters.
|
||||||
*
|
*
|
||||||
* @return null when parsing fails
|
* @return this string as a BigDecimal or null when parsing fails.
|
||||||
*/
|
*/
|
||||||
inline fun String?.safelyConvertToBigDecimal(): BigDecimal? {
|
inline fun String?.safelyConvertToBigDecimal(): BigDecimal? {
|
||||||
if (this.isNullOrEmpty()) return BigDecimal.ZERO
|
if (this.isNullOrEmpty()) return BigDecimal.ZERO
|
||||||
|
@ -223,8 +328,34 @@ inline fun String?.safelyConvertToBigDecimal(): BigDecimal? {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun String.toAbbreviatedAddress(startLength: Int = 8, endLength: Int = 8) = if (length > startLength + endLength) "${take(startLength)}…${takeLast(endLength)}" else this
|
/**
|
||||||
|
* Abbreviates this string which is assumed to be an address.
|
||||||
|
*
|
||||||
|
* @param startLength the number of characters to show before the elipsis.
|
||||||
|
* @param endLength the number of characters to show after the elipsis.
|
||||||
|
*
|
||||||
|
* @return the abbreviated string unless the string is too short, in which case the original string
|
||||||
|
* is returned.
|
||||||
|
*/
|
||||||
|
inline fun String.toAbbreviatedAddress(startLength: Int = 8, endLength: Int = 8) =
|
||||||
|
if (length > startLength + endLength) "${take(startLength)}…${takeLast(endLength)}" else this
|
||||||
|
|
||||||
internal inline fun String.masked(): String = if (startsWith("ztest") || startsWith("zs")) "****${takeLast(4)}" else "***masked***"
|
/**
|
||||||
|
* Masks the current string for use in logs. If this string appears to be an address, the last
|
||||||
|
* [addressCharsToShow] characters will be visible.
|
||||||
|
*
|
||||||
|
* @param addressCharsToShow the number of chars to show at the end, if this value appears to be an
|
||||||
|
* address.
|
||||||
|
*
|
||||||
|
* @return the masked version of this string, typically for use in logs.
|
||||||
|
*/
|
||||||
|
internal inline fun String.masked(addressCharsToShow: Int = 4): String =
|
||||||
|
if (startsWith("ztest") || startsWith("zs")) "****${takeLast(addressCharsToShow)}"
|
||||||
|
else "***masked***"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience function that returns true when this string starts with 'z'.
|
||||||
|
*
|
||||||
|
* @return true when this function starts with 'z' rather than 't'.
|
||||||
|
*/
|
||||||
inline fun String?.isShielded() = this != null && startsWith('z')
|
inline fun String?.isShielded() = this != null && startsWith('z')
|
|
@ -38,6 +38,10 @@ fun <T> Flow<T>.collectWith(scope: CoroutineScope, block: (T) -> Unit) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility for performing the given action on the first emission of a flow and running that action
|
||||||
|
* in the given scope.
|
||||||
|
*/
|
||||||
fun <T, S> Flow<T>.onFirstWith(scope: CoroutineScope, block: suspend (T) -> S) {
|
fun <T, S> Flow<T>.onFirstWith(scope: CoroutineScope, block: suspend (T) -> S) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
onEach {
|
onEach {
|
||||||
|
@ -46,6 +50,9 @@ fun <T, S> Flow<T>.onFirstWith(scope: CoroutineScope, block: suspend (T) -> S) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility for performing the given action on the first emission of a flow.
|
||||||
|
*/
|
||||||
suspend fun <T, S> Flow<T>.onFirst(block: suspend (T) -> S) {
|
suspend fun <T, S> Flow<T>.onFirst(block: suspend (T) -> S) {
|
||||||
onEach {
|
onEach {
|
||||||
block(it)
|
block(it)
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
package cash.z.wallet.sdk.ext
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import cash.z.wallet.sdk.BuildConfig
|
|
||||||
|
|
||||||
fun SharedPrefs(context: Context, name: String = "prefs"): SharedPreferences {
|
|
||||||
val fileName = "${BuildConfig.FLAVOR}.${BuildConfig.BUILD_TYPE}.$name".toLowerCase()
|
|
||||||
return context.getSharedPreferences(fileName, Context.MODE_PRIVATE)!!
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun SharedPreferences.edit(block: (SharedPreferences.Editor) -> Unit) {
|
|
||||||
edit().run {
|
|
||||||
block(this)
|
|
||||||
apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun SharedPreferences.set(key: String, value: Any?) {
|
|
||||||
when (value) {
|
|
||||||
is String? -> edit { it.putString(key, value) }
|
|
||||||
is Int -> edit { it.putInt(key, value) }
|
|
||||||
is Boolean -> edit { it.putBoolean(key, value) }
|
|
||||||
is Float -> edit { it.putFloat(key, value) }
|
|
||||||
is Long -> edit { it.putLong(key, value) }
|
|
||||||
else -> throw UnsupportedOperationException("Not yet implemented")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline operator fun <reified T : Any> SharedPreferences.get(key: String, defaultValue: T? = null): T? {
|
|
||||||
return when (T::class) {
|
|
||||||
String::class -> getString(key, defaultValue as? String) as T?
|
|
||||||
Int::class -> getInt(key, defaultValue as? Int ?: -1) as T?
|
|
||||||
Boolean::class -> getBoolean(key, defaultValue as? Boolean ?: false) as T?
|
|
||||||
Float::class -> getFloat(key, defaultValue as? Float ?: -1f) as T?
|
|
||||||
Long::class -> getLong(key, defaultValue as? Long ?: -1) as T?
|
|
||||||
else -> throw UnsupportedOperationException("Not yet implemented")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -10,7 +10,15 @@ internal typealias Leaf = String
|
||||||
* A tiny log.
|
* A tiny log.
|
||||||
*/
|
*/
|
||||||
interface Twig {
|
interface Twig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log the message. Simple.
|
||||||
|
*/
|
||||||
fun twig(logMessage: String = "")
|
fun twig(logMessage: String = "")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bundles twigs together
|
||||||
|
*/
|
||||||
operator fun plus(twig: Twig): Twig {
|
operator fun plus(twig: Twig): Twig {
|
||||||
// if the other twig is a composite twig, let it handle the addition
|
// if the other twig is a composite twig, let it handle the addition
|
||||||
return if(twig is CompositeTwig) twig.plus(this) else CompositeTwig(mutableListOf(this, twig))
|
return if(twig is CompositeTwig) twig.plus(this) else CompositeTwig(mutableListOf(this, twig))
|
||||||
|
@ -74,6 +82,10 @@ inline fun <R> twigTask(logMessage: String, block: () -> R): R = Bush.trunk.twig
|
||||||
* A tiny log that does nothing. No one hears this twig fall in the woods.
|
* A tiny log that does nothing. No one hears this twig fall in the woods.
|
||||||
*/
|
*/
|
||||||
class SilentTwig : Twig {
|
class SilentTwig : Twig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shh.
|
||||||
|
*/
|
||||||
override fun twig(logMessage: String) {
|
override fun twig(logMessage: String) {
|
||||||
// shh
|
// shh
|
||||||
}
|
}
|
||||||
|
@ -89,6 +101,10 @@ open class TroubleshootingTwig(
|
||||||
val formatter: (String) -> String = spiffy(5),
|
val formatter: (String) -> String = spiffy(5),
|
||||||
val printer: (String) -> Any = System.err::println
|
val printer: (String) -> Any = System.err::println
|
||||||
) : Twig {
|
) : Twig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actually print and format the log message, unlike the SilentTwig, which does nothing.
|
||||||
|
*/
|
||||||
override fun twig(logMessage: String) {
|
override fun twig(logMessage: String) {
|
||||||
printer(formatter(logMessage))
|
printer(formatter(logMessage))
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,6 +58,15 @@ inline fun retrySimple(retries: Int = 2, sleepTime: Long = 20L, block: (Int) ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the given block and if it fails, retry with an exponential backoff.
|
||||||
|
*
|
||||||
|
* @param onErrorListener a callback that gets the first shot at processing any error and can veto
|
||||||
|
* the retry behavior by returning false.
|
||||||
|
* @param initialDelayMillis the initial delay before retrying.
|
||||||
|
* @param maxDelayMillis the maximum delay between retrys.
|
||||||
|
* @param block the logic to run once and then run again if it fails.
|
||||||
|
*/
|
||||||
suspend inline fun retryWithBackoff(noinline onErrorListener: ((Throwable) -> Boolean)? = null, initialDelayMillis: Long = 1000L, maxDelayMillis: Long = MAX_BACKOFF_INTERVAL, block: () -> Unit) {
|
suspend inline fun retryWithBackoff(noinline onErrorListener: ((Throwable) -> Boolean)? = null, initialDelayMillis: Long = 1000L, maxDelayMillis: Long = MAX_BACKOFF_INTERVAL, block: () -> Unit) {
|
||||||
var sequence = 0 // count up to the max and then reset to half. So that we don't repeat the max but we also don't repeat too much.
|
var sequence = 0 // count up to the max and then reset to half. So that we don't repeat the max but we also don't repeat too much.
|
||||||
while (true) {
|
while (true) {
|
||||||
|
@ -83,6 +92,11 @@ suspend inline fun retryWithBackoff(noinline onErrorListener: ((Throwable) -> Bo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the given database already exists.
|
||||||
|
*
|
||||||
|
* @return true when the given database exists in the given context.
|
||||||
|
*/
|
||||||
internal fun dbExists(appContext: Context, dbFileName: String): Boolean {
|
internal fun dbExists(appContext: Context, dbFileName: String): Boolean {
|
||||||
return File(appContext.getDatabasePath(dbFileName).absolutePath).exists()
|
return File(appContext.getDatabasePath(dbFileName).absolutePath).exists()
|
||||||
}
|
}
|
|
@ -3,6 +3,8 @@ package cash.z.wallet.sdk.jni
|
||||||
/**
|
/**
|
||||||
* Contract defining the exposed capabilities of the Rust backend.
|
* Contract defining the exposed capabilities of the Rust backend.
|
||||||
* This is what welds the SDK to the Rust layer.
|
* This is what welds the SDK to the Rust layer.
|
||||||
|
* It is not documented because it is not intended to be used, directly.
|
||||||
|
* Instead, use the synchronizer or one of its subcomponents.
|
||||||
*/
|
*/
|
||||||
interface RustBackendWelding {
|
interface RustBackendWelding {
|
||||||
|
|
||||||
|
|
|
@ -17,10 +17,10 @@ import java.util.concurrent.TimeUnit
|
||||||
/**
|
/**
|
||||||
* Implementation of LightwalletService using gRPC for requests to lightwalletd.
|
* Implementation of LightwalletService using gRPC for requests to lightwalletd.
|
||||||
*
|
*
|
||||||
* @param channel the channel to use for communicating with the lightwalletd server.
|
* @property channel the channel to use for communicating with the lightwalletd server.
|
||||||
* @param singleRequestTimeoutSec the timeout to use for non-streaming requests. When a new stub is
|
* @property singleRequestTimeoutSec the timeout to use for non-streaming requests. When a new stub is
|
||||||
* created, it will use a deadline that is after the given duration from now.
|
* created, it will use a deadline that is after the given duration from now.
|
||||||
* @param streamingRequestTimeoutSec the timeout to use for streaming requests. When a new stub is
|
* @property streamingRequestTimeoutSec the timeout to use for streaming requests. When a new stub is
|
||||||
* created for streaming requests, it will use a deadline that is after the given duration from now.
|
* created for streaming requests, it will use a deadline that is after the given duration from now.
|
||||||
*/
|
*/
|
||||||
class LightWalletGrpcService private constructor(
|
class LightWalletGrpcService private constructor(
|
||||||
|
@ -29,6 +29,16 @@ class LightWalletGrpcService private constructor(
|
||||||
private val streamingRequestTimeoutSec: Long = 90L
|
private val streamingRequestTimeoutSec: Long = 90L
|
||||||
) : LightWalletService {
|
) : LightWalletService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct an instance that corresponds to the given host and port.
|
||||||
|
*
|
||||||
|
* @param appContext the application context used to check whether TLS is required by this build
|
||||||
|
* flavor.
|
||||||
|
* @param host the host of the server to use.
|
||||||
|
* @param port the port of the server to use.
|
||||||
|
* @param usePlaintext whether to use TLS or plaintext for requests. Plaintext is dangerous so
|
||||||
|
* it requires jumping through a few more hoops.
|
||||||
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
appContext: Context,
|
appContext: Context,
|
||||||
host: String,
|
host: String,
|
||||||
|
@ -38,12 +48,6 @@ class LightWalletGrpcService private constructor(
|
||||||
|
|
||||||
/* LightWalletService implementation */
|
/* LightWalletService implementation */
|
||||||
|
|
||||||
/**
|
|
||||||
* Blocking call to download all blocks in the given range.
|
|
||||||
*
|
|
||||||
* @param heightRange the inclusive range of block heights to download.
|
|
||||||
* @return a list of compact blocks for the given range
|
|
||||||
*/
|
|
||||||
override fun getBlockRange(heightRange: IntRange): List<CompactFormats.CompactBlock> {
|
override fun getBlockRange(heightRange: IntRange): List<CompactFormats.CompactBlock> {
|
||||||
channel.resetConnectBackoff()
|
channel.resetConnectBackoff()
|
||||||
return channel.createStub(streamingRequestTimeoutSec).getBlockRange(heightRange.toBlockRange()).toList()
|
return channel.createStub(streamingRequestTimeoutSec).getBlockRange(heightRange.toBlockRange()).toList()
|
||||||
|
@ -89,6 +93,11 @@ class LightWalletGrpcService private constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
/**
|
||||||
|
* Convenience function for creating the default channel to be used for all connections. It
|
||||||
|
* is important that this channel can handle transitioning from WiFi to Cellular connections
|
||||||
|
* and is properly setup to support TLS, when required.
|
||||||
|
*/
|
||||||
fun createDefaultChannel(
|
fun createDefaultChannel(
|
||||||
appContext: Context,
|
appContext: Context,
|
||||||
host: String,
|
host: String,
|
||||||
|
|
|
@ -13,16 +13,23 @@ interface LightWalletService {
|
||||||
*
|
*
|
||||||
* @param heightRange the inclusive range to fetch. For instance if 1..5 is given, then every
|
* @param heightRange the inclusive range to fetch. For instance if 1..5 is given, then every
|
||||||
* block in that range will be fetched, including 1 and 5.
|
* block in that range will be fetched, including 1 and 5.
|
||||||
|
*
|
||||||
|
* @return a list of compact blocks for the given range
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
fun getBlockRange(heightRange: IntRange): List<CompactFormats.CompactBlock>
|
fun getBlockRange(heightRange: IntRange): List<CompactFormats.CompactBlock>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the latest block height known to the service.
|
* Return the latest block height known to the service.
|
||||||
|
*
|
||||||
|
* @return the latest block height known to the service.
|
||||||
*/
|
*/
|
||||||
fun getLatestBlockHeight(): Int
|
fun getLatestBlockHeight(): Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submit a raw transaction.
|
* Submit a raw transaction.
|
||||||
|
*
|
||||||
|
* @return the response from the server.
|
||||||
*/
|
*/
|
||||||
fun submitTransaction(spendTransaction: ByteArray): Service.SendResponse
|
fun submitTransaction(spendTransaction: ByteArray): Service.SendResponse
|
||||||
|
|
||||||
|
|
|
@ -79,6 +79,9 @@ open class PagedTransactionRepository(
|
||||||
transactions.findMinedHeight(rawTransactionId)
|
transactions.findMinedHeight(rawTransactionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the underlying database.
|
||||||
|
*/
|
||||||
fun close() {
|
fun close() {
|
||||||
derivedDataDb.close()
|
derivedDataDb.close()
|
||||||
}
|
}
|
||||||
|
@ -109,7 +112,7 @@ open class PagedTransactionRepository(
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
val MIGRATION_4_3 = object : Migration(4, 3) {
|
private val MIGRATION_4_3 = object : Migration(4, 3) {
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
database.execSQL("PRAGMA foreign_keys = OFF;")
|
database.execSQL("PRAGMA foreign_keys = OFF;")
|
||||||
database.execSQL(
|
database.execSQL(
|
||||||
|
|
|
@ -5,7 +5,10 @@ import androidx.room.Room
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import cash.z.wallet.sdk.db.PendingTransactionDao
|
import cash.z.wallet.sdk.db.PendingTransactionDao
|
||||||
import cash.z.wallet.sdk.db.PendingTransactionDb
|
import cash.z.wallet.sdk.db.PendingTransactionDb
|
||||||
import cash.z.wallet.sdk.entity.*
|
import cash.z.wallet.sdk.entity.PendingTransaction
|
||||||
|
import cash.z.wallet.sdk.entity.PendingTransactionEntity
|
||||||
|
import cash.z.wallet.sdk.entity.isCancelled
|
||||||
|
import cash.z.wallet.sdk.entity.isSubmitted
|
||||||
import cash.z.wallet.sdk.ext.twig
|
import cash.z.wallet.sdk.ext.twig
|
||||||
import cash.z.wallet.sdk.service.LightWalletService
|
import cash.z.wallet.sdk.service.LightWalletService
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -13,13 +16,19 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.lang.IllegalStateException
|
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Facilitates persistent attempts to ensure a transaction occurs.
|
* Facilitates persistent attempts to ensure that an outbound transaction is completed.
|
||||||
|
*
|
||||||
|
* @param db the database where the wallet can freely write information related to pending
|
||||||
|
* transactions. This database effectively serves as the mempool for transactions created by this
|
||||||
|
* wallet.
|
||||||
|
* @property encoder responsible for encoding a transaction by taking all the inputs and returning
|
||||||
|
* an [cash.z.wallet.sdk.entity.EncodedTransaction] object containing the raw bytes and transaction
|
||||||
|
* id.
|
||||||
|
* @property service the lightwallet service used to submit transactions.
|
||||||
*/
|
*/
|
||||||
// TODO: consider having the manager register the fail listeners rather than having that responsibility spread elsewhere (synchronizer and the broom)
|
|
||||||
class PersistentTransactionManager(
|
class PersistentTransactionManager(
|
||||||
db: PendingTransactionDb,
|
db: PendingTransactionDb,
|
||||||
private val encoder: TransactionEncoder,
|
private val encoder: TransactionEncoder,
|
||||||
|
@ -52,10 +61,11 @@ class PersistentTransactionManager(
|
||||||
service
|
service
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize a [PendingTransaction] and then insert it in the database for monitoring and
|
//
|
||||||
* follow-up.
|
// OutboundTransactionManager implementation
|
||||||
*/
|
//
|
||||||
|
|
||||||
override suspend fun initSpend(
|
override suspend fun initSpend(
|
||||||
zatoshiValue: Long,
|
zatoshiValue: Long,
|
||||||
toAddress: String,
|
toAddress: String,
|
||||||
|
@ -79,7 +89,8 @@ class PersistentTransactionManager(
|
||||||
twig("successfully created TX in DB")
|
twig("successfully created TX in DB")
|
||||||
}
|
}
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
twig("Unknown error while attempting to create pending transaction: ${t.message} caused by: ${t.cause}")
|
twig("Unknown error while attempting to create pending transaction: ${t.message}" +
|
||||||
|
" caused by: ${t.cause}")
|
||||||
}
|
}
|
||||||
|
|
||||||
tx
|
tx
|
||||||
|
@ -92,21 +103,11 @@ class PersistentTransactionManager(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a transaction and pretend it never existed.
|
|
||||||
*/
|
|
||||||
suspend fun abortTransaction(existingTransaction: PendingTransaction) {
|
|
||||||
pendingTransactionDao {
|
|
||||||
delete(existingTransaction as PendingTransactionEntity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun encode(
|
override suspend fun encode(
|
||||||
spendingKey: String,
|
spendingKey: String,
|
||||||
pendingTx: PendingTransaction
|
pendingTx: PendingTransaction
|
||||||
): PendingTransaction = withContext(Dispatchers.IO) {
|
): PendingTransaction = withContext(Dispatchers.IO) {
|
||||||
twig("managing the creation of a transaction")
|
twig("managing the creation of a transaction")
|
||||||
//var tx = transaction.copy(expiryHeight = if (currentHeight == -1) -1 else currentHeight + EXPIRY_OFFSET)
|
|
||||||
var tx = pendingTx as PendingTransactionEntity
|
var tx = pendingTx as PendingTransactionEntity
|
||||||
try {
|
try {
|
||||||
twig("beginning to encode transaction with : $encoder")
|
twig("beginning to encode transaction with : $encoder")
|
||||||
|
@ -123,7 +124,7 @@ class PersistentTransactionManager(
|
||||||
val message = "failed to encode transaction due to : ${t.message} caused by: ${t.cause}"
|
val message = "failed to encode transaction due to : ${t.message} caused by: ${t.cause}"
|
||||||
twig(message)
|
twig(message)
|
||||||
message
|
message
|
||||||
tx = tx.copy(errorMessage = message, errorCode = 2000) //TODO: find a place for these error codes
|
tx = tx.copy(errorMessage = message, errorCode = ERROR_ENCODING)
|
||||||
} finally {
|
} finally {
|
||||||
tx = tx.copy(encodeAttempts = max(1, tx.encodeAttempts + 1))
|
tx = tx.copy(encodeAttempts = max(1, tx.encodeAttempts + 1))
|
||||||
}
|
}
|
||||||
|
@ -134,15 +135,19 @@ class PersistentTransactionManager(
|
||||||
|
|
||||||
override suspend fun submit(pendingTx: PendingTransaction): PendingTransaction = withContext(Dispatchers.IO) {
|
override suspend fun submit(pendingTx: PendingTransaction): PendingTransaction = withContext(Dispatchers.IO) {
|
||||||
// reload the tx to check for cancellation
|
// reload the tx to check for cancellation
|
||||||
var storedTx = pendingTransactionDao { findById(pendingTx.id) } ?: throw IllegalStateException("Error while submitting transaction. No pending transaction found that matches the one being submitted. Verify that the transaction still exists among the set of pending transactions.")
|
var storedTx = pendingTransactionDao { findById(pendingTx.id) }
|
||||||
|
?: throw IllegalStateException("Error while submitting transaction. No pending" +
|
||||||
|
" transaction found that matches the one being submitted. Verify that the" +
|
||||||
|
" transaction still exists among the set of pending transactions.")
|
||||||
var tx = storedTx
|
var tx = storedTx
|
||||||
try {
|
try {
|
||||||
// do nothing when cancelled
|
// do nothing when cancelled
|
||||||
if (!tx.isCancelled()) {
|
if (!tx.isCancelled()) {
|
||||||
twig("submitting transaction to lightwalletd - memo: ${tx.memo} amount: ${tx.value}")
|
twig("submitting transaction with memo: ${tx.memo} amount: ${tx.value}")
|
||||||
val response = service.submitTransaction(tx.raw)
|
val response = service.submitTransaction(tx.raw)
|
||||||
val error = response.errorCode < 0
|
val error = response.errorCode < 0
|
||||||
twig("${if (error) "FAILURE! " else "SUCCESS!"} submit transaction completed with response: ${response.errorCode}: ${response.errorMessage}")
|
twig("${if (error) "FAILURE! " else "SUCCESS!"} submit transaction completed with" +
|
||||||
|
" response: ${response.errorCode}: ${response.errorMessage}")
|
||||||
tx = tx.copy(
|
tx = tx.copy(
|
||||||
errorMessage = if (error) response.errorMessage else null,
|
errorMessage = if (error) response.errorMessage else null,
|
||||||
errorCode = response.errorCode,
|
errorCode = response.errorCode,
|
||||||
|
@ -157,7 +162,11 @@ class PersistentTransactionManager(
|
||||||
val message =
|
val message =
|
||||||
"Unknown error while submitting transaction: ${t.message} caused by: ${t.cause}"
|
"Unknown error while submitting transaction: ${t.message} caused by: ${t.cause}"
|
||||||
twig(message)
|
twig(message)
|
||||||
tx = tx.copy(errorMessage = t.message, errorCode = 3000, submitAttempts = max(1, tx.submitAttempts + 1)) //TODO: find a place for these error codes
|
tx = tx.copy(
|
||||||
|
errorMessage = t.message,
|
||||||
|
errorCode = ERROR_SUBMITTING,
|
||||||
|
submitAttempts = max(1, tx.submitAttempts + 1)
|
||||||
|
)
|
||||||
safeUpdate(tx)
|
safeUpdate(tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,18 +197,33 @@ class PersistentTransactionManager(
|
||||||
|
|
||||||
override fun getAll() = _dao.getAll()
|
override fun getAll() = _dao.getAll()
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// Helper functions
|
||||||
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a transaction and pretend it never existed.
|
||||||
|
*/
|
||||||
|
suspend fun abortTransaction(existingTransaction: PendingTransaction) {
|
||||||
|
pendingTransactionDao {
|
||||||
|
delete(existingTransaction as PendingTransactionEntity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updating the pending transaction is often done at the end of a function but still should
|
* Updating the pending transaction is often done at the end of a function but still should
|
||||||
* happen within a try/catch block, surrounded by logging. So this helps with that.
|
* happen within a try/catch block, surrounded by logging. So this helps with that.
|
||||||
*/
|
*/
|
||||||
private suspend fun safeUpdate(tx: PendingTransactionEntity): PendingTransaction {
|
private suspend fun safeUpdate(tx: PendingTransactionEntity): PendingTransaction {
|
||||||
return try {
|
return try {
|
||||||
twig("updating tx into DB: $tx")
|
twig("updating tx in DB: $tx")
|
||||||
pendingTransactionDao { update(tx) }
|
pendingTransactionDao { update(tx) }
|
||||||
twig("successfully updated TX into DB")
|
twig("successfully updated TX in DB")
|
||||||
tx
|
tx
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
twig("Unknown error while attempting to update pending transaction: ${t.message} caused by: ${t.cause}")
|
twig("Unknown error while attempting to update pending transaction: ${t.message}" +
|
||||||
|
" caused by: ${t.cause}")
|
||||||
tx
|
tx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -209,5 +233,12 @@ class PersistentTransactionManager(
|
||||||
_dao.block()
|
_dao.block()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** Error code for an error while encoding a transaction */
|
||||||
|
const val ERROR_ENCODING = 2000
|
||||||
|
/** Error code for an error while submitting a transaction */
|
||||||
|
const val ERROR_SUBMITTING = 3000
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,295 +0,0 @@
|
||||||
package cash.z.wallet.sdk.transaction
|
|
||||||
|
|
||||||
//import cash.z.wallet.sdk.transaction.PersistentTransactionSender.ChangeType.*
|
|
||||||
//import cash.z.wallet.sdk.transaction.TransactionUpdateRequest.RefreshSentTx
|
|
||||||
//import cash.z.wallet.sdk.transaction.TransactionUpdateRequest.SubmitPendingTx
|
|
||||||
//import cash.z.wallet.sdk.entity.PendingTransaction
|
|
||||||
//import cash.z.wallet.sdk.entity.isMined
|
|
||||||
//import cash.z.wallet.sdk.entity.isPending
|
|
||||||
//import cash.z.wallet.sdk.ext.retryWithBackoff
|
|
||||||
//import cash.z.wallet.sdk.ext.twig
|
|
||||||
//import cash.z.wallet.sdk.service.LightWalletService
|
|
||||||
//import kotlinx.coroutines.*
|
|
||||||
//import kotlinx.coroutines.Dispatchers.IO
|
|
||||||
//import kotlinx.coroutines.channels.SendChannel
|
|
||||||
//import kotlinx.coroutines.channels.actor
|
|
||||||
//import kotlin.math.min
|
|
||||||
//
|
|
||||||
//
|
|
||||||
///**
|
|
||||||
// * Monitors pending transactions and sends or retries them, when appropriate.
|
|
||||||
// */
|
|
||||||
//class PersistentTransactionSender (
|
|
||||||
// private val manager: TransactionManager,
|
|
||||||
// private val service: LightWalletService,
|
|
||||||
// private val ledger: TransactionRepository
|
|
||||||
//) : TransactionSender {
|
|
||||||
//
|
|
||||||
// private lateinit var channel: SendChannel<TransactionUpdateRequest>
|
|
||||||
// private var monitoringJob: Job? = null
|
|
||||||
// private val initialMonitorDelay = 45_000L
|
|
||||||
// private var listenerChannel: SendChannel<List<PendingTransaction>>? = null
|
|
||||||
// override var onSubmissionError: ((Throwable) -> Boolean)? = null
|
|
||||||
// private var updateResult: CompletableDeferred<ChangeType>? = null
|
|
||||||
// var lastChangeDetected: ChangeType = NoChange(0)
|
|
||||||
// set(value) {
|
|
||||||
// field = value
|
|
||||||
// val details = when(value) {
|
|
||||||
// is SizeChange -> " from ${value.oldSize} to ${value.newSize}"
|
|
||||||
// is Modified -> " The culprit: ${value.tx}"
|
|
||||||
// is NoChange -> " for the ${value.count.asOrdinal()} time"
|
|
||||||
// else -> ""
|
|
||||||
// }
|
|
||||||
// twig("Checking pending tx detected: ${value.description}$details")
|
|
||||||
// updateResult?.complete(field)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// fun CoroutineScope.requestUpdate(triggerSend: Boolean) = launch {
|
|
||||||
// if (!channel.isClosedForSend) {
|
|
||||||
// channel.send(if (triggerSend) SubmitPendingTx else RefreshSentTx)
|
|
||||||
// } else {
|
|
||||||
// twig("request ignored because the channel is closed for send!!!")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * Start an actor that listens for signals about what to do with transactions. This actor's lifespan is within the
|
|
||||||
// * provided [scope] and it will live until the scope is cancelled.
|
|
||||||
// */
|
|
||||||
// private fun CoroutineScope.startActor() = actor<TransactionUpdateRequest> {
|
|
||||||
// var pendingTransactionDao = 0 // actor state:
|
|
||||||
// for (msg in channel) { // iterate over incoming messages
|
|
||||||
// when (msg) {
|
|
||||||
// is SubmitPendingTx -> updatePendingTransactions()
|
|
||||||
// is RefreshSentTx -> refreshSentTransactions()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private fun CoroutineScope.startMonitor() = launch {
|
|
||||||
// delay(5000) // todo see if we need a formal initial delay
|
|
||||||
// while (!channel.isClosedForSend && isActive) {
|
|
||||||
// // TODO: consider refactoring this since we actually want to wait on the return value of requestUpdate
|
|
||||||
// updateResult = CompletableDeferred()
|
|
||||||
// requestUpdate(true)
|
|
||||||
// updateResult?.await()
|
|
||||||
// delay(calculateDelay())
|
|
||||||
// }
|
|
||||||
// twig("TransactionMonitor stopping!")
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private fun calculateDelay(): Long {
|
|
||||||
// // if we're actively waiting on results, then poll faster
|
|
||||||
// val delay = when (lastChangeDetected) {
|
|
||||||
// FirstChange -> initialMonitorDelay / 4
|
|
||||||
// is NothingPending, is NoChange -> {
|
|
||||||
// // simple linear offset when there has been no change
|
|
||||||
// val count = (lastChangeDetected as? BackoffEnabled)?.count ?: 0
|
|
||||||
// val offset = initialMonitorDelay / 5L * count
|
|
||||||
// if (previousSentTxs?.isNotEmpty() == true) {
|
|
||||||
// initialMonitorDelay / 4
|
|
||||||
// } else {
|
|
||||||
// initialMonitorDelay
|
|
||||||
// } + offset
|
|
||||||
// }
|
|
||||||
// is SizeChange -> initialMonitorDelay / 4
|
|
||||||
// is Modified -> initialMonitorDelay / 4
|
|
||||||
// }
|
|
||||||
// return min(delay, initialMonitorDelay * 8).also {
|
|
||||||
// twig("Checking for pending tx changes again in ${it/1000L}s")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override fun start(scope: CoroutineScope) {
|
|
||||||
// twig("TransactionMonitor starting!")
|
|
||||||
// channel = scope.startActor()
|
|
||||||
// monitoringJob?.cancel()
|
|
||||||
// monitoringJob = scope.startMonitor()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override fun stop() {
|
|
||||||
// channel.close()
|
|
||||||
// monitoringJob?.cancel()?.also { monitoringJob = null }
|
|
||||||
// manager.stop()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override fun notifyOnChange(channel: SendChannel<List<PendingTransaction>>) {
|
|
||||||
// if (channel != null) twig("warning: listener channel was not null but it probably should have been. Something else was listening with $channel!")
|
|
||||||
// listenerChannel = channel
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override suspend fun initTransaction(
|
|
||||||
// zatoshiValue: Long,
|
|
||||||
// toAddress: String,
|
|
||||||
// memo: String,
|
|
||||||
// fromAccountIndex: Int
|
|
||||||
// ) = withContext(IO) {
|
|
||||||
// manager.initTransaction(
|
|
||||||
// zatoshiValue,
|
|
||||||
// toAddress,
|
|
||||||
// memo,
|
|
||||||
// fromAccountIndex
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// /**
|
|
||||||
// * Generates newly persisted information about a transaction so that other processes can send.
|
|
||||||
// */
|
|
||||||
//// override suspend fun sendToAddress(
|
|
||||||
//// encoder: TransactionEncoder,
|
|
||||||
//// zatoshi: Long,
|
|
||||||
//// toAddress: String,
|
|
||||||
//// memo: String,
|
|
||||||
//// fromAccountId: Int
|
|
||||||
//// ): PendingTransaction = withContext(IO) {
|
|
||||||
//// val currentHeight = service.safeLatestBlockHeight()
|
|
||||||
//// (manager as PersistentTransactionManager).manageCreation(encoder, zatoshi, toAddress, memo, currentHeight).also {
|
|
||||||
//// requestUpdate(true)
|
|
||||||
//// }
|
|
||||||
//// }
|
|
||||||
//
|
|
||||||
//// override suspend fun prepareTransaction(
|
|
||||||
//// zatoshiValue: Long,
|
|
||||||
//// address: String,
|
|
||||||
//// memo: String
|
|
||||||
//// ): PendingTransaction? = withContext(IO) {
|
|
||||||
//// (manager as PersistentTransactionManager).initPlaceholder(zatoshiValue, address, memo).also {
|
|
||||||
//// // update UI to show what we've just created. No need to submit, it has no raw data yet!
|
|
||||||
//// requestUpdate(false)
|
|
||||||
//// }
|
|
||||||
//// }
|
|
||||||
//
|
|
||||||
//// override suspend fun sendPreparedTransaction(
|
|
||||||
//// encoder: TransactionEncoder,
|
|
||||||
//// tx: PendingTransaction
|
|
||||||
//// ): PendingTransaction = withContext(IO) {
|
|
||||||
//// val currentHeight = service.safeLatestBlockHeight()
|
|
||||||
//// (manager as PersistentTransactionManager).manageCreation(encoder, tx, currentHeight).also {
|
|
||||||
//// // submit what we've just created
|
|
||||||
//// requestUpdate(true)
|
|
||||||
//// }
|
|
||||||
//// }
|
|
||||||
//
|
|
||||||
// override suspend fun cleanupPreparedTransaction(tx: PendingTransaction) {
|
|
||||||
// if (tx.raw.isEmpty()) {
|
|
||||||
// (manager as PersistentTransactionManager).abortTransaction(tx)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // TODO: get this from the channel instead
|
|
||||||
// var previousSentTxs: List<PendingTransaction>? = null
|
|
||||||
//
|
|
||||||
// private suspend fun notifyIfChanged(currentSentTxs: List<PendingTransaction>) = withContext(IO) {
|
|
||||||
// if (hasChanged(previousSentTxs, currentSentTxs) && listenerChannel?.isClosedForSend != true) {
|
|
||||||
// twig("START notifying listenerChannel of changed txs")
|
|
||||||
// listenerChannel?.send(currentSentTxs)
|
|
||||||
// twig("DONE notifying listenerChannel of changed txs")
|
|
||||||
// previousSentTxs = currentSentTxs
|
|
||||||
// } else {
|
|
||||||
// twig("notifyIfChanged: did nothing because ${if(listenerChannel?.isClosedForSend == true) "the channel is closed." else "nothing changed."}")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override suspend fun cancel(existingTransaction: PendingTransaction) = withContext(IO) {
|
|
||||||
// (manager as PersistentTransactionManager).abortTransaction(existingTransaction). also {
|
|
||||||
// requestUpdate(false)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private fun hasChanged(
|
|
||||||
// previousSents: List<PendingTransaction>?,
|
|
||||||
// currentSents: List<PendingTransaction>
|
|
||||||
// ): Boolean {
|
|
||||||
// // shortcuts first
|
|
||||||
// if (currentSents.isEmpty() && previousSents.isNullOrEmpty()) return false.also {
|
|
||||||
// val count = if (lastChangeDetected is BackoffEnabled) ((lastChangeDetected as? BackoffEnabled)?.count ?: 0) + 1 else 1
|
|
||||||
// lastChangeDetected = NothingPending(count)
|
|
||||||
// }
|
|
||||||
// if (previousSents == null) return true.also { lastChangeDetected = FirstChange }
|
|
||||||
// if (previousSents.size != currentSents.size) return true.also { lastChangeDetected = SizeChange(previousSentTxs?.size ?: -1, currentSents.size) }
|
|
||||||
// for (tx in currentSents) {
|
|
||||||
// // note: implicit .equals check inside `contains` will also detect modifications
|
|
||||||
// if (!previousSents.contains(tx)) return true.also { lastChangeDetected = Modified(tx) }
|
|
||||||
// }
|
|
||||||
// return false.also {
|
|
||||||
// val count = if (lastChangeDetected is BackoffEnabled) ((lastChangeDetected as? BackoffEnabled)?.count ?: 0) + 1 else 1
|
|
||||||
// lastChangeDetected = NoChange(count)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// sealed class ChangeType(val description: String) {
|
|
||||||
// object FirstChange : ChangeType("This is the first time we've seen a change!")
|
|
||||||
// data class NothingPending(override val count: Int) : ChangeType("Nothing happened yet!"), BackoffEnabled
|
|
||||||
// data class NoChange(override val count: Int) : ChangeType("No changes"), BackoffEnabled
|
|
||||||
// class SizeChange(val oldSize: Int, val newSize: Int) : ChangeType("The total number of pending transactions has changed")
|
|
||||||
// class Modified(val tx: PendingTransaction) : ChangeType("At least one transaction has been modified")
|
|
||||||
// }
|
|
||||||
// interface BackoffEnabled {
|
|
||||||
// val count: Int
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * Check on all sent transactions and if they've changed, notify listeners. This method can be called proactively
|
|
||||||
// * when anything interesting has occurred with a transaction (via [requestUpdate]).
|
|
||||||
// */
|
|
||||||
// private suspend fun refreshSentTransactions(): List<PendingTransaction> = withContext(IO) {
|
|
||||||
// val allSentTransactions = (manager as PersistentTransactionManager).getAll() // TODO: make this crash and catch error gracefully
|
|
||||||
// notifyIfChanged(allSentTransactions)
|
|
||||||
// allSentTransactions
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * Submit all pending transactions that have not expired.
|
|
||||||
// */
|
|
||||||
// private suspend fun updatePendingTransactions() = withContext(IO) {
|
|
||||||
// try {
|
|
||||||
// val allTransactions = refreshSentTransactions()
|
|
||||||
// var pendingCount = 0
|
|
||||||
// val currentHeight = service.safeLatestBlockHeight()
|
|
||||||
// allTransactions.filter { !it.isMined() }.forEach { tx ->
|
|
||||||
// if (tx.isPending(currentHeight)) {
|
|
||||||
// pendingCount++
|
|
||||||
// retryWithBackoff(onSubmissionError, 1000L, 60_000L) {
|
|
||||||
// manager.manageSubmission(service, tx)
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// tx.rawTransactionId?.let {
|
|
||||||
// ledger.findTransactionByRawId(tx.rawTransactionId)
|
|
||||||
// }?.let {
|
|
||||||
// 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}")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//private fun Int.asOrdinal(): String {
|
|
||||||
// return "$this" + if (this % 100 in 11..13) "th" else when(this % 10) {
|
|
||||||
// 1 -> "st"
|
|
||||||
// 2 -> "nd"
|
|
||||||
// 3 -> "rd"
|
|
||||||
// else -> "th"
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//private fun LightWalletService.safeLatestBlockHeight(): Int {
|
|
||||||
// return try {
|
|
||||||
// getLatestBlockHeight()
|
|
||||||
// } catch (t: Throwable) {
|
|
||||||
// twig("Warning: LightWalletService failed to return the latest height and we are returning -1 instead.")
|
|
||||||
// -1
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//sealed class TransactionUpdateRequest {
|
|
||||||
// object SubmitPendingTx : TransactionUpdateRequest()
|
|
||||||
// object RefreshSentTx : TransactionUpdateRequest()
|
|
||||||
//}
|
|
|
@ -4,7 +4,17 @@ import cash.z.wallet.sdk.entity.EncodedTransaction
|
||||||
|
|
||||||
interface TransactionEncoder {
|
interface TransactionEncoder {
|
||||||
/**
|
/**
|
||||||
* Creates a signed transaction
|
* Creates a transaction, throwing an exception whenever things are missing. When the provided
|
||||||
|
* wallet implementation doesn't throw an exception, we wrap the issue into a descriptive
|
||||||
|
* exception ourselves (rather than using double-bangs for things).
|
||||||
|
*
|
||||||
|
* @param spendingKey the key associated with the notes that will be spent.
|
||||||
|
* @param zatoshi the amount of zatoshi to send.
|
||||||
|
* @param toAddress the recipient's address.
|
||||||
|
* @param memo the optional memo to include as part of the transaction.
|
||||||
|
* @param fromAccountIndex the optional account id to use. By default, the 1st account is used.
|
||||||
|
*
|
||||||
|
* @return the successfully encoded transaction or an exception
|
||||||
*/
|
*/
|
||||||
suspend fun createTransaction(
|
suspend fun createTransaction(
|
||||||
spendingKey: String,
|
spendingKey: String,
|
||||||
|
@ -14,6 +24,23 @@ interface TransactionEncoder {
|
||||||
fromAccountIndex: Int = 0
|
fromAccountIndex: Int = 0
|
||||||
): EncodedTransaction
|
): EncodedTransaction
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to help with validation. This is not called during [createTransaction]
|
||||||
|
* because this class asserts that all validation is done externally by the UI, for now.
|
||||||
|
*
|
||||||
|
* @param address the address to validate
|
||||||
|
*
|
||||||
|
* @return true when the given address is a valid z-addr
|
||||||
|
*/
|
||||||
suspend fun isValidShieldedAddress(address: String): Boolean
|
suspend fun isValidShieldedAddress(address: String): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to help with validation. This is not called during [createTransaction]
|
||||||
|
* because this class asserts that all validation is done externally by the UI, for now.
|
||||||
|
*
|
||||||
|
* @param address the address to validate
|
||||||
|
*
|
||||||
|
* @return true when the given address is a valid t-addr
|
||||||
|
*/
|
||||||
suspend fun isValidTransparentAddress(address: String): Boolean
|
suspend fun isValidTransparentAddress(address: String): Boolean
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,29 +9,110 @@ import kotlinx.coroutines.flow.Flow
|
||||||
* transactions through to completion.
|
* transactions through to completion.
|
||||||
*/
|
*/
|
||||||
interface OutboundTransactionManager {
|
interface OutboundTransactionManager {
|
||||||
|
/**
|
||||||
|
* Initialize a spend with the main purpose of creating an idea to use for tracking it until
|
||||||
|
* completion.
|
||||||
|
*
|
||||||
|
* @param zatoshi the amount to spend.
|
||||||
|
* @param toAddress the address to which funds will be sent.
|
||||||
|
* @param memo the optionally blank memo associated with this transaction.
|
||||||
|
* @param fromAccountIndex the account from which to spend funds.
|
||||||
|
*
|
||||||
|
* @return the associated pending transaction whose ID can be used to monitor for changes.
|
||||||
|
*/
|
||||||
suspend fun initSpend(
|
suspend fun initSpend(
|
||||||
zatoshi: Long,
|
zatoshi: Long,
|
||||||
toAddress: String,
|
toAddress: String,
|
||||||
memo: String,
|
memo: String,
|
||||||
fromAccountIndex: Int
|
fromAccountIndex: Int
|
||||||
): PendingTransaction
|
): PendingTransaction
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode the pending transaction using the given spending key. This is a local operation that
|
||||||
|
* produces a raw transaction to submit to lightwalletd.
|
||||||
|
*
|
||||||
|
* @param spendingKey the spendingKey to use for constructing the transaction.
|
||||||
|
* @param pendingTx the transaction information created by [initSpend] that will be used to
|
||||||
|
* construct a transaction.
|
||||||
|
*
|
||||||
|
* @return the resulting pending transaction whose ID can be used to monitor for changes.
|
||||||
|
*/
|
||||||
suspend fun encode(spendingKey: String, pendingTx: PendingTransaction): PendingTransaction
|
suspend fun encode(spendingKey: String, pendingTx: PendingTransaction): PendingTransaction
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits the transaction represented by [pendingTx] to lightwalletd to broadcast to the
|
||||||
|
* network and, hopefully, include in the next block.
|
||||||
|
*
|
||||||
|
* @param pendingTx the transaction information containing the raw bytes that will be submitted
|
||||||
|
* to lightwalletd.
|
||||||
|
*
|
||||||
|
* @return the resulting pending transaction whose ID can be used to monitor for changes.
|
||||||
|
*/
|
||||||
suspend fun submit(pendingTx: PendingTransaction): PendingTransaction
|
suspend fun submit(pendingTx: PendingTransaction): PendingTransaction
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a transaction and the height at which it was mined, update the transaction to indicate
|
||||||
|
* that it was mined.
|
||||||
|
*
|
||||||
|
* @param pendingTx the pending transaction that has been mineed.
|
||||||
|
* @param minedHeight the height at which the given transaction was mined, according to the data
|
||||||
|
* that has been processed from the blockchain.
|
||||||
|
*/
|
||||||
suspend fun applyMinedHeight(pendingTx: PendingTransaction, minedHeight: Int)
|
suspend fun applyMinedHeight(pendingTx: PendingTransaction, minedHeight: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a flow of information about the given id where a new pending transaction is emitted
|
||||||
|
* every time its state changes.
|
||||||
|
*
|
||||||
|
* @param id the id to monitor.
|
||||||
|
*
|
||||||
|
* @return a flow of pending transactions that are emitted anytime the transaction associated
|
||||||
|
* withh the given id changes.
|
||||||
|
*/
|
||||||
suspend fun monitorById(id: Long): Flow<PendingTransaction>
|
suspend fun monitorById(id: Long): Flow<PendingTransaction>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true when the given address is a valid t-addr.
|
||||||
|
*
|
||||||
|
* @param address the address to validate.
|
||||||
|
*
|
||||||
|
* @return true when the given address is a valid t-addr.
|
||||||
|
*/
|
||||||
suspend fun isValidShieldedAddress(address: String): Boolean
|
suspend fun isValidShieldedAddress(address: String): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true when the given address is a valid z-addr.
|
||||||
|
*
|
||||||
|
* @param address the address to validate.
|
||||||
|
*
|
||||||
|
* @return true when the given address is a valid z-addr.
|
||||||
|
*/
|
||||||
suspend fun isValidTransparentAddress(address: String): Boolean
|
suspend fun isValidTransparentAddress(address: String): Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to cancel a transaction.
|
* Attempt to cancel a transaction.
|
||||||
*
|
*
|
||||||
|
* @param pendingTx the transaction matching the ID of the transaction to cancel.
|
||||||
|
*
|
||||||
* @return true when the transaction was able to be cancelled.
|
* @return true when the transaction was able to be cancelled.
|
||||||
*/
|
*/
|
||||||
suspend fun cancel(pendingTx: PendingTransaction): Boolean
|
suspend fun cancel(pendingTx: PendingTransaction): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all pending transactions known to this wallet as a flow that is updated anytime the list
|
||||||
|
* changes.
|
||||||
|
*
|
||||||
|
* @return a flow of all pending transactions known to this wallet.
|
||||||
|
*/
|
||||||
fun getAll(): Flow<List<PendingTransaction>>
|
fun getAll(): Flow<List<PendingTransaction>>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for transaction errors.
|
||||||
|
*/
|
||||||
interface TransactionError {
|
interface TransactionError {
|
||||||
|
/**
|
||||||
|
* The message associated with this error.
|
||||||
|
*/
|
||||||
val message: String
|
val message: String
|
||||||
}
|
}
|
|
@ -8,9 +8,38 @@ import kotlinx.coroutines.flow.Flow
|
||||||
* Repository of wallet transactions, providing an agnostic interface to the underlying information.
|
* Repository of wallet transactions, providing an agnostic interface to the underlying information.
|
||||||
*/
|
*/
|
||||||
interface TransactionRepository {
|
interface TransactionRepository {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The last height scanned by this repository.
|
||||||
|
*
|
||||||
|
* @return the last height scanned by this repository.
|
||||||
|
*/
|
||||||
fun lastScannedHeight(): Int
|
fun lastScannedHeight(): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when this repository has been initialized and seeded with the initial checkpoint.
|
||||||
|
*
|
||||||
|
* @return true when this repository has been initialized and seeded with the initial checkpoint.
|
||||||
|
*/
|
||||||
fun isInitialized(): Boolean
|
fun isInitialized(): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the encoded transaction associated with the given id.
|
||||||
|
*
|
||||||
|
* @param txId the id of the transaction to find.
|
||||||
|
*
|
||||||
|
* @return the transaction or null when it cannot be found.
|
||||||
|
*/
|
||||||
suspend fun findEncodedTransactionById(txId: Long): EncodedTransaction?
|
suspend fun findEncodedTransactionById(txId: Long): EncodedTransaction?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the mined height that matches the given raw tx_id in bytes. This is useful for matching
|
||||||
|
* a pending transaction with one that we've decrypted from the blockchain.
|
||||||
|
*
|
||||||
|
* @param rawTransactionId the id of the transaction to find.
|
||||||
|
*
|
||||||
|
* @return the mined height of the given transaction, if it is known to this wallet.
|
||||||
|
*/
|
||||||
suspend fun findMinedHeight(rawTransactionId: ByteArray): Int?
|
suspend fun findMinedHeight(rawTransactionId: ByteArray): Int?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -23,7 +52,10 @@ interface TransactionRepository {
|
||||||
// Transactions
|
// Transactions
|
||||||
//
|
//
|
||||||
|
|
||||||
|
/** A flow of all the inbound confirmed transactions */
|
||||||
val receivedTransactions: Flow<PagedList<ConfirmedTransaction>>
|
val receivedTransactions: Flow<PagedList<ConfirmedTransaction>>
|
||||||
|
/** A flow of all the outbound confirmed transactions */
|
||||||
val sentTransactions: Flow<PagedList<ConfirmedTransaction>>
|
val sentTransactions: Flow<PagedList<ConfirmedTransaction>>
|
||||||
|
/** A flow of all the inbound and outbound confirmed transactions */
|
||||||
val allTransactions: Flow<PagedList<ConfirmedTransaction>>
|
val allTransactions: Flow<PagedList<ConfirmedTransaction>>
|
||||||
}
|
}
|
|
@ -1,23 +0,0 @@
|
||||||
package cash.z.wallet.sdk.transaction
|
|
||||||
|
|
||||||
import cash.z.wallet.sdk.entity.PendingTransaction
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.channels.SendChannel
|
|
||||||
|
|
||||||
// TODO: delete this entire class and use managed transactions, instead
|
|
||||||
interface TransactionSender {
|
|
||||||
fun start(scope: CoroutineScope)
|
|
||||||
fun stop()
|
|
||||||
fun notifyOnChange(channel: SendChannel<List<PendingTransaction>>)
|
|
||||||
/** only necessary when there is a long delay between starting a transaction and beginning to create it. Like when sweeping a wallet that first needs to be scanned. */
|
|
||||||
// suspend fun initTransaction(zatoshiValue: Long, toAddress: String, memo: String, fromAccountIndex: Int): ManagedTransaction
|
|
||||||
// suspend fun prepareTransaction(amount: Long, address: String, memo: String): PendingTransaction?
|
|
||||||
// suspend fun sendPreparedTransaction(encoder: TransactionEncoder, tx: PendingTransaction): PendingTransaction
|
|
||||||
suspend fun cleanupPreparedTransaction(tx: PendingTransaction)
|
|
||||||
// suspend fun sendToAddress(encoder: TransactionEncoder, zatoshi: Long, toAddress: String, memo: String = "", fromAccountId: Int = 0): PendingTransaction
|
|
||||||
suspend fun cancel(existingTransaction: PendingTransaction): Unit?
|
|
||||||
|
|
||||||
var onSubmissionError: ((Throwable) -> Boolean)?
|
|
||||||
}
|
|
||||||
|
|
||||||
class SendResult
|
|
|
@ -12,15 +12,32 @@ import kotlinx.coroutines.withContext
|
||||||
import okio.Okio
|
import okio.Okio
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class responsible for encoding a transaction in a consistent way. This bridges the gap by
|
||||||
|
* behaving like a stateless API so that callers can request [createTransaction] and receive a
|
||||||
|
* result, even though there are intermediate database interactions.
|
||||||
|
*
|
||||||
|
* @property rustBackend the instance of RustBackendWelding to use for creating and validating.
|
||||||
|
* @property repository the repository that stores information about the transactions being created
|
||||||
|
* such as the raw bytes and raw txId.
|
||||||
|
*/
|
||||||
class WalletTransactionEncoder(
|
class WalletTransactionEncoder(
|
||||||
private val rustBackend: RustBackendWelding,
|
private val rustBackend: RustBackendWelding,
|
||||||
private val repository: TransactionRepository
|
private val repository: TransactionRepository
|
||||||
) : TransactionEncoder {
|
) : TransactionEncoder {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a transaction, throwing an exception whenever things are missing. When the provided wallet implementation
|
* Creates a transaction, throwing an exception whenever things are missing. When the provided
|
||||||
* doesn't throw an exception, we wrap the issue into a descriptive exception ourselves (rather than using
|
* wallet implementation doesn't throw an exception, we wrap the issue into a descriptive
|
||||||
* double-bangs for things).
|
* exception ourselves (rather than using double-bangs for things).
|
||||||
|
*
|
||||||
|
* @param spendingKey the key associated with the notes that will be spent.
|
||||||
|
* @param zatoshi the amount of zatoshi to send.
|
||||||
|
* @param toAddress the recipient's address.
|
||||||
|
* @param memo the optional memo to include as part of the transaction.
|
||||||
|
* @param fromAccountIndex the optional account id to use. By default, the 1st account is used.
|
||||||
|
*
|
||||||
|
* @return the successfully encoded transaction or an exception
|
||||||
*/
|
*/
|
||||||
override suspend fun createTransaction(
|
override suspend fun createTransaction(
|
||||||
spendingKey: String,
|
spendingKey: String,
|
||||||
|
@ -37,6 +54,10 @@ class WalletTransactionEncoder(
|
||||||
/**
|
/**
|
||||||
* Utility function to help with validation. This is not called during [createTransaction]
|
* Utility function to help with validation. This is not called during [createTransaction]
|
||||||
* because this class asserts that all validation is done externally by the UI, for now.
|
* because this class asserts that all validation is done externally by the UI, for now.
|
||||||
|
*
|
||||||
|
* @param address the address to validate
|
||||||
|
*
|
||||||
|
* @return true when the given address is a valid z-addr
|
||||||
*/
|
*/
|
||||||
override suspend fun isValidShieldedAddress(address: String): Boolean = withContext(IO) {
|
override suspend fun isValidShieldedAddress(address: String): Boolean = withContext(IO) {
|
||||||
rustBackend.isValidShieldedAddr(address)
|
rustBackend.isValidShieldedAddr(address)
|
||||||
|
@ -45,6 +66,10 @@ class WalletTransactionEncoder(
|
||||||
/**
|
/**
|
||||||
* Utility function to help with validation. This is not called during [createTransaction]
|
* Utility function to help with validation. This is not called during [createTransaction]
|
||||||
* because this class asserts that all validation is done externally by the UI, for now.
|
* because this class asserts that all validation is done externally by the UI, for now.
|
||||||
|
*
|
||||||
|
* @param address the address to validate
|
||||||
|
*
|
||||||
|
* @return true when the given address is a valid t-addr
|
||||||
*/
|
*/
|
||||||
override suspend fun isValidTransparentAddress(address: String): Boolean = withContext(IO) {
|
override suspend fun isValidTransparentAddress(address: String): Boolean = withContext(IO) {
|
||||||
rustBackend.isValidTransparentAddr(address)
|
rustBackend.isValidTransparentAddr(address)
|
||||||
|
@ -54,21 +79,23 @@ class WalletTransactionEncoder(
|
||||||
* Does the proofs and processing required to create a transaction to spend funds and inserts
|
* Does the proofs and processing required to create a transaction to spend funds and inserts
|
||||||
* the result in the database. On average, this call takes over 10 seconds.
|
* the result in the database. On average, this call takes over 10 seconds.
|
||||||
*
|
*
|
||||||
* @param value the zatoshi value to send
|
* @param spendingKey the key associated with the notes that will be spent.
|
||||||
* @param toAddress the destination address
|
* @param zatoshi the amount of zatoshi to send.
|
||||||
* @param memo the memo, which is not augmented in any way
|
* @param toAddress the recipient's address.
|
||||||
|
* @param memo the optional memo to include as part of the transaction.
|
||||||
|
* @param fromAccountIndex the optional account id to use. By default, the 1st account is used.
|
||||||
*
|
*
|
||||||
* @return the row id in the transactions table that contains the spend transaction
|
* @return the row id in the transactions table that contains the spend transaction or -1 if it
|
||||||
* or -1 if it failed
|
* failed.
|
||||||
*/
|
*/
|
||||||
private suspend fun createSpend(
|
private suspend fun createSpend(
|
||||||
spendingKey: String,
|
spendingKey: String,
|
||||||
value: Long,
|
zatoshi: Long,
|
||||||
toAddress: String,
|
toAddress: String,
|
||||||
memo: ByteArray? = byteArrayOf(),
|
memo: ByteArray? = byteArrayOf(),
|
||||||
fromAccountIndex: Int = 0
|
fromAccountIndex: Int = 0
|
||||||
): Long = withContext(IO) {
|
): Long = withContext(IO) {
|
||||||
twigTask("creating transaction to spend $value zatoshi to" +
|
twigTask("creating transaction to spend $zatoshi zatoshi to" +
|
||||||
" ${toAddress.masked()} with memo $memo") {
|
" ${toAddress.masked()} with memo $memo") {
|
||||||
try {
|
try {
|
||||||
ensureParams((rustBackend as RustBackend).pathParamsDir)
|
ensureParams((rustBackend as RustBackend).pathParamsDir)
|
||||||
|
@ -77,7 +104,7 @@ class WalletTransactionEncoder(
|
||||||
fromAccountIndex,
|
fromAccountIndex,
|
||||||
spendingKey,
|
spendingKey,
|
||||||
toAddress,
|
toAddress,
|
||||||
value,
|
zatoshi,
|
||||||
memo
|
memo
|
||||||
)
|
)
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
|
@ -166,6 +193,8 @@ class WalletTransactionEncoder(
|
||||||
/**
|
/**
|
||||||
* Http client is only used for downloading sapling spend and output params data, which are
|
* Http client is only used for downloading sapling spend and output params data, which are
|
||||||
* necessary for the wallet to scan blocks.
|
* necessary for the wallet to scan blocks.
|
||||||
|
*
|
||||||
|
* @return an http client suitable for downloading params data.
|
||||||
*/
|
*/
|
||||||
private fun createHttpClient(): OkHttpClient {
|
private fun createHttpClient(): OkHttpClient {
|
||||||
//TODO: add logging and timeouts
|
//TODO: add logging and timeouts
|
||||||
|
|
Loading…
Reference in New Issue