[#748] Internal recipients for pending transactions

This commit is contained in:
Carter Jernigan 2022-10-26 21:37:40 -04:00 committed by GitHub
parent f169b112ef
commit 9af6986e43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 409 additions and 106 deletions

View File

@ -30,11 +30,9 @@ import com.google.android.material.navigation.NavigationView
@Suppress("TooManyFunctions")
class MainActivity :
AppCompatActivity(),
ClipboardManager.OnPrimaryClipChangedListener,
DrawerLayout.DrawerListener {
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var clipboard: ClipboardManager
private var clipboardListener: ((String?) -> Unit)? = null
var fabListener: BaseDemoFragment<out ViewBinding>? = null
/**
@ -49,7 +47,6 @@ class MainActivity :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.addPrimaryClipChangedListener(this)
setContentView(R.layout.activity_main)
val toolbar: Toolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar)
@ -136,19 +133,6 @@ class MainActivity :
}
}
override fun onPrimaryClipChanged() {
clipboardListener?.invoke(getClipboardText())
}
fun setClipboardListener(block: (String?) -> Unit) {
clipboardListener = block
block(getClipboardText())
}
fun removeClipboardListener() {
clipboardListener = null
}
fun hideKeyboard() {
val windowToken = window.decorView.rootView.windowToken
getSystemService<InputMethodManager>()?.hideSoftInputFromWindow(windowToken, 0)

View File

@ -1,21 +1,38 @@
package cash.z.ecc.android.sdk.demoapp
import androidx.lifecycle.ViewModel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.sdk.demoapp.util.fromResources
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.ZcashNetwork
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.runBlocking
/**
* Shared mutable state for the demo
*/
class SharedViewModel : ViewModel() {
class SharedViewModel(application: Application) : AndroidViewModel(application) {
private val _seedPhrase = MutableStateFlow(DemoConstants.INITIAL_SEED_WORDS)
private val _blockHeight = MutableStateFlow<BlockHeight?>(
runBlocking {
BlockHeight.ofLatestCheckpoint(
getApplication(),
ZcashNetwork.fromResources(application)
)
}
)
// publicly, this is read-only
val seedPhrase: StateFlow<String> get() = _seedPhrase
// publicly, this is read-only
val birthdayHeight: StateFlow<BlockHeight?> get() = _blockHeight
fun updateSeedPhrase(newPhrase: String?): Boolean {
return if (isValidSeedPhrase(newPhrase)) {
_seedPhrase.value = newPhrase!!

View File

@ -49,7 +49,7 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
network,
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
seed = seed,
birthday = null
birthday = sharedViewModel.birthdayHeight.value
)
}

View File

@ -32,7 +32,6 @@ class HomeFragment : BaseDemoFragment<FragmentHomeBinding>() {
override fun onResume() {
super.onResume()
mainActivity()?.setClipboardListener(::updatePasteButton)
lifecycleScope.launch {
sharedViewModel.seedPhrase.collect {
@ -43,7 +42,6 @@ class HomeFragment : BaseDemoFragment<FragmentHomeBinding>() {
override fun onPause() {
super.onPause()
mainActivity()?.removeClipboardListener()
}
@Suppress("UNUSED_PARAMETER")
@ -95,17 +93,6 @@ class HomeFragment : BaseDemoFragment<FragmentHomeBinding>() {
}
}
private fun updatePasteButton(clipboardText: String? = mainActivity()?.getClipboardText()) {
clipboardText.let {
val isEditing = binding.groupEdit.visibility == View.VISIBLE
if (isEditing && (it != null && it.split(' ').size > 2)) {
binding.buttonPaste.visibility = View.VISIBLE
} else {
binding.buttonPaste.visibility = View.GONE
}
}
}
private fun String.toAbbreviatedPhrase(): String {
this.trim().apply {
val firstSpace = indexOf(' ')

View File

@ -65,7 +65,7 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
network,
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
seed = seed,
birthday = null
birthday = sharedViewModel.birthdayHeight.value
)
}

View File

@ -72,7 +72,7 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
alias = "Demo_Utxos",
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
seed = seed,
birthday = null
birthday = sharedViewModel.birthdayHeight.value
)
}

View File

@ -76,7 +76,7 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
network,
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
seed = seed,
birthday = null
birthday = sharedViewModel.birthdayHeight.value
)
spendingKey = runBlocking {
DerivationTool.deriveUnifiedSpendingKey(
@ -187,15 +187,14 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
@Suppress("ComplexMethod")
private fun onPendingTxUpdated(pendingTransaction: PendingTransaction?) {
val id = pendingTransaction?.id ?: -1
val message = when {
pendingTransaction == null -> "Transaction not found"
pendingTransaction.isMined() -> "Transaction Mined (id: $id)!\n\nSEND COMPLETE".also { isSending = false }
pendingTransaction.isMined() -> "Transaction Mined!\n\nSEND COMPLETE".also { isSending = false }
pendingTransaction.isSubmitSuccess() -> "Successfully submitted transaction!\nAwaiting confirmation..."
pendingTransaction.isFailedEncoding() ->
"ERROR: failed to encode transaction! (id: $id)".also { isSending = false }
"ERROR: failed to encode transaction!".also { isSending = false }
pendingTransaction.isFailedSubmit() ->
"ERROR: failed to submit transaction! (id: $id)".also { isSending = false }
"ERROR: failed to submit transaction!".also { isSending = false }
pendingTransaction.isCreated() -> "Transaction creation complete! (id: $id)"
pendingTransaction.isCreating() -> "Creating transaction!".also { onResetInfo() }
else -> "Transaction updated!".also { twig("Unhandled TX state: $pendingTransaction") }

View File

@ -0,0 +1,130 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "5152277ebe83665392b173731792ccc8",
"entities": [
{
"tableName": "pending_transactions",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `to_address` TEXT, `to_internal_account_index` INTEGER, `value` INTEGER NOT NULL, `fee` INTEGER, `memo` BLOB, `sent_from_account_index` INTEGER NOT NULL, `mined_height` INTEGER NOT NULL, `expiry_height` INTEGER NOT NULL, `cancelled` INTEGER NOT NULL, `encode_attempts` INTEGER NOT NULL, `submit_attempts` INTEGER NOT NULL, `error_message` TEXT, `error_code` INTEGER, `create_time` INTEGER NOT NULL, `raw` BLOB NOT NULL, `raw_transaction_id` BLOB)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "toAddress",
"columnName": "to_address",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "toInternalAccountIndex",
"columnName": "to_internal_account_index",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "fee",
"columnName": "fee",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "memo",
"columnName": "memo",
"affinity": "BLOB",
"notNull": false
},
{
"fieldPath": "sentFromAccountIndex",
"columnName": "sent_from_account_index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minedHeight",
"columnName": "mined_height",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "expiryHeight",
"columnName": "expiry_height",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "cancelled",
"columnName": "cancelled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "encodeAttempts",
"columnName": "encode_attempts",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "submitAttempts",
"columnName": "submit_attempts",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "errorMessage",
"columnName": "error_message",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "errorCode",
"columnName": "error_code",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "createTime",
"columnName": "create_time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "raw",
"columnName": "raw",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "rawTransactionId",
"columnName": "raw_transaction_id",
"affinity": "BLOB",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5152277ebe83665392b173731792ccc8')"
]
}
}

View File

@ -14,6 +14,7 @@ import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.FirstClassByteArray
import cash.z.ecc.android.sdk.model.PendingTransaction
import cash.z.ecc.android.sdk.model.TransactionRecipient
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.test.ScopedTest
@ -99,7 +100,12 @@ class PersistentTransactionManagerTest : ScopedTest() {
@Test
fun testAbort() = runBlocking {
var tx: PendingTransaction? = manager.initSpend(Zatoshi(1234), "a", "b", Account.DEFAULT)
var tx: PendingTransaction? = manager.initSpend(
Zatoshi(1234),
TransactionRecipient.Address("a"),
"b",
Account.DEFAULT
)
assertNotNull(tx)
manager.abort(tx)
tx = manager.findById(tx.id)

View File

@ -46,6 +46,7 @@ import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
import cash.z.ecc.android.sdk.model.PendingTransaction
import cash.z.ecc.android.sdk.model.TransactionOverview
import cash.z.ecc.android.sdk.model.TransactionRecipient
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
@ -81,6 +82,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import java.io.File
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
@ -626,7 +628,7 @@ class SdkSynchronizer internal constructor(
memo: String
): Flow<PendingTransaction> {
// Emit the placeholder transaction, then switch to monitoring the database
val placeHolderTx = txManager.initSpend(amount, toAddress, memo, usk.account)
val placeHolderTx = txManager.initSpend(amount, TransactionRecipient.Address(toAddress), memo, usk.account)
txManager.encode(usk, placeHolderTx).let { encodedTx ->
txManager.submit(encodedTx)
@ -642,10 +644,14 @@ class SdkSynchronizer internal constructor(
twig("Initializing shielding transaction")
val tAddr = processor.getTransparentAddress(usk.account)
val tBalance = processor.getUtxoCacheBalance(tAddr)
val zAddr = getCurrentAddress(usk.account)
// Emit the placeholder transaction, then switch to monitoring the database
val placeHolderTx = txManager.initSpend(tBalance.available, zAddr, memo, usk.account)
val placeHolderTx = txManager.initSpend(
tBalance.available,
TransactionRecipient.Account(usk.account),
memo,
usk.account
)
val encodedTx = txManager.encode("", usk, placeHolderTx)
txManager.submit(encodedTx)
@ -767,12 +773,12 @@ internal object DefaultSynchronizerFactory {
): DerivedDataRepository =
DbDerivedDataRepository(DerivedDataDb.new(context, rustBackend, zcashNetwork, checkpoint, seed, viewingKeys))
internal fun defaultCompactBlockRepository(context: Context, rustBackend: RustBackend, zcashNetwork: ZcashNetwork):
internal fun defaultCompactBlockRepository(context: Context, cacheDbFile: File, zcashNetwork: ZcashNetwork):
CompactBlockRepository =
DbCompactBlockRepository.new(
context,
zcashNetwork,
rustBackend.cacheDbFile
cacheDbFile
)
fun defaultService(context: Context, lightWalletEndpoint: LightWalletEndpoint): LightWalletService =

View File

@ -507,6 +507,8 @@ interface Synchronizer {
birthday ?: zcashNetwork.saplingActivationHeight
)
val coordinator = DatabaseCoordinator.getInstance(context)
val rustBackend = DefaultSynchronizerFactory.defaultRustBackend(
applicationContext,
zcashNetwork,
@ -515,6 +517,12 @@ interface Synchronizer {
saplingParamTool
)
val blockStore = DefaultSynchronizerFactory.defaultCompactBlockRepository(
applicationContext,
coordinator.cacheDbFile(zcashNetwork, alias),
zcashNetwork
)
val viewingKeys = seed?.let {
DerivationTool.deriveUnifiedFullViewingKeys(
seed,
@ -532,11 +540,6 @@ interface Synchronizer {
viewingKeys
)
val blockStore = DefaultSynchronizerFactory.defaultCompactBlockRepository(
applicationContext,
rustBackend,
zcashNetwork
)
val service = DefaultSynchronizerFactory.defaultService(applicationContext, lightWalletEndpoint)
val encoder = DefaultSynchronizerFactory.defaultEncoder(rustBackend, saplingParamTool, repository)
val downloader = DefaultSynchronizerFactory.defaultDownloader(service, blockStore)

View File

@ -1013,7 +1013,7 @@ class CompactBlockProcessor internal constructor(
suspend fun getLastScannedHeight() =
repository.lastScannedHeight()
// TODO(str4d): CompactBlockProcessor is the wrong place for this, but it's where all the other APIs that need
// CompactBlockProcessor is the wrong place for this, but it's where all the other APIs that need
// access to the RustBackend live. This should be refactored.
internal suspend fun createAccount(seed: ByteArray): UnifiedSpendingKey =
rustBackend.createAccount(seed)

View File

@ -8,6 +8,8 @@ import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.RoomDatabase
import androidx.room.Update
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import kotlinx.coroutines.flow.Flow
//
@ -24,11 +26,67 @@ import kotlinx.coroutines.flow.Flow
entities = [
PendingTransactionEntity::class
],
version = 1,
version = 2,
exportSchema = true
)
abstract class PendingTransactionDb : RoomDatabase() {
internal abstract class PendingTransactionDb : RoomDatabase() {
abstract fun pendingTransactionDao(): PendingTransactionDao
companion object {
/*
* Non-automatic migration required because to_address became nullable.
*/
internal val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
ALTER TABLE pending_transactions RENAME TO pending_transactions_old;
CREATE TABLE pending_transactions(
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
to_address TEXT,
to_internal_account_index INTEGER,
sent_from_account_index INTEGER NOT NULL,
mined_height INTEGER,
expiry_height INTEGER,
cancelled INTEGER,
encode_attempts INTEGER DEFAULT (0),
error_message TEXT,
error_code INTEGER,
submit_attempts INTEGER DEFAULT (0),
create_time INTEGER,
txid BLOB,
value INTEGER NOT NULL,
raw BLOB,
memo BLOB,
fee INTEGER
);
INSERT INTO pending_transactions
SELECT
id,
toAddress,
NULL,
accountIndex,
minedHeight,
expiryHeight,
cancelled,
encodeAttempts,
errorMessage,
errorCode,
submitAttempts,
createTime,
txid,
value,
raw,
memo,
NULL
FROM pending_transactions_old;
DROP TABLE pending_transactions_old
"""
)
}
}
}
}
//
@ -40,7 +98,7 @@ abstract class PendingTransactionDb : RoomDatabase() {
*/
@Dao
@Suppress("TooManyFunctions")
interface PendingTransactionDao {
internal interface PendingTransactionDao {
@Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun create(transaction: PendingTransactionEntity): Long
@ -56,7 +114,7 @@ interface PendingTransactionDao {
@Query("SELECT * FROM pending_transactions WHERE id = :id")
suspend fun findById(id: Long): PendingTransactionEntity?
@Query("SELECT * FROM pending_transactions ORDER BY createTime")
@Query("SELECT * FROM pending_transactions ORDER BY create_time")
fun getAll(): Flow<List<PendingTransactionEntity>>
@Query("SELECT * FROM pending_transactions WHERE id = :id")
@ -66,24 +124,24 @@ interface PendingTransactionDao {
// Update helper functions
//
@Query("UPDATE pending_transactions SET rawTransactionId = null WHERE id = :id")
@Query("UPDATE pending_transactions SET raw_transaction_id = null WHERE id = :id")
suspend fun removeRawTransactionId(id: Long)
@Query("UPDATE pending_transactions SET minedHeight = :minedHeight WHERE id = :id")
@Query("UPDATE pending_transactions SET mined_height = :minedHeight WHERE id = :id")
suspend fun updateMinedHeight(id: Long, minedHeight: Long)
@Query(
"UPDATE pending_transactions SET raw = :raw, rawTransactionId = :rawTransactionId," +
" expiryHeight = :expiryHeight WHERE id = :id"
"UPDATE pending_transactions SET raw = :raw, raw_transaction_id = :rawTransactionId," +
" expiry_height = :expiryHeight WHERE id = :id"
)
suspend fun updateEncoding(id: Long, raw: ByteArray, rawTransactionId: ByteArray, expiryHeight: Long?)
@Query("UPDATE pending_transactions SET errorMessage = :errorMessage, errorCode = :errorCode WHERE id = :id")
@Query("UPDATE pending_transactions SET error_message = :errorMessage, error_code = :errorCode WHERE id = :id")
suspend fun updateError(id: Long, errorMessage: String?, errorCode: Int?)
@Query("UPDATE pending_transactions SET encodeAttempts = :attempts WHERE id = :id")
@Query("UPDATE pending_transactions SET encode_attempts = :attempts WHERE id = :id")
suspend fun updateEncodeAttempts(id: Long, attempts: Int)
@Query("UPDATE pending_transactions SET submitAttempts = :attempts WHERE id = :id")
@Query("UPDATE pending_transactions SET submit_attempts = :attempts WHERE id = :id")
suspend fun updateSubmitAttempts(id: Long, attempts: Int)
}

View File

@ -3,41 +3,68 @@ package cash.z.ecc.android.sdk.internal.db.pending
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.FirstClassByteArray
import cash.z.ecc.android.sdk.model.PendingTransaction
import cash.z.ecc.android.sdk.model.TransactionRecipient
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork
@Entity(tableName = "pending_transactions")
data class PendingTransactionEntity(
internal data class PendingTransactionEntity(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val toAddress: String,
@ColumnInfo(name = "to_address")
val toAddress: String?,
@ColumnInfo(name = "to_internal_account_index")
val toInternalAccountIndex: Int?,
val value: Long,
val fee: Long?,
val memo: ByteArray?,
val accountIndex: Int,
@ColumnInfo(name = "sent_from_account_index")
val sentFromAccountIndex: Int,
@ColumnInfo(name = "mined_height")
val minedHeight: Long = NO_BLOCK_HEIGHT,
@ColumnInfo(name = "expiry_height")
val expiryHeight: Long = NO_BLOCK_HEIGHT,
val cancelled: Int = 0,
@ColumnInfo(name = "encode_attempts")
val encodeAttempts: Int = -1,
@ColumnInfo(name = "submit_attempts")
val submitAttempts: Int = -1,
@ColumnInfo(name = "error_message")
val errorMessage: String? = null,
@ColumnInfo(name = "error_code")
val errorCode: Int? = null,
@ColumnInfo(name = "create_time")
val createTime: Long = System.currentTimeMillis(),
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
val raw: ByteArray = byteArrayOf(),
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
@ColumnInfo(name = "raw_transaction_id", typeAffinity = ColumnInfo.BLOB)
val rawTransactionId: ByteArray? = byteArrayOf()
) {
init {
require(
(null != toAddress && null == toInternalAccountIndex) ||
(null == toAddress && null != toInternalAccountIndex)
) {
"PendingTransaction cannot contain both a toAddress and internal account"
}
}
fun toPendingTransaction(zcashNetwork: ZcashNetwork) = PendingTransaction(
id = id,
value = Zatoshi(value),
fee = fee?.let { Zatoshi(it) },
memo = memo?.let { FirstClassByteArray(it) },
raw = FirstClassByteArray(raw),
toAddress = toAddress,
accountIndex = accountIndex,
recipient = TransactionRecipient.new(
toAddress,
toInternalAccountIndex?.let { Account(toInternalAccountIndex) }
),
sentFromAccount = Account(sentFromAccountIndex),
minedHeight = if (minedHeight == NO_BLOCK_HEIGHT) {
null
} else {
@ -66,12 +93,14 @@ data class PendingTransactionEntity(
if (id != other.id) return false
if (toAddress != other.toAddress) return false
if (toInternalAccountIndex != other.toInternalAccountIndex) return false
if (value != other.value) return false
if (fee != other.fee) 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 (sentFromAccountIndex != other.sentFromAccountIndex) return false
if (minedHeight != other.minedHeight) return false
if (expiryHeight != other.expiryHeight) return false
if (cancelled != other.cancelled) return false
@ -91,10 +120,12 @@ data class PendingTransactionEntity(
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + toAddress.hashCode()
result = 31 * result + (toAddress?.hashCode() ?: 0)
result = 31 * result + (toInternalAccountIndex ?: 0)
result = 31 * result + value.hashCode()
result = 31 * result + fee.hashCode()
result = 31 * result + (memo?.contentHashCode() ?: 0)
result = 31 * result + accountIndex
result = 31 * result + sentFromAccountIndex
result = 31 * result + minedHeight.hashCode()
result = 31 * result + expiryHeight.hashCode()
result = 31 * result + cancelled
@ -111,32 +142,72 @@ data class PendingTransactionEntity(
companion object {
const val NO_BLOCK_HEIGHT = -1L
fun from(pendingTransaction: PendingTransaction) = PendingTransactionEntity(
id = pendingTransaction.id,
value = pendingTransaction.value.value,
memo = pendingTransaction.memo?.byteArray,
raw = pendingTransaction.raw.byteArray,
toAddress = pendingTransaction.toAddress,
accountIndex = pendingTransaction.accountIndex,
minedHeight = pendingTransaction.minedHeight?.value ?: NO_BLOCK_HEIGHT,
expiryHeight = pendingTransaction.expiryHeight?.value ?: NO_BLOCK_HEIGHT,
cancelled = pendingTransaction.cancelled,
encodeAttempts = pendingTransaction.encodeAttempts,
submitAttempts = pendingTransaction.submitAttempts,
errorMessage = pendingTransaction.errorMessage,
errorCode = pendingTransaction.errorCode,
createTime = pendingTransaction.createTime,
rawTransactionId = pendingTransaction.rawTransactionId?.byteArray
)
fun from(pendingTransaction: PendingTransaction): PendingTransactionEntity {
val toAddress = if (pendingTransaction.recipient is TransactionRecipient.Address) {
pendingTransaction.recipient.addressValue
} else {
null
}
val toInternal = if (pendingTransaction.recipient is TransactionRecipient.Account) {
pendingTransaction.recipient.accountValue
} else {
null
}
return PendingTransactionEntity(
id = pendingTransaction.id,
value = pendingTransaction.value.value,
fee = pendingTransaction.fee?.value,
memo = pendingTransaction.memo?.byteArray,
raw = pendingTransaction.raw.byteArray,
toAddress = toAddress,
toInternalAccountIndex = toInternal?.value,
sentFromAccountIndex = pendingTransaction.sentFromAccount.value,
minedHeight = pendingTransaction.minedHeight?.value ?: NO_BLOCK_HEIGHT,
expiryHeight = pendingTransaction.expiryHeight?.value ?: NO_BLOCK_HEIGHT,
cancelled = pendingTransaction.cancelled,
encodeAttempts = pendingTransaction.encodeAttempts,
submitAttempts = pendingTransaction.submitAttempts,
errorMessage = pendingTransaction.errorMessage,
errorCode = pendingTransaction.errorCode,
createTime = pendingTransaction.createTime,
rawTransactionId = pendingTransaction.rawTransactionId?.byteArray
)
}
}
}
fun PendingTransactionEntity.isSubmitted(): Boolean {
internal val PendingTransactionEntity.recipient: TransactionRecipient
get() {
return TransactionRecipient.new(toAddress, toInternalAccountIndex?.let { Account(it) })
}
internal fun PendingTransactionEntity.isSubmitted(): Boolean {
return submitAttempts > 0
}
fun PendingTransactionEntity.isFailedEncoding() = raw.isNotEmpty() && encodeAttempts > 0
internal fun PendingTransactionEntity.isFailedEncoding() = raw.isNotEmpty() && encodeAttempts > 0
fun PendingTransactionEntity.isCancelled(): Boolean {
internal fun PendingTransactionEntity.isCancelled(): Boolean {
return cancelled > 0
}
private fun TransactionRecipient.Companion.new(
toAddress: String?,
toInternalAccountIndex: Account?
): TransactionRecipient {
require(
(null != toAddress && null == toInternalAccountIndex) ||
(null == toAddress && null != toInternalAccountIndex)
) {
"Pending transaction cannot contain both a toAddress and internal account"
}
if (null != toAddress) {
return TransactionRecipient.Address(toAddress)
} else if (null != toInternalAccountIndex) {
return TransactionRecipient.Account(toInternalAccountIndex)
}
error("Pending transaction recipient require a toAddress or an internal account")
}

View File

@ -2,6 +2,7 @@ package cash.z.ecc.android.sdk.internal.transaction
import android.content.Context
import androidx.room.RoomDatabase
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.internal.db.commonDatabaseBuilder
import cash.z.ecc.android.sdk.internal.db.pending.PendingTransactionDao
import cash.z.ecc.android.sdk.internal.db.pending.PendingTransactionDb
@ -9,11 +10,13 @@ import cash.z.ecc.android.sdk.internal.db.pending.PendingTransactionEntity
import cash.z.ecc.android.sdk.internal.db.pending.isCancelled
import cash.z.ecc.android.sdk.internal.db.pending.isFailedEncoding
import cash.z.ecc.android.sdk.internal.db.pending.isSubmitted
import cash.z.ecc.android.sdk.internal.db.pending.recipient
import cash.z.ecc.android.sdk.internal.service.LightWalletService
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.PendingTransaction
import cash.z.ecc.android.sdk.model.TransactionRecipient
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork
@ -60,16 +63,30 @@ internal class PersistentTransactionManager(
override suspend fun initSpend(
zatoshi: Zatoshi,
toAddress: String,
recipient: TransactionRecipient,
memo: String,
account: Account
): PendingTransaction = withContext(Dispatchers.IO) {
twig("constructing a placeholder transaction")
val toAddress = if (recipient is TransactionRecipient.Address) {
recipient.addressValue
} else {
null
}
val toInternal = if (recipient is TransactionRecipient.Account) {
recipient.accountValue
} else {
null
}
var tx = PendingTransactionEntity(
toAddress = toAddress,
toInternalAccountIndex = toInternal?.value,
value = zatoshi.value,
fee = ZcashSdk.MINERS_FEE.value,
memo = memo.toByteArray(),
accountIndex = account.value
sentFromAccountIndex = account.value
)
@Suppress("TooGenericExceptionCaught")
try {
@ -108,7 +125,7 @@ internal class PersistentTransactionManager(
val encodedTx = encoder.createTransaction(
usk,
pendingTx.value,
pendingTx.toAddress,
pendingTx.recipient,
pendingTx.memo?.byteArray
)
twig("successfully encoded transaction!")
@ -152,6 +169,7 @@ internal class PersistentTransactionManager(
twig("beginning to encode shielding transaction with : $encoder")
val encodedTx = encoder.createShieldingTransaction(
usk,
tx.recipient,
tx.memo
)
twig("successfully encoded shielding transaction!")
@ -344,7 +362,8 @@ internal class PersistentTransactionManager(
appContext,
PendingTransactionDb::class.java,
databaseFile
).setJournalMode(RoomDatabase.JournalMode.TRUNCATE).build(),
).setJournalMode(RoomDatabase.JournalMode.TRUNCATE)
.addMigrations(PendingTransactionDb.MIGRATION_1_2).build(),
zcashNetwork,
encoder,
service

View File

@ -1,6 +1,7 @@
package cash.z.ecc.android.sdk.internal.transaction
import cash.z.ecc.android.sdk.internal.model.EncodedTransaction
import cash.z.ecc.android.sdk.model.TransactionRecipient
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.Zatoshi
@ -20,7 +21,7 @@ internal interface TransactionEncoder {
suspend fun createTransaction(
usk: UnifiedSpendingKey,
amount: Zatoshi,
toAddress: String,
recipient: TransactionRecipient,
memo: ByteArray? = byteArrayOf()
): EncodedTransaction
@ -32,6 +33,7 @@ internal interface TransactionEncoder {
*/
suspend fun createShieldingTransaction(
usk: UnifiedSpendingKey,
recipient: TransactionRecipient,
memo: ByteArray? = byteArrayOf()
): EncodedTransaction

View File

@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.internal.transaction
import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.PendingTransaction
import cash.z.ecc.android.sdk.model.TransactionRecipient
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.Zatoshi
import kotlinx.coroutines.flow.Flow
@ -19,7 +20,7 @@ interface OutboundTransactionManager {
* completion.
*
* @param zatoshi the amount to spend.
* @param toAddress the address to which funds will be sent.
* @param recipient The destination for the transaction.
* @param memo the optionally blank memo associated with this transaction.
* @param account the account from which to spend funds.
*
@ -27,7 +28,7 @@ interface OutboundTransactionManager {
*/
suspend fun initSpend(
zatoshi: Zatoshi,
toAddress: String,
recipient: TransactionRecipient,
memo: String,
account: Account
): PendingTransaction

View File

@ -8,6 +8,7 @@ import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.internal.twigTask
import cash.z.ecc.android.sdk.jni.RustBackendWelding
import cash.z.ecc.android.sdk.model.TransactionRecipient
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.Zatoshi
@ -41,18 +42,23 @@ internal class WalletTransactionEncoder(
override suspend fun createTransaction(
usk: UnifiedSpendingKey,
amount: Zatoshi,
toAddress: String,
recipient: TransactionRecipient,
memo: ByteArray?
): EncodedTransaction {
val transactionId = createSpend(usk, amount, toAddress, memo)
require(recipient is TransactionRecipient.Address)
val transactionId = createSpend(usk, amount, recipient.addressValue, memo)
return repository.findEncodedTransactionById(transactionId)
?: throw TransactionEncoderException.TransactionNotFoundException(transactionId)
}
override suspend fun createShieldingTransaction(
usk: UnifiedSpendingKey,
recipient: TransactionRecipient,
memo: ByteArray?
): EncodedTransaction {
require(recipient is TransactionRecipient.Account)
val transactionId = createShieldingSpend(usk, memo)
return repository.findEncodedTransactionById(transactionId)
?: throw TransactionEncoderException.TransactionNotFoundException(transactionId)

View File

@ -31,7 +31,7 @@ val LightWalletEndpoint.Companion.Mainnet
val LightWalletEndpoint.Companion.Testnet
get() = LightWalletEndpoint(
"testnet.lightwalletd.com",
"lightwalletd.testnet.electriccoin.co",
DEFAULT_PORT,
isSecure = true
)

View File

@ -5,13 +5,14 @@ package cash.z.ecc.android.sdk.model
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.minutes
data class PendingTransaction(
data class PendingTransaction internal constructor(
val id: Long,
val value: Zatoshi,
val fee: Zatoshi?,
val memo: FirstClassByteArray?,
val raw: FirstClassByteArray,
val toAddress: String,
val accountIndex: Int,
val recipient: TransactionRecipient,
val sentFromAccount: Account,
val minedHeight: BlockHeight?,
val expiryHeight: BlockHeight?,
val cancelled: Int,
@ -23,6 +24,18 @@ data class PendingTransaction(
val rawTransactionId: FirstClassByteArray?
)
sealed class TransactionRecipient {
data class Address(val addressValue: String) : TransactionRecipient() {
override fun toString() = "TransactionRecipient.Address"
}
data class Account(val accountValue: cash.z.ecc.android.sdk.model.Account) : TransactionRecipient() {
override fun toString() = "TransactionRecipient.Account"
}
companion object
}
// Note there are some commented out methods which aren't being removed yet, as they might be needed before the
// Roomoval draft PR is completed
@ -32,7 +45,8 @@ data class PendingTransaction(
// fun PendingTransaction.isSameTxId(other: PendingTransaction) =
// rawTransactionId == other.rawTransactionId
internal fun PendingTransaction.hasRawTransactionId() = rawTransactionId?.byteArray?.isEmpty() == false
internal fun PendingTransaction.hasRawTransactionId() =
rawTransactionId?.byteArray?.isEmpty() == false
fun PendingTransaction.isCreating() =
raw.byteArray.isNotEmpty() && submitAttempts <= 0 && !isFailedSubmit() && !isFailedEncoding()

View File

@ -6,7 +6,7 @@ package cash.z.ecc.android.sdk.model
* Note that both sent and received transactions will have a positive net value. Consumers of this class must
*/
data class TransactionOverview internal constructor(
internal val id: Long,
val id: Long,
val rawId: FirstClassByteArray,
val minedHeight: BlockHeight,
val expiryHeight: BlockHeight,