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/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/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() { 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..afac9a08 100644 --- a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/core/EthereumKit.kt +++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/core/EthereumKit.kt @@ -64,7 +64,8 @@ import java.util.logging.Logger class EthereumKit( private val blockchain: IBlockchain, - private val transactionManager: TransactionManager, + private val nonceProvider: NonceProvider, + val transactionManager: TransactionManager, private val transactionSyncManager: TransactionSyncManager, private val connectionManager: ConnectionManager, private val address: Address, @@ -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,14 @@ class EthereumKit( transactionSyncManager.add(transactionSyncer) } + fun addNonceProvider(provider: INonceProvider) { + nonceProvider.addProvider(provider) + } + + fun addExtraDecorator(decorator: IExtraDecorator) { + decorationManager.addExtraDecorator(decorator) + } + fun addMethodDecorator(decorator: IMethodDecorator) { decorationManager.addMethodDecorator(decorator) } @@ -492,8 +501,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..7ceb74e2 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 @@ -106,6 +120,10 @@ interface IEventDecorator { fun contractEventInstances(logs: List): List } +interface IExtraDecorator { + fun extra(hash: ByteArray) : Map +} + interface ITransactionDecorator { fun decoration( from: Address?, @@ -125,3 +143,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 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 ) 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/.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..2a776832 --- /dev/null +++ b/merkleiokit/build.gradle @@ -0,0 +1,62 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'kotlin-kapt' + id 'kotlin-android-extensions' + id 'maven-publish' +} + +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + from components.release + } + } + } +} + +android { + compileSdkVersion 32 + + defaultConfig { + minSdkVersion 26 + targetSdkVersion 32 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + kotlinOptions { + jvmTarget = '11' + } +} + +dependencies { + + 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" + kapt "androidx.room:room-compiler:$room_version" + + implementation project(':ethereumkit') +} \ 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..c6bdebb0 --- /dev/null +++ b/merkleiokit/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ 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..70ce92cc --- /dev/null +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleRpcBlockchain.kt @@ -0,0 +1,57 @@ +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 +import io.horizontalsystems.ethereumkit.core.TransactionBuilder +import io.horizontalsystems.ethereumkit.models.Address +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 +import java.util.Optional + +class MerkleRpcBlockchain( + private val address: Address, + 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)) + .doOnSuccess { txHash -> + manager.save(MerkleTransactionHash(txHash)) + } + .map { tx } + } + + 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 new file mode 100644 index 00000000..b5ff6366 --- /dev/null +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionAdapter.kt @@ -0,0 +1,88 @@ +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.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.network.ConnectionManager +import io.reactivex.Single +import java.net.URI + +class MerkleTransactionAdapter( + val blockchain: MerkleRpcBlockchain, + val syncer: MerkleTransactionSyncer, + private val transactionManager: TransactionManager, +) { + fun send(rawTransaction: RawTransaction, signature: Signature): Single { + return blockchain.send(rawTransaction, signature) + .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", + Chain.BinanceSmartChain to "bsc", + Chain.Base to "base" + ) + + fun isProtected(transaction: FullTransaction): Boolean { + return transaction.extra[protectedKey] == true + } + + fun getInstance( + merkleIoPubKey: String, + address: Address, + chain: Chain, + context: Context, + walletId: String, + transactionManager: TransactionManager, + ): 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, + manager = merkleTransactionHashManager, + syncer = rpcSyncer, + transactionBuilder = transactionBuilder + ) + + val syncer = MerkleTransactionSyncer( + manager = merkleTransactionHashManager, + 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 new file mode 100644 index 00000000..e72e3d6e --- /dev/null +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionDao.kt @@ -0,0 +1,22 @@ +package io.horizontalsystems.merkleiokit + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query + +@Dao +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) + + @Query("DELETE FROM MerkleTransactionHash WHERE hash IN (:hashes)") + 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 new file mode 100644 index 00000000..e9f8f9c2 --- /dev/null +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHash.kt @@ -0,0 +1,10 @@ +package io.horizontalsystems.merkleiokit + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity +data class MerkleTransactionHash( + @PrimaryKey + 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 new file mode 100644 index 00000000..821f38de --- /dev/null +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionHashManager.kt @@ -0,0 +1,13 @@ +package io.horizontalsystems.merkleiokit + +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 new file mode 100644 index 00000000..052a36fe --- /dev/null +++ b/merkleiokit/src/main/java/io/horizontalsystems/merkleiokit/MerkleTransactionSyncer.kt @@ -0,0 +1,65 @@ +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 +import io.reactivex.Single +import kotlin.jvm.optionals.getOrNull + +class MerkleTransactionSyncer( + private val manager: MerkleTransactionHashManager, + private val blockchain: MerkleRpcBlockchain, + private val transactionManager: TransactionManager, +) : ITransactionSyncer, IExtraDecorator { + + @OptIn(ExperimentalStdlibApi::class) + override fun getTransactionsSingle(): Single, Boolean>> { + val hashes = manager.hashes() + if (hashes.isEmpty()) return Single.just(Pair(listOf(), false)) + + val singles = hashes.map { tx -> + blockchain.transaction(tx.hash) + .map { Pair(tx.hash, it.getOrNull()) } + .map { Result.success(it) } + .onErrorReturn { Result.failure(it) } + } + + val transactionsSingle = Single.merge(singles) + .filter { it.isSuccess } // Only keep successful ones + .map { it.getOrThrow() } // Extract the actual value + .toList() + + return transactionsSingle.map { rpcTransactions -> + val completedTxHashes = mutableListOf() + val failedTxHashes = mutableListOf() + val failedTxs = mutableListOf() + + rpcTransactions.forEach { (hash, rpcTransaction) -> + 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.handle(completedTxHashes + failedTxHashes) + + 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() + } + } +} 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'