From 01f2082a7a411f3262327736eea19f92a128cd8b Mon Sep 17 00:00:00 2001 From: abdrasulov Date: Wed, 30 Jul 2025 12:18:02 +0600 Subject: [PATCH 01/12] Add new module MerkleIoKit --- app/build.gradle | 1 + merkleiokit/.gitignore | 1 + merkleiokit/build.gradle | 41 +++++++++++++++++++ merkleiokit/consumer-rules.pro | 0 merkleiokit/proguard-rules.pro | 21 ++++++++++ .../merkleiokit/ExampleInstrumentedTest.kt | 22 ++++++++++ merkleiokit/src/main/AndroidManifest.xml | 4 ++ .../merkleiokit/ExampleUnitTest.kt | 16 ++++++++ settings.gradle | 1 + 9 files changed, 107 insertions(+) create mode 100644 merkleiokit/.gitignore create mode 100644 merkleiokit/build.gradle create mode 100644 merkleiokit/consumer-rules.pro create mode 100644 merkleiokit/proguard-rules.pro create mode 100644 merkleiokit/src/androidTest/java/io/horizontalsystems/merkleiokit/ExampleInstrumentedTest.kt create mode 100644 merkleiokit/src/main/AndroidManifest.xml create mode 100644 merkleiokit/src/test/java/io/horizontalsystems/merkleiokit/ExampleUnitTest.kt diff --git a/app/build.gradle b/app/build.gradle index bf4271a3..b18450fa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -93,4 +93,5 @@ dependencies { implementation project(':uniswapkit') implementation project(':oneinchkit') implementation project(':nftkit') + implementation project(':merkleiokit') } diff --git a/merkleiokit/.gitignore b/merkleiokit/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/merkleiokit/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/merkleiokit/build.gradle b/merkleiokit/build.gradle new file mode 100644 index 00000000..343b289a --- /dev/null +++ b/merkleiokit/build.gradle @@ -0,0 +1,41 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'io.horizontalsystems.merkleiokit' + compileSdk 32 + + defaultConfig { + minSdk 26 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = '11' + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.16.0' + implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0') + implementation 'androidx.appcompat:appcompat:1.7.1' + implementation 'com.google.android.material:material:1.12.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' +} \ No newline at end of file diff --git a/merkleiokit/consumer-rules.pro b/merkleiokit/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/merkleiokit/proguard-rules.pro b/merkleiokit/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/merkleiokit/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/merkleiokit/src/androidTest/java/io/horizontalsystems/merkleiokit/ExampleInstrumentedTest.kt b/merkleiokit/src/androidTest/java/io/horizontalsystems/merkleiokit/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..80e15808 --- /dev/null +++ b/merkleiokit/src/androidTest/java/io/horizontalsystems/merkleiokit/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package io.horizontalsystems.merkleiokit + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("io.horizontalsystems.merkleiokit.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/merkleiokit/src/main/AndroidManifest.xml b/merkleiokit/src/main/AndroidManifest.xml new file mode 100644 index 00000000..44008a43 --- /dev/null +++ b/merkleiokit/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/merkleiokit/src/test/java/io/horizontalsystems/merkleiokit/ExampleUnitTest.kt b/merkleiokit/src/test/java/io/horizontalsystems/merkleiokit/ExampleUnitTest.kt new file mode 100644 index 00000000..1801067f --- /dev/null +++ b/merkleiokit/src/test/java/io/horizontalsystems/merkleiokit/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package io.horizontalsystems.merkleiokit + +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 70c56cde..b13a3583 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,2 @@ include ':app', ':ethereumkit', ':erc20kit', ':uniswapkit', ':oneinchkit', ':nftkit' +include ':merkleiokit' From 5a72045c472efdcdfea7f06d5e0a4e8c47da4ddc Mon Sep 17 00:00:00 2001 From: abdrasulov Date: Wed, 30 Jul 2025 13:05:17 +0600 Subject: [PATCH 02/12] Extract interface INonceProvider --- .../ethereumkit/api/core/RpcBlockchain.kt | 3 +- .../ethereumkit/core/EthereumKit.kt | 13 +++++-- .../ethereumkit/core/Interfaces.kt | 20 ++++++++++- .../ethereumkit/core/NonceProvider.kt | 36 +++++++++++++++++++ 4 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/core/NonceProvider.kt diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/api/core/RpcBlockchain.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/api/core/RpcBlockchain.kt index 6d204831..f5279790 100644 --- a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/api/core/RpcBlockchain.kt +++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/api/core/RpcBlockchain.kt @@ -22,6 +22,7 @@ import io.horizontalsystems.ethereumkit.core.EthereumKit.SyncState import io.horizontalsystems.ethereumkit.core.IApiStorage import io.horizontalsystems.ethereumkit.core.IBlockchain import io.horizontalsystems.ethereumkit.core.IBlockchainListener +import io.horizontalsystems.ethereumkit.core.INonceProvider import io.horizontalsystems.ethereumkit.core.RpcApiProviderFactory import io.horizontalsystems.ethereumkit.core.TransactionBuilder import io.horizontalsystems.ethereumkit.models.Address @@ -42,7 +43,7 @@ class RpcBlockchain( private val storage: IApiStorage, private val syncer: IRpcSyncer, private val transactionBuilder: TransactionBuilder -) : IBlockchain, IRpcSyncerListener { +) : IBlockchain, IRpcSyncerListener, INonceProvider { private val disposables = CompositeDisposable() diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/core/EthereumKit.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/core/EthereumKit.kt index abf88d72..0619a624 100644 --- a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/core/EthereumKit.kt +++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/core/EthereumKit.kt @@ -64,6 +64,7 @@ import java.util.logging.Logger class EthereumKit( private val blockchain: IBlockchain, + private val nonceProvider: NonceProvider, private val transactionManager: TransactionManager, private val transactionSyncManager: TransactionSyncManager, private val connectionManager: ConnectionManager, @@ -153,7 +154,7 @@ class EthereumKit( } fun getNonce(defaultBlockParameter: DefaultBlockParameter): Single { - return blockchain.getNonce(defaultBlockParameter) + return nonceProvider.getNonce(defaultBlockParameter) } fun getFullTransactionsFlowable(tags: List>): Flowable> { @@ -224,7 +225,7 @@ class EthereumKit( gasLimit: Long, nonce: Long? = null ): Single { - val nonceSingle = nonce?.let { Single.just(it) } ?: blockchain.getNonce(DefaultBlockParameter.Pending) + val nonceSingle = nonce?.let { Single.just(it) } ?: nonceProvider.getNonce(DefaultBlockParameter.Pending) return nonceSingle.flatMap { nonce -> Single.just(RawTransaction(gasPrice, gasLimit, address, value, nonce, transactionInput)) @@ -311,6 +312,10 @@ class EthereumKit( transactionSyncManager.add(transactionSyncer) } + fun addNonceProvider(provider: INonceProvider) { + nonceProvider.addProvider(provider) + } + fun addMethodDecorator(decorator: IMethodDecorator) { decorationManager.addMethodDecorator(decorator) } @@ -492,8 +497,12 @@ class EthereumKit( transactionSyncManager.add(internalTransactionsSyncer) transactionSyncManager.add(ethereumTransactionSyncer) + val nonceProvider = NonceProvider() + nonceProvider.addProvider(blockchain) + val ethereumKit = EthereumKit( blockchain, + nonceProvider, transactionManager, transactionSyncManager, connectionManager, diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/core/Interfaces.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/core/Interfaces.kt index 4d2901a4..9d48a294 100644 --- a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/core/Interfaces.kt +++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/core/Interfaces.kt @@ -8,7 +8,21 @@ import io.horizontalsystems.ethereumkit.api.models.AccountState import io.horizontalsystems.ethereumkit.contracts.ContractEventInstance import io.horizontalsystems.ethereumkit.contracts.ContractMethod import io.horizontalsystems.ethereumkit.decorations.TransactionDecoration -import io.horizontalsystems.ethereumkit.models.* +import io.horizontalsystems.ethereumkit.models.Address +import io.horizontalsystems.ethereumkit.models.DefaultBlockParameter +import io.horizontalsystems.ethereumkit.models.Eip20Event +import io.horizontalsystems.ethereumkit.models.GasPrice +import io.horizontalsystems.ethereumkit.models.InternalTransaction +import io.horizontalsystems.ethereumkit.models.ProviderEip1155Transaction +import io.horizontalsystems.ethereumkit.models.ProviderEip721Transaction +import io.horizontalsystems.ethereumkit.models.ProviderInternalTransaction +import io.horizontalsystems.ethereumkit.models.ProviderTokenTransaction +import io.horizontalsystems.ethereumkit.models.ProviderTransaction +import io.horizontalsystems.ethereumkit.models.RawTransaction +import io.horizontalsystems.ethereumkit.models.Signature +import io.horizontalsystems.ethereumkit.models.Transaction +import io.horizontalsystems.ethereumkit.models.TransactionLog +import io.horizontalsystems.ethereumkit.models.TransactionTag import io.horizontalsystems.ethereumkit.spv.models.AccountStateSpv import io.horizontalsystems.ethereumkit.spv.models.BlockHeader import io.reactivex.Single @@ -125,3 +139,7 @@ interface ITransactionProvider { fun getEip721Transactions(startBlock: Long): Single> fun getEip1155Transactions(startBlock: Long): Single> } + +interface INonceProvider { + fun getNonce(defaultBlockParameter: DefaultBlockParameter): Single +} diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/core/NonceProvider.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/core/NonceProvider.kt new file mode 100644 index 00000000..cd53d4d7 --- /dev/null +++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/core/NonceProvider.kt @@ -0,0 +1,36 @@ +package io.horizontalsystems.ethereumkit.core + +import io.horizontalsystems.ethereumkit.models.DefaultBlockParameter +import io.reactivex.Single + +class NonceProvider : INonceProvider { + private val providers = mutableListOf() + + fun addProvider(provider: INonceProvider) { + providers.add(provider) + } + + override fun getNonce(defaultBlockParameter: DefaultBlockParameter): Single { + val singles = providers.map { + it.getNonce(defaultBlockParameter) + } + + return Single + .zip(singles) { objects: Array -> + var maxNonce = -1L + for (obj in objects) { + if (obj is Long) { + maxNonce = maxOf(maxNonce, obj) + } + } + maxNonce + } + .flatMap { nonce: Long -> + if (nonce == -1L) { + Single.error(IllegalStateException("Could not fetch nonce. None of the providers returned a value.")) + } else { + Single.just(nonce) + } + } + } +} \ No newline at end of file From 3583e3a272a12f6efcba7591b68e9251a351d957 Mon Sep 17 00:00:00 2001 From: abdrasulov Date: Thu, 31 Jul 2025 14:42:26 +0600 Subject: [PATCH 03/12] Implement sending tx via MerkleIo rpc --- merkleiokit/build.gradle | 11 +++ .../merkleiokit/MerkleDatabase.kt | 26 +++++++ .../merkleiokit/MerkleRpcBlockchain.kt | 50 +++++++++++++ .../merkleiokit/MerkleTransactionAdapter.kt | 74 +++++++++++++++++++ .../merkleiokit/MerkleTransactionDao.kt | 23 ++++++ .../merkleiokit/MerkleTransactionHash.kt | 11 +++ .../MerkleTransactionHashManager.kt | 44 +++++++++++ .../merkleiokit/MerkleTransactionSyncer.kt | 57 ++++++++++++++ 8 files changed, 296 insertions(+) create mode 100644 merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleDatabase.kt create mode 100644 merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleRpcBlockchain.kt create mode 100644 merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionAdapter.kt create mode 100644 merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionDao.kt create mode 100644 merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHash.kt create mode 100644 merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHashManager.kt create mode 100644 merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionSyncer.kt diff --git a/merkleiokit/build.gradle b/merkleiokit/build.gradle index 343b289a..14ac1356 100644 --- a/merkleiokit/build.gradle +++ b/merkleiokit/build.gradle @@ -1,6 +1,7 @@ plugins { id 'com.android.library' id 'org.jetbrains.kotlin.android' + id 'kotlin-kapt' } android { @@ -38,4 +39,14 @@ dependencies { testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.2.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' + + implementation 'io.reactivex.rxjava2:rxjava:2.2.19' + implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + + // Room + implementation "androidx.room:room-runtime:$room_version" + implementation "androidx.room:room-rxjava2:$room_version" + kapt "androidx.room:room-compiler:$room_version" + + implementation project(':ethereumkit') } \ No newline at end of file diff --git a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleDatabase.kt b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleDatabase.kt new file mode 100644 index 00000000..67c73875 --- /dev/null +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleDatabase.kt @@ -0,0 +1,26 @@ +package io.horizontalsystems.merkleiokit + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase + +@Database( + entities = [ + MerkleTransactionHash::class, + ], + version = 1, + exportSchema = false +) +abstract class MerkleDatabase : RoomDatabase() { + abstract fun merkleTransactionDao(): MerkleTransactionDao + + companion object Companion { + fun getInstance(context: Context, databaseName: String): MerkleDatabase { + return Room.databaseBuilder(context, MerkleDatabase::class.java, databaseName) + .fallbackToDestructiveMigration() + .allowMainThreadQueries() + .build() + } + } +} diff --git a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleRpcBlockchain.kt b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleRpcBlockchain.kt new file mode 100644 index 00000000..c444ec62 --- /dev/null +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleRpcBlockchain.kt @@ -0,0 +1,50 @@ +package io.horizontalsystems.merkleiokit + +import io.horizontalsystems.ethereumkit.api.core.IRpcSyncer +import io.horizontalsystems.ethereumkit.api.jsonrpc.GetTransactionByHashJsonRpc +import io.horizontalsystems.ethereumkit.api.jsonrpc.GetTransactionCountJsonRpc +import io.horizontalsystems.ethereumkit.api.jsonrpc.SendRawTransactionJsonRpc +import io.horizontalsystems.ethereumkit.api.jsonrpc.models.RpcTransaction +import io.horizontalsystems.ethereumkit.core.INonceProvider +import io.horizontalsystems.ethereumkit.core.TransactionBuilder +import io.horizontalsystems.ethereumkit.models.Address +import io.horizontalsystems.ethereumkit.models.Chain +import io.horizontalsystems.ethereumkit.models.DefaultBlockParameter +import io.horizontalsystems.ethereumkit.models.RawTransaction +import io.horizontalsystems.ethereumkit.models.Signature +import io.horizontalsystems.ethereumkit.models.Transaction +import io.reactivex.Single + +class MerkleRpcBlockchain( + private val address: Address, + val chain: Chain, + private val manager: MerkleTransactionHashManager, + private val syncer: IRpcSyncer, + private val transactionBuilder: TransactionBuilder +) : INonceProvider { + + override fun getNonce(defaultBlockParameter: DefaultBlockParameter): Single { + // sync only if needed pending/ because others will be same with main blockchain + if (defaultBlockParameter != DefaultBlockParameter.Pending) { + return Single.just(0) + } + + return syncer.single(GetTransactionCountJsonRpc(address, defaultBlockParameter)) + } + + fun send(rawTransaction: RawTransaction, signature: Signature): Single { + val tx = transactionBuilder.transaction(rawTransaction, signature) + val encoded = transactionBuilder.encode(rawTransaction, signature) + + return syncer.single(SendRawTransactionJsonRpc(encoded)) + .map { txHash -> + manager.save(MerkleTransactionHash(txHash, chain.id)) + + tx + } + } + + fun transaction(transactionHash: ByteArray): Single { + return syncer.single(GetTransactionByHashJsonRpc(transactionHash)) + } +} diff --git a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionAdapter.kt b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionAdapter.kt new file mode 100644 index 00000000..63d54c00 --- /dev/null +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionAdapter.kt @@ -0,0 +1,74 @@ +package io.horizontalsystems.merkleiokit + +import android.content.Context +import io.horizontalsystems.ethereumkit.api.core.ApiRpcSyncer +import io.horizontalsystems.ethereumkit.api.core.NodeApiProvider +import io.horizontalsystems.ethereumkit.core.EthereumKit +import io.horizontalsystems.ethereumkit.core.ITransactionSyncer +import io.horizontalsystems.ethereumkit.core.TransactionBuilder +import io.horizontalsystems.ethereumkit.models.Address +import io.horizontalsystems.ethereumkit.models.Chain +import io.horizontalsystems.ethereumkit.models.RawTransaction +import io.horizontalsystems.ethereumkit.models.Signature +import io.horizontalsystems.ethereumkit.models.Transaction +import io.horizontalsystems.ethereumkit.network.ConnectionManager +import io.reactivex.Single +import java.net.URI + +class MerkleTransactionAdapter( + val blockchain: MerkleRpcBlockchain, + val syncer: ITransactionSyncer, +) { + fun send(rawTransaction: RawTransaction, signature: Signature): Single { + return blockchain.send(rawTransaction, signature) + } + + companion object { + + private val blockchainPathMap = mapOf( + Chain.Ethereum to "eth", + Chain.BinanceSmartChain to "bsc", + Chain.Base to "base" + ) + + fun getInstance( + merkleIoPubKey: String, + address: Address, + chain: Chain, + context: Context, + walletId: String, + ): MerkleTransactionAdapter? { + val baseUrl = "https://mempool.merkle.io/rpc/" + val blockchainPath = blockchainPathMap[chain] ?: return null + + val url = URI("$baseUrl$blockchainPath/$merkleIoPubKey") + val rpcProvider = NodeApiProvider(listOf(url), EthereumKit.gson) + + val connectionManager = ConnectionManager(context) + val rpcSyncer = ApiRpcSyncer(rpcProvider, connectionManager, chain.syncInterval) + + val transactionBuilder = TransactionBuilder(address, chain.id) + + val dbName = "MerkleIo-${chain.id}-$walletId" + val merkleDatabase = MerkleDatabase.getInstance(context, dbName) + + val merkleTransactionHashManager = + MerkleTransactionHashManager(merkleDatabase.merkleTransactionDao()) + + val blockchain = MerkleRpcBlockchain( + address = address, + chain = chain, + manager = merkleTransactionHashManager, + syncer = rpcSyncer, + transactionBuilder = transactionBuilder + ) + + val syncer = MerkleTransactionSyncer( + manager = merkleTransactionHashManager, + blockchain = blockchain + ) + + return MerkleTransactionAdapter(blockchain, syncer) + } + } +} diff --git a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionDao.kt b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionDao.kt new file mode 100644 index 00000000..5468fe8a --- /dev/null +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionDao.kt @@ -0,0 +1,23 @@ +package io.horizontalsystems.merkleiokit + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query + +@Dao +interface MerkleTransactionDao { + + @Query("SELECT * FROM MerkleTransactionHash WHERE chainId = :chainId") + fun hashes(chainId: Int) : List + + @Insert + fun save(hash: MerkleTransactionHash) + + @Query("SELECT COUNT(*) > 0 FROM MerkleTransactionHash WHERE chainId = :chainId") + fun hasMerkleTransactions(chainId: Int) : Boolean + + @Delete + fun delete(hashes: List): Any + +} diff --git a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHash.kt b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHash.kt new file mode 100644 index 00000000..42aeecdf --- /dev/null +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHash.kt @@ -0,0 +1,11 @@ +package io.horizontalsystems.merkleiokit + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity +data class MerkleTransactionHash( + @PrimaryKey + val hash: ByteArray, + val chainId: Int, +) diff --git a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHashManager.kt b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHashManager.kt new file mode 100644 index 00000000..43757f45 --- /dev/null +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHashManager.kt @@ -0,0 +1,44 @@ +package io.horizontalsystems.merkleiokit + +import io.horizontalsystems.ethereumkit.models.Transaction + +class MerkleTransactionHashManager(private val dao: MerkleTransactionDao) { + + private val _hasMerkleTransactions = mutableMapOf() + + fun hasMerkleTransactions(chainId: Int): Boolean { + _hasMerkleTransactions[chainId]?.let { + return it + } + + val hasTxs = dao.hasMerkleTransactions(chainId) + + _hasMerkleTransactions[chainId] = hasTxs + + return hasTxs + } + + fun hashes(chainId: Int): List { + return dao.hashes(chainId) + } + + fun save(hash: MerkleTransactionHash) { + dao.save(hash) + _hasMerkleTransactions[hash.chainId] = true + } + + fun handle(transactions: List, chainId: Int) { + val toRemove = transactions.mapNotNull { tx -> + tx.blockNumber?.let { + MerkleTransactionHash(tx.hash, chainId) + } + } + + if (toRemove.isNotEmpty()) { + dao.delete(toRemove) + + _hasMerkleTransactions[chainId] = dao.hasMerkleTransactions(chainId) + } + } +} + diff --git a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionSyncer.kt b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionSyncer.kt new file mode 100644 index 00000000..5ec3b60c --- /dev/null +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionSyncer.kt @@ -0,0 +1,57 @@ +package io.horizontalsystems.merkleiokit + +import io.horizontalsystems.ethereumkit.api.jsonrpc.models.RpcTransaction +import io.horizontalsystems.ethereumkit.core.ITransactionSyncer +import io.horizontalsystems.ethereumkit.models.Transaction +import io.reactivex.Single + +class MerkleTransactionSyncer( + private val manager: MerkleTransactionHashManager, + private val blockchain: MerkleRpcBlockchain, +) : ITransactionSyncer { + + private fun convert(tx: RpcTransaction) = Transaction( + hash = tx.hash, + timestamp = 0, + isFailed = false, + blockNumber = tx.blockNumber, + transactionIndex = tx.transactionIndex, + from = tx.from, + to = tx.to, + value = tx.value, + input = tx.input, + nonce = tx.nonce, + gasPrice = tx.gasPrice, + maxFeePerGas = tx.maxFeePerGas, + maxPriorityFeePerGas = tx.maxPriorityFeePerGas, + gasLimit = tx.gasLimit, + gasUsed = 0, + replacedWith = null + ) + + + override fun getTransactionsSingle(): Single, Boolean>> { + val chainId = blockchain.chain.id + if (!manager.hasMerkleTransactions(chainId)) { + return Single.just(Pair(listOf(), false)) + } + + val hashes = manager.hashes(chainId) + val singles = hashes.map { tx -> + blockchain.transaction(tx.hash).map { convert(it) } + } + + val transactionsSingle = Single.zip(singles) { + it.filterIsInstance() + } + + return transactionsSingle + .doOnSuccess { + manager.handle(it, chainId) + } + .map { + Pair(it, false) + } + } + +} From 0effba0da86c059c1464bac013cb67528a154201 Mon Sep 17 00:00:00 2001 From: abdrasulov Date: Thu, 31 Jul 2025 15:33:07 +0600 Subject: [PATCH 04/12] Fix build --- merkleiokit/build.gradle | 40 +++++++++++++++--------- merkleiokit/src/main/AndroidManifest.xml | 5 +-- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/merkleiokit/build.gradle b/merkleiokit/build.gradle index 14ac1356..2a776832 100644 --- a/merkleiokit/build.gradle +++ b/merkleiokit/build.gradle @@ -1,15 +1,27 @@ plugins { id 'com.android.library' - id 'org.jetbrains.kotlin.android' + id 'kotlin-android' id 'kotlin-kapt' + id 'kotlin-android-extensions' + id 'maven-publish' +} + +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + from components.release + } + } + } } android { - namespace 'io.horizontalsystems.merkleiokit' - compileSdk 32 + compileSdkVersion 32 defaultConfig { - minSdk 26 + minSdkVersion 26 + targetSdkVersion 32 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" @@ -21,10 +33,7 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } - compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 - } + kotlinOptions { jvmTarget = '11' } @@ -32,17 +41,18 @@ android { dependencies { - implementation 'androidx.core:core-ktx:1.16.0' - implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0') - implementation 'androidx.appcompat:appcompat:1.7.1' - implementation 'com.google.android.material:material:1.12.0' - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.2.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + + implementation 'com.google.code.gson:gson:2.9.0' implementation 'io.reactivex.rxjava2:rxjava:2.2.19' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + implementation 'androidx.core:core-ktx:1.8.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + // Room implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-rxjava2:$room_version" diff --git a/merkleiokit/src/main/AndroidManifest.xml b/merkleiokit/src/main/AndroidManifest.xml index 44008a43..c6bdebb0 100644 --- a/merkleiokit/src/main/AndroidManifest.xml +++ b/merkleiokit/src/main/AndroidManifest.xml @@ -1,4 +1 @@ - - - - \ No newline at end of file + \ No newline at end of file From 5fd3666127a6f53c5cc0a96ed3caf203ac4fa797 Mon Sep 17 00:00:00 2001 From: abdrasulov Date: Thu, 31 Jul 2025 16:10:47 +0600 Subject: [PATCH 05/12] Return FullTransaction in method send --- .../horizontalsystems/ethereumkit/core/EthereumKit.kt | 2 +- .../merkleiokit/MerkleTransactionAdapter.kt | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/core/EthereumKit.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/core/EthereumKit.kt index 0619a624..e40f4fd6 100644 --- a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/core/EthereumKit.kt +++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/core/EthereumKit.kt @@ -65,7 +65,7 @@ import java.util.logging.Logger class EthereumKit( private val blockchain: IBlockchain, private val nonceProvider: NonceProvider, - private val transactionManager: TransactionManager, + val transactionManager: TransactionManager, private val transactionSyncManager: TransactionSyncManager, private val connectionManager: ConnectionManager, private val address: Address, diff --git a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionAdapter.kt b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionAdapter.kt index 63d54c00..c477e0e7 100644 --- a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionAdapter.kt +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionAdapter.kt @@ -6,11 +6,12 @@ import io.horizontalsystems.ethereumkit.api.core.NodeApiProvider import io.horizontalsystems.ethereumkit.core.EthereumKit import io.horizontalsystems.ethereumkit.core.ITransactionSyncer import io.horizontalsystems.ethereumkit.core.TransactionBuilder +import io.horizontalsystems.ethereumkit.core.TransactionManager import io.horizontalsystems.ethereumkit.models.Address import io.horizontalsystems.ethereumkit.models.Chain +import io.horizontalsystems.ethereumkit.models.FullTransaction import io.horizontalsystems.ethereumkit.models.RawTransaction import io.horizontalsystems.ethereumkit.models.Signature -import io.horizontalsystems.ethereumkit.models.Transaction import io.horizontalsystems.ethereumkit.network.ConnectionManager import io.reactivex.Single import java.net.URI @@ -18,9 +19,11 @@ import java.net.URI class MerkleTransactionAdapter( val blockchain: MerkleRpcBlockchain, val syncer: ITransactionSyncer, + private val transactionManager: TransactionManager, ) { - fun send(rawTransaction: RawTransaction, signature: Signature): Single { + fun send(rawTransaction: RawTransaction, signature: Signature): Single { return blockchain.send(rawTransaction, signature) + .map { transactionManager.handle(listOf(it)).first() } } companion object { @@ -37,6 +40,7 @@ class MerkleTransactionAdapter( chain: Chain, context: Context, walletId: String, + transactionManager: TransactionManager, ): MerkleTransactionAdapter? { val baseUrl = "https://mempool.merkle.io/rpc/" val blockchainPath = blockchainPathMap[chain] ?: return null @@ -68,7 +72,7 @@ class MerkleTransactionAdapter( blockchain = blockchain ) - return MerkleTransactionAdapter(blockchain, syncer) + return MerkleTransactionAdapter(blockchain, syncer, transactionManager) } } } From a3770b7c729c160e9eb257600e96aa9855b4d8a0 Mon Sep 17 00:00:00 2001 From: abdrasulov Date: Fri, 1 Aug 2025 12:21:36 +0600 Subject: [PATCH 06/12] Remove caching --- .../merkleiokit/MerkleTransactionHashManager.kt | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHashManager.kt b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHashManager.kt index 43757f45..520cf57d 100644 --- a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHashManager.kt +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHashManager.kt @@ -4,18 +4,8 @@ import io.horizontalsystems.ethereumkit.models.Transaction class MerkleTransactionHashManager(private val dao: MerkleTransactionDao) { - private val _hasMerkleTransactions = mutableMapOf() - fun hasMerkleTransactions(chainId: Int): Boolean { - _hasMerkleTransactions[chainId]?.let { - return it - } - - val hasTxs = dao.hasMerkleTransactions(chainId) - - _hasMerkleTransactions[chainId] = hasTxs - - return hasTxs + return dao.hasMerkleTransactions(chainId) } fun hashes(chainId: Int): List { @@ -24,7 +14,6 @@ class MerkleTransactionHashManager(private val dao: MerkleTransactionDao) { fun save(hash: MerkleTransactionHash) { dao.save(hash) - _hasMerkleTransactions[hash.chainId] = true } fun handle(transactions: List, chainId: Int) { @@ -36,8 +25,6 @@ class MerkleTransactionHashManager(private val dao: MerkleTransactionDao) { if (toRemove.isNotEmpty()) { dao.delete(toRemove) - - _hasMerkleTransactions[chainId] = dao.hasMerkleTransactions(chainId) } } } From 74b306ac2f2f0ba3aac0d0f335894407deff1002 Mon Sep 17 00:00:00 2001 From: abdrasulov Date: Fri, 1 Aug 2025 14:36:49 +0600 Subject: [PATCH 07/12] Remove chainId from MerkleTransactionHash --- .../merkleiokit/MerkleRpcBlockchain.kt | 9 +++------ .../merkleiokit/MerkleTransactionAdapter.kt | 1 - .../merkleiokit/MerkleTransactionDao.kt | 9 +++------ .../merkleiokit/MerkleTransactionHash.kt | 3 +-- .../merkleiokit/MerkleTransactionHashManager.kt | 12 ++++-------- .../merkleiokit/MerkleTransactionSyncer.kt | 7 +++---- 6 files changed, 14 insertions(+), 27 deletions(-) diff --git a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleRpcBlockchain.kt b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleRpcBlockchain.kt index c444ec62..c8eff910 100644 --- a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleRpcBlockchain.kt +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleRpcBlockchain.kt @@ -8,7 +8,6 @@ import io.horizontalsystems.ethereumkit.api.jsonrpc.models.RpcTransaction import io.horizontalsystems.ethereumkit.core.INonceProvider import io.horizontalsystems.ethereumkit.core.TransactionBuilder import io.horizontalsystems.ethereumkit.models.Address -import io.horizontalsystems.ethereumkit.models.Chain import io.horizontalsystems.ethereumkit.models.DefaultBlockParameter import io.horizontalsystems.ethereumkit.models.RawTransaction import io.horizontalsystems.ethereumkit.models.Signature @@ -17,7 +16,6 @@ import io.reactivex.Single class MerkleRpcBlockchain( private val address: Address, - val chain: Chain, private val manager: MerkleTransactionHashManager, private val syncer: IRpcSyncer, private val transactionBuilder: TransactionBuilder @@ -37,11 +35,10 @@ class MerkleRpcBlockchain( val encoded = transactionBuilder.encode(rawTransaction, signature) return syncer.single(SendRawTransactionJsonRpc(encoded)) - .map { txHash -> - manager.save(MerkleTransactionHash(txHash, chain.id)) - - tx + .doOnSuccess { txHash -> + manager.save(MerkleTransactionHash(txHash)) } + .map { tx } } fun transaction(transactionHash: ByteArray): Single { diff --git a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionAdapter.kt b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionAdapter.kt index c477e0e7..f9c77366 100644 --- a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionAdapter.kt +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionAdapter.kt @@ -61,7 +61,6 @@ class MerkleTransactionAdapter( val blockchain = MerkleRpcBlockchain( address = address, - chain = chain, manager = merkleTransactionHashManager, syncer = rpcSyncer, transactionBuilder = transactionBuilder diff --git a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionDao.kt b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionDao.kt index 5468fe8a..306ee88f 100644 --- a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionDao.kt +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionDao.kt @@ -8,16 +8,13 @@ import androidx.room.Query @Dao interface MerkleTransactionDao { - @Query("SELECT * FROM MerkleTransactionHash WHERE chainId = :chainId") - fun hashes(chainId: Int) : List + @Query("SELECT * FROM MerkleTransactionHash") + fun hashes() : List @Insert fun save(hash: MerkleTransactionHash) - @Query("SELECT COUNT(*) > 0 FROM MerkleTransactionHash WHERE chainId = :chainId") - fun hasMerkleTransactions(chainId: Int) : Boolean - @Delete - fun delete(hashes: List): Any + fun delete(hashes: List) } diff --git a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHash.kt b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHash.kt index 42aeecdf..e9f8f9c2 100644 --- a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHash.kt +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHash.kt @@ -6,6 +6,5 @@ import androidx.room.PrimaryKey @Entity data class MerkleTransactionHash( @PrimaryKey - val hash: ByteArray, - val chainId: Int, + val hash: ByteArray ) diff --git a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHashManager.kt b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHashManager.kt index 520cf57d..66b79573 100644 --- a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHashManager.kt +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHashManager.kt @@ -4,22 +4,18 @@ import io.horizontalsystems.ethereumkit.models.Transaction class MerkleTransactionHashManager(private val dao: MerkleTransactionDao) { - fun hasMerkleTransactions(chainId: Int): Boolean { - return dao.hasMerkleTransactions(chainId) - } - - fun hashes(chainId: Int): List { - return dao.hashes(chainId) + fun hashes(): List { + return dao.hashes() } fun save(hash: MerkleTransactionHash) { dao.save(hash) } - fun handle(transactions: List, chainId: Int) { + fun handle(transactions: List) { val toRemove = transactions.mapNotNull { tx -> tx.blockNumber?.let { - MerkleTransactionHash(tx.hash, chainId) + MerkleTransactionHash(tx.hash) } } diff --git a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionSyncer.kt b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionSyncer.kt index 5ec3b60c..3b215fa4 100644 --- a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionSyncer.kt +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionSyncer.kt @@ -31,12 +31,11 @@ class MerkleTransactionSyncer( override fun getTransactionsSingle(): Single, Boolean>> { - val chainId = blockchain.chain.id - if (!manager.hasMerkleTransactions(chainId)) { + val hashes = manager.hashes() + if (hashes.isEmpty()) { return Single.just(Pair(listOf(), false)) } - val hashes = manager.hashes(chainId) val singles = hashes.map { tx -> blockchain.transaction(tx.hash).map { convert(it) } } @@ -47,7 +46,7 @@ class MerkleTransactionSyncer( return transactionsSingle .doOnSuccess { - manager.handle(it, chainId) + manager.handle(it) } .map { Pair(it, false) From 9c4420a55b57628189df367b30153463857e6cb6 Mon Sep 17 00:00:00 2001 From: abdrasulov Date: Fri, 1 Aug 2025 17:16:50 +0600 Subject: [PATCH 08/12] Handle tx updates that were sent via MerkleIo --- .../ethereumkit/models/Transaction.kt | 2 +- .../merkleiokit/MerkleRpcBlockchain.kt | 12 +++- .../merkleiokit/MerkleTransactionAdapter.kt | 3 +- .../merkleiokit/MerkleTransactionDao.kt | 5 +- .../MerkleTransactionHashManager.kt | 22 +------ .../merkleiokit/MerkleTransactionSyncer.kt | 65 +++++++++---------- 6 files changed, 49 insertions(+), 60 deletions(-) diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/Transaction.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/Transaction.kt index e01ccdc3..1cb11d69 100644 --- a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/Transaction.kt +++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/Transaction.kt @@ -7,7 +7,7 @@ import io.horizontalsystems.ethereumkit.core.toHexString import java.math.BigInteger @Entity -class Transaction( +data class Transaction( @PrimaryKey val hash: ByteArray, val timestamp: Long, diff --git a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleRpcBlockchain.kt b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleRpcBlockchain.kt index c8eff910..70ce92cc 100644 --- a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleRpcBlockchain.kt +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleRpcBlockchain.kt @@ -3,6 +3,7 @@ package io.horizontalsystems.merkleiokit import io.horizontalsystems.ethereumkit.api.core.IRpcSyncer import io.horizontalsystems.ethereumkit.api.jsonrpc.GetTransactionByHashJsonRpc import io.horizontalsystems.ethereumkit.api.jsonrpc.GetTransactionCountJsonRpc +import io.horizontalsystems.ethereumkit.api.jsonrpc.JsonRpc import io.horizontalsystems.ethereumkit.api.jsonrpc.SendRawTransactionJsonRpc import io.horizontalsystems.ethereumkit.api.jsonrpc.models.RpcTransaction import io.horizontalsystems.ethereumkit.core.INonceProvider @@ -13,6 +14,7 @@ import io.horizontalsystems.ethereumkit.models.RawTransaction import io.horizontalsystems.ethereumkit.models.Signature import io.horizontalsystems.ethereumkit.models.Transaction import io.reactivex.Single +import java.util.Optional class MerkleRpcBlockchain( private val address: Address, @@ -41,7 +43,15 @@ class MerkleRpcBlockchain( .map { tx } } - fun transaction(transactionHash: ByteArray): Single { + fun transaction(transactionHash: ByteArray): Single> { return syncer.single(GetTransactionByHashJsonRpc(transactionHash)) + .map { Optional.of(it) } + .onErrorResumeNext { throwable -> + if (throwable is JsonRpc.ResponseError.InvalidResult) { + Single.just(Optional.empty()) + } else { + Single.error(throwable) + } + } } } diff --git a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionAdapter.kt b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionAdapter.kt index f9c77366..ff436d2f 100644 --- a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionAdapter.kt +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionAdapter.kt @@ -68,7 +68,8 @@ class MerkleTransactionAdapter( val syncer = MerkleTransactionSyncer( manager = merkleTransactionHashManager, - blockchain = blockchain + blockchain = blockchain, + transactionManager = transactionManager ) return MerkleTransactionAdapter(blockchain, syncer, transactionManager) diff --git a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionDao.kt b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionDao.kt index 306ee88f..d34cc3f7 100644 --- a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionDao.kt +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionDao.kt @@ -1,7 +1,6 @@ package io.horizontalsystems.merkleiokit import androidx.room.Dao -import androidx.room.Delete import androidx.room.Insert import androidx.room.Query @@ -14,7 +13,7 @@ interface MerkleTransactionDao { @Insert fun save(hash: MerkleTransactionHash) - @Delete - fun delete(hashes: List) + @Query("DELETE FROM MerkleTransactionHash WHERE hash IN (:hashes)") + fun delete(hashes: List) } diff --git a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHashManager.kt b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHashManager.kt index 66b79573..94833832 100644 --- a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHashManager.kt +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHashManager.kt @@ -1,27 +1,11 @@ package io.horizontalsystems.merkleiokit -import io.horizontalsystems.ethereumkit.models.Transaction - class MerkleTransactionHashManager(private val dao: MerkleTransactionDao) { - fun hashes(): List { - return dao.hashes() - } - - fun save(hash: MerkleTransactionHash) { - dao.save(hash) - } + fun hashes() = dao.hashes() - fun handle(transactions: List) { - val toRemove = transactions.mapNotNull { tx -> - tx.blockNumber?.let { - MerkleTransactionHash(tx.hash) - } - } + fun save(hash: MerkleTransactionHash) = dao.save(hash) - if (toRemove.isNotEmpty()) { - dao.delete(toRemove) - } - } + fun handleProcessed(confirmedTxHashes: List) = dao.delete(confirmedTxHashes) } diff --git a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionSyncer.kt b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionSyncer.kt index 3b215fa4..66a0ff2b 100644 --- a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionSyncer.kt +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionSyncer.kt @@ -1,56 +1,51 @@ package io.horizontalsystems.merkleiokit -import io.horizontalsystems.ethereumkit.api.jsonrpc.models.RpcTransaction import io.horizontalsystems.ethereumkit.core.ITransactionSyncer +import io.horizontalsystems.ethereumkit.core.TransactionManager import io.horizontalsystems.ethereumkit.models.Transaction import io.reactivex.Single +import kotlin.jvm.optionals.getOrNull class MerkleTransactionSyncer( private val manager: MerkleTransactionHashManager, private val blockchain: MerkleRpcBlockchain, + private val transactionManager: TransactionManager, ) : ITransactionSyncer { - private fun convert(tx: RpcTransaction) = Transaction( - hash = tx.hash, - timestamp = 0, - isFailed = false, - blockNumber = tx.blockNumber, - transactionIndex = tx.transactionIndex, - from = tx.from, - to = tx.to, - value = tx.value, - input = tx.input, - nonce = tx.nonce, - gasPrice = tx.gasPrice, - maxFeePerGas = tx.maxFeePerGas, - maxPriorityFeePerGas = tx.maxPriorityFeePerGas, - gasLimit = tx.gasLimit, - gasUsed = 0, - replacedWith = null - ) - - + @OptIn(ExperimentalStdlibApi::class) override fun getTransactionsSingle(): Single, Boolean>> { val hashes = manager.hashes() - if (hashes.isEmpty()) { - return Single.just(Pair(listOf(), false)) - } + if (hashes.isEmpty()) return Single.just(Pair(listOf(), false)) val singles = hashes.map { tx -> - blockchain.transaction(tx.hash).map { convert(it) } + blockchain.transaction(tx.hash) + .map { Pair(tx.hash, it.getOrNull()) } + .map { Result.success(it) } + .onErrorReturn { Result.failure(it) } } - val transactionsSingle = Single.zip(singles) { - it.filterIsInstance() - } + val transactionsSingle = Single.merge(singles) + .filter { it.isSuccess } // Only keep successful ones + .map { it.getOrThrow() } // Extract the actual value + .toList() - return transactionsSingle - .doOnSuccess { - manager.handle(it) - } - .map { - Pair(it, false) + return transactionsSingle.map { rpcTransactions -> + val processedTxHashes = mutableListOf() + val failedTxs = mutableListOf() + + rpcTransactions.forEach { (hash, rpcTransaction) -> + processedTxHashes.add(hash) + + if (rpcTransaction == null) { + transactionManager.getFullTransactions(listOf(hash)).firstOrNull()?.let { + failedTxs.add(it.transaction.copy(isFailed = true)) + } + } } - } + manager.handleProcessed(processedTxHashes) + + Pair(failedTxs, false) + } + } } From 0330006d29baed663cc7093ebdbb298ec5b26ec2 Mon Sep 17 00:00:00 2001 From: abdrasulov Date: Fri, 1 Aug 2025 18:30:05 +0600 Subject: [PATCH 09/12] Return null as ResponseError --- .../ethereumkit/api/jsonrpc/JsonRpc.kt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/api/jsonrpc/JsonRpc.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/api/jsonrpc/JsonRpc.kt index a7243756..7ae4c7a7 100644 --- a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/api/jsonrpc/JsonRpc.kt +++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/api/jsonrpc/JsonRpc.kt @@ -20,15 +20,19 @@ abstract class JsonRpc( if (response.error != null) { throw ResponseError.RpcError(response.error) } - return parseResult(response.result, gson) - } - fun parseResult(result: JsonElement?, gson: Gson): T { - return try { - gson.fromJson(result, typeOfResult) as T - } catch (error: Throwable) { + val result = parseResult(response.result, gson) + if (result == null) { throw ResponseError.InvalidResult(result.toString()) } + + return result + } + + private fun parseResult(result: JsonElement?, gson: Gson): T = try { + gson.fromJson(result, typeOfResult) + } catch (error: Throwable) { + throw ResponseError.InvalidResult(result.toString()) } sealed class ResponseError : Throwable() { From 0dcac520a7fc040d4f6ed0a43ebe175eb02aee1b Mon Sep 17 00:00:00 2001 From: abdrasulov Date: Mon, 4 Aug 2025 13:10:01 +0600 Subject: [PATCH 10/12] Process tx until it's completed or failed --- .../merkleiokit/MerkleTransactionHashManager.kt | 2 +- .../merkleiokit/MerkleTransactionSyncer.kt | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHashManager.kt b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHashManager.kt index 94833832..cfe01bba 100644 --- a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHashManager.kt +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHashManager.kt @@ -6,6 +6,6 @@ class MerkleTransactionHashManager(private val dao: MerkleTransactionDao) { fun save(hash: MerkleTransactionHash) = dao.save(hash) - fun handleProcessed(confirmedTxHashes: List) = dao.delete(confirmedTxHashes) + fun handle(txHashes: List) = dao.delete(txHashes) } diff --git a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionSyncer.kt b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionSyncer.kt index 66a0ff2b..29ad4939 100644 --- a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionSyncer.kt +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionSyncer.kt @@ -30,20 +30,23 @@ class MerkleTransactionSyncer( .toList() return transactionsSingle.map { rpcTransactions -> - val processedTxHashes = mutableListOf() + val completedTxHashes = mutableListOf() + val failedTxHashes = mutableListOf() val failedTxs = mutableListOf() rpcTransactions.forEach { (hash, rpcTransaction) -> - processedTxHashes.add(hash) - if (rpcTransaction == null) { + failedTxHashes.add(hash) + transactionManager.getFullTransactions(listOf(hash)).firstOrNull()?.let { failedTxs.add(it.transaction.copy(isFailed = true)) } + } else if (rpcTransaction.blockNumber != null) { + completedTxHashes.add(hash) } } - manager.handleProcessed(processedTxHashes) + manager.handle(completedTxHashes + failedTxHashes) Pair(failedTxs, false) } From e47a940c5f5822aa4eb14a36ff2098df2d715acd Mon Sep 17 00:00:00 2001 From: abdrasulov Date: Mon, 4 Aug 2025 15:04:13 +0600 Subject: [PATCH 11/12] Add extra data to FullTransaction via IExtraDecorator --- .../ethereumkit/core/EthereumKit.kt | 4 ++++ .../ethereumkit/core/Interfaces.kt | 4 ++++ .../decorations/DecorationManager.kt | 24 ++++++++++++++++--- .../ethereumkit/models/FullTransaction.kt | 3 ++- 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/core/EthereumKit.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/core/EthereumKit.kt index e40f4fd6..afac9a08 100644 --- a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/core/EthereumKit.kt +++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/core/EthereumKit.kt @@ -316,6 +316,10 @@ class EthereumKit( nonceProvider.addProvider(provider) } + fun addExtraDecorator(decorator: IExtraDecorator) { + decorationManager.addExtraDecorator(decorator) + } + fun addMethodDecorator(decorator: IMethodDecorator) { decorationManager.addMethodDecorator(decorator) } diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/core/Interfaces.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/core/Interfaces.kt index 9d48a294..7ceb74e2 100644 --- a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/core/Interfaces.kt +++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/core/Interfaces.kt @@ -120,6 +120,10 @@ interface IEventDecorator { fun contractEventInstances(logs: List): List } +interface IExtraDecorator { + fun extra(hash: ByteArray) : Map +} + interface ITransactionDecorator { fun decoration( from: Address?, diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/decorations/DecorationManager.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/decorations/DecorationManager.kt index 23bd3458..2584a58b 100644 --- a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/decorations/DecorationManager.kt +++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/decorations/DecorationManager.kt @@ -4,16 +4,34 @@ import io.horizontalsystems.ethereumkit.contracts.ContractEventInstance import io.horizontalsystems.ethereumkit.contracts.ContractMethod import io.horizontalsystems.ethereumkit.contracts.EmptyMethod import io.horizontalsystems.ethereumkit.core.IEventDecorator +import io.horizontalsystems.ethereumkit.core.IExtraDecorator import io.horizontalsystems.ethereumkit.core.IMethodDecorator import io.horizontalsystems.ethereumkit.core.ITransactionDecorator import io.horizontalsystems.ethereumkit.core.ITransactionStorage -import io.horizontalsystems.ethereumkit.models.* +import io.horizontalsystems.ethereumkit.models.Address +import io.horizontalsystems.ethereumkit.models.FullRpcTransaction +import io.horizontalsystems.ethereumkit.models.FullTransaction +import io.horizontalsystems.ethereumkit.models.InternalTransaction +import io.horizontalsystems.ethereumkit.models.Transaction +import io.horizontalsystems.ethereumkit.models.TransactionData +import io.horizontalsystems.ethereumkit.models.TransactionLog import java.math.BigInteger class DecorationManager(private val userAddress: Address, private val storage: ITransactionStorage) { private val methodDecorators = mutableListOf() private val eventDecorators = mutableListOf() private val transactionDecorators = mutableListOf() + private val extraDecorators = mutableListOf() + + private fun extra(hash: ByteArray) = buildMap { + extraDecorators.forEach { extraDecorator -> + putAll(extraDecorator.extra(hash)) + } + } + + fun addExtraDecorator(decorator: IExtraDecorator) { + extraDecorators.add(decorator) + } fun addMethodDecorator(decorator: IMethodDecorator) { methodDecorators.add(decorator) @@ -58,7 +76,7 @@ class DecorationManager(private val userAddress: Address, private val storage: I eventInstancesMap[transaction.hashString] ?: listOf() ) - return@map FullTransaction(transaction, decoration) + return@map FullTransaction(transaction, decoration, extra(transaction.hash)) } } @@ -81,7 +99,7 @@ class DecorationManager(private val userAddress: Address, private val storage: I fullRpcTransaction.rpcReceipt?.logs?.let { eventInstances(it) } ?: listOf() ) - return FullTransaction(transaction, decoration) + return FullTransaction(transaction, decoration, extra(transaction.hash)) } private fun getInternalTransactionsMap(transactions: List): Map> { diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/FullTransaction.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/FullTransaction.kt index 9489e3de..7617395c 100644 --- a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/FullTransaction.kt +++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/FullTransaction.kt @@ -4,5 +4,6 @@ import io.horizontalsystems.ethereumkit.decorations.TransactionDecoration class FullTransaction( val transaction: Transaction, - val decoration: TransactionDecoration + val decoration: TransactionDecoration, + val extra: Map ) From 9c80b4d3f38b53107e4889afc87c5d2db2b82dd5 Mon Sep 17 00:00:00 2001 From: abdrasulov Date: Mon, 4 Aug 2025 15:31:36 +0600 Subject: [PATCH 12/12] Set protected flag in tx via extra data --- .../merkleiokit/MerkleTransactionAdapter.kt | 14 ++++++++++++-- .../merkleiokit/MerkleTransactionDao.kt | 3 +++ .../merkleiokit/MerkleTransactionHashManager.kt | 2 ++ .../merkleiokit/MerkleTransactionSyncer.kt | 13 ++++++++++++- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionAdapter.kt b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionAdapter.kt index ff436d2f..b5ff6366 100644 --- a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionAdapter.kt +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionAdapter.kt @@ -4,7 +4,6 @@ import android.content.Context import io.horizontalsystems.ethereumkit.api.core.ApiRpcSyncer import io.horizontalsystems.ethereumkit.api.core.NodeApiProvider import io.horizontalsystems.ethereumkit.core.EthereumKit -import io.horizontalsystems.ethereumkit.core.ITransactionSyncer import io.horizontalsystems.ethereumkit.core.TransactionBuilder import io.horizontalsystems.ethereumkit.core.TransactionManager import io.horizontalsystems.ethereumkit.models.Address @@ -18,7 +17,7 @@ import java.net.URI class MerkleTransactionAdapter( val blockchain: MerkleRpcBlockchain, - val syncer: ITransactionSyncer, + val syncer: MerkleTransactionSyncer, private val transactionManager: TransactionManager, ) { fun send(rawTransaction: RawTransaction, signature: Signature): Single { @@ -26,7 +25,14 @@ class MerkleTransactionAdapter( .map { transactionManager.handle(listOf(it)).first() } } + fun registerInKit(ethereumKit: EthereumKit) { + ethereumKit.addNonceProvider(blockchain) + ethereumKit.addTransactionSyncer(syncer) + ethereumKit.addExtraDecorator(syncer) + } + companion object { + val protectedKey = "protected" private val blockchainPathMap = mapOf( Chain.Ethereum to "eth", @@ -34,6 +40,10 @@ class MerkleTransactionAdapter( Chain.Base to "base" ) + fun isProtected(transaction: FullTransaction): Boolean { + return transaction.extra[protectedKey] == true + } + fun getInstance( merkleIoPubKey: String, address: Address, diff --git a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionDao.kt b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionDao.kt index d34cc3f7..e72e3d6e 100644 --- a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionDao.kt +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionDao.kt @@ -10,6 +10,9 @@ interface MerkleTransactionDao { @Query("SELECT * FROM MerkleTransactionHash") fun hashes() : List + @Query("SELECT * FROM MerkleTransactionHash WHERE hash = :hash") + fun hash(hash: ByteArray) : MerkleTransactionHash? + @Insert fun save(hash: MerkleTransactionHash) diff --git a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHashManager.kt b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHashManager.kt index cfe01bba..821f38de 100644 --- a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHashManager.kt +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHashManager.kt @@ -4,6 +4,8 @@ class MerkleTransactionHashManager(private val dao: MerkleTransactionDao) { fun hashes() = dao.hashes() + fun hash(hash: ByteArray) = dao.hash(hash) + fun save(hash: MerkleTransactionHash) = dao.save(hash) fun handle(txHashes: List) = dao.delete(txHashes) diff --git a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionSyncer.kt b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionSyncer.kt index 29ad4939..052a36fe 100644 --- a/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionSyncer.kt +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionSyncer.kt @@ -1,5 +1,6 @@ package io.horizontalsystems.merkleiokit +import io.horizontalsystems.ethereumkit.core.IExtraDecorator import io.horizontalsystems.ethereumkit.core.ITransactionSyncer import io.horizontalsystems.ethereumkit.core.TransactionManager import io.horizontalsystems.ethereumkit.models.Transaction @@ -10,7 +11,7 @@ class MerkleTransactionSyncer( private val manager: MerkleTransactionHashManager, private val blockchain: MerkleRpcBlockchain, private val transactionManager: TransactionManager, -) : ITransactionSyncer { +) : ITransactionSyncer, IExtraDecorator { @OptIn(ExperimentalStdlibApi::class) override fun getTransactionsSingle(): Single, Boolean>> { @@ -51,4 +52,14 @@ class MerkleTransactionSyncer( Pair(failedTxs, false) } } + + override fun extra(hash: ByteArray): Map { + val merkleTransactionHash = manager.hash(hash) + + return if (merkleTransactionHash != null) { + mapOf(MerkleTransactionAdapter.protectedKey to true) + } else { + mapOf() + } + } }