[#748] Internal recipients for pending transactions
This commit is contained in:
parent
f169b112ef
commit
9af6986e43
|
@ -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)
|
||||
|
|
|
@ -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!!
|
||||
|
|
|
@ -49,7 +49,7 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
|
|||
network,
|
||||
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
|
||||
seed = seed,
|
||||
birthday = null
|
||||
birthday = sharedViewModel.birthdayHeight.value
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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(' ')
|
||||
|
|
|
@ -65,7 +65,7 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
|
|||
network,
|
||||
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
|
||||
seed = seed,
|
||||
birthday = null
|
||||
birthday = sharedViewModel.birthdayHeight.value
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
|
|||
alias = "Demo_Utxos",
|
||||
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
|
||||
seed = seed,
|
||||
birthday = null
|
||||
birthday = sharedViewModel.birthdayHeight.value
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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") }
|
||||
|
|
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue