From a9bcf6c6a0884d01079eebdbde6bf280b896f78d Mon Sep 17 00:00:00 2001 From: Vitaly Katz Date: Mon, 28 Aug 2023 15:11:53 +0200 Subject: [PATCH 01/23] Add table for locally submitted transactions --- .../gnosis/safe/di/modules/DatabaseModule.kt | 9 +- .../10.json | 317 ++++++++++++++++++ .../io/gnosis/data/db/HeimdallDatabaseTest.kt | 7 +- .../io/gnosis/data/db/HeimdallDatabase.kt | 16 +- .../data/db/daos/TransactionLocalDao.kt | 29 ++ .../io/gnosis/data/models/TransactionLocal.kt | 57 ++++ 6 files changed, 430 insertions(+), 5 deletions(-) create mode 100644 data/schemas/io.gnosis.data.db.HeimdallDatabase/10.json create mode 100644 data/src/main/java/io/gnosis/data/db/daos/TransactionLocalDao.kt create mode 100644 data/src/main/java/io/gnosis/data/models/TransactionLocal.kt diff --git a/app/src/main/java/io/gnosis/safe/di/modules/DatabaseModule.kt b/app/src/main/java/io/gnosis/safe/di/modules/DatabaseModule.kt index 4b8f3ab5e1..864b72fd62 100644 --- a/app/src/main/java/io/gnosis/safe/di/modules/DatabaseModule.kt +++ b/app/src/main/java/io/gnosis/safe/di/modules/DatabaseModule.kt @@ -13,9 +13,11 @@ import io.gnosis.data.db.HeimdallDatabase.Companion.MIGRATION_5_6 import io.gnosis.data.db.HeimdallDatabase.Companion.MIGRATION_6_7 import io.gnosis.data.db.HeimdallDatabase.Companion.MIGRATION_7_8 import io.gnosis.data.db.HeimdallDatabase.Companion.MIGRATION_8_9 +import io.gnosis.data.db.HeimdallDatabase.Companion.MIGRATION_9_10 import io.gnosis.data.db.daos.ChainDao import io.gnosis.data.db.daos.OwnerDao import io.gnosis.data.db.daos.SafeDao +import io.gnosis.data.db.daos.TransactionLocalDao import io.gnosis.safe.di.ApplicationContext import javax.inject.Singleton @@ -34,7 +36,8 @@ class DatabaseModule { MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, - MIGRATION_8_9 + MIGRATION_8_9, + MIGRATION_9_10 ) .build() @@ -49,4 +52,8 @@ class DatabaseModule { @Provides @Singleton fun providesChainDao(heimdallDatabase: HeimdallDatabase): ChainDao = heimdallDatabase.chainDao() + + @Provides + @Singleton + fun providesTransactionLocalDao(heimdallDatabase: HeimdallDatabase): TransactionLocalDao = heimdallDatabase.transactionLocalDao() } diff --git a/data/schemas/io.gnosis.data.db.HeimdallDatabase/10.json b/data/schemas/io.gnosis.data.db.HeimdallDatabase/10.json new file mode 100644 index 0000000000..f1f73496da --- /dev/null +++ b/data/schemas/io.gnosis.data.db.HeimdallDatabase/10.json @@ -0,0 +1,317 @@ +{ + "formatVersion": 1, + "database": { + "version": 10, + "identityHash": "8c491039b43bd8018d232bb2c807bb49", + "entities": [ + { + "tableName": "safes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`address` TEXT NOT NULL, `local_name` TEXT NOT NULL, `chain_id` TEXT NOT NULL, `version` TEXT, PRIMARY KEY(`address`, `chain_id`))", + "fields": [ + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "localName", + "columnName": "local_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chainId", + "columnName": "chain_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "address", + "chain_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "owners", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`address` TEXT NOT NULL, `name` TEXT, `type` INTEGER NOT NULL, `private_key` TEXT, `seed_phrase` TEXT, `derivation_path` TEXT, `source_fingerprint` TEXT, PRIMARY KEY(`address`))", + "fields": [ + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "private_key", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "seedPhrase", + "columnName": "seed_phrase", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "keyDerivationPath", + "columnName": "derivation_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceFingerprint", + "columnName": "source_fingerprint", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "address" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "chains", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`chain_id` TEXT NOT NULL, `l2` INTEGER NOT NULL, `chain_name` TEXT NOT NULL, `chain_short_name` TEXT NOT NULL, `text_color` TEXT NOT NULL, `background_color` TEXT NOT NULL, `rpc_uri` TEXT NOT NULL, `rpc_authentication` INTEGER NOT NULL, `block_explorer_address_uri` TEXT NOT NULL, `block_explorer_tx_hash_uri` TEXT NOT NULL, `ens_registry_address` TEXT, `features` TEXT NOT NULL, PRIMARY KEY(`chain_id`))", + "fields": [ + { + "fieldPath": "chainId", + "columnName": "chain_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "l2", + "columnName": "l2", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "chain_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "chain_short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "textColor", + "columnName": "text_color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "backgroundColor", + "columnName": "background_color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rpcUri", + "columnName": "rpc_uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rpcAuthentication", + "columnName": "rpc_authentication", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "blockExplorerTemplateAddress", + "columnName": "block_explorer_address_uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blockExplorerTemplateTxHash", + "columnName": "block_explorer_tx_hash_uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ensRegistryAddress", + "columnName": "ens_registry_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "features", + "columnName": "features", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "chain_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "native_currency", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`chain_id` TEXT NOT NULL, `name` TEXT NOT NULL, `symbol` TEXT NOT NULL, `decimals` INTEGER NOT NULL, `logo_uri` TEXT NOT NULL, PRIMARY KEY(`chain_id`), FOREIGN KEY(`chain_id`) REFERENCES `chains`(`chain_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "chainId", + "columnName": "chain_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "decimals", + "columnName": "decimals", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "logoUri", + "columnName": "logo_uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "chain_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "chains", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "chain_id" + ], + "referencedColumns": [ + "chain_id" + ] + } + ] + }, + { + "tableName": "local_transactions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`chain_id` TEXT NOT NULL, `safe_address` TEXT NOT NULL, `safe_tx_nonce` TEXT NOT NULL, `safe_tx_hash` TEXT NOT NULL, `eth_tx_hash` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`chain_id`, `safe_address`, `safe_tx_hash`), FOREIGN KEY(`chain_id`, `safe_address`) REFERENCES `safes`(`chain_id`, `address`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "chainId", + "columnName": "chain_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "safeAddress", + "columnName": "safe_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "safeTxNonce", + "columnName": "safe_tx_nonce", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "safeTxHash", + "columnName": "safe_tx_hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ethTxHash", + "columnName": "eth_tx_hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "chain_id", + "safe_address", + "safe_tx_hash" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "safes", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "chain_id", + "safe_address" + ], + "referencedColumns": [ + "chain_id", + "address" + ] + } + ] + } + ], + "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, '8c491039b43bd8018d232bb2c807bb49')" + ] + } +} \ No newline at end of file diff --git a/data/src/androidTest/java/io/gnosis/data/db/HeimdallDatabaseTest.kt b/data/src/androidTest/java/io/gnosis/data/db/HeimdallDatabaseTest.kt index f38d1b0595..f11bf5451d 100644 --- a/data/src/androidTest/java/io/gnosis/data/db/HeimdallDatabaseTest.kt +++ b/data/src/androidTest/java/io/gnosis/data/db/HeimdallDatabaseTest.kt @@ -49,7 +49,8 @@ class HeimdallDatabaseTest { HeimdallDatabase.MIGRATION_5_6, HeimdallDatabase.MIGRATION_6_7, HeimdallDatabase.MIGRATION_7_8, - HeimdallDatabase.MIGRATION_8_9 + HeimdallDatabase.MIGRATION_8_9, + HeimdallDatabase.MIGRATION_9_10 ) // Open latest version of the database. Room will validate the schema @@ -242,6 +243,7 @@ class HeimdallDatabaseTest { fun migrate6To7() { val chain = Chain( Chain.ID_MAINNET, + false, "Mainnet", "eth", "", @@ -250,7 +252,8 @@ class HeimdallDatabaseTest { RpcAuthentication.API_KEY_PATH, "", "", - null + null, + listOf() ) helper.createDatabase(TEST_DB, 6).apply { diff --git a/data/src/main/java/io/gnosis/data/db/HeimdallDatabase.kt b/data/src/main/java/io/gnosis/data/db/HeimdallDatabase.kt index cb06832706..37a68f4863 100644 --- a/data/src/main/java/io/gnosis/data/db/HeimdallDatabase.kt +++ b/data/src/main/java/io/gnosis/data/db/HeimdallDatabase.kt @@ -9,6 +9,7 @@ import io.gnosis.data.BuildConfig import io.gnosis.data.db.daos.ChainDao import io.gnosis.data.db.daos.OwnerDao import io.gnosis.data.db.daos.SafeDao +import io.gnosis.data.db.daos.TransactionLocalDao import io.gnosis.data.models.* import pm.gnosis.svalinn.security.db.EncryptedByteArray import pm.gnosis.svalinn.security.db.EncryptedString @@ -18,7 +19,8 @@ import pm.gnosis.svalinn.security.db.EncryptedString Safe::class, Owner::class, Chain::class, - Chain.Currency::class + Chain.Currency::class, + TransactionLocal::class ], version = HeimdallDatabase.LATEST_DB_VERSION ) @TypeConverters( @@ -38,9 +40,11 @@ abstract class HeimdallDatabase : RoomDatabase() { abstract fun chainDao(): ChainDao + abstract fun transactionLocalDao(): TransactionLocalDao + companion object { const val DB_NAME = "safe_db" - const val LATEST_DB_VERSION = 9 + const val LATEST_DB_VERSION = 10 val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { @@ -136,5 +140,13 @@ abstract class HeimdallDatabase : RoomDatabase() { ) } } + + val MIGRATION_9_10 = object : Migration(9, 10) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + """CREATE TABLE IF NOT EXISTS `${TransactionLocal.TABLE_NAME}` (`${TransactionLocal.COL_CHAIN_ID}` TEXT NOT NULL, `${TransactionLocal.COL_SAFE_ADDRESS}` TEXT NOT NULL, `${TransactionLocal.COL_SAFE_TX_NONCE}` TEXT NOT NULL, `${TransactionLocal.COL_SAFE_TX_HASH}` TEXT NOT NULL, `${TransactionLocal.COL_ETH_TX_HASH}` TEXT NOT NULL, `${TransactionLocal.COL_STATUS}` TEXT NOT NULL, PRIMARY KEY(`${TransactionLocal.COL_CHAIN_ID}`, `${TransactionLocal.COL_SAFE_ADDRESS}`, `${TransactionLocal.COL_SAFE_TX_HASH}`), FOREIGN KEY(`${TransactionLocal.COL_CHAIN_ID}`, `${TransactionLocal.COL_SAFE_ADDRESS}`) REFERENCES `${Safe.TABLE_NAME}`(`${Safe.COL_CHAIN_ID}`, `${Safe.COL_ADDRESS}`) ON UPDATE CASCADE ON DELETE CASCADE )""" + ) + } + } } } diff --git a/data/src/main/java/io/gnosis/data/db/daos/TransactionLocalDao.kt b/data/src/main/java/io/gnosis/data/db/daos/TransactionLocalDao.kt new file mode 100644 index 0000000000..30ad626459 --- /dev/null +++ b/data/src/main/java/io/gnosis/data/db/daos/TransactionLocalDao.kt @@ -0,0 +1,29 @@ +package io.gnosis.data.db.daos + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import io.gnosis.data.models.TransactionLocal +import pm.gnosis.model.Solidity +import java.math.BigInteger + +@Dao +interface TransactionLocalDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun save(tx: TransactionLocal) + + @Delete + suspend fun delete(tx: TransactionLocal) + + @Query("DELETE FROM ${TransactionLocal.TABLE_NAME}") + abstract fun deleteAll() + + @Query("SELECT * FROM ${TransactionLocal.TABLE_NAME} WHERE ${TransactionLocal.COL_CHAIN_ID} = :chainId AND ${TransactionLocal.COL_SAFE_ADDRESS} = :safeAddress AND ${TransactionLocal.COL_SAFE_TX_HASH} = :safeTxHash") + suspend fun loadyByEthTxHash(chainId: BigInteger, safeAddress: Solidity.Address, safeTxHash: String): TransactionLocal? + + @Query("SELECT * FROM ${TransactionLocal.TABLE_NAME} WHERE ${TransactionLocal.COL_CHAIN_ID} = :chainId AND ${TransactionLocal.COL_SAFE_ADDRESS} = :safeAddress ORDER BY ${TransactionLocal.COL_SAFE_TX_NONCE} DESC LIMIT 1") + suspend fun loadLatest(chainId: BigInteger, safeAddress: Solidity.Address): TransactionLocal? +} diff --git a/data/src/main/java/io/gnosis/data/models/TransactionLocal.kt b/data/src/main/java/io/gnosis/data/models/TransactionLocal.kt new file mode 100644 index 0000000000..a1373d682d --- /dev/null +++ b/data/src/main/java/io/gnosis/data/models/TransactionLocal.kt @@ -0,0 +1,57 @@ +package io.gnosis.data.models + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import io.gnosis.data.models.TransactionLocal.Companion.COL_CHAIN_ID +import io.gnosis.data.models.TransactionLocal.Companion.COL_SAFE_ADDRESS +import io.gnosis.data.models.TransactionLocal.Companion.COL_SAFE_TX_HASH +import io.gnosis.data.models.TransactionLocal.Companion.TABLE_NAME +import io.gnosis.data.models.transaction.TransactionStatus +import pm.gnosis.model.Solidity +import java.math.BigInteger + +@Entity( + tableName = TABLE_NAME, + primaryKeys = [COL_CHAIN_ID, COL_SAFE_ADDRESS, COL_SAFE_TX_HASH], + foreignKeys = [ + ForeignKey( + entity = Safe::class, + parentColumns = [Safe.COL_CHAIN_ID, Safe.COL_ADDRESS], + childColumns = [COL_CHAIN_ID, COL_SAFE_ADDRESS], + onUpdate = ForeignKey.CASCADE, + onDelete = ForeignKey.CASCADE + ) + ] +) +data class TransactionLocal( + + @ColumnInfo(name = COL_CHAIN_ID) + val chainId: BigInteger, + + @ColumnInfo(name = COL_SAFE_ADDRESS) + val safeAddress: Solidity.Address, + + @ColumnInfo(name = COL_SAFE_TX_NONCE) + val safeTxNonce: BigInteger, + + @ColumnInfo(name = COL_SAFE_TX_HASH) + val safeTxHash: String, + + @ColumnInfo(name = COL_ETH_TX_HASH) + val ethTxHash: String, + + @ColumnInfo(name = COL_STATUS) + val status: TransactionStatus +) { + companion object { + const val TABLE_NAME = "local_transactions" + + const val COL_CHAIN_ID = "chain_id" + const val COL_SAFE_ADDRESS = "safe_address" + const val COL_STATUS = "status" + const val COL_ETH_TX_HASH = "eth_tx_hash" + const val COL_SAFE_TX_HASH = "safe_tx_hash" + const val COL_SAFE_TX_NONCE = "safe_tx_nonce" + } +} From c4b0ae1b75fafddb8250ef53c0dff7ed7117e699 Mon Sep 17 00:00:00 2001 From: Vitaly Katz Date: Mon, 28 Aug 2023 15:13:18 +0200 Subject: [PATCH 02/23] Add repository for local transactions --- .../safe/di/modules/RepositoryModule.kt | 6 ++ .../TransactionLocalRepository.kt | 71 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 data/src/main/java/io/gnosis/data/repositories/TransactionLocalRepository.kt diff --git a/app/src/main/java/io/gnosis/safe/di/modules/RepositoryModule.kt b/app/src/main/java/io/gnosis/safe/di/modules/RepositoryModule.kt index c51f07616e..4bd63aa994 100644 --- a/app/src/main/java/io/gnosis/safe/di/modules/RepositoryModule.kt +++ b/app/src/main/java/io/gnosis/safe/di/modules/RepositoryModule.kt @@ -7,6 +7,7 @@ import io.gnosis.data.backend.GatewayApi import io.gnosis.data.backend.rpc.RpcClient import io.gnosis.data.db.daos.ChainDao import io.gnosis.data.db.daos.SafeDao +import io.gnosis.data.db.daos.TransactionLocalDao import io.gnosis.data.repositories.* import io.gnosis.safe.BuildConfig import io.gnosis.safe.workers.WorkRepository @@ -75,6 +76,11 @@ class RepositoryModule { fun providesTransactionRepository(gatewayApi: GatewayApi): TransactionRepository = TransactionRepository(gatewayApi) + @Provides + @Singleton + fun providesTransactionLocalRepository(localTxDao: TransactionLocalDao, rpcClient: RpcClient): TransactionLocalRepository = + TransactionLocalRepository(localTxDao, rpcClient) + @Provides @Singleton fun providesWorkRepository(workManager: WorkManager): WorkRepository = diff --git a/data/src/main/java/io/gnosis/data/repositories/TransactionLocalRepository.kt b/data/src/main/java/io/gnosis/data/repositories/TransactionLocalRepository.kt new file mode 100644 index 0000000000..abe6bbd5df --- /dev/null +++ b/data/src/main/java/io/gnosis/data/repositories/TransactionLocalRepository.kt @@ -0,0 +1,71 @@ +package io.gnosis.data.repositories + +import io.gnosis.data.backend.rpc.RpcClient +import io.gnosis.data.db.daos.TransactionLocalDao +import io.gnosis.data.models.Safe +import io.gnosis.data.models.TransactionLocal +import io.gnosis.data.models.transaction.TransactionStatus +import pm.gnosis.models.Transaction +import java.math.BigInteger + +class TransactionLocalRepository( + private val localTxDao: TransactionLocalDao, + private val rpcClient: RpcClient +) { + + suspend fun saveLocally(tx: Transaction, txHash: String, safeTxHash: String, safeTxNonce: BigInteger) { + // clean up old txs + // only latest tx submitted for execution is relevant + localTxDao.deleteAll() + val localTx = TransactionLocal( + safeAddress = tx.to, + chainId = tx.chainId, + safeTxNonce = safeTxNonce, + safeTxHash = safeTxHash, + ethTxHash = txHash, + status = TransactionStatus.PENDING + ) + localTxDao.save(localTx) + } + + suspend fun save(localTx: TransactionLocal) { + localTxDao.save(localTx) + } + + suspend fun delete(localTx: TransactionLocal) { + localTxDao.delete(localTx) + } + + suspend fun getLocalTx(safe: Safe, safeTxHash: String): TransactionLocal? = + localTxDao.loadyByEthTxHash(safe.chainId, safe.address, safeTxHash) + + suspend fun getLocalTxLatest(safe: Safe): TransactionLocal? = + localTxDao.loadLatest(safe.chainId, safe.address) + + suspend fun updateLocalTx(safe: Safe, safeTxHash: String): TransactionLocal? { + return getLocalTx(safe, safeTxHash)?.let { + updateLocalTx(safe, it) + } + } + + suspend fun updateLocalTxLatest(safe: Safe): TransactionLocal? { + return getLocalTxLatest(safe)?.let { + updateLocalTx(safe, it) + } + } + + suspend fun updateLocalTx(safe: Safe, localTx: TransactionLocal): TransactionLocal? { + var localTx = localTx + kotlin.runCatching { + rpcClient.getTransactionReceipt(safe.chain, localTx.ethTxHash) + }.onSuccess { txReceipt -> + localTx = localTx.copy( + status = if (txReceipt.status == BigInteger.ONE) TransactionStatus.SUCCESS else TransactionStatus.FAILED + ) + localTxDao.save(localTx) + }.onFailure { + // fail silently + } + return localTx + } +} From 578c9003d9473fff755a2606dde933757fe4bd1b Mon Sep 17 00:00:00 2001 From: Vitaly Katz Date: Mon, 28 Aug 2023 15:21:20 +0200 Subject: [PATCH 03/23] Update tx status using local tx data --- .../transactions/TransactionListViewModel.kt | 87 +++++++++++++++---- .../details/TransactionDetailsViewModel.kt | 41 +++++++-- .../viewdata/TransactionDetailsViewData.kt | 19 +++- .../execution/TxReviewViewModel.kt | 10 ++- .../TransactionListViewModelTest.kt | 69 ++++++++++----- .../TransactionDetailsViewModelTest.kt | 28 +++++- 6 files changed, 199 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/io/gnosis/safe/ui/transactions/TransactionListViewModel.kt b/app/src/main/java/io/gnosis/safe/ui/transactions/TransactionListViewModel.kt index f21d5e1434..3be2749a23 100644 --- a/app/src/main/java/io/gnosis/safe/ui/transactions/TransactionListViewModel.kt +++ b/app/src/main/java/io/gnosis/safe/ui/transactions/TransactionListViewModel.kt @@ -10,17 +10,33 @@ import io.gnosis.data.models.AddressInfo import io.gnosis.data.models.Chain import io.gnosis.data.models.Owner import io.gnosis.data.models.Safe -import io.gnosis.data.models.transaction.* +import io.gnosis.data.models.TransactionLocal +import io.gnosis.data.models.transaction.ConflictType +import io.gnosis.data.models.transaction.SafeAppInfo +import io.gnosis.data.models.transaction.Transaction +import io.gnosis.data.models.transaction.TransactionDirection +import io.gnosis.data.models.transaction.TransactionInfo +import io.gnosis.data.models.transaction.TransactionStatus +import io.gnosis.data.models.transaction.TransferInfo +import io.gnosis.data.models.transaction.TransferType +import io.gnosis.data.models.transaction.TxListEntry +import io.gnosis.data.models.transaction.decimals +import io.gnosis.data.models.transaction.symbol +import io.gnosis.data.models.transaction.value import io.gnosis.data.repositories.CredentialsRepository import io.gnosis.data.repositories.SafeRepository +import io.gnosis.data.repositories.TransactionLocalRepository import io.gnosis.safe.R import io.gnosis.safe.ui.base.AppDispatchers import io.gnosis.safe.ui.base.BaseStateViewModel import io.gnosis.safe.ui.transactions.paging.TransactionPagingProvider import io.gnosis.safe.ui.transactions.paging.TransactionPagingSource -import io.gnosis.safe.utils.* +import io.gnosis.safe.utils.BalanceFormatter +import io.gnosis.safe.utils.DEFAULT_ERC20_SYMBOL +import io.gnosis.safe.utils.DEFAULT_ERC721_SYMBOL +import io.gnosis.safe.utils.formatBackendDateTime +import io.gnosis.safe.utils.formatBackendTimeOfDay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.map import pm.gnosis.utils.asEthereumAddressString @@ -30,6 +46,7 @@ import javax.inject.Inject class TransactionListViewModel @Inject constructor( private val transactionsPager: TransactionPagingProvider, + private val transactionLocalRepository: TransactionLocalRepository, private val safeRepository: SafeRepository, private val credentialsRepository: CredentialsRepository, private val balanceFormatter: BalanceFormatter, @@ -66,37 +83,69 @@ class TransactionListViewModel } } - private fun getTransactions( + private suspend fun getTransactions( safe: Safe, safes: List, owners: List, type: TransactionPagingSource.Type ): Flow> { + var txLocal: TransactionLocal? = null + if (type == TransactionPagingSource.Type.QUEUE ) { + txLocal = transactionLocalRepository.updateLocalTxLatest(safe) + } + val safeTxItems: Flow> = transactionsPager.getTransactionsStream(safe, type) .map { pagingData -> pagingData .map { txListEntry -> when (txListEntry) { is TxListEntry.Transaction -> { - val isConflict = txListEntry.conflictType != ConflictType.None - val txView = - getTransactionView( - chain = safe.chain, - transaction = txListEntry.transaction, - safes = safes, - needsYourConfirmation = txListEntry.transaction.canBeSignedByAnyOwner(owners), - isConflict = isConflict, - localOwners = owners - ) - if (isConflict) { - TransactionView.Conflict(txView, txListEntry.conflictType) - } else txView + // conflict is resolved if there is local tx with same nonce + // that was submitted for execution + if (txLocal?.safeTxNonce == txListEntry.transaction.executionInfo?.nonce) { + if (txLocal?.safeTxHash == txListEntry.transaction.id.split("_").last() && txListEntry.transaction.txStatus == TransactionStatus.AWAITING_EXECUTION) { + val tx = txListEntry.transaction.copy(txStatus = TransactionStatus.PENDING) + txListEntry.transaction + getTransactionView( + chain = safe.chain, + transaction = tx, + safes = safes, + needsYourConfirmation = false, + isConflict = false, + localOwners = owners + ) + } else { + TransactionView.Unknown + } + } else { + val isConflict = txListEntry.conflictType != ConflictType.None + val txView = + getTransactionView( + chain = safe.chain, + transaction = txListEntry.transaction, + safes = safes, + needsYourConfirmation = txListEntry.transaction.canBeSignedByAnyOwner(owners), + isConflict = isConflict, + localOwners = owners + ) + if (isConflict) { + TransactionView.Conflict(txView, txListEntry.conflictType) + } else txView + } } is TxListEntry.DateLabel -> TransactionView.SectionDateHeader(date = txListEntry.timestamp) is TxListEntry.Label -> TransactionView.SectionLabelHeader(label = txListEntry.label) - is TxListEntry.ConflictHeader -> TransactionView.SectionConflictHeader(nonce = txListEntry.nonce) - TxListEntry.Unknown -> TransactionView.Unknown + is TxListEntry.ConflictHeader -> { + // conflict is resolved if there is local tx with same nonce + // that was submitted for execution + if (txLocal?.safeTxNonce == txListEntry.nonce.toBigInteger()) { + TransactionView.Unknown + } else { + TransactionView.SectionConflictHeader(nonce = txListEntry.nonce) + } + } + else -> TransactionView.Unknown } } .filter { it !is TransactionView.Unknown } diff --git a/app/src/main/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModel.kt b/app/src/main/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModel.kt index 72cc6d9432..c5f0631fe0 100644 --- a/app/src/main/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModel.kt +++ b/app/src/main/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModel.kt @@ -3,11 +3,13 @@ package io.gnosis.safe.ui.transactions.details import androidx.annotation.VisibleForTesting import io.gnosis.data.models.Owner import io.gnosis.data.models.Safe +import io.gnosis.data.models.TransactionLocal import io.gnosis.data.models.transaction.DetailedExecutionInfo import io.gnosis.data.models.transaction.TransactionDetails import io.gnosis.data.models.transaction.TransactionStatus import io.gnosis.data.repositories.CredentialsRepository import io.gnosis.data.repositories.SafeRepository +import io.gnosis.data.repositories.TransactionLocalRepository import io.gnosis.data.repositories.TransactionRepository import io.gnosis.data.utils.SemVer import io.gnosis.data.utils.calculateSafeTxHash @@ -28,6 +30,7 @@ import javax.inject.Inject class TransactionDetailsViewModel @Inject constructor( private val transactionRepository: TransactionRepository, + private val transactionLocalRepository: TransactionLocalRepository, private val safeRepository: SafeRepository, private val credentialsRepository: CredentialsRepository, private val settingsHandler: SettingsHandler, @@ -48,6 +51,8 @@ class TransactionDetailsViewModel activeSafe.chainId, txId ) + var txLocal: TransactionLocal? = null + val safes = safeRepository.getSafes() val executionInfo = txDetails?.detailedExecutionInfo @@ -61,20 +66,37 @@ class TransactionDetailsViewModel canExecute = canBeExecutedFromDevice(executionInfo, owners) nextInLine = safeInfo.nonce == executionInfo.nonce safeOwner = isOwner(executionInfo, owners) + txLocal = transactionLocalRepository.updateLocalTx(activeSafe, executionInfo.safeTxHash) + } + + var txDetailsViewData = txDetails?.toTransactionDetailsViewData( + safes = safes, + canSign = canSign, + canExecute = canExecute, + owners = owners, + nextInLine = nextInLine, + hasOwnerKey = safeOwner + ) + + txLocal?.let { + when { + txDetailsViewData?.txStatus == TransactionStatus.AWAITING_EXECUTION -> { + txDetailsViewData = txDetailsViewData?.copy(txStatus = TransactionStatus.PENDING) + } + txDetailsViewData?.txStatus == TransactionStatus.SUCCESS || + txDetailsViewData?.txStatus == TransactionStatus.FAILED -> { + // tx was indexed by the transaction service + // local transaction can be deleted + transactionLocalRepository.delete(it) + } + } } updateState { TransactionDetailsViewState(ViewAction.Loading(false)) } updateState { TransactionDetailsViewState( UpdateDetails( - txDetails?.toTransactionDetailsViewData( - safes = safes, - canSign = canSign, - canExecute = canExecute, - owners = owners, - nextInLine = nextInLine, - hasOwnerKey = safeOwner - ) + txDetailsViewData ) ) } @@ -255,6 +277,9 @@ class TransactionDetailsViewModel val canExecute = canBeExecutedFromDevice(newExecutionInfo, owners) val safeOwner = isOwner(newExecutionInfo, owners) +//// reload details +// loadDetails(newExecutionInfo.safeTxHash) + updateState { TransactionDetailsViewState( ConfirmationSubmitted( diff --git a/app/src/main/java/io/gnosis/safe/ui/transactions/details/viewdata/TransactionDetailsViewData.kt b/app/src/main/java/io/gnosis/safe/ui/transactions/details/viewdata/TransactionDetailsViewData.kt index 475ab644e5..3620bf423d 100644 --- a/app/src/main/java/io/gnosis/safe/ui/transactions/details/viewdata/TransactionDetailsViewData.kt +++ b/app/src/main/java/io/gnosis/safe/ui/transactions/details/viewdata/TransactionDetailsViewData.kt @@ -2,13 +2,24 @@ package io.gnosis.safe.ui.transactions.details.viewdata import android.os.Parcelable import androidx.annotation.VisibleForTesting +import io.gnosis.data.adapters.SolidityAddressNullableParceler +import io.gnosis.data.adapters.SolidityAddressParceler import io.gnosis.data.models.AddressInfo import io.gnosis.data.models.Owner import io.gnosis.data.models.Safe -import io.gnosis.data.models.transaction.* +import io.gnosis.data.models.transaction.DataDecoded +import io.gnosis.data.models.transaction.DetailedExecutionInfo +import io.gnosis.data.models.transaction.SafeAppInfo +import io.gnosis.data.models.transaction.SettingsInfo +import io.gnosis.data.models.transaction.SettingsInfoType +import io.gnosis.data.models.transaction.TransactionDetails +import io.gnosis.data.models.transaction.TransactionDirection +import io.gnosis.data.models.transaction.TransactionInfo +import io.gnosis.data.models.transaction.TransactionStatus +import io.gnosis.data.models.transaction.TransactionType +import io.gnosis.data.models.transaction.TransferInfo +import io.gnosis.data.models.transaction.TxData import io.gnosis.safe.ui.transactions.AddressInfoData -import io.gnosis.data.adapters.SolidityAddressNullableParceler -import io.gnosis.data.adapters.SolidityAddressParceler import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import kotlinx.parcelize.TypeParceler @@ -16,7 +27,7 @@ import pm.gnosis.crypto.utils.asEthereumAddressChecksumString import pm.gnosis.model.Solidity import pm.gnosis.utils.asEthereumAddressString import java.math.BigInteger -import java.util.* +import java.util.Date @Parcelize data class TransactionDetailsViewData( diff --git a/app/src/main/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModel.kt b/app/src/main/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModel.kt index 79116da48d..737b5a66d5 100644 --- a/app/src/main/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModel.kt +++ b/app/src/main/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModel.kt @@ -8,6 +8,7 @@ import io.gnosis.data.models.Safe import io.gnosis.data.models.transaction.DetailedExecutionInfo import io.gnosis.data.models.transaction.TxData import io.gnosis.data.repositories.CredentialsRepository +import io.gnosis.data.repositories.TransactionLocalRepository import io.gnosis.data.repositories.SafeRepository import io.gnosis.data.utils.toSignature import io.gnosis.safe.Tracker @@ -36,6 +37,7 @@ class TxReviewViewModel @Inject constructor( private val safeRepository: SafeRepository, private val credentialsRepository: CredentialsRepository, + private val localTxRepository: TransactionLocalRepository, private val settingsHandler: SettingsHandler, private val rpcClient: RpcClient, private val balanceFormatter: BalanceFormatter, @@ -453,7 +455,13 @@ class TxReviewViewModel safeLaunch { ethTxSignature?.let { kotlin.runCatching { - rpcClient.send(ethTx!!, it) + val txHash = rpcClient.send(ethTx!!, it) + localTxRepository.saveLocally( + tx = ethTx!!, + txHash = txHash, + safeTxHash = (executionInfo as DetailedExecutionInfo.MultisigExecutionDetails).safeTxHash, + safeTxNonce = (executionInfo as DetailedExecutionInfo.MultisigExecutionDetails).nonce + ) }.onSuccess { updateState { TxReviewState( diff --git a/app/src/test/java/io/gnosis/safe/ui/transactions/TransactionListViewModelTest.kt b/app/src/test/java/io/gnosis/safe/ui/transactions/TransactionListViewModelTest.kt index 74ab9ffb4b..b94b6086cd 100644 --- a/app/src/test/java/io/gnosis/safe/ui/transactions/TransactionListViewModelTest.kt +++ b/app/src/test/java/io/gnosis/safe/ui/transactions/TransactionListViewModelTest.kt @@ -2,11 +2,29 @@ package io.gnosis.safe.ui.transactions import androidx.paging.PagingData import io.gnosis.data.BuildConfig -import io.gnosis.data.models.* +import io.gnosis.data.models.AddressInfo +import io.gnosis.data.models.Chain +import io.gnosis.data.models.Owner +import io.gnosis.data.models.Page +import io.gnosis.data.models.Safe import io.gnosis.data.models.assets.TokenInfo import io.gnosis.data.models.assets.TokenType -import io.gnosis.data.models.transaction.* -import io.gnosis.data.models.transaction.TransactionStatus.* +import io.gnosis.data.models.transaction.DataDecoded +import io.gnosis.data.models.transaction.ExecutionInfo +import io.gnosis.data.models.transaction.Param +import io.gnosis.data.models.transaction.SettingsInfo +import io.gnosis.data.models.transaction.Transaction +import io.gnosis.data.models.transaction.TransactionDirection +import io.gnosis.data.models.transaction.TransactionInfo +import io.gnosis.data.models.transaction.TransactionStatus +import io.gnosis.data.models.transaction.TransactionStatus.AWAITING_CONFIRMATIONS +import io.gnosis.data.models.transaction.TransactionStatus.AWAITING_EXECUTION +import io.gnosis.data.models.transaction.TransactionStatus.CANCELLED +import io.gnosis.data.models.transaction.TransactionStatus.FAILED +import io.gnosis.data.models.transaction.TransactionStatus.PENDING +import io.gnosis.data.models.transaction.TransactionStatus.SUCCESS +import io.gnosis.data.models.transaction.TransferInfo +import io.gnosis.data.models.transaction.TxListEntry import io.gnosis.data.repositories.CredentialsRepository import io.gnosis.data.repositories.SafeRepository import io.gnosis.data.repositories.SafeRepository.Companion.METHOD_CHANGE_MASTER_COPY @@ -14,8 +32,12 @@ import io.gnosis.data.repositories.SafeRepository.Companion.METHOD_DISABLE_MODUL import io.gnosis.data.repositories.SafeRepository.Companion.METHOD_ENABLE_MODULE import io.gnosis.data.repositories.SafeRepository.Companion.METHOD_REMOVE_OWNER import io.gnosis.data.repositories.SafeRepository.Companion.METHOD_SET_FALLBACK_HANDLER +import io.gnosis.data.repositories.TransactionLocalRepository import io.gnosis.data.repositories.TransactionRepository -import io.gnosis.safe.* +import io.gnosis.safe.R +import io.gnosis.safe.TestLifecycleRule +import io.gnosis.safe.TestLiveDataObserver +import io.gnosis.safe.appDispatchers import io.gnosis.safe.ui.base.BaseStateViewModel import io.gnosis.safe.ui.transactions.TransactionListViewModel.Companion.OPACITY_FULL import io.gnosis.safe.ui.transactions.TransactionListViewModel.Companion.OPACITY_HALF @@ -24,7 +46,11 @@ import io.gnosis.safe.ui.transactions.paging.TransactionPagingSource import io.gnosis.safe.utils.BalanceFormatter import io.gnosis.safe.utils.formatBackendDateTime import io.gnosis.safe.utils.formatBackendTimeOfDay -import io.mockk.* +import io.mockk.Called +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.coVerifySequence +import io.mockk.mockk import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flow import org.junit.Assert.assertEquals @@ -35,7 +61,7 @@ import pm.gnosis.model.Solidity import pm.gnosis.utils.asEthereumAddress import pm.gnosis.utils.asEthereumAddressString import java.math.BigInteger -import java.util.* +import java.util.Date class TransactionListViewModelTest { @@ -46,6 +72,7 @@ class TransactionListViewModelTest { private val safeRepository = mockk() private val transactionRepository = mockk() + private val transactionLocalRepository = mockk() private val credentialsRepository = mockk() private val transactionPagingProvider = mockk() @@ -78,7 +105,7 @@ class TransactionListViewModelTest { val testObserver = TestLiveDataObserver() coEvery { safeRepository.activeSafeFlow() } returns emptyFlow() transactionListViewModel = - TransactionListViewModel(transactionPagingProvider, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) + TransactionListViewModel(transactionPagingProvider, transactionLocalRepository, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) transactionListViewModel.state.observeForever(testObserver) @@ -97,7 +124,7 @@ class TransactionListViewModelTest { val throwable = Throwable() coEvery { safeRepository.activeSafeFlow() } throws throwable transactionListViewModel = - TransactionListViewModel(transactionPagingProvider, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) + TransactionListViewModel(transactionPagingProvider, transactionLocalRepository, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) transactionListViewModel.state.observeForever(testObserver) testObserver.assertValueCount(1) @@ -123,7 +150,7 @@ class TransactionListViewModelTest { ) } returns flow { emit(PagingData.empty()) } transactionListViewModel = - TransactionListViewModel(transactionPagingProvider, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) + TransactionListViewModel(transactionPagingProvider, transactionLocalRepository, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) transactionListViewModel.state.observeForever(testObserver) @@ -152,7 +179,7 @@ class TransactionListViewModelTest { coEvery { transactionRepository.getHistoryTransactions(any()) } throws throwable coEvery { transactionPagingProvider.getTransactionsStream(any(), TransactionPagingSource.Type.HISTORY) } throws throwable transactionListViewModel = - TransactionListViewModel(transactionPagingProvider, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) + TransactionListViewModel(transactionPagingProvider, transactionLocalRepository, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) transactionListViewModel.load(TransactionPagingSource.Type.HISTORY) transactionListViewModel.state.observeForever(testObserver) @@ -173,7 +200,7 @@ class TransactionListViewModelTest { fun `mapToTransactionView (tx list with no transfer) should map to empty list`() { transactionListViewModel = - TransactionListViewModel(transactionPagingProvider, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) + TransactionListViewModel(transactionPagingProvider, transactionLocalRepository, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) val transactions = createEmptyTransactionList() val transactionViews = transactions.results.map { transactionListViewModel.getTransactionView(CHAIN, it, safes) } @@ -185,7 +212,7 @@ class TransactionListViewModelTest { fun `mapToTransactionView (tx list with queued transfers) should map to queued and ether transfer list`() { transactionListViewModel = - TransactionListViewModel(transactionPagingProvider, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) + TransactionListViewModel(transactionPagingProvider, transactionLocalRepository, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) val transactions = createTransactionListWithStatus( PENDING, @@ -210,7 +237,7 @@ class TransactionListViewModelTest { fun `mapToTransactionView (tx list with conflicting queued transfers) should map to queued and ether transfer list`() { transactionListViewModel = - TransactionListViewModel(transactionPagingProvider, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) + TransactionListViewModel(transactionPagingProvider, transactionLocalRepository, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) val transactions = createTransactionListWithStatus( PENDING, @@ -236,7 +263,7 @@ class TransactionListViewModelTest { fun `mapTransactionView (tx list with queued and historic transfer) should map to queued and transfer list`() { transactionListViewModel = - TransactionListViewModel(transactionPagingProvider, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) + TransactionListViewModel(transactionPagingProvider, transactionLocalRepository, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) val transactions = createTransactionListWithStatus(PENDING, SUCCESS) val transactionViews = transactions.results.map { transactionListViewModel.getTransactionView(CHAIN, it, safes) } @@ -249,7 +276,7 @@ class TransactionListViewModelTest { fun `mapTransactionView (tx list with queued and historic ETH transfers) should map to queued and ETH transfer list`() { transactionListViewModel = - TransactionListViewModel(transactionPagingProvider, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) + TransactionListViewModel(transactionPagingProvider, transactionLocalRepository, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) val transactions = listOf( buildTransfer( @@ -392,7 +419,7 @@ class TransactionListViewModelTest { fun `mapTransactionView (tx list with historic ether transfers) should map to ether transfer list`() { transactionListViewModel = - TransactionListViewModel(transactionPagingProvider, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) + TransactionListViewModel(transactionPagingProvider, transactionLocalRepository, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) val transactions = listOf( buildTransfer(serviceTokenInfo = ERC20_TOKEN_INFO_NO_SYMBOL, sender = defaultFromAddress, recipient = defaultSafeAddress), @@ -476,7 +503,7 @@ class TransactionListViewModelTest { fun `mapTransactionView (tx list with historic custom txs) should map to custom transactions list`() { transactionListViewModel = - TransactionListViewModel(transactionPagingProvider, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) + TransactionListViewModel(transactionPagingProvider, transactionLocalRepository, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) val transactions = listOf( buildCustom(status = AWAITING_EXECUTION, confirmations = 2, actionCount = 2), @@ -578,7 +605,7 @@ class TransactionListViewModelTest { @Test fun `mapTransactionView (tx list with historic setting changes) should map to settings changes list`() { transactionListViewModel = - TransactionListViewModel(transactionPagingProvider, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) + TransactionListViewModel(transactionPagingProvider, transactionLocalRepository, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) val transactions = listOf( // queued @@ -801,7 +828,7 @@ class TransactionListViewModelTest { fun `mapToTransactionView (tx list with creation tx) should map to list with creation tx`() { transactionListViewModel = - TransactionListViewModel(transactionPagingProvider, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) + TransactionListViewModel(transactionPagingProvider, transactionLocalRepository, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) val transactions = createTransactionListWithCreationTx() val transactionViews = transactions.results.map { transactionListViewModel.getTransactionView(CHAIN, it, safes) } @@ -824,7 +851,7 @@ class TransactionListViewModelTest { fun `mapToTransactionView (tx list with needs confirmation transactions) should map to list with items having correct needs confirmation string`() { transactionListViewModel = - TransactionListViewModel(transactionPagingProvider, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) + TransactionListViewModel(transactionPagingProvider, transactionLocalRepository, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) val safe = Safe(Solidity.Address(BigInteger.ONE), "test_safe") val ownerAddress = AddressInfo(Solidity.Address(BigInteger.ONE)) @@ -938,7 +965,7 @@ class TransactionListViewModelTest { fun `mapTransactionView () should perform correct known names resolution`() { transactionListViewModel = - TransactionListViewModel(transactionPagingProvider, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) + TransactionListViewModel(transactionPagingProvider, transactionLocalRepository, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) val transactions = listOf( buildCustom(addressInfo = AddressInfo(defaultSafeAddress), actionCount = 1), diff --git a/app/src/test/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModelTest.kt b/app/src/test/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModelTest.kt index 62eeb767b0..290c17cf76 100644 --- a/app/src/test/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModelTest.kt +++ b/app/src/test/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModelTest.kt @@ -14,12 +14,22 @@ import io.gnosis.data.models.transaction.TransactionDetails import io.gnosis.data.models.transaction.TransactionStatus import io.gnosis.data.repositories.CredentialsRepository import io.gnosis.data.repositories.SafeRepository +import io.gnosis.data.repositories.TransactionLocalRepository import io.gnosis.data.repositories.TransactionRepository -import io.gnosis.safe.* +import io.gnosis.safe.TestLifecycleRule +import io.gnosis.safe.TestLiveDataObserver +import io.gnosis.safe.Tracker +import io.gnosis.safe.appDispatchers +import io.gnosis.safe.readJsonFrom +import io.gnosis.safe.test import io.gnosis.safe.ui.base.BaseStateViewModel import io.gnosis.safe.ui.settings.app.SettingsHandler import io.gnosis.safe.ui.transactions.details.viewdata.toTransactionDetailsViewData -import io.mockk.* +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.just +import io.mockk.mockk import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals @@ -38,6 +48,7 @@ class TransactionDetailsViewModelTest { val instantExecutorRule = TestLifecycleRule() private val transactionRepository = mockk() + private val transactionLocalRepository = mockk() private val safeRepository = mockk() private val credentialsRepository = mockk() private val settingsHandler = mockk() @@ -76,6 +87,7 @@ class TransactionDetailsViewModelTest { viewModel = TransactionDetailsViewModel( transactionRepository, + transactionLocalRepository, safeRepository, credentialsRepository, settingsHandler, @@ -137,6 +149,7 @@ class TransactionDetailsViewModelTest { viewModel = TransactionDetailsViewModel( transactionRepository, + transactionLocalRepository, safeRepository, credentialsRepository, settingsHandler, @@ -169,6 +182,7 @@ class TransactionDetailsViewModelTest { viewModel = TransactionDetailsViewModel( transactionRepository, + transactionLocalRepository, safeRepository, credentialsRepository, settingsHandler, @@ -195,6 +209,7 @@ class TransactionDetailsViewModelTest { viewModel = TransactionDetailsViewModel( transactionRepository, + transactionLocalRepository, safeRepository, credentialsRepository, settingsHandler, @@ -223,6 +238,7 @@ class TransactionDetailsViewModelTest { viewModel = TransactionDetailsViewModel( transactionRepository, + transactionLocalRepository, safeRepository, credentialsRepository, settingsHandler, @@ -258,6 +274,7 @@ class TransactionDetailsViewModelTest { viewModel = TransactionDetailsViewModel( transactionRepository, + transactionLocalRepository, safeRepository, credentialsRepository, settingsHandler, @@ -293,6 +310,7 @@ class TransactionDetailsViewModelTest { viewModel = TransactionDetailsViewModel( transactionRepository, + transactionLocalRepository, safeRepository, credentialsRepository, settingsHandler, @@ -328,6 +346,7 @@ class TransactionDetailsViewModelTest { viewModel = TransactionDetailsViewModel( transactionRepository, + transactionLocalRepository, safeRepository, credentialsRepository, settingsHandler, @@ -362,6 +381,7 @@ class TransactionDetailsViewModelTest { viewModel = TransactionDetailsViewModel( transactionRepository, + transactionLocalRepository, safeRepository, credentialsRepository, settingsHandler, @@ -407,6 +427,7 @@ class TransactionDetailsViewModelTest { viewModel = TransactionDetailsViewModel( transactionRepository, + transactionLocalRepository, safeRepository, credentialsRepository, settingsHandler, @@ -457,6 +478,7 @@ class TransactionDetailsViewModelTest { viewModel = TransactionDetailsViewModel( transactionRepository, + transactionLocalRepository, safeRepository, credentialsRepository, settingsHandler, @@ -508,6 +530,7 @@ class TransactionDetailsViewModelTest { viewModel = TransactionDetailsViewModel( transactionRepository, + transactionLocalRepository, safeRepository, credentialsRepository, settingsHandler, @@ -556,6 +579,7 @@ class TransactionDetailsViewModelTest { viewModel = TransactionDetailsViewModel( transactionRepository, + transactionLocalRepository, safeRepository, credentialsRepository, settingsHandler, From 062c9386dc5103b9b7152cb5874412b8368ac047 Mon Sep 17 00:00:00 2001 From: Vitaly Katz Date: Mon, 28 Aug 2023 15:36:26 +0200 Subject: [PATCH 04/23] Adjust test --- .../ui/transactions/details/TransactionDetailsViewModelTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/test/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModelTest.kt b/app/src/test/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModelTest.kt index 290c17cf76..774d0cd526 100644 --- a/app/src/test/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModelTest.kt +++ b/app/src/test/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModelTest.kt @@ -120,6 +120,7 @@ class TransactionDetailsViewModelTest { any() ) } returns transactionDetails + coEvery { transactionLocalRepository.updateLocalTx(any(), any()) } returns null coEvery { safeRepository.getSafes() } returns emptyList() coEvery { credentialsRepository.owners() } returns listOf() coEvery { safeRepository.getActiveSafe() } returns Safe(someAddress, "safe_name", CHAIN_ID) From 10ca01e1f5164865186be7623987e2fb04b561ab Mon Sep 17 00:00:00 2001 From: Vitaly Katz Date: Tue, 29 Aug 2023 08:24:58 +0200 Subject: [PATCH 05/23] Don't rely on internal chain id structure; add submittedAt field --- .../transactions/TransactionListViewModel.kt | 3 +- .../execution/TxReviewViewModel.kt | 12 ++++---- .../10.json | 30 +++++++------------ .../io/gnosis/data/db/HeimdallDatabase.kt | 2 +- .../data/db/daos/TransactionLocalDao.kt | 7 ++--- .../io/gnosis/data/models/TransactionLocal.kt | 24 ++++++--------- .../TransactionLocalRepository.kt | 11 ++++--- 7 files changed, 38 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/io/gnosis/safe/ui/transactions/TransactionListViewModel.kt b/app/src/main/java/io/gnosis/safe/ui/transactions/TransactionListViewModel.kt index 3be2749a23..62de8a2e82 100644 --- a/app/src/main/java/io/gnosis/safe/ui/transactions/TransactionListViewModel.kt +++ b/app/src/main/java/io/gnosis/safe/ui/transactions/TransactionListViewModel.kt @@ -104,7 +104,8 @@ class TransactionListViewModel // conflict is resolved if there is local tx with same nonce // that was submitted for execution if (txLocal?.safeTxNonce == txListEntry.transaction.executionInfo?.nonce) { - if (txLocal?.safeTxHash == txListEntry.transaction.id.split("_").last() && txListEntry.transaction.txStatus == TransactionStatus.AWAITING_EXECUTION) { + // use submittedAt timestamp to distinguish between conflicting transactions + if (txLocal?.submittedAt == txListEntry.transaction.timestamp.time && txListEntry.transaction.txStatus == TransactionStatus.AWAITING_EXECUTION) { val tx = txListEntry.transaction.copy(txStatus = TransactionStatus.PENDING) txListEntry.transaction getTransactionView( diff --git a/app/src/main/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModel.kt b/app/src/main/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModel.kt index 737b5a66d5..6c81b426a2 100644 --- a/app/src/main/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModel.kt +++ b/app/src/main/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModel.kt @@ -455,14 +455,16 @@ class TxReviewViewModel safeLaunch { ethTxSignature?.let { kotlin.runCatching { - val txHash = rpcClient.send(ethTx!!, it) + rpcClient.send(ethTx!!, it) + }.onSuccess { + val executionInfo = executionInfo as DetailedExecutionInfo.MultisigExecutionDetails localTxRepository.saveLocally( tx = ethTx!!, - txHash = txHash, - safeTxHash = (executionInfo as DetailedExecutionInfo.MultisigExecutionDetails).safeTxHash, - safeTxNonce = (executionInfo as DetailedExecutionInfo.MultisigExecutionDetails).nonce + txHash = it, + safeTxHash = executionInfo.safeTxHash, + safeTxNonce = executionInfo.nonce, + submittedAt = executionInfo.submittedAt.time ) - }.onSuccess { updateState { TxReviewState( viewAction = diff --git a/data/schemas/io.gnosis.data.db.HeimdallDatabase/10.json b/data/schemas/io.gnosis.data.db.HeimdallDatabase/10.json index f1f73496da..38aaa63100 100644 --- a/data/schemas/io.gnosis.data.db.HeimdallDatabase/10.json +++ b/data/schemas/io.gnosis.data.db.HeimdallDatabase/10.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 10, - "identityHash": "8c491039b43bd8018d232bb2c807bb49", + "identityHash": "2e9a1088f3438046177df042dfb4f0bd", "entities": [ { "tableName": "safes", @@ -243,7 +243,7 @@ }, { "tableName": "local_transactions", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`chain_id` TEXT NOT NULL, `safe_address` TEXT NOT NULL, `safe_tx_nonce` TEXT NOT NULL, `safe_tx_hash` TEXT NOT NULL, `eth_tx_hash` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`chain_id`, `safe_address`, `safe_tx_hash`), FOREIGN KEY(`chain_id`, `safe_address`) REFERENCES `safes`(`chain_id`, `address`) ON UPDATE CASCADE ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`chain_id` TEXT NOT NULL, `safe_address` TEXT NOT NULL, `safe_tx_nonce` TEXT NOT NULL, `safe_tx_hash` TEXT NOT NULL, `eth_tx_hash` TEXT NOT NULL, `status` TEXT NOT NULL, `submitted_at` INTEGER NOT NULL, PRIMARY KEY(`safe_address`, `chain_id`, `safe_tx_hash`))", "fields": [ { "fieldPath": "chainId", @@ -280,38 +280,30 @@ "columnName": "status", "affinity": "TEXT", "notNull": true + }, + { + "fieldPath": "submittedAt", + "columnName": "submitted_at", + "affinity": "INTEGER", + "notNull": true } ], "primaryKey": { "columnNames": [ - "chain_id", "safe_address", + "chain_id", "safe_tx_hash" ], "autoGenerate": false }, "indices": [], - "foreignKeys": [ - { - "table": "safes", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "chain_id", - "safe_address" - ], - "referencedColumns": [ - "chain_id", - "address" - ] - } - ] + "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, '8c491039b43bd8018d232bb2c807bb49')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2e9a1088f3438046177df042dfb4f0bd')" ] } } \ No newline at end of file diff --git a/data/src/main/java/io/gnosis/data/db/HeimdallDatabase.kt b/data/src/main/java/io/gnosis/data/db/HeimdallDatabase.kt index 37a68f4863..3bb8f79c3b 100644 --- a/data/src/main/java/io/gnosis/data/db/HeimdallDatabase.kt +++ b/data/src/main/java/io/gnosis/data/db/HeimdallDatabase.kt @@ -144,7 +144,7 @@ abstract class HeimdallDatabase : RoomDatabase() { val MIGRATION_9_10 = object : Migration(9, 10) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL( - """CREATE TABLE IF NOT EXISTS `${TransactionLocal.TABLE_NAME}` (`${TransactionLocal.COL_CHAIN_ID}` TEXT NOT NULL, `${TransactionLocal.COL_SAFE_ADDRESS}` TEXT NOT NULL, `${TransactionLocal.COL_SAFE_TX_NONCE}` TEXT NOT NULL, `${TransactionLocal.COL_SAFE_TX_HASH}` TEXT NOT NULL, `${TransactionLocal.COL_ETH_TX_HASH}` TEXT NOT NULL, `${TransactionLocal.COL_STATUS}` TEXT NOT NULL, PRIMARY KEY(`${TransactionLocal.COL_CHAIN_ID}`, `${TransactionLocal.COL_SAFE_ADDRESS}`, `${TransactionLocal.COL_SAFE_TX_HASH}`), FOREIGN KEY(`${TransactionLocal.COL_CHAIN_ID}`, `${TransactionLocal.COL_SAFE_ADDRESS}`) REFERENCES `${Safe.TABLE_NAME}`(`${Safe.COL_CHAIN_ID}`, `${Safe.COL_ADDRESS}`) ON UPDATE CASCADE ON DELETE CASCADE )""" + """CREATE TABLE IF NOT EXISTS `${TransactionLocal.TABLE_NAME}` (`${TransactionLocal.COL_CHAIN_ID}` TEXT NOT NULL, `${TransactionLocal.COL_SAFE_ADDRESS}` TEXT NOT NULL, `${TransactionLocal.COL_SAFE_TX_NONCE}` TEXT NOT NULL, `${TransactionLocal.COL_SAFE_TX_HASH}` TEXT NOT NULL, `${TransactionLocal.COL_ETH_TX_HASH}` TEXT NOT NULL, `${TransactionLocal.COL_STATUS}` TEXT NOT NULL, `${TransactionLocal.COL_SUBMITTED_AT}` INTEGER NOT NULL, PRIMARY KEY(`${TransactionLocal.COL_SAFE_ADDRESS}`, `${TransactionLocal.COL_CHAIN_ID}`, `${TransactionLocal.COL_SAFE_TX_HASH}`))""" ) } } diff --git a/data/src/main/java/io/gnosis/data/db/daos/TransactionLocalDao.kt b/data/src/main/java/io/gnosis/data/db/daos/TransactionLocalDao.kt index 30ad626459..eb4fc8314f 100644 --- a/data/src/main/java/io/gnosis/data/db/daos/TransactionLocalDao.kt +++ b/data/src/main/java/io/gnosis/data/db/daos/TransactionLocalDao.kt @@ -17,12 +17,11 @@ interface TransactionLocalDao { @Delete suspend fun delete(tx: TransactionLocal) - - @Query("DELETE FROM ${TransactionLocal.TABLE_NAME}") - abstract fun deleteAll() + @Query("DELETE FROM ${TransactionLocal.TABLE_NAME} WHERE ${TransactionLocal.COL_CHAIN_ID} = :chainId AND ${TransactionLocal.COL_SAFE_ADDRESS} = :safeAddress") + suspend fun clearOldRecords(chainId: BigInteger, safeAddress: Solidity.Address) @Query("SELECT * FROM ${TransactionLocal.TABLE_NAME} WHERE ${TransactionLocal.COL_CHAIN_ID} = :chainId AND ${TransactionLocal.COL_SAFE_ADDRESS} = :safeAddress AND ${TransactionLocal.COL_SAFE_TX_HASH} = :safeTxHash") - suspend fun loadyByEthTxHash(chainId: BigInteger, safeAddress: Solidity.Address, safeTxHash: String): TransactionLocal? + suspend fun loadyBySafeTxHash(chainId: BigInteger, safeAddress: Solidity.Address, safeTxHash: String): TransactionLocal? @Query("SELECT * FROM ${TransactionLocal.TABLE_NAME} WHERE ${TransactionLocal.COL_CHAIN_ID} = :chainId AND ${TransactionLocal.COL_SAFE_ADDRESS} = :safeAddress ORDER BY ${TransactionLocal.COL_SAFE_TX_NONCE} DESC LIMIT 1") suspend fun loadLatest(chainId: BigInteger, safeAddress: Solidity.Address): TransactionLocal? diff --git a/data/src/main/java/io/gnosis/data/models/TransactionLocal.kt b/data/src/main/java/io/gnosis/data/models/TransactionLocal.kt index a1373d682d..a5cb7b1c5e 100644 --- a/data/src/main/java/io/gnosis/data/models/TransactionLocal.kt +++ b/data/src/main/java/io/gnosis/data/models/TransactionLocal.kt @@ -2,7 +2,6 @@ package io.gnosis.data.models import androidx.room.ColumnInfo import androidx.room.Entity -import androidx.room.ForeignKey import io.gnosis.data.models.TransactionLocal.Companion.COL_CHAIN_ID import io.gnosis.data.models.TransactionLocal.Companion.COL_SAFE_ADDRESS import io.gnosis.data.models.TransactionLocal.Companion.COL_SAFE_TX_HASH @@ -13,16 +12,7 @@ import java.math.BigInteger @Entity( tableName = TABLE_NAME, - primaryKeys = [COL_CHAIN_ID, COL_SAFE_ADDRESS, COL_SAFE_TX_HASH], - foreignKeys = [ - ForeignKey( - entity = Safe::class, - parentColumns = [Safe.COL_CHAIN_ID, Safe.COL_ADDRESS], - childColumns = [COL_CHAIN_ID, COL_SAFE_ADDRESS], - onUpdate = ForeignKey.CASCADE, - onDelete = ForeignKey.CASCADE - ) - ] + primaryKeys = [COL_SAFE_ADDRESS, COL_CHAIN_ID, COL_SAFE_TX_HASH] ) data class TransactionLocal( @@ -42,16 +32,20 @@ data class TransactionLocal( val ethTxHash: String, @ColumnInfo(name = COL_STATUS) - val status: TransactionStatus + val status: TransactionStatus, + + @ColumnInfo(name = COL_SUBMITTED_AT) + val submittedAt: Long, ) { companion object { const val TABLE_NAME = "local_transactions" const val COL_CHAIN_ID = "chain_id" const val COL_SAFE_ADDRESS = "safe_address" - const val COL_STATUS = "status" - const val COL_ETH_TX_HASH = "eth_tx_hash" - const val COL_SAFE_TX_HASH = "safe_tx_hash" const val COL_SAFE_TX_NONCE = "safe_tx_nonce" + const val COL_SAFE_TX_HASH = "safe_tx_hash" + const val COL_ETH_TX_HASH = "eth_tx_hash" + const val COL_STATUS = "status" + const val COL_SUBMITTED_AT = "submitted_at" } } diff --git a/data/src/main/java/io/gnosis/data/repositories/TransactionLocalRepository.kt b/data/src/main/java/io/gnosis/data/repositories/TransactionLocalRepository.kt index abe6bbd5df..6f256dd71a 100644 --- a/data/src/main/java/io/gnosis/data/repositories/TransactionLocalRepository.kt +++ b/data/src/main/java/io/gnosis/data/repositories/TransactionLocalRepository.kt @@ -13,17 +13,16 @@ class TransactionLocalRepository( private val rpcClient: RpcClient ) { - suspend fun saveLocally(tx: Transaction, txHash: String, safeTxHash: String, safeTxNonce: BigInteger) { - // clean up old txs - // only latest tx submitted for execution is relevant - localTxDao.deleteAll() + suspend fun saveLocally(tx: Transaction, txHash: String, safeTxHash: String, safeTxNonce: BigInteger, submittedAt: Long) { + localTxDao.clearOldRecords(tx.chainId, tx.to) val localTx = TransactionLocal( safeAddress = tx.to, chainId = tx.chainId, safeTxNonce = safeTxNonce, safeTxHash = safeTxHash, ethTxHash = txHash, - status = TransactionStatus.PENDING + status = TransactionStatus.PENDING, + submittedAt = submittedAt ) localTxDao.save(localTx) } @@ -37,7 +36,7 @@ class TransactionLocalRepository( } suspend fun getLocalTx(safe: Safe, safeTxHash: String): TransactionLocal? = - localTxDao.loadyByEthTxHash(safe.chainId, safe.address, safeTxHash) + localTxDao.loadyBySafeTxHash(safe.chainId, safe.address, safeTxHash) suspend fun getLocalTxLatest(safe: Safe): TransactionLocal? = localTxDao.loadLatest(safe.chainId, safe.address) From c16b02c6462813355bd3516d06eeac5539b2b3ca Mon Sep 17 00:00:00 2001 From: Vitaly Katz Date: Tue, 29 Aug 2023 09:35:52 +0200 Subject: [PATCH 06/23] Cleanup; add tracking for tx submitted --- .../ui/transactions/details/TransactionDetailsViewModel.kt | 3 --- .../gnosis/safe/ui/transactions/execution/TxReviewViewModel.kt | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModel.kt b/app/src/main/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModel.kt index c5f0631fe0..c773b24493 100644 --- a/app/src/main/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModel.kt +++ b/app/src/main/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModel.kt @@ -277,9 +277,6 @@ class TransactionDetailsViewModel val canExecute = canBeExecutedFromDevice(newExecutionInfo, owners) val safeOwner = isOwner(newExecutionInfo, owners) -//// reload details -// loadDetails(newExecutionInfo.safeTxHash) - updateState { TransactionDetailsViewState( ConfirmationSubmitted( diff --git a/app/src/main/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModel.kt b/app/src/main/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModel.kt index 6c81b426a2..e0629cdc38 100644 --- a/app/src/main/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModel.kt +++ b/app/src/main/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModel.kt @@ -457,6 +457,7 @@ class TxReviewViewModel kotlin.runCatching { rpcClient.send(ethTx!!, it) }.onSuccess { + tracker.logTxExecSubmitted() val executionInfo = executionInfo as DetailedExecutionInfo.MultisigExecutionDetails localTxRepository.saveLocally( tx = ethTx!!, From 643f87683c10af85ab5f6d452540c667e635842e Mon Sep 17 00:00:00 2001 From: Vitaly Katz Date: Tue, 29 Aug 2023 11:51:27 +0200 Subject: [PATCH 07/23] Add tests --- .../execution/TxReviewViewModelTest.kt | 213 ++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 app/src/test/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModelTest.kt diff --git a/app/src/test/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModelTest.kt b/app/src/test/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModelTest.kt new file mode 100644 index 0000000000..baf5fdaef6 --- /dev/null +++ b/app/src/test/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModelTest.kt @@ -0,0 +1,213 @@ +package io.gnosis.safe.ui.transactions.execution + +import io.gnosis.data.backend.rpc.RpcClient +import io.gnosis.data.models.Chain +import io.gnosis.data.models.Owner +import io.gnosis.data.models.Safe +import io.gnosis.data.repositories.CredentialsRepository +import io.gnosis.data.repositories.SafeRepository +import io.gnosis.data.repositories.TransactionLocalRepository +import io.gnosis.safe.TestLifecycleRule +import io.gnosis.safe.Tracker +import io.gnosis.safe.appDispatchers +import io.gnosis.safe.test +import io.gnosis.safe.ui.base.BaseStateViewModel +import io.gnosis.safe.ui.settings.app.SettingsHandler +import io.gnosis.safe.ui.settings.owner.list.OwnerViewData +import io.gnosis.safe.utils.BalanceFormatter +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.just +import io.mockk.mockk +import org.junit.Assert +import org.junit.Rule +import org.junit.Test +import pm.gnosis.model.Solidity +import pm.gnosis.models.Wei +import java.math.BigInteger + +class TxReviewViewModelTest { + + @get:Rule + val instantExecutorRule = TestLifecycleRule() + + private val safeRepository = mockk() + private val credentialsRepository = mockk() + private val localTxRepository = mockk() + private val settingsHandler = mockk() + private val rpcClient = mockk() + private val balanceFormatter = BalanceFormatter() + private val tracker = mockk() + + private lateinit var viewModel: TxReviewViewModel + + @Test + fun `loadDefaultKey() (success) should emit executionKey`() { + coEvery { safeRepository.getActiveSafe() } returns TEST_SAFE.apply { + signingOwners = listOf(Solidity.Address(BigInteger.ONE), Solidity.Address(BigInteger.TEN)) + } + coEvery { credentialsRepository.owners() } returns listOf(TEST_SAFE_OWNER1) + coEvery { rpcClient.getBalances(any()) } returns listOf(Wei(BigInteger.TEN.pow(Chain.DEFAULT_CHAIN.currency.decimals))) + + viewModel = TxReviewViewModel( + safeRepository, + credentialsRepository, + localTxRepository, + settingsHandler, + rpcClient, + balanceFormatter, + tracker, + appDispatchers + ) + + viewModel.loadDefaultKey() + + with(viewModel.state.test().values()) { + Assert.assertEquals( + DefaultKey( + OwnerViewData( + TEST_SAFE_OWNER1.address, + TEST_SAFE_OWNER1.name, + Owner.Type.IMPORTED, + "1 ${Chain.DEFAULT_CHAIN.currency.symbol}", + false + ) + ), this[0].viewAction + ) + } + } + + @Test + fun `loadDefaultKey() (getBalances failure) should emit LoadBalancesFailed`() { + coEvery { safeRepository.getActiveSafe() } returns TEST_SAFE.apply { + signingOwners = listOf(Solidity.Address(BigInteger.ONE), Solidity.Address(BigInteger.TEN)) + } + coEvery { credentialsRepository.owners() } returns listOf(TEST_SAFE_OWNER1) + coEvery { rpcClient.getBalances(any()) } throws Throwable() + + viewModel = TxReviewViewModel( + safeRepository, + credentialsRepository, + localTxRepository, + settingsHandler, + rpcClient, + balanceFormatter, + tracker, + appDispatchers + ) + + viewModel.loadDefaultKey() + + with(viewModel.state.test().values()) { + Assert.assertEquals( + BaseStateViewModel.ViewAction.ShowError( + LoadBalancesFailed + ), this[0].viewAction + ) + } + } + + @Test + fun `updateDefaultKey(different address) emit DefaultKey and track key changed`() { + coEvery { safeRepository.getActiveSafe() } returns TEST_SAFE.apply { + signingOwners = listOf(Solidity.Address(BigInteger.ONE), Solidity.Address(BigInteger.TEN)) + } + coEvery { credentialsRepository.owner(any()) } returns TEST_SAFE_OWNER1 + coEvery { tracker.logTxExecKeyChanged() } just Runs + + viewModel = TxReviewViewModel( + safeRepository, + credentialsRepository, + localTxRepository, + settingsHandler, + rpcClient, + balanceFormatter, + tracker, + appDispatchers + ) + + viewModel.updateDefaultKey(TEST_SAFE_OWNER2.address) + + with(viewModel.state.test().values()) { + Assert.assertEquals( + DefaultKey( + OwnerViewData( + TEST_SAFE_OWNER1.address, + TEST_SAFE_OWNER1.name, + Owner.Type.IMPORTED, + null, + false + ) + ), this[0].viewAction + ) + } + + coVerify(exactly = 1) { + tracker.logTxExecKeyChanged() + } + } + + @Test + fun `updateDefaultKey(same address) emit DefaultKey and not track key changed`() { + coEvery { safeRepository.getActiveSafe() } returns TEST_SAFE.apply { + signingOwners = listOf(Solidity.Address(BigInteger.ONE), Solidity.Address(BigInteger.TEN)) + } + coEvery { credentialsRepository.owners() } returns listOf(TEST_SAFE_OWNER1) + coEvery { credentialsRepository.owner(any()) } returns TEST_SAFE_OWNER1 + coEvery { rpcClient.getBalances(any()) } returns listOf(Wei(BigInteger.TEN.pow(Chain.DEFAULT_CHAIN.currency.decimals))) + coEvery { tracker.logTxExecKeyChanged() } just Runs + + viewModel = TxReviewViewModel( + safeRepository, + credentialsRepository, + localTxRepository, + settingsHandler, + rpcClient, + balanceFormatter, + tracker, + appDispatchers + ) + + viewModel.loadDefaultKey() + viewModel.updateDefaultKey(TEST_SAFE_OWNER1.address) + + with(viewModel.state.test().values()) { + Assert.assertEquals( + DefaultKey( + OwnerViewData( + TEST_SAFE_OWNER1.address, + TEST_SAFE_OWNER1.name, + Owner.Type.IMPORTED, + null, + false + ) + ), this[0].viewAction + ) + } + + coVerify(exactly = 0) { + tracker.logTxExecKeyChanged() + } + } + + companion object { + val TEST_SAFE = Safe( + Solidity.Address(BigInteger.ZERO), + "safe_name", + Chain.DEFAULT_CHAIN.chainId + ) + + val TEST_SAFE_OWNER1 = Owner( + Solidity.Address(BigInteger.ONE), + "owner1", + Owner.Type.IMPORTED + ) + + val TEST_SAFE_OWNER2 = Owner( + Solidity.Address(BigInteger.TEN), + "owner2", + Owner.Type.IMPORTED + ) + } +} From 1e16f14ef2addc7f97402ec9592675eda2ed07f1 Mon Sep 17 00:00:00 2001 From: Vitaly Katz Date: Tue, 29 Aug 2023 12:20:25 +0200 Subject: [PATCH 08/23] Add tests --- .../execution/TxReviewViewModelTest.kt | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/app/src/test/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModelTest.kt b/app/src/test/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModelTest.kt index baf5fdaef6..673bca709a 100644 --- a/app/src/test/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModelTest.kt +++ b/app/src/test/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModelTest.kt @@ -25,6 +25,7 @@ import org.junit.Rule import org.junit.Test import pm.gnosis.model.Solidity import pm.gnosis.models.Wei +import java.math.BigDecimal import java.math.BigInteger class TxReviewViewModelTest { @@ -109,7 +110,7 @@ class TxReviewViewModelTest { } @Test - fun `updateDefaultKey(different address) emit DefaultKey and track key changed`() { + fun `updateDefaultKey(different address) should emit DefaultKey and track key changed`() { coEvery { safeRepository.getActiveSafe() } returns TEST_SAFE.apply { signingOwners = listOf(Solidity.Address(BigInteger.ONE), Solidity.Address(BigInteger.TEN)) } @@ -149,7 +150,7 @@ class TxReviewViewModelTest { } @Test - fun `updateDefaultKey(same address) emit DefaultKey and not track key changed`() { + fun `updateDefaultKey(same address) should emit DefaultKey and not track key changed`() { coEvery { safeRepository.getActiveSafe() } returns TEST_SAFE.apply { signingOwners = listOf(Solidity.Address(BigInteger.ONE), Solidity.Address(BigInteger.TEN)) } @@ -191,6 +192,35 @@ class TxReviewViewModelTest { } } + @Test + fun `updateEstimationParams should emit UpdateFee`() { + coEvery { safeRepository.getActiveSafe() } returns TEST_SAFE.apply { + signingOwners = listOf(Solidity.Address(BigInteger.ONE), Solidity.Address(BigInteger.TEN)) + } + coEvery { tracker.logTxExecFieldsEdit(any()) } just Runs + + viewModel = TxReviewViewModel( + safeRepository, + credentialsRepository, + localTxRepository, + settingsHandler, + rpcClient, + balanceFormatter, + tracker, + appDispatchers + ) + + viewModel.updateEstimationParams(BigInteger.ZERO, BigInteger.ZERO, BigDecimal.ZERO, BigDecimal.ZERO) + + with(viewModel.state.test().values()) { + Assert.assertEquals( + UpdateFee( + "0 ${Chain.DEFAULT_CHAIN.currency.symbol}" + ), this[0].viewAction + ) + } + } + companion object { val TEST_SAFE = Safe( Solidity.Address(BigInteger.ZERO), From 1de5eb89cc62cdf849895ea58c66442182f70aa4 Mon Sep 17 00:00:00 2001 From: Vitaly Katz Date: Tue, 29 Aug 2023 14:05:43 +0200 Subject: [PATCH 09/23] Add tests --- .../execution/TxReviewViewModelTest.kt | 68 ++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/app/src/test/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModelTest.kt b/app/src/test/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModelTest.kt index 673bca709a..ba64d3b740 100644 --- a/app/src/test/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModelTest.kt +++ b/app/src/test/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModelTest.kt @@ -1,9 +1,14 @@ package io.gnosis.safe.ui.transactions.execution import io.gnosis.data.backend.rpc.RpcClient +import io.gnosis.data.backend.rpc.models.EstimationParams +import io.gnosis.data.models.AddressInfo import io.gnosis.data.models.Chain import io.gnosis.data.models.Owner import io.gnosis.data.models.Safe +import io.gnosis.data.models.transaction.DetailedExecutionInfo +import io.gnosis.data.models.transaction.Operation +import io.gnosis.data.models.transaction.TxData import io.gnosis.data.repositories.CredentialsRepository import io.gnosis.data.repositories.SafeRepository import io.gnosis.data.repositories.TransactionLocalRepository @@ -24,9 +29,12 @@ import org.junit.Assert import org.junit.Rule import org.junit.Test import pm.gnosis.model.Solidity +import pm.gnosis.models.Transaction import pm.gnosis.models.Wei import java.math.BigDecimal import java.math.BigInteger +import java.time.Instant +import java.util.Date class TxReviewViewModelTest { @@ -44,7 +52,7 @@ class TxReviewViewModelTest { private lateinit var viewModel: TxReviewViewModel @Test - fun `loadDefaultKey() (success) should emit executionKey`() { + fun `loadDefaultKey(success) should emit executionKey`() { coEvery { safeRepository.getActiveSafe() } returns TEST_SAFE.apply { signingOwners = listOf(Solidity.Address(BigInteger.ONE), Solidity.Address(BigInteger.TEN)) } @@ -80,7 +88,7 @@ class TxReviewViewModelTest { } @Test - fun `loadDefaultKey() (getBalances failure) should emit LoadBalancesFailed`() { + fun `loadDefaultKey(getBalances failure) should emit LoadBalancesFailed`() { coEvery { safeRepository.getActiveSafe() } returns TEST_SAFE.apply { signingOwners = listOf(Solidity.Address(BigInteger.ONE), Solidity.Address(BigInteger.TEN)) } @@ -221,6 +229,62 @@ class TxReviewViewModelTest { } } + @Test + fun `estimate(success) should emit UpdateFee`() { + coEvery { safeRepository.getActiveSafe() } returns TEST_SAFE.apply { + signingOwners = listOf(Solidity.Address(BigInteger.ONE), Solidity.Address(BigInteger.TEN)) + } + coEvery { credentialsRepository.owners() } returns listOf(TEST_SAFE_OWNER1) + coEvery { rpcClient.getBalances(any()) } returns listOf(Wei(BigInteger.TEN.pow(Chain.DEFAULT_CHAIN.currency.decimals))) + coEvery { rpcClient.updateRpcUrl(any()) } just Runs + coEvery { rpcClient.ethTransaction(any(), any(), any(), any()) } returns Transaction.Legacy( + Chain.DEFAULT_CHAIN.chainId, + Solidity.Address(BigInteger.ZERO), + Solidity.Address(BigInteger.ZERO), + Wei(BigInteger.ZERO) + ) + coEvery { rpcClient.estimate(any()) } returns EstimationParams( + BigInteger.ZERO, + BigInteger.ZERO, + BigInteger.ZERO, + true, + BigInteger.ZERO + ) + + viewModel = TxReviewViewModel( + safeRepository, + credentialsRepository, + localTxRepository, + settingsHandler, + rpcClient, + balanceFormatter, + tracker, + appDispatchers + ) + + viewModel.setTxData( + TxData( + "", + null, + AddressInfo(value = Solidity.Address(BigInteger.ZERO)), + BigInteger.ZERO, + Operation.CALL + ), + DetailedExecutionInfo.MultisigExecutionDetails( + Date.from(Instant.now()), + BigInteger.ZERO + ) + ) + + with(viewModel.state.test().values()) { + Assert.assertEquals( + UpdateFee( + "0 ${Chain.DEFAULT_CHAIN.currency.symbol}" + ), this[0].viewAction + ) + } + } + companion object { val TEST_SAFE = Safe( Solidity.Address(BigInteger.ZERO), From 360169f150e85669ead7782014f692b950a9d1ad Mon Sep 17 00:00:00 2001 From: Vitaly Katz Date: Tue, 29 Aug 2023 14:38:33 +0200 Subject: [PATCH 10/23] Add test --- .../TransactionDetailsViewModelTest.kt | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/app/src/test/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModelTest.kt b/app/src/test/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModelTest.kt index 774d0cd526..4fc7cbf3b5 100644 --- a/app/src/test/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModelTest.kt +++ b/app/src/test/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModelTest.kt @@ -8,6 +8,7 @@ import io.gnosis.data.models.Chain import io.gnosis.data.models.Owner import io.gnosis.data.models.Safe import io.gnosis.data.models.SafeInfo +import io.gnosis.data.models.TransactionLocal import io.gnosis.data.models.VersionState import io.gnosis.data.models.transaction.DetailedExecutionInfo import io.gnosis.data.models.transaction.TransactionDetails @@ -174,6 +175,81 @@ class TransactionDetailsViewModelTest { } } + @Test + fun `loadDetails (successful, awaiting execution with local pending tx) should emit txDetails with pending state`() = runTest(UnconfinedTestDispatcher()) { + val transactionDetailsDto = adapter.readJsonFrom("tx_details_transfer.json") + val transactionDetails = toTransactionDetails(transactionDetailsDto).copy(txStatus = TransactionStatus.AWAITING_EXECUTION) + val someAddress = "0x1230B3d59858296A31053C1b8562Ecf89A2f888b".asEthereumAddress()!! + + coEvery { + transactionRepository.getTransactionDetails( + any(), + any() + ) + } returns transactionDetails + coEvery { transactionLocalRepository.updateLocalTx(any(), any()) } returns TransactionLocal( + CHAIN_ID, + Solidity.Address(BigInteger.ZERO), + BigInteger.ZERO, + (transactionDetails.detailedExecutionInfo as DetailedExecutionInfo.MultisigExecutionDetails).safeTxHash, + "", + TransactionStatus.PENDING, + 0 + ) + coEvery { transactionLocalRepository.delete(any()) } just Runs + coEvery { safeRepository.getSafes() } returns emptyList() + coEvery { credentialsRepository.owners() } returns listOf() + coEvery { safeRepository.getActiveSafe() } returns Safe(someAddress, "safe_name", CHAIN_ID) + coEvery { safeRepository.getSafeInfo(any()) } returns SafeInfo( + AddressInfo(Solidity.Address(BigInteger.ONE)), + BigInteger.ONE, + 1, + listOf( + AddressInfo(Solidity.Address(BigInteger.ONE)) + ), + AddressInfo(Solidity.Address(BigInteger.ONE)), + listOf(AddressInfo(Solidity.Address(BigInteger.ONE))), + AddressInfo(Solidity.Address(BigInteger.ONE)), + null, + "1.1.1", + VersionState.OUTDATED + ) + val expectedTransactionInfoViewData = + transactionDetails.toTransactionDetailsViewData( + emptyList(), + canSign = false, + canExecute = false, + nextInLine = false, + owners = emptyList(), + hasOwnerKey = false + ).copy(txStatus = TransactionStatus.PENDING) + + viewModel = TransactionDetailsViewModel( + transactionRepository, + transactionLocalRepository, + safeRepository, + credentialsRepository, + settingsHandler, + tracker, + appDispatchers + ) + + viewModel.loadDetails("tx_details_id") + + with(viewModel.state.test().values()) { + assertEquals( + UpdateDetails(txDetails = expectedTransactionInfoViewData), + this[0].viewAction + ) + } + coVerify(exactly = 1) { + transactionRepository.getTransactionDetails( + CHAIN_ID, + "tx_details_id" + ) + } + } + @Test fun `isAwaitingOwnerConfirmation (wrong status) should return false`() = runTest(UnconfinedTestDispatcher()) { From cd0de1bea885616668576acb23346aad7c2f44d9 Mon Sep 17 00:00:00 2001 From: Vitaly Katz Date: Tue, 29 Aug 2023 15:02:16 +0200 Subject: [PATCH 11/23] Add test --- .../execution/TxReviewViewModelTest.kt | 73 ++++++++++++++++++- .../data/db/daos/TransactionLocalDao.kt | 1 + 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/app/src/test/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModelTest.kt b/app/src/test/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModelTest.kt index ba64d3b740..fe646583e0 100644 --- a/app/src/test/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModelTest.kt +++ b/app/src/test/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModelTest.kt @@ -16,7 +16,7 @@ import io.gnosis.safe.TestLifecycleRule import io.gnosis.safe.Tracker import io.gnosis.safe.appDispatchers import io.gnosis.safe.test -import io.gnosis.safe.ui.base.BaseStateViewModel +import io.gnosis.safe.ui.base.BaseStateViewModel.ViewAction.* import io.gnosis.safe.ui.settings.app.SettingsHandler import io.gnosis.safe.ui.settings.owner.list.OwnerViewData import io.gnosis.safe.utils.BalanceFormatter @@ -28,6 +28,7 @@ import io.mockk.mockk import org.junit.Assert import org.junit.Rule import org.junit.Test +import pm.gnosis.crypto.ECDSASignature import pm.gnosis.model.Solidity import pm.gnosis.models.Transaction import pm.gnosis.models.Wei @@ -110,7 +111,7 @@ class TxReviewViewModelTest { with(viewModel.state.test().values()) { Assert.assertEquals( - BaseStateViewModel.ViewAction.ShowError( + ShowError( LoadBalancesFailed ), this[0].viewAction ) @@ -285,6 +286,74 @@ class TxReviewViewModelTest { } } + @Test + fun `sendForExecution(success) should send ethTx, save it locally, and emit NavigateTo`() { + coEvery { safeRepository.getActiveSafe() } returns TEST_SAFE.apply { + signingOwners = listOf(Solidity.Address(BigInteger.ONE), Solidity.Address(BigInteger.TEN)) + } + coEvery { credentialsRepository.owners() } returns listOf(TEST_SAFE_OWNER1) + coEvery { credentialsRepository.owner(any()) } returns TEST_SAFE_OWNER1 + coEvery { credentialsRepository.signWithOwner(any(), any()) } returns ECDSASignature(BigInteger.ONE, BigInteger.ONE) + coEvery { settingsHandler.usePasscode } returns false + coEvery { rpcClient.getBalances(any()) } returns listOf(Wei(BigInteger.TEN.pow(Chain.DEFAULT_CHAIN.currency.decimals))) + coEvery { rpcClient.updateRpcUrl(any()) } just Runs + coEvery { rpcClient.ethTransaction(any(), any(), any(), any()) } returns Transaction.Legacy( + Chain.DEFAULT_CHAIN.chainId, + Solidity.Address(BigInteger.ZERO), + Solidity.Address(BigInteger.ZERO), + Wei(BigInteger.ZERO) + ) + coEvery { rpcClient.estimate(any()) } returns EstimationParams( + BigInteger.ZERO, + BigInteger.ZERO, + BigInteger.ZERO, + true, + BigInteger.ZERO + ) + coEvery { rpcClient.send(any(), any()) } returns "0x0" + coEvery { localTxRepository.saveLocally(any(), any(), any(), any(), any()) } just Runs + coEvery { tracker.logTxExecSubmitted() } just Runs + + viewModel = TxReviewViewModel( + safeRepository, + credentialsRepository, + localTxRepository, + settingsHandler, + rpcClient, + balanceFormatter, + tracker, + appDispatchers + ) + + viewModel.setTxData( + TxData( + "", + null, + AddressInfo(value = Solidity.Address(BigInteger.ZERO)), + BigInteger.ZERO, + Operation.CALL + ), + DetailedExecutionInfo.MultisigExecutionDetails( + Date.from(Instant.now()), + BigInteger.ZERO + ) + ) + viewModel.signAndExecute() + + with(viewModel.state.test().values()) { + Assert.assertEquals( + NavigateTo( + TxReviewFragmentDirections.actionTxReviewFragmentToTxSuccessFragment() + ), this[0].viewAction + ) + } + + coVerify(exactly = 1) { + tracker.logTxExecSubmitted() + localTxRepository.saveLocally(any(), any(), any(), any(), any()) + } + } + companion object { val TEST_SAFE = Safe( Solidity.Address(BigInteger.ZERO), diff --git a/data/src/main/java/io/gnosis/data/db/daos/TransactionLocalDao.kt b/data/src/main/java/io/gnosis/data/db/daos/TransactionLocalDao.kt index eb4fc8314f..cb305f6b19 100644 --- a/data/src/main/java/io/gnosis/data/db/daos/TransactionLocalDao.kt +++ b/data/src/main/java/io/gnosis/data/db/daos/TransactionLocalDao.kt @@ -17,6 +17,7 @@ interface TransactionLocalDao { @Delete suspend fun delete(tx: TransactionLocal) + @Query("DELETE FROM ${TransactionLocal.TABLE_NAME} WHERE ${TransactionLocal.COL_CHAIN_ID} = :chainId AND ${TransactionLocal.COL_SAFE_ADDRESS} = :safeAddress") suspend fun clearOldRecords(chainId: BigInteger, safeAddress: Solidity.Address) From 173aa94b1cd70a0a8b5c92a7dd2477d975210cd7 Mon Sep 17 00:00:00 2001 From: Vitaly Katz Date: Tue, 29 Aug 2023 16:08:08 +0200 Subject: [PATCH 12/23] Use latest svalinn release --- buildsystem/versions.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildsystem/versions.gradle b/buildsystem/versions.gradle index 3e896a69cb..dc8aa854f4 100644 --- a/buildsystem/versions.gradle +++ b/buildsystem/versions.gradle @@ -44,7 +44,7 @@ ext { play_services_auth : '17.0.0', retrofit : '2.6.2', status_keycard : '3.0.1', - svalinn : 'fe7817a4d2',//''v0.16.0', + svalinn : 'v0.16.1', timber : '4.7.1', zxing : '3.3.1', play_core : '1.9.1', From 1472c34c4aecef5a72e14cd3686e4843d0724429 Mon Sep 17 00:00:00 2001 From: Vitaly Katz Date: Tue, 29 Aug 2023 16:18:45 +0200 Subject: [PATCH 13/23] Add test for default owner with highest balance --- .../execution/TxReviewViewModelTest.kt | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/app/src/test/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModelTest.kt b/app/src/test/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModelTest.kt index fe646583e0..e46a6ccba6 100644 --- a/app/src/test/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModelTest.kt +++ b/app/src/test/java/io/gnosis/safe/ui/transactions/execution/TxReviewViewModelTest.kt @@ -53,7 +53,7 @@ class TxReviewViewModelTest { private lateinit var viewModel: TxReviewViewModel @Test - fun `loadDefaultKey(success) should emit executionKey`() { + fun `loadDefaultKey(success, one owner key) should emit executionKey`() { coEvery { safeRepository.getActiveSafe() } returns TEST_SAFE.apply { signingOwners = listOf(Solidity.Address(BigInteger.ONE), Solidity.Address(BigInteger.TEN)) } @@ -88,6 +88,45 @@ class TxReviewViewModelTest { } } + @Test + fun `loadDefaultKey(success, several owner keys) should emit executionKey with highest balance`() { + coEvery { safeRepository.getActiveSafe() } returns TEST_SAFE.apply { + signingOwners = listOf(Solidity.Address(BigInteger.ONE), Solidity.Address(BigInteger.TEN)) + } + coEvery { credentialsRepository.owners() } returns listOf(TEST_SAFE_OWNER1, TEST_SAFE_OWNER2) + coEvery { rpcClient.getBalances(listOf(TEST_SAFE_OWNER1.address, TEST_SAFE_OWNER2.address)) } returns listOf( + Wei(BigInteger.ONE), + Wei(BigInteger.TEN.pow(Chain.DEFAULT_CHAIN.currency.decimals)) + ) + + viewModel = TxReviewViewModel( + safeRepository, + credentialsRepository, + localTxRepository, + settingsHandler, + rpcClient, + balanceFormatter, + tracker, + appDispatchers + ) + + viewModel.loadDefaultKey() + + with(viewModel.state.test().values()) { + Assert.assertEquals( + DefaultKey( + OwnerViewData( + TEST_SAFE_OWNER2.address, + TEST_SAFE_OWNER2.name, + Owner.Type.IMPORTED, + "1 ${Chain.DEFAULT_CHAIN.currency.symbol}", + false + ) + ), this[0].viewAction + ) + } + } + @Test fun `loadDefaultKey(getBalances failure) should emit LoadBalancesFailed`() { coEvery { safeRepository.getActiveSafe() } returns TEST_SAFE.apply { From aa399b6a8907cbcf2145b3b1b764ba0ffddfb94b Mon Sep 17 00:00:00 2001 From: Vitaly Katz Date: Tue, 29 Aug 2023 16:50:05 +0200 Subject: [PATCH 14/23] Add test --- .../TransactionDetailsViewModelTest.kt | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/app/src/test/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModelTest.kt b/app/src/test/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModelTest.kt index 4fc7cbf3b5..615db09230 100644 --- a/app/src/test/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModelTest.kt +++ b/app/src/test/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModelTest.kt @@ -248,6 +248,85 @@ class TransactionDetailsViewModelTest { "tx_details_id" ) } + coVerify(exactly = 0) { + transactionLocalRepository.delete(any()) + } + } + + @Test + fun `loadDetails (successful, success with local pending tx) should emit txDetails and delete local tx`() = runTest(UnconfinedTestDispatcher()) { + val transactionDetailsDto = adapter.readJsonFrom("tx_details_transfer.json") + val transactionDetails = toTransactionDetails(transactionDetailsDto) + val someAddress = "0x1230B3d59858296A31053C1b8562Ecf89A2f888b".asEthereumAddress()!! + + coEvery { + transactionRepository.getTransactionDetails( + any(), + any() + ) + } returns transactionDetails + coEvery { transactionLocalRepository.updateLocalTx(any(), any()) } returns TransactionLocal( + CHAIN_ID, + Solidity.Address(BigInteger.ZERO), + BigInteger.ZERO, + (transactionDetails.detailedExecutionInfo as DetailedExecutionInfo.MultisigExecutionDetails).safeTxHash, + "", + TransactionStatus.PENDING, + 0 + ) + coEvery { transactionLocalRepository.delete(any()) } just Runs + coEvery { safeRepository.getSafes() } returns emptyList() + coEvery { credentialsRepository.owners() } returns listOf() + coEvery { safeRepository.getActiveSafe() } returns Safe(someAddress, "safe_name", CHAIN_ID) + coEvery { safeRepository.getSafeInfo(any()) } returns SafeInfo( + AddressInfo(Solidity.Address(BigInteger.ONE)), + BigInteger.ONE, + 1, + listOf( + AddressInfo(Solidity.Address(BigInteger.ONE)) + ), + AddressInfo(Solidity.Address(BigInteger.ONE)), + listOf(AddressInfo(Solidity.Address(BigInteger.ONE))), + AddressInfo(Solidity.Address(BigInteger.ONE)), + null, + "1.1.1", + VersionState.OUTDATED + ) + val expectedTransactionInfoViewData = + transactionDetails.toTransactionDetailsViewData( + emptyList(), + canSign = false, + canExecute = false, + nextInLine = false, + owners = emptyList(), + hasOwnerKey = false + ) + + viewModel = TransactionDetailsViewModel( + transactionRepository, + transactionLocalRepository, + safeRepository, + credentialsRepository, + settingsHandler, + tracker, + appDispatchers + ) + + viewModel.loadDetails("tx_details_id") + + with(viewModel.state.test().values()) { + assertEquals( + UpdateDetails(txDetails = expectedTransactionInfoViewData), + this[0].viewAction + ) + } + coVerify(exactly = 1) { + transactionRepository.getTransactionDetails( + CHAIN_ID, + "tx_details_id" + ) + transactionLocalRepository.delete(any()) + } } @Test From 6ba1d285fe0ad6cb7b9236a5a8fc2382db8b0cb0 Mon Sep 17 00:00:00 2001 From: Vitaly Katz Date: Tue, 29 Aug 2023 16:52:07 +0200 Subject: [PATCH 15/23] PR comments --- .../ui/transactions/details/TransactionDetailsViewModelTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/test/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModelTest.kt b/app/src/test/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModelTest.kt index 615db09230..95e706b7c0 100644 --- a/app/src/test/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModelTest.kt +++ b/app/src/test/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModelTest.kt @@ -172,6 +172,7 @@ class TransactionDetailsViewModelTest { CHAIN_ID, "tx_details_id" ) + transactionLocalRepository.updateLocalTx(any(), any()) } } From 4017efef1bb7535ab6014c0758ec6f0a1175fd86 Mon Sep 17 00:00:00 2001 From: Vitaly Katz Date: Tue, 29 Aug 2023 17:09:11 +0200 Subject: [PATCH 16/23] Extract list entry mapping --- .../transactions/TransactionListViewModel.kt | 113 ++++++++++-------- 1 file changed, 62 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/io/gnosis/safe/ui/transactions/TransactionListViewModel.kt b/app/src/main/java/io/gnosis/safe/ui/transactions/TransactionListViewModel.kt index 62de8a2e82..f8abfa9f51 100644 --- a/app/src/main/java/io/gnosis/safe/ui/transactions/TransactionListViewModel.kt +++ b/app/src/main/java/io/gnosis/safe/ui/transactions/TransactionListViewModel.kt @@ -1,6 +1,7 @@ package io.gnosis.safe.ui.transactions import androidx.annotation.StringRes +import androidx.annotation.VisibleForTesting import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.cachedIn @@ -89,65 +90,15 @@ class TransactionListViewModel owners: List, type: TransactionPagingSource.Type ): Flow> { - var txLocal: TransactionLocal? = null if (type == TransactionPagingSource.Type.QUEUE ) { txLocal = transactionLocalRepository.updateLocalTxLatest(safe) } - val safeTxItems: Flow> = transactionsPager.getTransactionsStream(safe, type) .map { pagingData -> pagingData .map { txListEntry -> - when (txListEntry) { - is TxListEntry.Transaction -> { - // conflict is resolved if there is local tx with same nonce - // that was submitted for execution - if (txLocal?.safeTxNonce == txListEntry.transaction.executionInfo?.nonce) { - // use submittedAt timestamp to distinguish between conflicting transactions - if (txLocal?.submittedAt == txListEntry.transaction.timestamp.time && txListEntry.transaction.txStatus == TransactionStatus.AWAITING_EXECUTION) { - val tx = txListEntry.transaction.copy(txStatus = TransactionStatus.PENDING) - txListEntry.transaction - getTransactionView( - chain = safe.chain, - transaction = tx, - safes = safes, - needsYourConfirmation = false, - isConflict = false, - localOwners = owners - ) - } else { - TransactionView.Unknown - } - } else { - val isConflict = txListEntry.conflictType != ConflictType.None - val txView = - getTransactionView( - chain = safe.chain, - transaction = txListEntry.transaction, - safes = safes, - needsYourConfirmation = txListEntry.transaction.canBeSignedByAnyOwner(owners), - isConflict = isConflict, - localOwners = owners - ) - if (isConflict) { - TransactionView.Conflict(txView, txListEntry.conflictType) - } else txView - } - } - is TxListEntry.DateLabel -> TransactionView.SectionDateHeader(date = txListEntry.timestamp) - is TxListEntry.Label -> TransactionView.SectionLabelHeader(label = txListEntry.label) - is TxListEntry.ConflictHeader -> { - // conflict is resolved if there is local tx with same nonce - // that was submitted for execution - if (txLocal?.safeTxNonce == txListEntry.nonce.toBigInteger()) { - TransactionView.Unknown - } else { - TransactionView.SectionConflictHeader(nonce = txListEntry.nonce) - } - } - else -> TransactionView.Unknown - } + mapTxListEntry(txListEntry, safe, safes, owners, type, txLocal) } .filter { it !is TransactionView.Unknown } } @@ -156,6 +107,66 @@ class TransactionListViewModel return safeTxItems } + @VisibleForTesting + fun mapTxListEntry( + txListEntry: TxListEntry, + safe: Safe, + safes: List, + owners: List, + type: TransactionPagingSource.Type, + txLocal: TransactionLocal? = null + ): TransactionView { + return when (txListEntry) { + is TxListEntry.Transaction -> { + // conflict is resolved if there is local tx with same nonce + // that was submitted for execution + if (txLocal?.safeTxNonce == txListEntry.transaction.executionInfo?.nonce) { + // use submittedAt timestamp to distinguish between conflicting transactions + if (txLocal?.submittedAt == txListEntry.transaction.timestamp.time && txListEntry.transaction.txStatus == TransactionStatus.AWAITING_EXECUTION) { + val tx = txListEntry.transaction.copy(txStatus = TransactionStatus.PENDING) + txListEntry.transaction + getTransactionView( + chain = safe.chain, + transaction = tx, + safes = safes, + needsYourConfirmation = false, + isConflict = false, + localOwners = owners + ) + } else { + TransactionView.Unknown + } + } else { + val isConflict = txListEntry.conflictType != ConflictType.None + val txView = + getTransactionView( + chain = safe.chain, + transaction = txListEntry.transaction, + safes = safes, + needsYourConfirmation = txListEntry.transaction.canBeSignedByAnyOwner(owners), + isConflict = isConflict, + localOwners = owners + ) + if (isConflict) { + TransactionView.Conflict(txView, txListEntry.conflictType) + } else txView + } + } + is TxListEntry.DateLabel -> TransactionView.SectionDateHeader(date = txListEntry.timestamp) + is TxListEntry.Label -> TransactionView.SectionLabelHeader(label = txListEntry.label) + is TxListEntry.ConflictHeader -> { + // conflict is resolved if there is local tx with same nonce + // that was submitted for execution + if (txLocal?.safeTxNonce == txListEntry.nonce.toBigInteger()) { + TransactionView.Unknown + } else { + TransactionView.SectionConflictHeader(nonce = txListEntry.nonce) + } + } + else -> TransactionView.Unknown + } + } + fun getTransactionView( chain: Chain, transaction: Transaction, From 85a5d49812a1de6400133e9b7bf5e297972308e6 Mon Sep 17 00:00:00 2001 From: Vitaly Katz Date: Tue, 29 Aug 2023 17:11:05 +0200 Subject: [PATCH 17/23] Cleanup --- .../io/gnosis/safe/ui/transactions/TransactionListViewModel.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/io/gnosis/safe/ui/transactions/TransactionListViewModel.kt b/app/src/main/java/io/gnosis/safe/ui/transactions/TransactionListViewModel.kt index f8abfa9f51..a43a4addb5 100644 --- a/app/src/main/java/io/gnosis/safe/ui/transactions/TransactionListViewModel.kt +++ b/app/src/main/java/io/gnosis/safe/ui/transactions/TransactionListViewModel.kt @@ -98,7 +98,7 @@ class TransactionListViewModel .map { pagingData -> pagingData .map { txListEntry -> - mapTxListEntry(txListEntry, safe, safes, owners, type, txLocal) + mapTxListEntry(txListEntry, safe, safes, owners, txLocal) } .filter { it !is TransactionView.Unknown } } @@ -113,7 +113,6 @@ class TransactionListViewModel safe: Safe, safes: List, owners: List, - type: TransactionPagingSource.Type, txLocal: TransactionLocal? = null ): TransactionView { return when (txListEntry) { From e4d8898b2d27bb5af573a5d3f192d44a812f65f6 Mon Sep 17 00:00:00 2001 From: Vitaly Katz Date: Tue, 29 Aug 2023 17:59:40 +0200 Subject: [PATCH 18/23] Add test --- .../TransactionListViewModelTest.kt | 87 ++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/app/src/test/java/io/gnosis/safe/ui/transactions/TransactionListViewModelTest.kt b/app/src/test/java/io/gnosis/safe/ui/transactions/TransactionListViewModelTest.kt index b94b6086cd..c623005730 100644 --- a/app/src/test/java/io/gnosis/safe/ui/transactions/TransactionListViewModelTest.kt +++ b/app/src/test/java/io/gnosis/safe/ui/transactions/TransactionListViewModelTest.kt @@ -7,8 +7,10 @@ import io.gnosis.data.models.Chain import io.gnosis.data.models.Owner import io.gnosis.data.models.Page import io.gnosis.data.models.Safe +import io.gnosis.data.models.TransactionLocal import io.gnosis.data.models.assets.TokenInfo import io.gnosis.data.models.assets.TokenType +import io.gnosis.data.models.transaction.ConflictType import io.gnosis.data.models.transaction.DataDecoded import io.gnosis.data.models.transaction.ExecutionInfo import io.gnosis.data.models.transaction.Param @@ -196,6 +198,86 @@ class TransactionListViewModelTest { } } + @Test + fun `mapTxListEntry(Transaction, localTx is null) should map to TransactionView with same state`() { + val safe = Safe(Solidity.Address(BigInteger.ONE), "test_safe").apply { + chain = CHAIN + } + + val transfer = buildTransfer(status = AWAITING_EXECUTION, confirmations = 1, threshold = 1) + val txListEnry = TxListEntry.Transaction(transfer, ConflictType.None) + + transactionListViewModel = + TransactionListViewModel(transactionPagingProvider, transactionLocalRepository, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) + + val transactionView = transactionListViewModel.mapTxListEntry(txListEnry, safe, listOf(safe), listOf()) + + assertEquals(TransactionView.TransferQueued( + "", + AWAITING_EXECUTION, + Chain.DEFAULT_CHAIN, + R.string.tx_status_needs_execution, + R.color.warning, + "< -0.00001 ETH", + Date(0), + R.drawable.ic_arrow_red_10dp, + R.string.tx_list_send, + R.color.label_primary, + 1, + 1, + R.color.success, + R.drawable.ic_confirmations_green_16dp, + "1" + ), transactionView) + } + + @Test + fun `mapTxListEntry(Transaction, localTx) should map to TransactionView with pending state`() { + val safe = Safe(Solidity.Address(BigInteger.ONE), "test_safe").apply { + chain = CHAIN + } + + val transfer = buildTransfer(status = AWAITING_EXECUTION, confirmations = 1, threshold = 1) + val txListEnry = TxListEntry.Transaction(transfer, ConflictType.None) + + transactionListViewModel = + TransactionListViewModel(transactionPagingProvider, transactionLocalRepository, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) + + val transactionView = transactionListViewModel.mapTxListEntry( + txListEnry, + safe, + listOf(safe), + listOf(), + TransactionLocal( + CHAIN.chainId, + Solidity.Address(BigInteger.ZERO), + BigInteger.ONE, + "", + "", + PENDING, + 0 + ) + ) + + assertEquals(TransactionView.TransferQueued( + "", + PENDING, + Chain.DEFAULT_CHAIN, + R.string.tx_status_pending, + R.color.warning, + "< -0.00001 ETH", + Date(0), + R.drawable.ic_arrow_red_10dp, + R.string.tx_list_send, + R.color.label_primary, + 1, + 1, + R.color.success, + R.drawable.ic_confirmations_green_16dp, + "1" + ), transactionView) + } + @Test fun `mapToTransactionView (tx list with no transfer) should map to empty list`() { @@ -1105,7 +1187,8 @@ class TransactionListViewModelTest { value: BigInteger = BigInteger.ONE, date: Date = Date(0), serviceTokenInfo: TokenInfo = NATIVE_CURRENCY_INFO, - nonce: BigInteger = defaultNonce + nonce: BigInteger = defaultNonce, + threshold: Int = defaultThreshold ): Transaction = Transaction( id = "", @@ -1118,7 +1201,7 @@ class TransactionListViewModelTest { ), executionInfo = ExecutionInfo( nonce = nonce, - confirmationsRequired = defaultThreshold, + confirmationsRequired = threshold, confirmationsSubmitted = confirmations, missingSigners = missingSigners ), From 2b238be0fcacc0b67862f0af97818ec1ce01e807 Mon Sep 17 00:00:00 2001 From: Vitaly Katz Date: Tue, 29 Aug 2023 18:09:41 +0200 Subject: [PATCH 19/23] Add test --- .../TransactionListViewModelTest.kt | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/app/src/test/java/io/gnosis/safe/ui/transactions/TransactionListViewModelTest.kt b/app/src/test/java/io/gnosis/safe/ui/transactions/TransactionListViewModelTest.kt index c623005730..1b5b0b7d0a 100644 --- a/app/src/test/java/io/gnosis/safe/ui/transactions/TransactionListViewModelTest.kt +++ b/app/src/test/java/io/gnosis/safe/ui/transactions/TransactionListViewModelTest.kt @@ -198,6 +198,50 @@ class TransactionListViewModelTest { } } + @Test + fun `load - (queue) should check latest local tx`() { + val safe = Safe(Solidity.Address(BigInteger.ONE), "test_safe").apply { + chain = CHAIN + } + val testObserver = TestLiveDataObserver() + coEvery { safeRepository.activeSafeFlow() } returns flow { emit(safe) } + coEvery { safeRepository.getActiveSafe() } returns safe + coEvery { safeRepository.getSafes() } returns listOf(safe) + coEvery { credentialsRepository.ownerCount() } returns 0 + coEvery { credentialsRepository.owners() } returns listOf() + coEvery { + transactionPagingProvider.getTransactionsStream( + any(), + TransactionPagingSource.Type.QUEUE + ) + } returns flow { emit(PagingData.empty()) } + coEvery { transactionLocalRepository.updateLocalTxLatest(any()) } returns TransactionLocal( + CHAIN.chainId, + Solidity.Address(BigInteger.ZERO), + BigInteger.ONE, + "", + "", + PENDING, + 0 + ) + transactionListViewModel = + TransactionListViewModel(transactionPagingProvider, transactionLocalRepository, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) + transactionListViewModel.load(TransactionPagingSource.Type.QUEUE) + + transactionListViewModel.state.observeForever(testObserver) + + with(testObserver.values()[0]) { + assertEquals(true, viewAction is LoadTransactions) + } + coVerifySequence { + safeRepository.activeSafeFlow() + safeRepository.getActiveSafe() + safeRepository.getSafes() + credentialsRepository.owners() + transactionLocalRepository.updateLocalTxLatest(any()) + } + } + @Test fun `mapTxListEntry(Transaction, localTx is null) should map to TransactionView with same state`() { val safe = Safe(Solidity.Address(BigInteger.ONE), "test_safe").apply { From 4260f5f8a5969b4fe4e0bd66194dbe271138a6ac Mon Sep 17 00:00:00 2001 From: Vitaly Katz Date: Tue, 29 Aug 2023 18:20:23 +0200 Subject: [PATCH 20/23] Cleanup --- .../ui/transactions/TransactionListViewModelTest.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/test/java/io/gnosis/safe/ui/transactions/TransactionListViewModelTest.kt b/app/src/test/java/io/gnosis/safe/ui/transactions/TransactionListViewModelTest.kt index 1b5b0b7d0a..a1dcee4808 100644 --- a/app/src/test/java/io/gnosis/safe/ui/transactions/TransactionListViewModelTest.kt +++ b/app/src/test/java/io/gnosis/safe/ui/transactions/TransactionListViewModelTest.kt @@ -254,7 +254,12 @@ class TransactionListViewModelTest { transactionListViewModel = TransactionListViewModel(transactionPagingProvider, transactionLocalRepository, safeRepository, credentialsRepository, balanceFormatter, appDispatchers) - val transactionView = transactionListViewModel.mapTxListEntry(txListEnry, safe, listOf(safe), listOf()) + val transactionView = transactionListViewModel.mapTxListEntry( + txListEnry, + safe, + listOf(safe), + listOf() + ) assertEquals(TransactionView.TransferQueued( "", @@ -263,7 +268,7 @@ class TransactionListViewModelTest { R.string.tx_status_needs_execution, R.color.warning, "< -0.00001 ETH", - Date(0), + transfer.timestamp, R.drawable.ic_arrow_red_10dp, R.string.tx_list_send, R.color.label_primary, @@ -310,7 +315,7 @@ class TransactionListViewModelTest { R.string.tx_status_pending, R.color.warning, "< -0.00001 ETH", - Date(0), + transfer.timestamp, R.drawable.ic_arrow_red_10dp, R.string.tx_list_send, R.color.label_primary, From d77cf7048c7d96cf45400d68f986087dcd8e6ec5 Mon Sep 17 00:00:00 2001 From: Vitaly Katz Date: Tue, 29 Aug 2023 18:49:01 +0200 Subject: [PATCH 21/23] Temp fix --- .../TransactionListViewModelTest.kt | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/app/src/test/java/io/gnosis/safe/ui/transactions/TransactionListViewModelTest.kt b/app/src/test/java/io/gnosis/safe/ui/transactions/TransactionListViewModelTest.kt index a1dcee4808..8ba204d2e7 100644 --- a/app/src/test/java/io/gnosis/safe/ui/transactions/TransactionListViewModelTest.kt +++ b/app/src/test/java/io/gnosis/safe/ui/transactions/TransactionListViewModelTest.kt @@ -260,24 +260,24 @@ class TransactionListViewModelTest { listOf(safe), listOf() ) - - assertEquals(TransactionView.TransferQueued( - "", - AWAITING_EXECUTION, - Chain.DEFAULT_CHAIN, - R.string.tx_status_needs_execution, - R.color.warning, - "< -0.00001 ETH", - transfer.timestamp, - R.drawable.ic_arrow_red_10dp, - R.string.tx_list_send, - R.color.label_primary, - 1, - 1, - R.color.success, - R.drawable.ic_confirmations_green_16dp, - "1" - ), transactionView) +//FIXME: test runs locally but fails on CI +// assertEquals(TransactionView.TransferQueued( +// "", +// AWAITING_EXECUTION, +// Chain.DEFAULT_CHAIN, +// R.string.tx_status_needs_execution, +// R.color.warning, +// "< -0.00001 ETH", +// transfer.timestamp, +// R.drawable.ic_arrow_red_10dp, +// R.string.tx_list_send, +// R.color.label_primary, +// 1, +// 1, +// R.color.success, +// R.drawable.ic_confirmations_green_16dp, +// "1" +// ), transactionView) } @Test @@ -307,24 +307,24 @@ class TransactionListViewModelTest { 0 ) ) - - assertEquals(TransactionView.TransferQueued( - "", - PENDING, - Chain.DEFAULT_CHAIN, - R.string.tx_status_pending, - R.color.warning, - "< -0.00001 ETH", - transfer.timestamp, - R.drawable.ic_arrow_red_10dp, - R.string.tx_list_send, - R.color.label_primary, - 1, - 1, - R.color.success, - R.drawable.ic_confirmations_green_16dp, - "1" - ), transactionView) +//FIXME: test runs locally but fails on CI +// assertEquals(TransactionView.TransferQueued( +// "", +// PENDING, +// Chain.DEFAULT_CHAIN, +// R.string.tx_status_pending, +// R.color.warning, +// "< -0.00001 ETH", +// transfer.timestamp, +// R.drawable.ic_arrow_red_10dp, +// R.string.tx_list_send, +// R.color.label_primary, +// 1, +// 1, +// R.color.success, +// R.drawable.ic_confirmations_green_16dp, +// "1" +// ), transactionView) } @Test From 5a0737a6770efc8beda2e54bd73bca957db4941f Mon Sep 17 00:00:00 2001 From: Vitaly Katz Date: Tue, 29 Aug 2023 18:54:52 +0200 Subject: [PATCH 22/23] Adjust test for CI --- .../TransactionListViewModelTest.kt | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/app/src/test/java/io/gnosis/safe/ui/transactions/TransactionListViewModelTest.kt b/app/src/test/java/io/gnosis/safe/ui/transactions/TransactionListViewModelTest.kt index 8ba204d2e7..a28b998d4a 100644 --- a/app/src/test/java/io/gnosis/safe/ui/transactions/TransactionListViewModelTest.kt +++ b/app/src/test/java/io/gnosis/safe/ui/transactions/TransactionListViewModelTest.kt @@ -260,24 +260,24 @@ class TransactionListViewModelTest { listOf(safe), listOf() ) -//FIXME: test runs locally but fails on CI -// assertEquals(TransactionView.TransferQueued( -// "", -// AWAITING_EXECUTION, -// Chain.DEFAULT_CHAIN, -// R.string.tx_status_needs_execution, -// R.color.warning, -// "< -0.00001 ETH", -// transfer.timestamp, -// R.drawable.ic_arrow_red_10dp, -// R.string.tx_list_send, -// R.color.label_primary, -// 1, -// 1, -// R.color.success, -// R.drawable.ic_confirmations_green_16dp, -// "1" -// ), transactionView) + + assertEquals(TransactionView.TransferQueued( + "", + AWAITING_EXECUTION, + Chain.DEFAULT_CHAIN, + R.string.tx_status_needs_execution, + R.color.warning, + "< -0${DS}00001 ETH", + transfer.timestamp, + R.drawable.ic_arrow_red_10dp, + R.string.tx_list_send, + R.color.label_primary, + 1, + 1, + R.color.success, + R.drawable.ic_confirmations_green_16dp, + "1" + ), transactionView) } @Test @@ -307,24 +307,24 @@ class TransactionListViewModelTest { 0 ) ) -//FIXME: test runs locally but fails on CI -// assertEquals(TransactionView.TransferQueued( -// "", -// PENDING, -// Chain.DEFAULT_CHAIN, -// R.string.tx_status_pending, -// R.color.warning, -// "< -0.00001 ETH", -// transfer.timestamp, -// R.drawable.ic_arrow_red_10dp, -// R.string.tx_list_send, -// R.color.label_primary, -// 1, -// 1, -// R.color.success, -// R.drawable.ic_confirmations_green_16dp, -// "1" -// ), transactionView) + + assertEquals(TransactionView.TransferQueued( + "", + PENDING, + Chain.DEFAULT_CHAIN, + R.string.tx_status_pending, + R.color.warning, + "< -0${DS}00001 ETH", + transfer.timestamp, + R.drawable.ic_arrow_red_10dp, + R.string.tx_list_send, + R.color.label_primary, + 1, + 1, + R.color.success, + R.drawable.ic_confirmations_green_16dp, + "1" + ), transactionView) } @Test From 683ea6bc17baeadc61f6bd86cc8eb8cc253d3be0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dirk=20Ja=CC=88ckel?= Date: Wed, 30 Aug 2023 11:01:12 +0200 Subject: [PATCH 23/23] Test for specific parameters in method calls --- .../TransactionDetailsViewModelTest.kt | 141 ++++++++++-------- 1 file changed, 78 insertions(+), 63 deletions(-) diff --git a/app/src/test/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModelTest.kt b/app/src/test/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModelTest.kt index 95e706b7c0..5f1ec4b99c 100644 --- a/app/src/test/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModelTest.kt +++ b/app/src/test/java/io/gnosis/safe/ui/transactions/details/TransactionDetailsViewModelTest.kt @@ -59,6 +59,8 @@ class TransactionDetailsViewModelTest { private val adapter = dataMoshi.adapter(TransactionDetails::class.java) + private val someSafeTxHash = "0xb3bb5fe5221dd17b3fe68388c115c73db01a1528cf351f9de4ec85f7f8182a67" + @Test fun `loadDetails (transactionRepository failure) should emit error`() = runTest(UnconfinedTestDispatcher()) { @@ -110,69 +112,72 @@ class TransactionDetailsViewModelTest { } @Test - fun `loadDetails (successful) should emit txDetails`() = runTest(UnconfinedTestDispatcher()) { - val transactionDetailsDto = adapter.readJsonFrom("tx_details_transfer.json") - val transactionDetails = toTransactionDetails(transactionDetailsDto) - val someAddress = "0x1230B3d59858296A31053C1b8562Ecf89A2f888b".asEthereumAddress()!! + fun `loadDetails (successful) should emit txDetails`() { + runTest(UnconfinedTestDispatcher()) { + val transactionDetailsDto = adapter.readJsonFrom("tx_details_transfer.json") + val transactionDetails = toTransactionDetails(transactionDetailsDto) + val someAddress = "0x1230B3d59858296A31053C1b8562Ecf89A2f888b".asEthereumAddress()!! + val someSafe = Safe(someAddress, "safe_name", CHAIN_ID) - coEvery { - transactionRepository.getTransactionDetails( - any(), - any() - ) - } returns transactionDetails - coEvery { transactionLocalRepository.updateLocalTx(any(), any()) } returns null - coEvery { safeRepository.getSafes() } returns emptyList() - coEvery { credentialsRepository.owners() } returns listOf() - coEvery { safeRepository.getActiveSafe() } returns Safe(someAddress, "safe_name", CHAIN_ID) - coEvery { safeRepository.getSafeInfo(any()) } returns SafeInfo( - AddressInfo(Solidity.Address(BigInteger.ONE)), - BigInteger.ONE, - 1, - listOf( - AddressInfo(Solidity.Address(BigInteger.ONE)) - ), - AddressInfo(Solidity.Address(BigInteger.ONE)), - listOf(AddressInfo(Solidity.Address(BigInteger.ONE))), - AddressInfo(Solidity.Address(BigInteger.ONE)), - null, - "1.1.1", - VersionState.OUTDATED - ) - val expectedTransactionInfoViewData = - transactionDetails.toTransactionDetailsViewData( - emptyList(), - canSign = false, - canExecute = false, - nextInLine = false, - owners = emptyList(), - hasOwnerKey = false + coEvery { + transactionRepository.getTransactionDetails( + any(), + any() + ) + } returns transactionDetails + coEvery { transactionLocalRepository.updateLocalTx(any(), any()) } returns null + coEvery { safeRepository.getSafes() } returns emptyList() + coEvery { credentialsRepository.owners() } returns listOf() + coEvery { safeRepository.getActiveSafe() } returns someSafe + coEvery { safeRepository.getSafeInfo(any()) } returns SafeInfo( + AddressInfo(Solidity.Address(BigInteger.ONE)), + BigInteger.ONE, + 1, + listOf( + AddressInfo(Solidity.Address(BigInteger.ONE)) + ), + AddressInfo(Solidity.Address(BigInteger.ONE)), + listOf(AddressInfo(Solidity.Address(BigInteger.ONE))), + AddressInfo(Solidity.Address(BigInteger.ONE)), + null, + "1.1.1", + VersionState.OUTDATED ) + val expectedTransactionInfoViewData = + transactionDetails.toTransactionDetailsViewData( + emptyList(), + canSign = false, + canExecute = false, + nextInLine = false, + owners = emptyList(), + hasOwnerKey = false + ) - viewModel = TransactionDetailsViewModel( - transactionRepository, - transactionLocalRepository, - safeRepository, - credentialsRepository, - settingsHandler, - tracker, - appDispatchers - ) + viewModel = TransactionDetailsViewModel( + transactionRepository, + transactionLocalRepository, + safeRepository, + credentialsRepository, + settingsHandler, + tracker, + appDispatchers + ) - viewModel.loadDetails("tx_details_id") + viewModel.loadDetails("tx_details_id") - with(viewModel.state.test().values()) { - assertEquals( - UpdateDetails(txDetails = expectedTransactionInfoViewData), - this[0].viewAction - ) - } - coVerify(exactly = 1) { - transactionRepository.getTransactionDetails( - CHAIN_ID, - "tx_details_id" - ) - transactionLocalRepository.updateLocalTx(any(), any()) + with(viewModel.state.test().values()) { + assertEquals( + UpdateDetails(txDetails = expectedTransactionInfoViewData), + this[0].viewAction + ) + } + coVerify(exactly = 1) { + transactionRepository.getTransactionDetails( + CHAIN_ID, + "tx_details_id" + ) + transactionLocalRepository.updateLocalTx(someSafe, someSafeTxHash) + } } } @@ -326,7 +331,17 @@ class TransactionDetailsViewModelTest { CHAIN_ID, "tx_details_id" ) - transactionLocalRepository.delete(any()) + + val localTx = TransactionLocal( + safeAddress = Solidity.Address(BigInteger.ZERO), + chainId = CHAIN_ID, + safeTxNonce = BigInteger.ZERO, + safeTxHash = "0xb3bb5fe5221dd17b3fe68388c115c73db01a1528cf351f9de4ec85f7f8182a67", + ethTxHash = "", + status = TransactionStatus.PENDING, + submittedAt = 0 + ) + transactionLocalRepository.delete(localTx) } } @@ -604,7 +619,7 @@ class TransactionDetailsViewModelTest { coVerify(exactly = 1) { credentialsRepository.signWithOwner( owner, - "0xb3bb5fe5221dd17b3fe68388c115c73db01a1528cf351f9de4ec85f7f8182a67".hexToByteArray() + someSafeTxHash.hexToByteArray() ) } coVerify(exactly = 1) { credentialsRepository.owners() } @@ -655,7 +670,7 @@ class TransactionDetailsViewModelTest { coVerify(exactly = 1) { credentialsRepository.signWithOwner( owner, - "0xb3bb5fe5221dd17b3fe68388c115c73db01a1528cf351f9de4ec85f7f8182a67".hexToByteArray() + someSafeTxHash.hexToByteArray() ) } coVerify(exactly = 1) { credentialsRepository.owners() } @@ -717,7 +732,7 @@ class TransactionDetailsViewModelTest { coVerify(exactly = 1) { credentialsRepository.signWithOwner( owner, - "0xb3bb5fe5221dd17b3fe68388c115c73db01a1528cf351f9de4ec85f7f8182a67".hexToByteArray() + someSafeTxHash.hexToByteArray() ) } coVerify(exactly = 1) { credentialsRepository.owners() } @@ -759,7 +774,7 @@ class TransactionDetailsViewModelTest { ).toTypedArray(), signingMode = SigningMode.CONFIRMATION, chain = Chain.DEFAULT_CHAIN, - safeTxHash = "0xb3bb5fe5221dd17b3fe68388c115c73db01a1528cf351f9de4ec85f7f8182a67" + safeTxHash = someSafeTxHash ) ).toString(), viewAction.toString() )