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 cash.z.wallet.sdk.exception.BirthdayException
|
||||
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 com.google.gson.Gson
|
||||
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.
|
||||
*
|
||||
* @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 toAddress the recipient's address.
|
||||
* @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
|
||||
* 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
|
||||
//
|
||||
|
||||
/**
|
||||
* 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(
|
||||
entities = [CompactBlockEntity::class],
|
||||
version = 1,
|
||||
|
@ -22,6 +28,9 @@ abstract class CompactBlockDb : RoomDatabase() {
|
|||
// Data Access Objects
|
||||
//
|
||||
|
||||
/**
|
||||
* Data access object for compact blocks in the "Cache DB."
|
||||
*/
|
||||
@Dao
|
||||
interface CompactBlockDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
|
|
|
@ -11,6 +11,13 @@ import cash.z.wallet.sdk.entity.*
|
|||
// Database
|
||||
//
|
||||
|
||||
/**
|
||||
* The "Data DB," where all data derived from the compact blocks is stored. Most importantly, this
|
||||
* database contains transaction information and can be queried for the current balance. The
|
||||
* "blocks" table contains a copy of everything that has been scanned. In the future, that table can
|
||||
* be truncated up to the last scanned block, for storage efficiency. Wallets should only read from,
|
||||
* but never write to, this database.
|
||||
*/
|
||||
@Database(
|
||||
entities = [
|
||||
TransactionEntity::class,
|
||||
|
@ -34,6 +41,9 @@ abstract class DerivedDataDb : RoomDatabase() {
|
|||
// Data Access Objects
|
||||
//
|
||||
|
||||
/**
|
||||
* The data access object for blocks, used for determining the last scanned height.
|
||||
*/
|
||||
@Dao
|
||||
interface BlockDao {
|
||||
@Query("SELECT COUNT(height) FROM blocks")
|
||||
|
@ -43,18 +53,28 @@ interface BlockDao {
|
|||
fun lastScannedHeight(): Int
|
||||
}
|
||||
|
||||
/**
|
||||
* The data access object for notes, used for determining whether transactions exist.
|
||||
*/
|
||||
@Dao
|
||||
interface ReceivedDao {
|
||||
@Query("SELECT COUNT(tx) FROM received_notes")
|
||||
fun count(): Int
|
||||
}
|
||||
|
||||
/**
|
||||
* The data access object for sent notes, used for determining whether outbound transactions exist.
|
||||
*/
|
||||
@Dao
|
||||
interface SentDao {
|
||||
@Query("SELECT COUNT(tx) FROM sent_notes")
|
||||
fun count(): Int
|
||||
}
|
||||
|
||||
/**
|
||||
* The data access object for transactions, used for querying all transaction information, including
|
||||
* whether transactions are mined.
|
||||
*/
|
||||
@Dao
|
||||
interface TransactionDao {
|
||||
@Query("SELECT COUNT(id_tx) FROM transactions")
|
||||
|
@ -79,12 +99,6 @@ interface TransactionDao {
|
|||
""")
|
||||
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.
|
||||
*/
|
||||
|
@ -137,6 +151,9 @@ interface TransactionDao {
|
|||
""")
|
||||
fun getReceivedTransactions(limit: Int = Int.MAX_VALUE): DataSource.Factory<Int, ConfirmedTransaction>
|
||||
|
||||
/**
|
||||
* Query all transactions, joining outbound and inbound transactions into the same table.
|
||||
*/
|
||||
@Query("""
|
||||
SELECT transactions.id_tx AS id,
|
||||
transactions.block AS minedHeight,
|
||||
|
|
|
@ -9,6 +9,12 @@ import kotlinx.coroutines.flow.Flow
|
|||
// Database
|
||||
//
|
||||
|
||||
/**
|
||||
* Database for pending transaction information. Unlike with the "Data DB," the wallet is free to
|
||||
* write to this database. In a way, this almost serves as a local mempool for all transactions
|
||||
* initiated by this wallet. Currently, the data necessary to support expired transactions is there
|
||||
* but it is not being leveraged.
|
||||
*/
|
||||
@Database(
|
||||
entities = [
|
||||
PendingTransactionEntity::class
|
||||
|
@ -25,6 +31,9 @@ abstract class PendingTransactionDb : RoomDatabase() {
|
|||
// Data Access Objects
|
||||
//
|
||||
|
||||
/**
|
||||
* Data access object providing crud for pending transactions.
|
||||
*/
|
||||
@Dao
|
||||
interface PendingTransactionDao {
|
||||
@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)
|
||||
}
|
||||
|
||||
/**
|
||||
* User-facing exceptions thrown by the transaction repository.
|
||||
*/
|
||||
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 " +
|
||||
"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) {
|
||||
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."
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Potentially user-facing exceptions that occur while processing compact blocks.
|
||||
*/
|
||||
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 " +
|
||||
"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.")
|
||||
}
|
||||
|
||||
sealed class CompactBlockStreamException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
||||
object ConnectionClosed: CompactBlockStreamException("Cannot start stream when connection is closed.")
|
||||
class FalseStart(cause: Throwable?): CompactBlockStreamException("Failed to start compact block stream due to " +
|
||||
"$cause caused by ${cause?.cause}")
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Exceptions related to the wallet's birthday.
|
||||
*/
|
||||
sealed class BirthdayException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
||||
object UninitializedBirthdayException : BirthdayException("Error the birthday cannot be" +
|
||||
" 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){
|
||||
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" +
|
||||
|
@ -86,6 +95,9 @@ sealed class InitializerException(message: String, cause: Throwable? = null) :
|
|||
" we can store data.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Exceptions thrown while interacting with lightwalletd.
|
||||
*/
|
||||
sealed class LightwalletException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
||||
object InsecureConnection : LightwalletException("Error: attempted to connect to lightwalletd" +
|
||||
" 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.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Potentially user-facing exceptions thrown while encoding transactions.
|
||||
*/
|
||||
sealed class TransactionEncoderException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
||||
class FetchParamsException(message: String) : TransactionEncoderException("Failed to fetch params due to: $message")
|
||||
object MissingParamsException : TransactionEncoderException(
|
||||
|
|
|
@ -9,6 +9,14 @@ import java.math.RoundingMode
|
|||
import java.text.NumberFormat
|
||||
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
|
||||
// For now, just make these vars so at least they could be modified in one place
|
||||
object Conversions {
|
||||
|
@ -29,11 +37,15 @@ object Conversions {
|
|||
|
||||
|
||||
/**
|
||||
* Format a Zatoshi value into Zec with the given number of digits, represented as a string.
|
||||
* Start with Zatoshi -> End with Zec.
|
||||
* Format a Zatoshi value into ZEC with the given number of digits, represented as a string.
|
||||
* 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.
|
||||
*
|
||||
* @return this Zatoshi value represented as ZEC, in a string with at least [minDecimals] and at
|
||||
* most [maxDecimals]
|
||||
*/
|
||||
inline fun Long?.convertZatoshiToZecString(
|
||||
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.
|
||||
* Start with ZeC -> End with Zec.
|
||||
* Format a ZEC value into ZEC with the given number of digits, represented as a string.
|
||||
* 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.
|
||||
*
|
||||
* @return this Double ZEC value represented as a string with at least [minDecimals] and at most
|
||||
* [maxDecimals].
|
||||
*/
|
||||
inline fun Double?.toZecString(
|
||||
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.
|
||||
* Start with ZeC -> End with Zec.
|
||||
* Format a Zatoshi value into ZEC with the given number of decimal places, represented as a string.
|
||||
* 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.
|
||||
*
|
||||
* @return this BigDecimal ZEC value represented as a string with at least [minDecimals] and at most
|
||||
* [maxDecimals].
|
||||
*/
|
||||
inline fun BigDecimal?.toZecString(
|
||||
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.
|
||||
*
|
||||
* @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(
|
||||
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.
|
||||
* @param maxDecimals the number of decimal places to use in the format. Default is 6 because Zec is glorious.
|
||||
* Format a USD value into USD with the given number of decimal places, 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
|
||||
* glorious.
|
||||
* @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(
|
||||
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
|
||||
* the other formatting functions leverage this, instead. Leverages the default rounding mode for zec found in
|
||||
* ZEC_FORMATTER.
|
||||
* Create a number formatter for use with converting currency to strings. This probably isn't needed
|
||||
* externally since the other formatting functions leverage this, instead. Leverages the default
|
||||
* 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 {
|
||||
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
|
||||
* order to preserve rounding that minimizes cumulative error when applied repeatedly over a sequence of calculations.
|
||||
* Start with Zatoshi -> End with Zec.
|
||||
* Convert a Zatoshi value into ZEC, right-padded to the given number of fraction digits,
|
||||
* represented as a BigDecimal in order to preserve rounding that minimizes cumulative error when
|
||||
* 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 {
|
||||
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 {
|
||||
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()
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a Double Zec value as a BigDecimal Zec value, right-padded to the given number of fraction digits.
|
||||
* Start with Zec -> End with Zec.
|
||||
* Format a Double ZEC value as a BigDecimal ZEC value, right-padded to the given number of fraction
|
||||
* 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 {
|
||||
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.
|
||||
* 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 {
|
||||
return this.toZec(decimals).convertZecToZatoshi()
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a BigDecimal Zec value as a BigDecimal Zec value, right-padded to the given number of fraction digits.
|
||||
* Start with Zec -> End with Zec.
|
||||
* Format a BigDecimal ZEC value as a BigDecimal ZEC value, right-padded to the given number of
|
||||
* 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 {
|
||||
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 {
|
||||
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 {
|
||||
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.
|
||||
* Start with ZEC -> End with USD.
|
||||
*
|
||||
* @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 {
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
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.
|
||||
*
|
||||
* @return null when parsing fails
|
||||
* @return this string as a BigDecimal or null when parsing fails.
|
||||
*/
|
||||
inline fun String?.safelyConvertToBigDecimal(): BigDecimal? {
|
||||
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')
|
|
@ -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) {
|
||||
scope.launch {
|
||||
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) {
|
||||
onEach {
|
||||
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.
|
||||
*/
|
||||
interface Twig {
|
||||
|
||||
/**
|
||||
* Log the message. Simple.
|
||||
*/
|
||||
fun twig(logMessage: String = "")
|
||||
|
||||
/**
|
||||
* Bundles twigs together
|
||||
*/
|
||||
operator fun plus(twig: Twig): Twig {
|
||||
// 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))
|
||||
|
@ -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.
|
||||
*/
|
||||
class SilentTwig : Twig {
|
||||
|
||||
/**
|
||||
* Shh.
|
||||
*/
|
||||
override fun twig(logMessage: String) {
|
||||
// shh
|
||||
}
|
||||
|
@ -89,6 +101,10 @@ open class TroubleshootingTwig(
|
|||
val formatter: (String) -> String = spiffy(5),
|
||||
val printer: (String) -> Any = System.err::println
|
||||
) : Twig {
|
||||
|
||||
/**
|
||||
* Actually print and format the log message, unlike the SilentTwig, which does nothing.
|
||||
*/
|
||||
override fun twig(logMessage: String) {
|
||||
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) {
|
||||
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) {
|
||||
|
@ -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 {
|
||||
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.
|
||||
* 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 {
|
||||
|
||||
|
|
|
@ -17,10 +17,10 @@ import java.util.concurrent.TimeUnit
|
|||
/**
|
||||
* Implementation of LightwalletService using gRPC for requests to lightwalletd.
|
||||
*
|
||||
* @param 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 channel the channel to use for communicating with the lightwalletd server.
|
||||
* @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.
|
||||
* @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.
|
||||
*/
|
||||
class LightWalletGrpcService private constructor(
|
||||
|
@ -29,6 +29,16 @@ class LightWalletGrpcService private constructor(
|
|||
private val streamingRequestTimeoutSec: Long = 90L
|
||||
) : 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(
|
||||
appContext: Context,
|
||||
host: String,
|
||||
|
@ -38,12 +48,6 @@ class LightWalletGrpcService private constructor(
|
|||
|
||||
/* 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> {
|
||||
channel.resetConnectBackoff()
|
||||
return channel.createStub(streamingRequestTimeoutSec).getBlockRange(heightRange.toBlockRange()).toList()
|
||||
|
@ -89,6 +93,11 @@ class LightWalletGrpcService private constructor(
|
|||
}
|
||||
|
||||
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(
|
||||
appContext: Context,
|
||||
host: String,
|
||||
|
|
|
@ -13,16 +13,23 @@ interface LightWalletService {
|
|||
*
|
||||
* @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.
|
||||
*
|
||||
* @return a list of compact blocks for the given range
|
||||
*
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
fun getLatestBlockHeight(): Int
|
||||
|
||||
/**
|
||||
* Submit a raw transaction.
|
||||
*
|
||||
* @return the response from the server.
|
||||
*/
|
||||
fun submitTransaction(spendTransaction: ByteArray): Service.SendResponse
|
||||
|
||||
|
|
|
@ -79,6 +79,9 @@ open class PagedTransactionRepository(
|
|||
transactions.findMinedHeight(rawTransactionId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the underlying database.
|
||||
*/
|
||||
fun 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) {
|
||||
database.execSQL("PRAGMA foreign_keys = OFF;")
|
||||
database.execSQL(
|
||||
|
|
|
@ -5,7 +5,10 @@ import androidx.room.Room
|
|||
import androidx.room.RoomDatabase
|
||||
import cash.z.wallet.sdk.db.PendingTransactionDao
|
||||
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.service.LightWalletService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -13,13 +16,19 @@ import kotlinx.coroutines.flow.Flow
|
|||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.lang.IllegalStateException
|
||||
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(
|
||||
db: PendingTransactionDb,
|
||||
private val encoder: TransactionEncoder,
|
||||
|
@ -52,10 +61,11 @@ class PersistentTransactionManager(
|
|||
service
|
||||
)
|
||||
|
||||
/**
|
||||
* Initialize a [PendingTransaction] and then insert it in the database for monitoring and
|
||||
* follow-up.
|
||||
*/
|
||||
|
||||
//
|
||||
// OutboundTransactionManager implementation
|
||||
//
|
||||
|
||||
override suspend fun initSpend(
|
||||
zatoshiValue: Long,
|
||||
toAddress: String,
|
||||
|
@ -79,7 +89,8 @@ class PersistentTransactionManager(
|
|||
twig("successfully created TX in DB")
|
||||
}
|
||||
} 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
|
||||
|
@ -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(
|
||||
spendingKey: String,
|
||||
pendingTx: PendingTransaction
|
||||
): PendingTransaction = withContext(Dispatchers.IO) {
|
||||
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
|
||||
try {
|
||||
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}"
|
||||
twig(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 {
|
||||
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) {
|
||||
// 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
|
||||
try {
|
||||
// do nothing when cancelled
|
||||
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 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(
|
||||
errorMessage = if (error) response.errorMessage else null,
|
||||
errorCode = response.errorCode,
|
||||
|
@ -157,7 +162,11 @@ class PersistentTransactionManager(
|
|||
val message =
|
||||
"Unknown error while submitting transaction: ${t.message} caused by: ${t.cause}"
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -188,18 +197,33 @@ class PersistentTransactionManager(
|
|||
|
||||
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
|
||||
* happen within a try/catch block, surrounded by logging. So this helps with that.
|
||||
*/
|
||||
private suspend fun safeUpdate(tx: PendingTransactionEntity): PendingTransaction {
|
||||
return try {
|
||||
twig("updating tx into DB: $tx")
|
||||
twig("updating tx in DB: $tx")
|
||||
pendingTransactionDao { update(tx) }
|
||||
twig("successfully updated TX into DB")
|
||||
twig("successfully updated TX in DB")
|
||||
tx
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
@ -209,5 +233,12 @@ class PersistentTransactionManager(
|
|||
_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 {
|
||||
/**
|
||||
* 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(
|
||||
spendingKey: String,
|
||||
|
@ -14,6 +24,23 @@ interface TransactionEncoder {
|
|||
fromAccountIndex: Int = 0
|
||||
): 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
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
|
|
@ -9,29 +9,110 @@ import kotlinx.coroutines.flow.Flow
|
|||
* transactions through to completion.
|
||||
*/
|
||||
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(
|
||||
zatoshi: Long,
|
||||
toAddress: String,
|
||||
memo: String,
|
||||
fromAccountIndex: Int
|
||||
): 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
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
||||
/**
|
||||
* 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>
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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>>
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for transaction errors.
|
||||
*/
|
||||
interface TransactionError {
|
||||
/**
|
||||
* The message associated with this error.
|
||||
*/
|
||||
val message: String
|
||||
}
|
|
@ -8,9 +8,38 @@ import kotlinx.coroutines.flow.Flow
|
|||
* Repository of wallet transactions, providing an agnostic interface to the underlying information.
|
||||
*/
|
||||
interface TransactionRepository {
|
||||
|
||||
/**
|
||||
* The last height scanned by this repository.
|
||||
*
|
||||
* @return the last height scanned by this repository.
|
||||
*/
|
||||
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
|
||||
|
||||
/**
|
||||
* 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?
|
||||
|
||||
/**
|
||||
* 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?
|
||||
|
||||
/**
|
||||
|
@ -23,7 +52,10 @@ interface TransactionRepository {
|
|||
// Transactions
|
||||
//
|
||||
|
||||
/** A flow of all the inbound confirmed transactions */
|
||||
val receivedTransactions: Flow<PagedList<ConfirmedTransaction>>
|
||||
/** A flow of all the outbound confirmed transactions */
|
||||
val sentTransactions: Flow<PagedList<ConfirmedTransaction>>
|
||||
/** A flow of all the inbound and outbound confirmed transactions */
|
||||
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 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(
|
||||
private val rustBackend: RustBackendWelding,
|
||||
private val repository: TransactionRepository
|
||||
) : TransactionEncoder {
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* 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
|
||||
*/
|
||||
override suspend fun createTransaction(
|
||||
spendingKey: String,
|
||||
|
@ -37,6 +54,10 @@ class WalletTransactionEncoder(
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
override suspend fun isValidShieldedAddress(address: String): Boolean = withContext(IO) {
|
||||
rustBackend.isValidShieldedAddr(address)
|
||||
|
@ -45,6 +66,10 @@ class WalletTransactionEncoder(
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
override suspend fun isValidTransparentAddress(address: String): Boolean = withContext(IO) {
|
||||
rustBackend.isValidTransparentAddr(address)
|
||||
|
@ -54,21 +79,23 @@ class WalletTransactionEncoder(
|
|||
* 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.
|
||||
*
|
||||
* @param value the zatoshi value to send
|
||||
* @param toAddress the destination address
|
||||
* @param memo the memo, which is not augmented in any way
|
||||
* @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 row id in the transactions table that contains the spend transaction
|
||||
* or -1 if it failed
|
||||
* @return the row id in the transactions table that contains the spend transaction or -1 if it
|
||||
* failed.
|
||||
*/
|
||||
private suspend fun createSpend(
|
||||
spendingKey: String,
|
||||
value: Long,
|
||||
zatoshi: Long,
|
||||
toAddress: String,
|
||||
memo: ByteArray? = byteArrayOf(),
|
||||
fromAccountIndex: Int = 0
|
||||
): Long = withContext(IO) {
|
||||
twigTask("creating transaction to spend $value zatoshi to" +
|
||||
twigTask("creating transaction to spend $zatoshi zatoshi to" +
|
||||
" ${toAddress.masked()} with memo $memo") {
|
||||
try {
|
||||
ensureParams((rustBackend as RustBackend).pathParamsDir)
|
||||
|
@ -77,7 +104,7 @@ class WalletTransactionEncoder(
|
|||
fromAccountIndex,
|
||||
spendingKey,
|
||||
toAddress,
|
||||
value,
|
||||
zatoshi,
|
||||
memo
|
||||
)
|
||||
} catch (t: Throwable) {
|
||||
|
@ -166,6 +193,8 @@ class WalletTransactionEncoder(
|
|||
/**
|
||||
* Http client is only used for downloading sapling spend and output params data, which are
|
||||
* necessary for the wallet to scan blocks.
|
||||
*
|
||||
* @return an http client suitable for downloading params data.
|
||||
*/
|
||||
private fun createHttpClient(): OkHttpClient {
|
||||
//TODO: add logging and timeouts
|
||||
|
|
Loading…
Reference in New Issue