checkpoint: things are back functional after some heavy re-writing

This commit is contained in:
Kevin Gorham 2019-06-16 00:53:48 -04:00
parent 2e6531c860
commit 25945a5ef9
No known key found for this signature in database
GPG Key ID: ABA38E928749A19E
22 changed files with 403 additions and 229 deletions

View File

@ -6,15 +6,16 @@ import cash.z.android.wallet.BuildConfig
import cash.z.android.wallet.ChipBucket import cash.z.android.wallet.ChipBucket
import cash.z.android.wallet.InMemoryChipBucket import cash.z.android.wallet.InMemoryChipBucket
import cash.z.android.wallet.ZcashWalletApplication import cash.z.android.wallet.ZcashWalletApplication
import cash.z.android.wallet.data.StaticTransactionRepository import cash.z.android.wallet.data.*
import cash.z.android.wallet.extention.toDbPath import cash.z.android.wallet.extention.toDbPath
import cash.z.android.wallet.sample.* import cash.z.android.wallet.sample.CarolWallet
import cash.z.android.wallet.sample.SampleProperties.COMPACT_BLOCK_PORT import cash.z.android.wallet.sample.SampleProperties.COMPACT_BLOCK_PORT
import cash.z.android.wallet.sample.SampleProperties.DEFAULT_BLOCK_POLL_FREQUENCY_MILLIS import cash.z.android.wallet.sample.SampleProperties.DEFAULT_BLOCK_POLL_FREQUENCY_MILLIS
import cash.z.android.wallet.sample.SampleProperties.DEFAULT_SERVER import cash.z.android.wallet.sample.SampleProperties.DEFAULT_SERVER
import cash.z.android.wallet.sample.SampleProperties.DEFAULT_TRANSACTION_POLL_FREQUENCY_MILLIS import cash.z.android.wallet.sample.SampleProperties.DEFAULT_TRANSACTION_POLL_FREQUENCY_MILLIS
import cash.z.android.wallet.sample.SampleProperties.PREFS_SERVER_NAME import cash.z.android.wallet.sample.SampleProperties.PREFS_SERVER_NAME
import cash.z.android.wallet.sample.SampleProperties.PREFS_WALLET_DISPLAY_NAME import cash.z.android.wallet.sample.Servers
import cash.z.android.wallet.sample.WalletConfig
import cash.z.android.wallet.ui.util.Broom import cash.z.android.wallet.ui.util.Broom
import cash.z.wallet.sdk.block.* import cash.z.wallet.sdk.block.*
import cash.z.wallet.sdk.data.* import cash.z.wallet.sdk.data.*
@ -28,6 +29,7 @@ import cash.z.wallet.sdk.service.LightWalletGrpcService
import cash.z.wallet.sdk.service.LightWalletService import cash.z.wallet.sdk.service.LightWalletService
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.android.DispatchingAndroidInjector
import javax.inject.Named import javax.inject.Named
import javax.inject.Singleton import javax.inject.Singleton
@ -168,30 +170,30 @@ internal object SynchronizerModule {
fun provideManager(wallet: Wallet, repository: TransactionRepository, service: LightWalletService): ActiveTransactionManager { fun provideManager(wallet: Wallet, repository: TransactionRepository, service: LightWalletService): ActiveTransactionManager {
return ActiveTransactionManager(repository, service, wallet) return ActiveTransactionManager(repository, service, wallet)
} }
//
@JvmStatic // @JvmStatic
@Provides // @Provides
@Singleton // @Singleton
fun provideSynchronizer( // fun provideSynchronizer(
processor: CompactBlockProcessor, // processor: CompactBlockProcessor,
repository: TransactionRepository, // repository: TransactionRepository,
manager: ActiveTransactionManager, // manager: ActiveTransactionManager,
wallet: Wallet // wallet: Wallet
): Synchronizer { // ): Synchronizer {
return SdkSynchronizer(processor, repository, manager, wallet, DEFAULT_STALE_TOLERANCE) // return SdkSynchronizer(processor, repository, manager, wallet, DEFAULT_STALE_TOLERANCE)
} // }
@JvmStatic @JvmStatic
@Provides @Provides
@Singleton @Singleton
fun provideBroom( fun provideBroom(
service: LightWalletService, sender: TransactionSender,
wallet: Wallet, wallet: Wallet,
rustBackend: RustBackendWelding, rustBackend: RustBackendWelding,
walletConfig: WalletConfig walletConfig: WalletConfig
): Broom { ): Broom {
return Broom( return Broom(
service, sender,
rustBackend, rustBackend,
walletConfig.cacheDbName, walletConfig.cacheDbName,
wallet wallet
@ -204,4 +206,33 @@ internal object SynchronizerModule {
fun provideChipBucket(): ChipBucket { fun provideChipBucket(): ChipBucket {
return InMemoryChipBucket() return InMemoryChipBucket()
} }
@JvmStatic
@Provides
@Singleton
fun provideTransactionManager(): TransactionManager {
return PersistentTransactionManager()
}
@JvmStatic
@Provides
@Singleton
fun provideTransactionSender(manager: TransactionManager, service: LightWalletService): TransactionSender {
return PersistentTransactionMonitor(manager, service)
}
@JvmStatic
@Provides
@Singleton
fun provideTransactionEncoder(wallet: Wallet, repository: TransactionRepository): RawTransactionEncoder {
return WalletTransactionEncoder(wallet, repository)
}
@JvmStatic
@Provides
@Singleton
fun provideDataSynchronizer(wallet: Wallet, encoder: RawTransactionEncoder, sender: TransactionSender) : DataSyncronizer {
return StableSynchronizer(wallet, encoder, sender)
}
} }

View File

@ -0,0 +1,35 @@
package cash.z.android.wallet.data
import cash.z.wallet.sdk.data.twig
import cash.z.wallet.sdk.service.LightWalletService
class PersistentTransactionManager: TransactionManager {
init {
}
override suspend fun manageCreation(encoder: RawTransactionEncoder, value: Long, toAddress: String, memo: String) {
twig("managing the creation of a transaction")
encoder.create(value, toAddress, memo)
}
override suspend fun manageSubmission(service: LightWalletService, rawTransaction: ByteArray) {
try {
// TODO: stuff before you send
twig("managing the preparation to submit transaction")
val response = service.submitTransaction(rawTransaction)
twig("management of submit transaction completed with response: ${response.errorCode}: ${response.errorMessage}")
// TODO: stuff after sending
if (response.errorCode < 0) {
} else {
}
} catch (t: Throwable) {
twig("error while managing submitting transaction: ${t.message}")
}
}
override suspend fun getAllPending(): List<ByteArray> {
return listOf()
}
}

View File

@ -1,51 +1,31 @@
package cash.z.android.wallet.data package cash.z.android.wallet.data
import cash.z.wallet.sdk.data.twig
import cash.z.wallet.sdk.service.LightWalletService import cash.z.wallet.sdk.service.LightWalletService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.channels.actor import kotlinx.coroutines.channels.actor
import kotlinx.coroutines.launch
class PersistentTransactionSender(
private val manager: TransactionManager,
private val factory: RawTransactionFactory
) : TransactionSender {
/**
* Generates newly persisted information about a transaction so that other processes can send.
*/
override fun sendToAddress(zatoshi: Long, toAddress: String, memo: String, fromAccountId: Int) {
val txId = manager.new() // creates a new transaction with a lifecycle of CREATING
try {
val rawTransaction = factory.create(zatoshi, toAddress, memo)
manager.setRawTransaction(txId, rawTransaction) // returns new transaction with lifecycle of CREATED
} catch (t: Throwable) {
manager.setCreationError(txId, t.message.toTxError())
return
}
}
}
class PersistentTransactionMonitor ( class PersistentTransactionMonitor (
private val scope: CoroutineScope,
private val manager: TransactionManager, private val manager: TransactionManager,
private val service: LightWalletService private val service: LightWalletService
) { ) : TransactionSender {
private val channel: SendChannel<TransactionUpdateRequest> private lateinit var channel: SendChannel<TransactionUpdateRequest>
private var monitoringJob: Job? = null
private val initialMonitorDelay = 45_000L
init { fun CoroutineScope.requestUpdate() = launch {
channel = scope.startActor() if (!channel.isClosedForSend) {
} channel.send(SubmitPending)
}
fun update() = scope.launch {
channel.send(SubmitPending)
} }
/** /**
* Start an actor that listens for signals about what to do with transactions. This actor's lifespan is within the * 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. * provided [scope] and it will live until the scope is cancelled.
*/ */
fun CoroutineScope.startActor() = actor<TransactionUpdateRequest> { private fun CoroutineScope.startActor() = actor<TransactionUpdateRequest> {
var pendingTransactionDao = 0 // actor state: var pendingTransactionDao = 0 // actor state:
for (msg in channel) { // iterate over incoming messages for (msg in channel) { // iterate over incoming messages
when (msg) { when (msg) {
@ -54,27 +34,59 @@ class PersistentTransactionMonitor (
} }
} }
private fun submitPendingTransactions() { private fun CoroutineScope.startMonitor() = launch {
val transactions = manager.getAllPendingRawTransactions().forEach { (txId, rawTransaction) -> while (!channel.isClosedForSend && isActive) {
submitPendingTransaction(txId, rawTransaction) delay(calculateDelay())
requestUpdate()
} }
twig("TransactionMonitor stopping!")
} }
private fun submitPendingTransaction(txId: Long, rawTransaction: ByteArray) { private fun calculateDelay(): Long {
try { return initialMonitorDelay
manager.setSubmissionStarted(txId) }
val response = service.submitTransaction(rawTransaction)
if (response.errorCode < 0) { override fun start(scope: CoroutineScope) {
manager.setSubmissionComplete(txId, false, response.errorMessage.toTxError()) twig("TransactionMonitor starting!")
} else { channel = scope.startActor()
manager.setSubmissionComplete(txId, true) monitoringJob?.cancel()
monitoringJob = scope.startMonitor()
}
override fun stop() {
channel.close()
monitoringJob?.cancel()?.also { monitoringJob = null }
}
/**
* Generates newly persisted information about a transaction so that other processes can send.
*/
override suspend fun sendToAddress(
encoder: RawTransactionEncoder,
zatoshi: Long,
toAddress: String,
memo: String,
fromAccountId: Int
) = withContext(IO) {
manager.manageCreation(encoder, zatoshi, toAddress, memo)
requestUpdate()
Unit
}
/**
* Submit all pending transactions that have not expired.
*/
private suspend fun submitPendingTransactions() = withContext(IO) {
twig("received request to submit pending transactions")
with(manager) {
getAllPending().also { twig("found ${it.size} pending txs to submit") }.forEach { rawTx ->
manageSubmission(service, rawTx)
} }
} catch (t: Throwable) {
manager.setSubmissionComplete(txId, false, t.message.toTxError())
} }
} }
} }
sealed class TransactionUpdateRequest sealed class TransactionUpdateRequest
object SubmitPending : TransactionUpdateRequest() object SubmitPending : TransactionUpdateRequest()
@ -97,4 +109,38 @@ SUBMITTED
INVALID INVALID
** attempting submission ** attempting submission
** attempted submission ** attempted submission
bookkeeper, register, treasurer, mint, ledger
private fun checkTx(transactionId: Long) {
if (transactionId < 0) {
throw SweepException.Creation
} else {
twig("successfully created transaction!")
}
}
private fun checkRawTx(transactionRaw: ByteArray?) {
if (transactionRaw == null) {
throw SweepException.Disappeared
} else {
twig("found raw transaction in the dataDb")
}
}
private fun checkResponse(response: Service.SendResponse) {
if (response.errorCode < 0) {
throw SweepException.IncompletePass(response)
} else {
twig("successfully submitted. error code: ${response.errorCode}")
}
}
sealed class SweepException(val errorMessage: String) : RuntimeException(errorMessage) {
object Creation : SweepException("failed to create raw transaction")
object Disappeared : SweepException("unable to find a matching raw transaction. This means the rust backend said it created a TX but when we looked for it in the DB it was missing!")
class IncompletePass(response: Service.SendResponse) : SweepException("submit failed with error code: ${response.errorCode} and message ${response.errorMessage}")
}
*/ */

View File

@ -0,0 +1,8 @@
package cash.z.android.wallet.data
interface RawTransactionEncoder {
/**
* Creates a raw transaction that is unsigned.
*/
suspend fun create(zatoshi: Long, toAddress: String, memo: String = ""): ByteArray
}

View File

@ -1,8 +0,0 @@
package cash.z.android.wallet.data
interface RawTransactionFactory {
/**
* Creates a raw transaction that is unsigned.
*/
fun create(value: Long, toAddress: String, memo: String = ""): ByteArray
}

View File

@ -1,30 +1,71 @@
package cash.z.android.wallet.data package cash.z.android.wallet.data
import androidx.lifecycle.LifecycleOwner import cash.z.wallet.sdk.data.ActiveTransaction
import cash.z.android.wallet.ui.util.BaseLifecycleObserver import cash.z.wallet.sdk.data.TransactionState
import cash.z.wallet.sdk.data.twig
import cash.z.wallet.sdk.secure.Wallet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.withContext
import javax.inject.Inject
/** /**
* A synchronizer that attempts to remain operational, despite any number of errors that can occur. * A synchronizer that attempts to remain operational, despite any number of errors that can occur.
*
* @param lifecycleOwner the lifecycle to observe while synchronizing
*/ */
class StableSynchronizer( @ExperimentalCoroutinesApi
private val lifecycleOwner: LifecycleOwner, class StableSynchronizer @Inject constructor(
sender: TransactionSender private val wallet: Wallet,
) : BaseLifecycleObserver(), TransactionSender by sender { private val encoder: RawTransactionEncoder,
init { private val sender: TransactionSender
lifecycleOwner.lifecycle.addObserver(this) ) : DataSyncronizer {
private val balanceChannel = ConflatedBroadcastChannel<Wallet.WalletBalance>()
private val progressChannel = ConflatedBroadcastChannel<Int>()
private val pendingChannel = ConflatedBroadcastChannel<List<PendingTransaction>>()
override fun start(scope: CoroutineScope) {
twig("Staring sender!")
sender.start(scope)
} }
// override fun stop() {
// Lifecycle sender.stop()
//
override fun onCreate(owner: LifecycleOwner) {
} }
override fun onDestroy(owner: LifecycleOwner) {
//
// Channels
//
override fun balances(): ReceiveChannel<Wallet.WalletBalance> {
return balanceChannel.openSubscription()
}
override fun progress(): ReceiveChannel<Int> {
return progressChannel.openSubscription()
}
override fun pendingTransactions(): ReceiveChannel<List<PendingTransaction>> {
return pendingChannel.openSubscription()
}
//
// Send / Receive
//
override suspend fun getAddress(accountId: Int): String = withContext(IO) { wallet.getAddress() }
override suspend fun sendToAddress(
zatoshi: Long,
toAddress: String,
memo: String,
fromAccountId: Int
) = withContext(IO) {
sender.sendToAddress(encoder, zatoshi, toAddress, memo, fromAccountId)
} }
@ -34,13 +75,22 @@ class StableSynchronizer(
// override fun transactions(): Flow<WalletTransaction> { // override fun transactions(): Flow<WalletTransaction> {
// } // }
// //
// override fun balance(): Flow<Wallet.WalletBalance> {
// }
//
// override fun progress(): Flow<Int> { // override fun progress(): Flow<Int> {
// } // }
// //
// override fun status(): Flow<FlowSynchronizer.SyncStatus> { // override fun status(): Flow<FlowSynchronizer.SyncStatus> {
// } // }
}
interface DataSyncronizer {
fun start(scope: CoroutineScope)
fun stop()
suspend fun getAddress(accountId: Int = 0): String
suspend fun sendToAddress(zatoshi: Long, toAddress: String, memo: String = "", fromAccountId: Int = 0)
fun balances(): ReceiveChannel<Wallet.WalletBalance>
fun progress(): ReceiveChannel<Int>
fun pendingTransactions(): ReceiveChannel<List<PendingTransaction>>
} }

View File

@ -1,37 +1,64 @@
package cash.z.android.wallet.data package cash.z.android.wallet.data
import cash.z.wallet.sdk.service.LightWalletService
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
/**
* Manage transactions with the main purpose of reporting which ones are still pending, particularly after failed
* attempts or dropped connectivity. The intent is to help see transactions through to completion.
*/
interface TransactionManager { interface TransactionManager {
/** suspend fun manageCreation(encoder: RawTransactionEncoder, value: Long, toAddress: String, memo: String)
* Initialize a transaction and return its ID. suspend fun manageSubmission(service: LightWalletService, rawTransaction: ByteArray)
* suspend fun getAllPending(): List<ByteArray>
* @return the id of the transaction to use with subsequent calls to this manager instance. //
*/ // /**
fun new(): Long // * Initialize a transaction and return its ID.
// *
// * @return the id of the transaction to use with subsequent calls to this manager instance.
// */
// fun new(): Long
//
// /**
// * Set the rawTransaction data for the given transaction. Typically, this would transition the state of the
// * transaction to something like CREATED. Some implementations might derive the state, based on whether this raw
// * transaction data has been provided.
// *
// * @param txId the id of the transaction to update
// * @param rawTransaction the raw transaction data
// */
// fun setRawTransaction(txId: Long, rawTransaction: ByteArray)
//
// /**
// * Signal that there has been an error while attempting to create a transaction.
// *
// * @param txId the id of the transaction to update
// * @param error information about the error that occurred
// */
// fun setCreationError(txId: Long, error: TransactionError)
//
// fun setSubmissionStarted(txId: Long)
// fun setSubmissionComplete(txId: Long, isSuccess: Boolean, error: TransactionError? = null)
// fun getAllPendingRawTransactions(): Map<Long, ByteArray>
/**
* Set the rawTransaction data for the given transaction. Typically, this would transition the state of the
* transaction to something like CREATED. Some implementations might derive the state, based on whether this raw
* transaction data has been provided.
*
* @param txId the id of the transaction to update
* @param rawTransaction the raw transaction data
*/
fun setRawTransaction(txId: Long, rawTransaction: ByteArray)
/**
* Signal that there has been an error while attempting to create a transaction.
*
* @param txId the id of the transaction to update
* @param error information about the error that occurred
*/
fun setCreationError(txId: Long, error: TransactionError)
fun setSubmissionStarted(txId: Long)
fun setSubmissionComplete(txId: Long, isSuccess: Boolean, error: TransactionError? = null)
fun getAllPendingRawTransactions(): Map<Long, ByteArray>
} }
interface TransactionError { interface TransactionError {
val message: String val message: String
} }
data class PendingTransaction(
val id: Long = -1,
val isMined: Boolean = false,
val hasRaw: Boolean = false,
val submitCount: Int = 0,
val expiryHeight: Int = -1,
val expiryTime: Long = -1,
val errorMessage: String? = null
)
fun PendingTransaction.isFailure(): Boolean {
return errorMessage != null
}

View File

@ -1,5 +1,9 @@
package cash.z.android.wallet.data package cash.z.android.wallet.data
import kotlinx.coroutines.CoroutineScope
interface TransactionSender { interface TransactionSender {
fun sendToAddress(zatoshi: Long, toAddress: String, memo: String, fromAccountId: Int) fun start(scope: CoroutineScope)
fun stop()
suspend fun sendToAddress(encoder: RawTransactionEncoder, zatoshi: Long, toAddress: String, memo: String = "", fromAccountId: Int = 0)
} }

View File

@ -0,0 +1,16 @@
package cash.z.android.wallet.data
import cash.z.wallet.sdk.data.TransactionRepository
import cash.z.wallet.sdk.secure.Wallet
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext
class WalletTransactionEncoder(
private val wallet: Wallet,
private val repository: TransactionRepository
) : RawTransactionEncoder {
override suspend fun create(zatoshi: Long, toAddress: String, memo: String): ByteArray = withContext(IO) {
val transactionId = wallet.createRawSendTransaction(zatoshi, toAddress, memo)
repository.findTransactionById(transactionId)?.raw!!
}
}

View File

@ -17,12 +17,13 @@ import androidx.navigation.Navigation
import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp import androidx.navigation.ui.navigateUp
import cash.z.android.wallet.* import cash.z.android.wallet.*
import cash.z.android.wallet.data.DataSyncronizer
import cash.z.android.wallet.data.StableSynchronizer
import cash.z.android.wallet.databinding.ActivityMainBinding import cash.z.android.wallet.databinding.ActivityMainBinding
import cash.z.android.wallet.di.annotation.ActivityScope import cash.z.android.wallet.di.annotation.ActivityScope
import cash.z.android.wallet.extention.Toaster import cash.z.android.wallet.extention.Toaster
import cash.z.android.wallet.extention.alert import cash.z.android.wallet.extention.alert
import cash.z.android.wallet.extention.copyToClipboard import cash.z.android.wallet.extention.copyToClipboard
import cash.z.android.wallet.sample.WalletConfig
import cash.z.android.wallet.ui.fragment.ScanFragment import cash.z.android.wallet.ui.fragment.ScanFragment
import cash.z.android.wallet.ui.presenter.BalancePresenter import cash.z.android.wallet.ui.presenter.BalancePresenter
import cash.z.android.wallet.ui.presenter.MainPresenter import cash.z.android.wallet.ui.presenter.MainPresenter
@ -34,10 +35,11 @@ import cash.z.android.wallet.ui.util.Analytics.Tap.*
import cash.z.android.wallet.ui.util.Analytics.trackAction import cash.z.android.wallet.ui.util.Analytics.trackAction
import cash.z.android.wallet.ui.util.Analytics.trackFunnelStep import cash.z.android.wallet.ui.util.Analytics.trackFunnelStep
import cash.z.android.wallet.ui.util.Broom import cash.z.android.wallet.ui.util.Broom
import cash.z.wallet.sdk.data.Synchronizer
import cash.z.wallet.sdk.data.twig import cash.z.wallet.sdk.data.twig
import cash.z.wallet.sdk.ext.convertZatoshiToZecString import cash.z.wallet.sdk.ext.convertZatoshiToZecString
import dagger.Binds
import dagger.Module import dagger.Module
import dagger.Provides
import dagger.android.ContributesAndroidInjector import dagger.android.ContributesAndroidInjector
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ -46,14 +48,11 @@ import kotlin.random.Random
class MainActivity : BaseActivity(), Animator.AnimatorListener, ScanFragment.BarcodeCallback, MainPresenter.MainView { class MainActivity : BaseActivity(), Animator.AnimatorListener, ScanFragment.BarcodeCallback, MainPresenter.MainView {
@Inject @Inject
lateinit var synchronizer: Synchronizer lateinit var synchronizer: DataSyncronizer
@Inject @Inject
lateinit var mainPresenter: MainPresenter lateinit var mainPresenter: MainPresenter
@Inject
lateinit var walletConfig: WalletConfig
@Inject @Inject
lateinit var broom: Broom lateinit var broom: Broom
@ -72,14 +71,13 @@ class MainActivity : BaseActivity(), Animator.AnimatorListener, ScanFragment.Bar
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
this.lifecycle
chipBucket.restore() chipBucket.restore()
binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.activity = this binding.activity = this
initAppBar() initAppBar()
loadMessages = generateFunLoadMessages().shuffled() loadMessages = generateFunLoadMessages().shuffled()
synchronizer.start(this)
synchronizer.onSynchronizerErrorListener = ::onSynchronizerError
balancePresenter = BalancePresenter() balancePresenter = BalancePresenter()
} }
@ -92,7 +90,8 @@ class MainActivity : BaseActivity(), Animator.AnimatorListener, ScanFragment.Bar
super.onResume() super.onResume()
chipBucket.restore() chipBucket.restore()
launch { launch {
balancePresenter.start(this, synchronizer) synchronizer.start(this)
balancePresenter.start(this, synchronizer.balances())
mainPresenter.start() mainPresenter.start()
} }
} }
@ -106,7 +105,6 @@ class MainActivity : BaseActivity(), Animator.AnimatorListener, ScanFragment.Bar
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
synchronizer.stop()
Analytics.clear() Analytics.clear()
} }
@ -421,7 +419,7 @@ class MainActivity : BaseActivity(), Animator.AnimatorListener, ScanFragment.Bar
} }
override fun orderUpdated(processing: MainPresenter.PurchaseResult.Processing) { override fun orderUpdated(processing: MainPresenter.PurchaseResult.Processing) {
Toaster.short(processing.state.toString()) Toaster.short(processing.pendingTransaction.toString())
} }
fun onSynchronizerError(error: Throwable?): Boolean { fun onSynchronizerError(error: Throwable?): Boolean {
@ -454,7 +452,9 @@ class MainActivity : BaseActivity(), Animator.AnimatorListener, ScanFragment.Bar
fun copyAddress(view: View) { fun copyAddress(view: View) {
trackAction(TAPPED_COPY_ADDRESS) trackAction(TAPPED_COPY_ADDRESS)
Toaster.short("Address copied!") Toaster.short("Address copied!")
copyToClipboard(synchronizer.getAddress()) launch {
copyToClipboard(synchronizer.getAddress())
}
} }

View File

@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.View import android.view.View
import android.widget.ProgressBar import android.widget.ProgressBar
import androidx.annotation.IdRes import androidx.annotation.IdRes
import cash.z.android.wallet.data.DataSyncronizer
import cash.z.android.wallet.ui.presenter.ProgressPresenter import cash.z.android.wallet.ui.presenter.ProgressPresenter
import cash.z.wallet.sdk.data.Synchronizer import cash.z.wallet.sdk.data.Synchronizer
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -15,7 +16,7 @@ abstract class ProgressFragment(
ProgressPresenter.ProgressView { ProgressPresenter.ProgressView {
@Inject @Inject
protected lateinit var synchronizer: Synchronizer protected lateinit var synchronizer: DataSyncronizer
protected lateinit var progressPresenter: ProgressPresenter protected lateinit var progressPresenter: ProgressPresenter
private lateinit var progressBar: ProgressBar private lateinit var progressBar: ProgressBar

View File

@ -10,12 +10,14 @@ import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import cash.z.android.qrecycler.QRecycler import cash.z.android.qrecycler.QRecycler
import cash.z.android.wallet.R import cash.z.android.wallet.R
import cash.z.android.wallet.data.DataSyncronizer
import cash.z.android.wallet.di.annotation.FragmentScope import cash.z.android.wallet.di.annotation.FragmentScope
import cash.z.android.wallet.ui.util.AddressPartNumberSpan import cash.z.android.wallet.ui.util.AddressPartNumberSpan
import cash.z.wallet.sdk.data.Synchronizer import cash.z.wallet.sdk.data.Synchronizer
import dagger.Module import dagger.Module
import dagger.android.ContributesAndroidInjector import dagger.android.ContributesAndroidInjector
import kotlinx.android.synthetic.main.fragment_receive.* import kotlinx.android.synthetic.main.fragment_receive.*
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -27,7 +29,7 @@ class ReceiveFragment : BaseFragment() {
lateinit var qrecycler: QRecycler lateinit var qrecycler: QRecycler
@Inject @Inject
lateinit var synchronizer: Synchronizer lateinit var synchronizer: DataSyncronizer
lateinit var addressParts: Array<TextView> lateinit var addressParts: Array<TextView>
@ -61,7 +63,9 @@ class ReceiveFragment : BaseFragment() {
super.onResume() super.onResume()
// TODO: replace these with channels. For now just wire the logic together // TODO: replace these with channels. For now just wire the logic together
onAddressLoaded(loadAddress()) launch {
onAddressLoaded(synchronizer.getAddress())
}
// converter.scanBlocks() // converter.scanBlocks()
} }
@ -85,12 +89,6 @@ class ReceiveFragment : BaseFragment() {
addressParts[index].text = textSpan addressParts[index].text = textSpan
} }
// TODO: replace with tiered load. First check memory reference (textview contents?) then check DB, then load from JNI and write to DB
private fun loadAddress(): String {
return synchronizer.getAddress()
}
} }
@Module @Module

View File

@ -66,7 +66,7 @@ class SyncFragment : ProgressFragment(R.id.progress_sync) {
(view?.parent as? ViewGroup)?.doOnPreDraw { (view?.parent as? ViewGroup)?.doOnPreDraw {
startPostponedEnterTransition() startPostponedEnterTransition()
} }
synchronizer.onSynchronizerErrorListener = ::onSynchronizerError // synchronizer.onSynchronizerErrorListener = ::onSynchronizerError
} }
override fun onResume() { override fun onResume() {

View File

@ -19,6 +19,7 @@ import cash.z.android.wallet.ui.presenter.BalancePresenter
import cash.z.android.wallet.ui.presenter.TransactionPresenter import cash.z.android.wallet.ui.presenter.TransactionPresenter
import cash.z.android.wallet.ui.presenter.TransactionPresenterModule import cash.z.android.wallet.ui.presenter.TransactionPresenterModule
import cash.z.wallet.sdk.dao.WalletTransaction import cash.z.wallet.sdk.dao.WalletTransaction
import cash.z.wallet.sdk.data.twig
import cash.z.wallet.sdk.ext.MINERS_FEE_ZATOSHI import cash.z.wallet.sdk.ext.MINERS_FEE_ZATOSHI
import cash.z.wallet.sdk.ext.convertZatoshiToZecString import cash.z.wallet.sdk.ext.convertZatoshiToZecString
import cash.z.wallet.sdk.secure.Wallet import cash.z.wallet.sdk.secure.Wallet
@ -114,10 +115,6 @@ class Zcon1HomeFragment : BaseFragment(), BalancePresenter.BalanceView, Transact
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
chipBucket.setOnBucketChangedListener(this) chipBucket.setOnBucketChangedListener(this)
}
override fun onStart() {
super.onStart()
mainActivity?.balancePresenter?.addBalanceView(this) mainActivity?.balancePresenter?.addBalanceView(this)
chipBucket.setOnBucketChangedListener(this) chipBucket.setOnBucketChangedListener(this)
launch { launch {
@ -125,8 +122,8 @@ class Zcon1HomeFragment : BaseFragment(), BalancePresenter.BalanceView, Transact
} }
} }
override fun onStop() { override fun onPause() {
super.onStop() super.onPause()
mainActivity?.balancePresenter?.removeBalanceView(this) mainActivity?.balancePresenter?.removeBalanceView(this)
chipBucket.removeOnBucketChangedListener(this) chipBucket.removeOnBucketChangedListener(this)
transactionPresenter.stop() transactionPresenter.stop()

View File

@ -29,12 +29,12 @@ class BalancePresenter {
// LifeCycle // LifeCycle
// //
fun start(scope: CoroutineScope, synchronizer: Synchronizer) { fun start(scope: CoroutineScope, balanceChannel: ReceiveChannel<Wallet.WalletBalance>) {
Twig.sprout("BalancePresenter") Twig.sprout("BalancePresenter")
twig("balancePresenter starting!") twig("balancePresenter starting!")
balanceJob?.cancel() balanceJob?.cancel()
balanceJob = Job() balanceJob = Job()
balanceJob = scope.launchBalanceBinder(synchronizer.balances()) balanceJob = scope.launchBalanceBinder(balanceChannel)
} }
fun stop() { fun stop() {

View File

@ -1,5 +1,6 @@
package cash.z.android.wallet.ui.presenter package cash.z.android.wallet.ui.presenter
import cash.z.android.wallet.data.DataSyncronizer
import cash.z.android.wallet.di.annotation.FragmentScope import cash.z.android.wallet.di.annotation.FragmentScope
import cash.z.android.wallet.ui.fragment.HistoryFragment import cash.z.android.wallet.ui.fragment.HistoryFragment
import cash.z.android.wallet.ui.presenter.Presenter.PresenterView import cash.z.android.wallet.ui.presenter.Presenter.PresenterView
@ -19,7 +20,7 @@ import kotlin.coroutines.CoroutineContext
class HistoryPresenter @Inject constructor( class HistoryPresenter @Inject constructor(
private val view: HistoryFragment, private val view: HistoryFragment,
private var synchronizer: Synchronizer private var synchronizer: DataSyncronizer
) : Presenter { ) : Presenter {
private var job: Job? = null private var job: Job? = null
@ -32,7 +33,7 @@ class HistoryPresenter @Inject constructor(
job?.cancel() job?.cancel()
job = Job() job = Job()
twig("historyPresenter starting!") twig("historyPresenter starting!")
view.launchTransactionBinder(synchronizer.allTransactions()) // view.launchTransactionBinder(synchronizer.allTransactions())
} }
override fun stop() { override fun stop() {

View File

@ -1,5 +1,6 @@
package cash.z.android.wallet.ui.presenter package cash.z.android.wallet.ui.presenter
import cash.z.android.wallet.data.DataSyncronizer
import cash.z.android.wallet.di.annotation.FragmentScope import cash.z.android.wallet.di.annotation.FragmentScope
import cash.z.android.wallet.extention.alert import cash.z.android.wallet.extention.alert
import cash.z.android.wallet.ui.fragment.HomeFragment import cash.z.android.wallet.ui.fragment.HomeFragment
@ -18,7 +19,7 @@ import javax.inject.Singleton
class HomePresenter @Inject constructor( class HomePresenter @Inject constructor(
private val view: HomeFragment, private val view: HomeFragment,
private val synchronizer: Synchronizer private val synchronizer: DataSyncronizer
) : Presenter { ) : Presenter {
private var job: Job? = null private var job: Job? = null
@ -32,15 +33,15 @@ class HomePresenter @Inject constructor(
} }
override suspend fun start() { override suspend fun start() {
job?.cancel() // job?.cancel()
job = Job() // job = Job()
twig("homePresenter starting! from ${this.hashCode()}") // twig("homePresenter starting! from ${this.hashCode()}")
with(view) { // with(view) {
launchBalanceBinder(synchronizer.balances()) // launchBalanceBinder(synchronizer.balances())
launchTransactionBinder(synchronizer.allTransactions()) // launchTransactionBinder(synchronizer.allTransactions())
launchActiveTransactionMonitor(synchronizer.activeTransactions()) // launchActiveTransactionMonitor(synchronizer.activeTransactions())
} // }
synchronizer.onSynchronizerErrorListener = view::onSynchronizerError // synchronizer.onSynchronizerErrorListener = view::onSynchronizerError
} }
override fun stop() { override fun stop() {
@ -99,11 +100,11 @@ class HomePresenter @Inject constructor(
} }
fun onCancelActiveTransaction(transaction: ActiveSendTransaction) { fun onCancelActiveTransaction(transaction: ActiveSendTransaction) {
twig("requesting to cancel send for transaction ${transaction.internalId}") // twig("requesting to cancel send for transaction ${transaction.internalId}")
val isTooLate = !synchronizer.cancelSend(transaction) // val isTooLate = !synchronizer.cancelSend(transaction)
if (isTooLate) { // if (isTooLate) {
view.onCancelledTooLate() // view.onCancelledTooLate()
} // }
} }
} }

View File

@ -1,5 +1,8 @@
package cash.z.android.wallet.ui.presenter package cash.z.android.wallet.ui.presenter
import cash.z.android.wallet.data.DataSyncronizer
import cash.z.android.wallet.data.PendingTransaction
import cash.z.android.wallet.data.isFailure
import cash.z.android.wallet.ui.activity.MainActivity import cash.z.android.wallet.ui.activity.MainActivity
import cash.z.android.wallet.ui.presenter.Presenter.PresenterView import cash.z.android.wallet.ui.presenter.Presenter.PresenterView
import cash.z.wallet.sdk.data.* import cash.z.wallet.sdk.data.*
@ -13,7 +16,7 @@ import javax.inject.Inject
class MainPresenter @Inject constructor( class MainPresenter @Inject constructor(
private val view: MainActivity, private val view: MainActivity,
private val synchronizer: Synchronizer private val synchronizer: DataSyncronizer
) : Presenter { ) : Presenter {
interface MainView : PresenterView { interface MainView : PresenterView {
@ -34,7 +37,7 @@ class MainPresenter @Inject constructor(
purchaseJob?.cancel() purchaseJob?.cancel()
purchaseJob = Job() purchaseJob = Job()
purchaseJob = view.launchPurchaseBinder(synchronizer.activeTransactions()) purchaseJob = view.launchPurchaseBinder(synchronizer.pendingTransactions())
} }
override fun stop() { override fun stop() {
@ -43,7 +46,7 @@ class MainPresenter @Inject constructor(
purchaseJob?.cancel()?.also { purchaseJob = null } purchaseJob?.cancel()?.also { purchaseJob = null }
} }
fun CoroutineScope.launchPurchaseBinder(channel: ReceiveChannel<Map<ActiveTransaction, TransactionState>>) = launch { private fun CoroutineScope.launchPurchaseBinder(channel: ReceiveChannel<List<PendingTransaction>>) = launch {
twig("main purchase binder starting!") twig("main purchase binder starting!")
for (new in channel) { for (new in channel) {
twig("main polled a purchase info") twig("main polled a purchase info")
@ -57,18 +60,18 @@ class MainPresenter @Inject constructor(
// Events // Events
// //
private fun bind(activeTransactions: Map<ActiveTransaction, TransactionState>) { private fun bind(activeTransactions: List<PendingTransaction>) {
val newestState = activeTransactions.entries.last().value val newest = activeTransactions.last()
if (newestState is TransactionState.Failure) { if (newest.isFailure()) {
view.orderFailed(PurchaseResult.Failure(newestState.reason)) view.orderFailed(PurchaseResult.Failure(newest.errorMessage))
} else { } else {
view.orderUpdated(PurchaseResult.Processing(newestState)) view.orderUpdated(PurchaseResult.Processing(newest))
} }
} }
sealed class PurchaseResult { sealed class PurchaseResult {
data class Processing(val state: TransactionState = TransactionState.Creating) : PurchaseResult() data class Processing(val pendingTransaction: PendingTransaction) : PurchaseResult()
data class Failure(val reason: String = "") : PurchaseResult() data class Failure(val reason: String? = "") : PurchaseResult()
} }
} }

View File

@ -1,5 +1,6 @@
package cash.z.android.wallet.ui.presenter package cash.z.android.wallet.ui.presenter
import cash.z.android.wallet.data.DataSyncronizer
import cash.z.android.wallet.ui.presenter.Presenter.PresenterView import cash.z.android.wallet.ui.presenter.Presenter.PresenterView
import cash.z.wallet.sdk.data.Synchronizer import cash.z.wallet.sdk.data.Synchronizer
import cash.z.wallet.sdk.data.Twig import cash.z.wallet.sdk.data.Twig
@ -11,7 +12,7 @@ import kotlin.coroutines.CoroutineContext
class ProgressPresenter @Inject constructor( class ProgressPresenter @Inject constructor(
private val view: ProgressView, private val view: ProgressView,
private var synchronizer: Synchronizer private var synchronizer: DataSyncronizer
) : Presenter { ) : Presenter {
private var job: Job? = null private var job: Job? = null
@ -29,7 +30,6 @@ class ProgressPresenter @Inject constructor(
job?.cancel() job?.cancel()
job = Job() job = Job()
Twig.sprout("ProgressPresenter") Twig.sprout("ProgressPresenter")
twig("starting")
view.launchProgressMonitor(synchronizer.progress()) view.launchProgressMonitor(synchronizer.progress())
} }
@ -40,7 +40,7 @@ class ProgressPresenter @Inject constructor(
} }
private fun CoroutineScope.launchProgressMonitor(channel: ReceiveChannel<Int>) = launch { private fun CoroutineScope.launchProgressMonitor(channel: ReceiveChannel<Int>) = launch {
twig("progress monitor starting on thread ${Thread.currentThread().name}!") twig("Progress monitor starting")
for (i in channel) { for (i in channel) {
bind(i) bind(i)
} }

View File

@ -1,6 +1,7 @@
package cash.z.android.wallet.ui.presenter package cash.z.android.wallet.ui.presenter
import cash.z.android.wallet.R import cash.z.android.wallet.R
import cash.z.android.wallet.data.DataSyncronizer
import cash.z.android.wallet.di.annotation.FragmentScope import cash.z.android.wallet.di.annotation.FragmentScope
import cash.z.android.wallet.extention.toAppString import cash.z.android.wallet.extention.toAppString
import cash.z.android.wallet.sample.SampleProperties import cash.z.android.wallet.sample.SampleProperties
@ -23,7 +24,7 @@ import javax.inject.Inject
class SendPresenter @Inject constructor( class SendPresenter @Inject constructor(
private val view: SendFragment, private val view: SendFragment,
private val synchronizer: Synchronizer private val synchronizer: DataSyncronizer
) : Presenter { ) : Presenter {
interface SendView : PresenterView { interface SendView : PresenterView {

View File

@ -1,5 +1,6 @@
package cash.z.android.wallet.ui.presenter package cash.z.android.wallet.ui.presenter
import cash.z.android.wallet.data.DataSyncronizer
import cash.z.android.wallet.ui.fragment.Zcon1HomeFragment import cash.z.android.wallet.ui.fragment.Zcon1HomeFragment
import cash.z.android.wallet.ui.presenter.Presenter.PresenterView import cash.z.android.wallet.ui.presenter.Presenter.PresenterView
import cash.z.wallet.sdk.dao.WalletTransaction import cash.z.wallet.sdk.dao.WalletTransaction
@ -14,7 +15,7 @@ import javax.inject.Inject
class TransactionPresenter @Inject constructor( class TransactionPresenter @Inject constructor(
private val view: Zcon1HomeFragment, private val view: Zcon1HomeFragment,
private val synchronizer: Synchronizer private val synchronizer: DataSyncronizer
) : Presenter { ) : Presenter {
interface TransactionView : PresenterView { interface TransactionView : PresenterView {
@ -35,7 +36,7 @@ class TransactionPresenter @Inject constructor(
transactionJob?.cancel() transactionJob?.cancel()
transactionJob = Job() transactionJob = Job()
// transactionJob = view.launchPurchaseBinder(synchronizer.activeTransactions()) // transactionJob = view.launchPurchaseBinder(synchronizer.activeTransactions())
transactionJob = view.launchTransactionBinder(synchronizer.allTransactions()) // transactionJob = view.launchTransactionBinder(synchronizer.allTransactions())
} }
override fun stop() { override fun stop() {

View File

@ -1,20 +1,16 @@
package cash.z.android.wallet.ui.util package cash.z.android.wallet.ui.util
import cash.z.android.wallet.PokerChip
import cash.z.android.wallet.ZcashWalletApplication import cash.z.android.wallet.ZcashWalletApplication
import cash.z.android.wallet.data.StaticTransactionRepository import cash.z.android.wallet.data.StaticTransactionRepository
import cash.z.android.wallet.data.TransactionSender
import cash.z.android.wallet.data.WalletTransactionEncoder
import cash.z.android.wallet.extention.toDbPath import cash.z.android.wallet.extention.toDbPath
import cash.z.android.wallet.extention.tryIgnore import cash.z.android.wallet.extention.tryIgnore
import cash.z.android.wallet.sample.SampleProperties
import cash.z.wallet.sdk.data.PollingTransactionRepository
import cash.z.wallet.sdk.data.TransactionRepository
import cash.z.wallet.sdk.data.Twig import cash.z.wallet.sdk.data.Twig
import cash.z.wallet.sdk.data.twig import cash.z.wallet.sdk.data.twig
import cash.z.wallet.sdk.ext.MINERS_FEE_ZATOSHI import cash.z.wallet.sdk.ext.MINERS_FEE_ZATOSHI
import cash.z.wallet.sdk.jni.RustBackendWelding import cash.z.wallet.sdk.jni.RustBackendWelding
import cash.z.wallet.sdk.rpc.Service
import cash.z.wallet.sdk.secure.Wallet import cash.z.wallet.sdk.secure.Wallet
import cash.z.wallet.sdk.service.LightWalletService
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
@ -22,7 +18,7 @@ import kotlin.properties.Delegates
import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadOnlyProperty
class Broom( class Broom(
private val service: LightWalletService, private val sender: TransactionSender,
private val rustBackend: RustBackendWelding, private val rustBackend: RustBackendWelding,
private val cacheDbName: String, private val cacheDbName: String,
private val appWallet: Wallet private val appWallet: Wallet
@ -39,7 +35,7 @@ class Broom(
// cloneCachedBlocks() // optional? // cloneCachedBlocks() // optional?
try { try {
val wallet = initWallet(walletSeedProvider) val encoder = initEncoder(walletSeedProvider)
// verify & scan // verify & scan
//TODO: for now, assume validation is happening elsewhere and just scan here //TODO: for now, assume validation is happening elsewhere and just scan here
Twig.sprout("broom-scan") Twig.sprout("broom-scan")
@ -48,12 +44,7 @@ class Broom(
Twig.clip("broom-scan") Twig.clip("broom-scan")
if (scanResult) { if (scanResult) {
twig("successfully scanned blocks! Ready to sweep!!!") twig("successfully scanned blocks! Ready to sweep!!!")
val memo = "swag shirt test" sender.sendToAddress(encoder, amount, appWallet.getAddress())
val address = "ztestsapling1yu2zy9aanf8pjf2qvm4qmn4k6q57y2d9fcs3vz0guthxx3m2aq57qm6hkx0580m9u9635xh6ttr"
// val address = appWallet.getAddress()
val transactionId = wallet.createRawSendTransaction(amount, address).also { checkTx(it) }
val transactionRaw: ByteArray? = repository.findTransactionById(transactionId)?.raw.also { checkRawTx(it) }
service.submitTransaction(transactionRaw!!).also { checkResponse(it) }
} else { } else {
twig("failed to scan!") twig("failed to scan!")
} }
@ -67,34 +58,10 @@ class Broom(
} }
} }
private fun checkTx(transactionId: Long) { private fun initEncoder(seedProvider: ReadOnlyProperty<Any?, ByteArray>): WalletTransactionEncoder {
if (transactionId < 0) {
throw SweepException.Creation
} else {
twig("successfully created transaction!")
}
}
private fun checkRawTx(transactionRaw: ByteArray?) {
if (transactionRaw == null) {
throw SweepException.Disappeared
} else {
twig("found raw transaction in the dataDb")
}
}
private fun checkResponse(response: Service.SendResponse) {
if (response.errorCode < 0) {
throw SweepException.IncompletePass(response)
} else {
twig("successfully submitted. error code: ${response.errorCode}")
}
}
private fun initWallet(seedProvider: ReadOnlyProperty<Any?, ByteArray>): Wallet {
// TODO: maybe let this one live and make a new one? // TODO: maybe let this one live and make a new one?
DATA_DB_PATH.absoluteFile.delete() DATA_DB_PATH.absoluteFile.delete()
return Wallet( val wallet = Wallet(
ZcashWalletApplication.instance, ZcashWalletApplication.instance,
rustBackend, rustBackend,
DATA_DB_PATH.absolutePath, DATA_DB_PATH.absolutePath,
@ -107,6 +74,7 @@ class Broom(
it.initialize() it.initialize()
} }
} }
return WalletTransactionEncoder(wallet, repository)
} }
companion object { companion object {
@ -114,10 +82,4 @@ class Broom(
private val DATA_DB_PATH: File = ZcashWalletApplication.instance.getDatabasePath(DATA_DB_NAME) private val DATA_DB_PATH: File = ZcashWalletApplication.instance.getDatabasePath(DATA_DB_NAME)
} }
sealed class SweepException(val errorMessage: String) : RuntimeException(errorMessage) {
object Creation : SweepException("failed to create raw transaction")
object Disappeared : SweepException("unable to find a matching raw transaction. This means the rust backend said it created a TX but when we looked for it in the DB it was missing!")
class IncompletePass(response: Service.SendResponse) : SweepException("submit failed with error code: ${response.errorCode} and message ${response.errorMessage}")
}
} }