forked from hush/hush-android-wallet-sdk
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
396 lines
14 KiB
396 lines
14 KiB
package cash.z.ecc.android.sdk.db.entity
|
|
|
|
import android.text.format.DateUtils
|
|
import androidx.room.ColumnInfo
|
|
import androidx.room.Entity
|
|
import androidx.room.ForeignKey
|
|
import androidx.room.PrimaryKey
|
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
|
import cash.z.ecc.android.sdk.model.Zatoshi
|
|
|
|
//
|
|
// Entities
|
|
//
|
|
|
|
@Entity(
|
|
primaryKeys = ["id_tx"],
|
|
tableName = "transactions",
|
|
foreignKeys = [
|
|
ForeignKey(
|
|
entity = Block::class,
|
|
parentColumns = ["height"],
|
|
childColumns = ["block"]
|
|
)
|
|
]
|
|
)
|
|
data class TransactionEntity(
|
|
@ColumnInfo(name = "id_tx")
|
|
val id: Long?,
|
|
|
|
@ColumnInfo(typeAffinity = ColumnInfo.BLOB, name = "txid")
|
|
val transactionId: ByteArray,
|
|
|
|
@ColumnInfo(name = "tx_index")
|
|
val transactionIndex: Int?,
|
|
|
|
val created: String?,
|
|
|
|
@ColumnInfo(name = "expiry_height")
|
|
val expiryHeight: Int?,
|
|
|
|
@ColumnInfo(name = "block")
|
|
val minedHeight: Int?,
|
|
|
|
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
|
val raw: ByteArray?
|
|
) {
|
|
override fun equals(other: Any?): Boolean {
|
|
if (this === other) return true
|
|
if (other !is TransactionEntity) return false
|
|
|
|
if (id != other.id) return false
|
|
if (!transactionId.contentEquals(other.transactionId)) return false
|
|
if (transactionIndex != other.transactionIndex) return false
|
|
if (created != other.created) return false
|
|
if (expiryHeight != other.expiryHeight) return false
|
|
if (minedHeight != other.minedHeight) return false
|
|
if (raw != null) {
|
|
if (other.raw == null) return false
|
|
if (!raw.contentEquals(other.raw)) return false
|
|
} else if (other.raw != null) return false
|
|
|
|
return true
|
|
}
|
|
|
|
override fun hashCode(): Int {
|
|
var result = id.hashCode()
|
|
result = 31 * result + transactionId.contentHashCode()
|
|
result = 31 * result + (transactionIndex ?: 0)
|
|
result = 31 * result + (created?.hashCode() ?: 0)
|
|
result = 31 * result + (expiryHeight ?: 0)
|
|
result = 31 * result + (minedHeight ?: 0)
|
|
result = 31 * result + (raw?.contentHashCode() ?: 0)
|
|
return result
|
|
}
|
|
}
|
|
|
|
@Entity(tableName = "pending_transactions")
|
|
data class PendingTransactionEntity(
|
|
@PrimaryKey(autoGenerate = true)
|
|
override val id: Long = 0,
|
|
override val toAddress: String = "",
|
|
override val value: Long = -1,
|
|
override val memo: ByteArray? = byteArrayOf(),
|
|
override val accountIndex: Int,
|
|
override val minedHeight: Long = -1,
|
|
override val expiryHeight: Long = -1,
|
|
|
|
override val cancelled: Int = 0,
|
|
override val encodeAttempts: Int = -1,
|
|
override val submitAttempts: Int = -1,
|
|
override val errorMessage: String? = null,
|
|
override val errorCode: Int? = null,
|
|
override val createTime: Long = System.currentTimeMillis(),
|
|
|
|
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
|
override val raw: ByteArray = byteArrayOf(),
|
|
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
|
override val rawTransactionId: ByteArray? = byteArrayOf()
|
|
) : PendingTransaction {
|
|
|
|
val valueZatoshi: Zatoshi
|
|
get() = Zatoshi(value)
|
|
|
|
override fun equals(other: Any?): Boolean {
|
|
if (this === other) return true
|
|
if (other !is PendingTransactionEntity) return false
|
|
|
|
if (id != other.id) return false
|
|
if (toAddress != other.toAddress) return false
|
|
if (value != other.value) return false
|
|
if (memo != null) {
|
|
if (other.memo == null) return false
|
|
if (!memo.contentEquals(other.memo)) return false
|
|
} else if (other.memo != null) return false
|
|
if (accountIndex != other.accountIndex) return false
|
|
if (minedHeight != other.minedHeight) return false
|
|
if (expiryHeight != other.expiryHeight) return false
|
|
if (cancelled != other.cancelled) return false
|
|
if (encodeAttempts != other.encodeAttempts) return false
|
|
if (submitAttempts != other.submitAttempts) return false
|
|
if (errorMessage != other.errorMessage) return false
|
|
if (errorCode != other.errorCode) return false
|
|
if (createTime != other.createTime) return false
|
|
if (!raw.contentEquals(other.raw)) return false
|
|
if (rawTransactionId != null) {
|
|
if (other.rawTransactionId == null) return false
|
|
if (!rawTransactionId.contentEquals(other.rawTransactionId)) return false
|
|
} else if (other.rawTransactionId != null) return false
|
|
|
|
return true
|
|
}
|
|
|
|
override fun hashCode(): Int {
|
|
var result = id.hashCode()
|
|
result = 31 * result + toAddress.hashCode()
|
|
result = 31 * result + value.hashCode()
|
|
result = 31 * result + (memo?.contentHashCode() ?: 0)
|
|
result = 31 * result + accountIndex
|
|
result = 31 * result + minedHeight.hashCode()
|
|
result = 31 * result + expiryHeight.hashCode()
|
|
result = 31 * result + cancelled
|
|
result = 31 * result + encodeAttempts
|
|
result = 31 * result + submitAttempts
|
|
result = 31 * result + (errorMessage?.hashCode() ?: 0)
|
|
result = 31 * result + (errorCode ?: 0)
|
|
result = 31 * result + createTime.hashCode()
|
|
result = 31 * result + raw.contentHashCode()
|
|
result = 31 * result + (rawTransactionId?.contentHashCode() ?: 0)
|
|
return result
|
|
}
|
|
}
|
|
|
|
//
|
|
// Query Objects
|
|
//
|
|
|
|
/**
|
|
* A mined, shielded transaction. Since this is a [MinedTransaction], it represents data
|
|
* on the blockchain.
|
|
*/
|
|
data class ConfirmedTransaction(
|
|
override val id: Long = 0L,
|
|
override val value: Long = 0L,
|
|
override val memo: ByteArray? = ByteArray(0),
|
|
override val noteId: Long = 0L,
|
|
override val blockTimeInSeconds: Long = 0L,
|
|
override val minedHeight: Long = -1,
|
|
override val transactionIndex: Int,
|
|
override val rawTransactionId: ByteArray = ByteArray(0),
|
|
|
|
// properties that differ from received transactions
|
|
val toAddress: String? = null,
|
|
val expiryHeight: Int? = null,
|
|
override val raw: ByteArray? = byteArrayOf()
|
|
) : MinedTransaction, SignedTransaction {
|
|
|
|
val minedBlockHeight
|
|
get() = if (minedHeight == -1L) {
|
|
null
|
|
} else {
|
|
BlockHeight(minedHeight)
|
|
}
|
|
|
|
override fun equals(other: Any?): Boolean {
|
|
if (this === other) return true
|
|
if (other !is ConfirmedTransaction) return false
|
|
|
|
if (id != other.id) return false
|
|
if (value != other.value) return false
|
|
if (memo != null) {
|
|
if (other.memo == null) return false
|
|
if (!memo.contentEquals(other.memo)) return false
|
|
} else if (other.memo != null) return false
|
|
if (noteId != other.noteId) return false
|
|
if (blockTimeInSeconds != other.blockTimeInSeconds) return false
|
|
if (minedHeight != other.minedHeight) return false
|
|
if (transactionIndex != other.transactionIndex) return false
|
|
if (!rawTransactionId.contentEquals(other.rawTransactionId)) return false
|
|
if (toAddress != other.toAddress) return false
|
|
if (expiryHeight != other.expiryHeight) return false
|
|
if (raw != null) {
|
|
if (other.raw == null) return false
|
|
if (!raw.contentEquals(other.raw)) return false
|
|
} else if (other.raw != null) return false
|
|
|
|
return true
|
|
}
|
|
|
|
override fun hashCode(): Int {
|
|
var result = id.hashCode()
|
|
result = 31 * result + value.hashCode()
|
|
result = 31 * result + (memo?.contentHashCode() ?: 0)
|
|
result = 31 * result + noteId.hashCode()
|
|
result = 31 * result + blockTimeInSeconds.hashCode()
|
|
result = 31 * result + minedHeight.hashCode()
|
|
result = 31 * result + transactionIndex
|
|
result = 31 * result + rawTransactionId.contentHashCode()
|
|
result = 31 * result + (toAddress?.hashCode() ?: 0)
|
|
result = 31 * result + (expiryHeight ?: 0)
|
|
result = 31 * result + (raw?.contentHashCode() ?: 0)
|
|
return result
|
|
}
|
|
}
|
|
|
|
val ConfirmedTransaction.valueInZatoshi
|
|
get() = Zatoshi(value)
|
|
|
|
data class EncodedTransaction(val txId: ByteArray, override val raw: ByteArray, val expiryHeight: Long?) :
|
|
SignedTransaction {
|
|
|
|
val expiryBlockHeight
|
|
get() = expiryHeight?.let { BlockHeight(it) }
|
|
|
|
override fun equals(other: Any?): Boolean {
|
|
if (this === other) return true
|
|
if (other !is EncodedTransaction) return false
|
|
|
|
if (!txId.contentEquals(other.txId)) return false
|
|
if (!raw.contentEquals(other.raw)) return false
|
|
if (expiryHeight != other.expiryHeight) return false
|
|
|
|
return true
|
|
}
|
|
|
|
override fun hashCode(): Int {
|
|
var result = txId.contentHashCode()
|
|
result = 31 * result + raw.contentHashCode()
|
|
result = 31 * result + (expiryHeight?.hashCode() ?: 0)
|
|
return result
|
|
}
|
|
}
|
|
|
|
//
|
|
// Transaction Interfaces
|
|
//
|
|
|
|
/**
|
|
* Common interface between confirmed transactions on the blockchain and pending transactions being
|
|
* constructed.
|
|
*/
|
|
interface Transaction {
|
|
val id: Long
|
|
val value: Long
|
|
val memo: ByteArray?
|
|
}
|
|
|
|
/**
|
|
* Interface for anything that's able to provide signed transaction bytes.
|
|
*/
|
|
interface SignedTransaction {
|
|
val raw: ByteArray?
|
|
}
|
|
|
|
/**
|
|
* Parent type for transactions that have been mined. This is useful for putting all transactions in
|
|
* one list for things like history. A mined tx should have all properties, except possibly a memo.
|
|
*/
|
|
interface MinedTransaction : Transaction {
|
|
val minedHeight: Long
|
|
val noteId: Long
|
|
val blockTimeInSeconds: Long
|
|
val transactionIndex: Int
|
|
val rawTransactionId: ByteArray
|
|
}
|
|
|
|
interface PendingTransaction : SignedTransaction, Transaction {
|
|
override val id: Long
|
|
override val value: Long
|
|
override val memo: ByteArray?
|
|
val toAddress: String
|
|
val accountIndex: Int
|
|
val minedHeight: Long // apparently this can be -1 as an uninitialized value
|
|
val expiryHeight: Long // apparently this can be -1 as an uninitialized value
|
|
val cancelled: Int
|
|
val encodeAttempts: Int
|
|
val submitAttempts: Int
|
|
val errorMessage: String?
|
|
val errorCode: Int?
|
|
val createTime: Long
|
|
val rawTransactionId: ByteArray?
|
|
}
|
|
|
|
//
|
|
// Extension-oriented design
|
|
//
|
|
|
|
fun PendingTransaction.isSameTxId(other: MinedTransaction): Boolean {
|
|
return rawTransactionId != null && rawTransactionId!!.contentEquals(other.rawTransactionId)
|
|
}
|
|
|
|
fun PendingTransaction.isSameTxId(other: PendingTransaction): Boolean {
|
|
return rawTransactionId != null && other.rawTransactionId != null &&
|
|
rawTransactionId!!.contentEquals(other.rawTransactionId!!)
|
|
}
|
|
|
|
fun PendingTransaction.hasRawTransactionId(): Boolean {
|
|
return rawTransactionId != null && (rawTransactionId?.isNotEmpty() == true)
|
|
}
|
|
|
|
fun PendingTransaction.isCreating(): Boolean {
|
|
return (raw?.isEmpty() != false) && submitAttempts <= 0 && !isFailedSubmit() && !isFailedEncoding()
|
|
}
|
|
|
|
fun PendingTransaction.isCreated(): Boolean {
|
|
return (raw?.isEmpty() == false) && submitAttempts <= 0 && !isFailedSubmit() && !isFailedEncoding()
|
|
}
|
|
|
|
fun PendingTransaction.isFailedEncoding(): Boolean {
|
|
return (raw?.isEmpty() != false) && encodeAttempts > 0
|
|
}
|
|
|
|
fun PendingTransaction.isFailedSubmit(): Boolean {
|
|
return errorMessage != null || (errorCode != null && errorCode!! < 0)
|
|
}
|
|
|
|
fun PendingTransaction.isFailure(): Boolean {
|
|
return isFailedEncoding() || isFailedSubmit()
|
|
}
|
|
|
|
fun PendingTransaction.isCancelled(): Boolean {
|
|
return cancelled > 0
|
|
}
|
|
|
|
fun PendingTransaction.isMined(): Boolean {
|
|
return minedHeight > 0
|
|
}
|
|
|
|
fun PendingTransaction.isSubmitted(): Boolean {
|
|
return submitAttempts > 0
|
|
}
|
|
|
|
fun PendingTransaction.isExpired(latestHeight: BlockHeight?, saplingActivationHeight: BlockHeight): Boolean {
|
|
// TODO: test for off-by-one error here. Should we use <= or <
|
|
if (latestHeight == null || latestHeight.value < saplingActivationHeight.value || expiryHeight < saplingActivationHeight.value) return false
|
|
return expiryHeight < latestHeight.value
|
|
}
|
|
|
|
// if we don't have info on a pendingtx after 100 blocks then it's probably safe to stop polling!
|
|
fun PendingTransaction.isLongExpired(latestHeight: BlockHeight?, saplingActivationHeight: BlockHeight): Boolean {
|
|
if (latestHeight == null || latestHeight.value < saplingActivationHeight.value || expiryHeight < saplingActivationHeight.value) return false
|
|
return (latestHeight.value - expiryHeight) > 100
|
|
}
|
|
|
|
fun PendingTransaction.isMarkedForDeletion(): Boolean {
|
|
return rawTransactionId == null && (errorCode ?: 0) == -9090
|
|
}
|
|
|
|
fun PendingTransaction.isSafeToDiscard(): Boolean {
|
|
// invalid dates shouldn't happen or should be temporary
|
|
if (createTime < 0) return false
|
|
|
|
val age = System.currentTimeMillis() - createTime
|
|
val smallThreshold = 30 * DateUtils.MINUTE_IN_MILLIS
|
|
val hugeThreshold = 30 * DateUtils.DAY_IN_MILLIS
|
|
return when {
|
|
// if it is mined, then it is not pending so it can be deleted fairly quickly from this db
|
|
isMined() && age > smallThreshold -> true
|
|
// if a tx fails to encode, then there's not much we can do with it
|
|
isFailedEncoding() && age > smallThreshold -> true
|
|
// don't delete failed submissions until they've been cleaned up, properly, or else we lose
|
|
// the ability to remove them in librustzcash prior to expiration
|
|
isFailedSubmit() && isMarkedForDeletion() -> true
|
|
!isMined() && age > hugeThreshold -> true
|
|
else -> false
|
|
}
|
|
}
|
|
|
|
fun PendingTransaction.isPending(currentHeight: BlockHeight?): Boolean {
|
|
// not mined and not expired and successfully created
|
|
return !isSubmitSuccess() && minedHeight == -1L &&
|
|
(expiryHeight == -1L || expiryHeight > (currentHeight?.value ?: 0L)) && raw != null
|
|
}
|
|
|
|
fun PendingTransaction.isSubmitSuccess(): Boolean {
|
|
return submitAttempts > 0 && (errorCode != null && errorCode!! >= 0) && errorMessage == null
|
|
}
|
|
|