diff --git a/CHANGELOG.md b/CHANGELOG.md index b3419e1c..48469782 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ Change Log - `Synchronizer.shieldFunds` now takes a `UnifiedSpendingKey` instead of separately encoded Sapling and transparent keys. - `Synchronizer` methods that previously took an `Int` for account index now take an `Account` object + - `Synchronizer.sendToAddress()` and `Synchronizer.shieldFunds()` return flows that can now be collected multiple times. Prior versions of the SDK had a bug that could submit transactions multiple times if the flow was collected more than once. ### Removed - `cash.z.ecc.android.sdk`: diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt index 686ccc75..4f9c3db2 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt @@ -71,13 +71,16 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -627,41 +630,57 @@ class SdkSynchronizer internal constructor( override suspend fun getTransparentAddress(account: Account): String = processor.getTransparentAddress(account) - override suspend fun sendToAddress( + override fun sendToAddress( usk: UnifiedSpendingKey, amount: Zatoshi, toAddress: String, memo: String ): Flow { - // Emit the placeholder transaction, then switch to monitoring the database - val placeHolderTx = txManager.initSpend(amount, TransactionRecipient.Address(toAddress), memo, usk.account) + // Using a job to ensure that even if the flow is collected multiple times, the transaction is only submitted + // once + val deferred = coroutineScope.async { + // Emit the placeholder transaction, then switch to monitoring the database + val placeHolderTx = txManager.initSpend(amount, TransactionRecipient.Address(toAddress), memo, usk.account) - txManager.encode(usk, placeHolderTx).let { encodedTx -> - txManager.submit(encodedTx) + txManager.encode(usk, placeHolderTx).let { encodedTx -> + txManager.submit(encodedTx) + } + + placeHolderTx.id } - return txManager.monitorById(placeHolderTx.id) + return flow { + val placeHolderTxId = deferred.await() + emitAll(txManager.monitorById(placeHolderTxId)) + } } - override suspend fun shieldFunds( + override fun shieldFunds( usk: UnifiedSpendingKey, memo: String ): Flow { twig("Initializing shielding transaction") - val tAddr = processor.getTransparentAddress(usk.account) - val tBalance = processor.getUtxoCacheBalance(tAddr) + val deferred = coroutineScope.async { + val tAddr = processor.getTransparentAddress(usk.account) + val tBalance = processor.getUtxoCacheBalance(tAddr) - // Emit the placeholder transaction, then switch to monitoring the database - val placeHolderTx = txManager.initSpend( - tBalance.available, - TransactionRecipient.Account(usk.account), - memo, - usk.account - ) - val encodedTx = txManager.encode("", usk, placeHolderTx) - txManager.submit(encodedTx) + // Emit the placeholder transaction, then switch to monitoring the database + val placeHolderTx = txManager.initSpend( + tBalance.available, + TransactionRecipient.Account(usk.account), + memo, + usk.account + ) + val encodedTx = txManager.encode("", usk, placeHolderTx) + txManager.submit(encodedTx) - return txManager.monitorById(placeHolderTx.id) + placeHolderTx.id + } + + return flow { + val placeHolderTxId = deferred.await() + emitAll(txManager.monitorById(placeHolderTxId)) + } } override suspend fun refreshUtxos(tAddr: String, since: BlockHeight): Int? { diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt index 49634122..9750cd8f 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt @@ -239,14 +239,14 @@ interface Synchronizer { * useful for updating the UI without needing to poll. Of course, polling is always an option * for any wallet that wants to ignore this return value. */ - suspend fun sendToAddress( + fun sendToAddress( usk: UnifiedSpendingKey, amount: Zatoshi, toAddress: String, memo: String = "" ): Flow - suspend fun shieldFunds( + fun shieldFunds( usk: UnifiedSpendingKey, memo: String = ZcashSdk.DEFAULT_SHIELD_FUNDS_MEMO_PREFIX ): Flow