diff --git a/app/src/androidTest/java/wallet/android/app/ConfirmParamsTest.kt b/app/src/androidTest/java/wallet/android/app/ConfirmParamsTest.kt index 7451d1d50..6863c593d 100644 --- a/app/src/androidTest/java/wallet/android/app/ConfirmParamsTest.kt +++ b/app/src/androidTest/java/wallet/android/app/ConfirmParamsTest.kt @@ -1,8 +1,10 @@ package wallet.android.app import com.gemwallet.android.model.ConfirmParams +import com.wallet.core.primitives.Account import com.wallet.core.primitives.AssetId import com.wallet.core.primitives.Chain +import com.wallet.core.primitives.TransactionType import org.junit.Assert.assertEquals import org.junit.Test import java.math.BigInteger @@ -16,9 +18,9 @@ class ConfirmParamsTest { } @Test fun testConfirmParamsPack() { - val params: ConfirmParams = ConfirmParams.DelegateParams(AssetId(Chain.Cosmos), amount = BigInteger.TEN, validatorId = "cosmosaddress") + val params: ConfirmParams = ConfirmParams.Stake.DelegateParams(AssetId(Chain.Cosmos), amount = BigInteger.TEN, validatorId = "cosmosaddress", from = Account(Chain.Cosmos, "", "", "")) val pack = params.pack() - val unpack = ConfirmParams.unpack(ConfirmParams.DelegateParams::class.java, pack!!)!! - assertEquals("cosmosaddress", unpack.validatorId) + val unpack = ConfirmParams.unpack(TransactionType.StakeDelegate, pack!!) + assertEquals("cosmosaddress", (unpack as ConfirmParams.Stake.DelegateParams).validatorId) } } diff --git a/app/src/androidTest/java/wallet/android/app/signers/TestAptosSign.kt b/app/src/androidTest/java/wallet/android/app/signers/TestAptosSign.kt index 6dc37f401..1822411d3 100644 --- a/app/src/androidTest/java/wallet/android/app/signers/TestAptosSign.kt +++ b/app/src/androidTest/java/wallet/android/app/signers/TestAptosSign.kt @@ -10,6 +10,7 @@ import com.gemwallet.android.model.DestinationAddress import com.gemwallet.android.model.GasFee import com.gemwallet.android.model.SignerParams import com.gemwallet.android.model.TxSpeed +import com.wallet.core.primitives.Account import com.wallet.core.primitives.Chain import kotlinx.coroutines.runBlocking import org.junit.Assert @@ -32,14 +33,14 @@ class TestAptosSign { val sign = runBlocking { signClient.signTransfer( params = SignerParams( - input = ConfirmParams.TransferParams( - assetId = com.wallet.core.primitives.Chain.Aptos.asset().id, - amount = BigInteger.TEN.pow(com.wallet.core.primitives.Chain.Aptos.asset().decimals), + input = ConfirmParams.TransferParams.Native( + assetId = Chain.Aptos.asset().id, + amount = BigInteger.TEN.pow(Chain.Aptos.asset().decimals), destination = DestinationAddress("0x82111f2975a0f6080d178236369b7479f6aed1203ef4a23f8205e4b91716b783"), + from = Account(chain = Chain.Aptos, "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", "", ""), ), - owner = "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", - finalAmount = BigInteger.TEN.pow(com.wallet.core.primitives.Chain.Aptos.asset().decimals), - info = AptosSignerPreloader.Info( + finalAmount = BigInteger.TEN.pow(Chain.Aptos.asset().decimals), + chainData = AptosSignerPreloader.AptosChainData( sequence = 1L, fee = GasFee( speed = TxSpeed.Normal, @@ -47,7 +48,7 @@ class TestAptosSign { limit = BigInteger("21000"), minerFee = BigInteger.TEN, relay = BigInteger.TEN, - feeAssetId = com.wallet.core.primitives.Chain.Aptos.asset().id, + feeAssetId = Chain.Aptos.asset().id, ) ) ), diff --git a/app/src/androidTest/java/wallet/android/app/signers/TestCosmosSign.kt b/app/src/androidTest/java/wallet/android/app/signers/TestCosmosSign.kt index 00be29922..40b149f14 100644 --- a/app/src/androidTest/java/wallet/android/app/signers/TestCosmosSign.kt +++ b/app/src/androidTest/java/wallet/android/app/signers/TestCosmosSign.kt @@ -10,6 +10,7 @@ import com.gemwallet.android.model.DestinationAddress import com.gemwallet.android.model.GasFee import com.gemwallet.android.model.SignerParams import com.gemwallet.android.model.TxSpeed +import com.wallet.core.primitives.Account import com.wallet.core.primitives.Chain import kotlinx.coroutines.runBlocking import org.junit.Assert @@ -32,14 +33,14 @@ class TestCosmosSign { val sign = runBlocking { signClient.signTransfer( params = SignerParams( - input = ConfirmParams.TransferParams( - assetId = com.wallet.core.primitives.Chain.Cosmos.asset().id, + input = ConfirmParams.TransferParams.Native( + assetId = Chain.Cosmos.asset().id, amount = BigInteger.TEN.pow(com.wallet.core.primitives.Chain.Cosmos.asset().decimals), destination = DestinationAddress("cosmos1kglemumu8mn658j6g4z9jzn3zef2qdyyydv7tr"), + from = Account(chain = Chain.Cosmos, address = "cosmos1kglemumu8mn658j6g4z9jzn3zef2qdyyydv7tr", "", "") ), - owner = "cosmos1kglemumu8mn658j6g4z9jzn3zef2qdyyydv7tr", finalAmount = BigInteger.TEN.pow(com.wallet.core.primitives.Chain.Cosmos.asset().decimals), - info = CosmosSignerPreloader.Info( + chainData = CosmosSignerPreloader.CosmosChainData( chainId = "", accountNumber = 1L, sequence = 1L, diff --git a/app/src/androidTest/java/wallet/android/app/signers/TestEthSign.kt b/app/src/androidTest/java/wallet/android/app/signers/TestEthSign.kt deleted file mode 100644 index bd468762a..000000000 --- a/app/src/androidTest/java/wallet/android/app/signers/TestEthSign.kt +++ /dev/null @@ -1,116 +0,0 @@ -package wallet.android.app.signers - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import com.gemwallet.android.blockchain.clients.ethereum.EvmSignClient -import com.gemwallet.android.blockchain.clients.ethereum.EvmSignerPreloader -import com.gemwallet.android.ext.asset -import com.gemwallet.android.math.toHexString -import com.gemwallet.android.model.ConfirmParams -import com.gemwallet.android.model.DestinationAddress -import com.gemwallet.android.model.GasFee -import com.gemwallet.android.model.SignerParams -import com.gemwallet.android.model.TxSpeed -import com.wallet.core.primitives.AssetId -import com.wallet.core.primitives.Chain -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals -import org.junit.Test -import org.junit.runner.RunWith -import wallet.android.app.testPhrase -import wallet.core.jni.CoinType -import wallet.core.jni.HDWallet -import java.math.BigInteger - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class TestEthSign { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.gemwallet.android", appContext.packageName) - } - - @Test - fun testEthSign() { - val signClient = EvmSignClient(Chain.Ethereum) - val privateKey = HDWallet(testPhrase, "") - .getKeyForCoin(CoinType.ETHEREUM) - - val sign = runBlocking { - signClient.signTransfer( - params = SignerParams( - input = ConfirmParams.TransferParams( - assetId = com.wallet.core.primitives.Chain.Ethereum.asset().id, - amount = BigInteger.TEN.pow(com.wallet.core.primitives.Chain.Ethereum.asset().decimals), - destination = DestinationAddress("0x9b1DB81180c31B1b428572Be105E209b5A6222b7"), - ), - owner = "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", - finalAmount = BigInteger.TEN.pow(com.wallet.core.primitives.Chain.Ethereum.asset().decimals), - info = EvmSignerPreloader.Info( - chainId = BigInteger.ONE, - nonce = BigInteger.ONE, - fee = GasFee( - maxGasPrice = BigInteger.TEN, - limit = BigInteger("21000"), - minerFee = BigInteger.TEN, - relay = BigInteger.TEN, - speed = TxSpeed.Normal, - feeAssetId = com.wallet.core.primitives.Chain.Ethereum.asset().id, - ) - ) - ), - txSpeed = TxSpeed.Normal, - privateKey.data(), - ) - } - assertEquals( - "0x02f86a01010a0a825208949b1db81180c31b1b428572be105e209b5a6222b7880de0b6b3a764000080c001a04936670cff2d450a1375fb2c42cf9f97130f9f9365197e4e8461a8c43fe24786a041833ce7835a78604c8518ef4dcef4e6dcd2e2d031dce759cd89790b6054fa07", - sign.toHexString() - ) - } - - @Test - fun testEthTokenSign() { - val signClient = EvmSignClient(Chain.Ethereum) - val privateKey = HDWallet("seminar cruel gown pause law tortoise step stairs size amused pond weapon", "") - .getKeyForCoin(CoinType.ETHEREUM) - - val sign = runBlocking { - signClient.signTransfer( - params = SignerParams( - input = ConfirmParams.TransferParams( - assetId = AssetId(Chain.Ethereum, "0xdAC17F958D2ee523a2206206994597C13D831ec7"), - amount = BigInteger.TEN.pow(com.wallet.core.primitives.Chain.Ethereum.asset().decimals), - destination = DestinationAddress("0x9b1DB81180c31B1b428572Be105E209b5A6222b7"), - ), - owner = "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", - finalAmount = BigInteger.TEN.pow(com.wallet.core.primitives.Chain.Ethereum.asset().decimals), - info = EvmSignerPreloader.Info( - chainId = BigInteger.ONE, - nonce = BigInteger.ONE, - fee = GasFee( - maxGasPrice = BigInteger.TEN, - limit = BigInteger("91000"), - minerFee = BigInteger.TEN, - relay = BigInteger.TEN, - speed = TxSpeed.Normal, - feeAssetId = com.wallet.core.primitives.Chain.Ethereum.asset().id, - ) - ) - ), - txSpeed = TxSpeed.Normal, - privateKey.data(), - ) - } - assertEquals( - "0x02f8a801010a0a8301637894dac17f958d2ee523a2206206994597c13d831ec780b844a9059cbb0000000000000000000000009b1db81180c31b1b428572be105e209b5a6222b70000000000000000000000000000000000000000000000000de0b6b3a7640000c001a02a975d0be8ce97d4518cd22407a21697f0177b8ca7c057b868eaba32aefd6887a00b20f74083ac147b7733c0b72d2f87cc96fc75be610da14b4f6265703421d273", - sign.toHexString() - ) - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/wallet/android/app/signers/TestSolanaSign.kt b/app/src/androidTest/java/wallet/android/app/signers/TestSolanaSign.kt index 17e0ac959..0fcd67a72 100644 --- a/app/src/androidTest/java/wallet/android/app/signers/TestSolanaSign.kt +++ b/app/src/androidTest/java/wallet/android/app/signers/TestSolanaSign.kt @@ -11,6 +11,7 @@ import com.gemwallet.android.model.DestinationAddress import com.gemwallet.android.model.GasFee import com.gemwallet.android.model.SignerParams import com.gemwallet.android.model.TxSpeed +import com.wallet.core.primitives.Account import com.wallet.core.primitives.Asset import com.wallet.core.primitives.AssetId import com.wallet.core.primitives.Chain @@ -40,14 +41,14 @@ class TestSolanaSign { val sign = runBlocking { signClient.signTransfer( params = SignerParams( - input = ConfirmParams.TransferParams( + input = ConfirmParams.TransferParams.Token( assetId = Chain.Solana.asset().id, amount = BigInteger.TEN.pow(Chain.Solana.asset().decimals), + from = Account(chain = Chain.Solana, address = "4Yu2e1Wz5T1Ci2hAPswDqvMgSnJ1Ftw7ZZh8x7xKLx7S", "", null), destination = DestinationAddress("4Yu2e1Wz5T1Ci2hAPswDqvMgSnJ1Ftw7ZZh8x7xKLx7S"), ), - owner = "4Yu2e1Wz5T1Ci2hAPswDqvMgSnJ1Ftw7ZZh8x7xKLx7S", finalAmount = BigInteger.TEN.pow(Chain.Solana.asset().decimals), - info = SolanaSignerPreloader.Info( + chainData = SolanaSignerPreloader.SolanaChainData( blockhash = "DzfXchZJoLMG3cNftcf2sw7qatkkuwQf4xH15N5wkKAb", senderTokenAddress = "", recipientTokenAddress = null, diff --git a/app/src/main/java/com/gemwallet/android/di/DataModule.kt b/app/src/main/java/com/gemwallet/android/di/DataModule.kt index 10834610e..c65195632 100644 --- a/app/src/main/java/com/gemwallet/android/di/DataModule.kt +++ b/app/src/main/java/com/gemwallet/android/di/DataModule.kt @@ -1,11 +1,14 @@ package com.gemwallet.android.di import com.gemwallet.android.blockchain.RpcClientAdapter +import com.gemwallet.android.blockchain.clients.ApprovalTransactionPreloader import com.gemwallet.android.blockchain.clients.BroadcastClientProxy import com.gemwallet.android.blockchain.clients.NodeStatusClientProxy import com.gemwallet.android.blockchain.clients.SignClientProxy -import com.gemwallet.android.blockchain.clients.SignerPreload import com.gemwallet.android.blockchain.clients.SignerPreloaderProxy +import com.gemwallet.android.blockchain.clients.StakeTransactionPreloader +import com.gemwallet.android.blockchain.clients.SwapTransactionPreloader +import com.gemwallet.android.blockchain.clients.TokenTransferPreloader import com.gemwallet.android.blockchain.clients.aptos.AptosBroadcastClient import com.gemwallet.android.blockchain.clients.aptos.AptosNodeStatusClient import com.gemwallet.android.blockchain.clients.aptos.AptosSignClient @@ -92,22 +95,29 @@ object DataModule { @Singleton fun provideSignerPreloader( rpcClients: RpcClientAdapter, - ): SignerPreload = SignerPreloaderProxy( - Chain.available().map { + ): SignerPreloaderProxy { + val preloaders = Chain.available().map { when (it.toChainType()) { - ChainType.Bitcoin -> BitcoinSignerPreloader(it, rpcClients.getClient(it)) - ChainType.Ethereum -> EvmSignerPreloader(it, rpcClients.getClient(it)) - ChainType.Solana -> SolanaSignerPreloader(it, rpcClients.getClient(Chain.Solana)) + ChainType.Bitcoin -> BitcoinSignerPreloader(it, rpcClients.getClient(it), rpcClients.getClient(it)) + ChainType.Ethereum -> EvmSignerPreloader(it, rpcClients.getClient(it), rpcClients.getClient(it)) + ChainType.Solana -> SolanaSignerPreloader(it, rpcClients.getClient(Chain.Solana), rpcClients.getClient(Chain.Solana), rpcClients.getClient(Chain.Solana)) ChainType.Cosmos -> CosmosSignerPreloader(it, rpcClients.getClient(it)) ChainType.Ton -> TonSignerPreloader(it, rpcClients.getClient(it)) ChainType.Tron -> TronSignerPreloader(it, rpcClients.getClient(Chain.Tron)) - ChainType.Aptos -> AptosSignerPreloader(it, rpcClients.getClient(it)) + ChainType.Aptos -> AptosSignerPreloader(it, rpcClients.getClient(it), rpcClients.getClient(it)) ChainType.Sui -> SuiSignerPreloader(it, rpcClients.getClient(it)) ChainType.Xrp -> XrpSignerPreloader(it, rpcClients.getClient(it)) ChainType.Near -> NearSignerPreloader(it, rpcClients.getClient(it)) } - }, - ) + } + return SignerPreloaderProxy( + nativeTransferClients = preloaders, + tokenTransferClients = preloaders.mapNotNull { it as? TokenTransferPreloader }, + stakeTransactionClients = preloaders.mapNotNull { it as? StakeTransactionPreloader }, + swapTransactionClients = preloaders.mapNotNull { it as? SwapTransactionPreloader }, + approvalTransactionClients = preloaders.mapNotNull { it as? ApprovalTransactionPreloader }, + ) + } @Provides @Singleton diff --git a/app/src/main/java/com/gemwallet/android/features/amount/viewmodels/AmountViewModel.kt b/app/src/main/java/com/gemwallet/android/features/amount/viewmodels/AmountViewModel.kt index 015a73cbd..12cb5ffbe 100644 --- a/app/src/main/java/com/gemwallet/android/features/amount/viewmodels/AmountViewModel.kt +++ b/app/src/main/java/com/gemwallet/android/features/amount/viewmodels/AmountViewModel.kt @@ -194,7 +194,7 @@ class AmountViewModel @Inject constructor( val memo = params.memo inputErrorState.update { AmountError.None } nextErrorState.update { AmountError.None } - val builder = ConfirmParams.Builder(asset.id, amount.atomicValue) + val builder = ConfirmParams.Builder(asset.id, state.assetInfo.owner, amount.atomicValue) val nextParams = when (params.txType) { TransactionType.Transfer -> builder.transfer( destination = destination!!, diff --git a/app/src/main/java/com/gemwallet/android/features/asset/details/views/AssetDetailsScene.kt b/app/src/main/java/com/gemwallet/android/features/asset/details/views/AssetDetailsScene.kt index 969cad6ac..c4282c177 100644 --- a/app/src/main/java/com/gemwallet/android/features/asset/details/views/AssetDetailsScene.kt +++ b/app/src/main/java/com/gemwallet/android/features/asset/details/views/AssetDetailsScene.kt @@ -146,9 +146,7 @@ private fun Success( }, actions = { IconButton( - onClick = { - onPriceAlert(uiState.asset.id) - } + onClick = { onPriceAlert(uiState.asset.id) } ) { if (priceAlertEnabled) { Icon(Icons.Default.Notifications, "") @@ -157,9 +155,7 @@ private fun Success( } } IconButton( - onClick = { - clipboardManager.setText(AnnotatedString(uiState.account.owner)) - } + onClick = { clipboardManager.setText(AnnotatedString(uiState.account.owner)) } ) { Icon(Icons.Default.ContentCopy, "") } diff --git a/app/src/main/java/com/gemwallet/android/features/bridge/request/RequestScene.kt b/app/src/main/java/com/gemwallet/android/features/bridge/request/RequestScene.kt index 11024b113..0bb10e28a 100644 --- a/app/src/main/java/com/gemwallet/android/features/bridge/request/RequestScene.kt +++ b/app/src/main/java/com/gemwallet/android/features/bridge/request/RequestScene.kt @@ -57,8 +57,9 @@ fun RequestScene( ) is RequestSceneState.SendTransaction -> { ConfirmScreen( - params = ConfirmParams.TransferParams( - assetId = AssetId((sceneState as RequestSceneState.SendTransaction).chain), + params = ConfirmParams.TransferParams.Native( + from = (sceneState as RequestSceneState.SendTransaction).account, + assetId = AssetId((sceneState as RequestSceneState.SendTransaction).account.chain), amount = (sceneState as RequestSceneState.SendTransaction).value, destination = DestinationAddress(address = (sceneState as RequestSceneState.SendTransaction).to), memo = (sceneState as RequestSceneState.SendTransaction).data, diff --git a/app/src/main/java/com/gemwallet/android/features/bridge/request/RequestViewModel.kt b/app/src/main/java/com/gemwallet/android/features/bridge/request/RequestViewModel.kt index 151df476e..f86af045f 100644 --- a/app/src/main/java/com/gemwallet/android/features/bridge/request/RequestViewModel.kt +++ b/app/src/main/java/com/gemwallet/android/features/bridge/request/RequestViewModel.kt @@ -7,12 +7,14 @@ import com.gemwallet.android.blockchain.operators.LoadPrivateKeyOperator import com.gemwallet.android.blockchain.operators.PasswordStore import com.gemwallet.android.data.repositoreis.bridge.findByNamespace import com.gemwallet.android.data.repositoreis.session.SessionRepository +import com.gemwallet.android.ext.getAccount import com.gemwallet.android.features.bridge.model.PeerUI import com.gemwallet.android.math.decodeHex import com.gemwallet.android.math.hexToBigInteger import com.gemwallet.android.math.toHexString import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit +import com.wallet.core.primitives.Account import com.wallet.core.primitives.Chain import com.wallet.core.primitives.WalletConnectionMethods import dagger.hilt.android.lifecycle.HiltViewModel @@ -176,11 +178,13 @@ private data class RequestViewModelState( if (sessionRequest == null) { return RequestSceneState.Loading } + val account = wallet?.getAccount(chain!!)!! return when (sessionRequest.request.method) { WalletConnectionMethods.eth_sign.string, WalletConnectionMethods.personal_sign.string, WalletConnectionMethods.eth_sign_typed_data_v4.string, WalletConnectionMethods.eth_sign_typed_data.string -> RequestSceneState.SignMessage( + account = account, walletName = wallet?.name ?: "", peer = PeerUI( peerName = sessionRequest.peerMetaData?.name ?: "", @@ -198,7 +202,7 @@ private data class RequestViewModelState( val value = jObj.optString("value", "0x0").hexToBigInteger() ?: BigInteger.ZERO val data = jObj.getString("data") RequestSceneState.SendTransaction( - chain = chain!!, + account = account, to = to, value = value, data = data, @@ -222,12 +226,13 @@ sealed interface RequestSceneState { class SignMessage( val walletName: String, + val account: Account, val peer: PeerUI, val params: String, ) : RequestSceneState class SendTransaction( - val chain: Chain, + val account: Account, val to: String, val value: BigInteger, val data: String, diff --git a/app/src/main/java/com/gemwallet/android/features/confirm/models/ConfirmError.kt b/app/src/main/java/com/gemwallet/android/features/confirm/models/ConfirmError.kt index 8d0665d37..fc2241b1d 100644 --- a/app/src/main/java/com/gemwallet/android/features/confirm/models/ConfirmError.kt +++ b/app/src/main/java/com/gemwallet/android/features/confirm/models/ConfirmError.kt @@ -6,7 +6,7 @@ sealed class ConfirmError(message: String) : Exception(message){ class Init(message: String) : ConfirmError(message) - data object CalculateFee : ConfirmError("Calculate fee error") + class PreloadError(message: String) : ConfirmError(message) data object TransactionIncorrect : ConfirmError("Transaction data incorrect") diff --git a/app/src/main/java/com/gemwallet/android/features/confirm/navigation/ConfirmNavigation.kt b/app/src/main/java/com/gemwallet/android/features/confirm/navigation/ConfirmNavigation.kt index ec93e2a2f..6ea4f2d33 100644 --- a/app/src/main/java/com/gemwallet/android/features/confirm/navigation/ConfirmNavigation.kt +++ b/app/src/main/java/com/gemwallet/android/features/confirm/navigation/ConfirmNavigation.kt @@ -52,19 +52,7 @@ fun NavGraphBuilder.confirm( cancelAction() return@composable } - val params = ConfirmParams.unpack( - when (txType) { - TransactionType.Transfer -> ConfirmParams.TransferParams::class.java - TransactionType.Swap -> ConfirmParams.SwapParams::class.java - TransactionType.TokenApproval -> ConfirmParams.TokenApprovalParams::class.java - TransactionType.StakeDelegate -> ConfirmParams.DelegateParams::class.java - TransactionType.StakeUndelegate -> ConfirmParams.UndelegateParams::class.java - TransactionType.StakeRewards -> ConfirmParams.RewardsParams::class.java - TransactionType.StakeRedelegate -> ConfirmParams.RedeleateParams::class.java - TransactionType.StakeWithdraw -> ConfirmParams.WithdrawParams::class.java - }, - paramsPack, - ) + val params = ConfirmParams.unpack(txType, paramsPack) if (params == null) { cancelAction() diff --git a/app/src/main/java/com/gemwallet/android/features/confirm/viewmodels/ConfirmViewModel.kt b/app/src/main/java/com/gemwallet/android/features/confirm/viewmodels/ConfirmViewModel.kt index f6d2ea1e7..abef10aed 100644 --- a/app/src/main/java/com/gemwallet/android/features/confirm/viewmodels/ConfirmViewModel.kt +++ b/app/src/main/java/com/gemwallet/android/features/confirm/viewmodels/ConfirmViewModel.kt @@ -6,7 +6,7 @@ import androidx.lifecycle.viewModelScope import com.gemwallet.android.R import com.gemwallet.android.blockchain.clients.BroadcastClientProxy import com.gemwallet.android.blockchain.clients.SignClientProxy -import com.gemwallet.android.blockchain.clients.SignerPreload +import com.gemwallet.android.blockchain.clients.SignerPreloaderProxy import com.gemwallet.android.blockchain.operators.LoadPrivateKeyOperator import com.gemwallet.android.blockchain.operators.PasswordStore import com.gemwallet.android.cases.transactions.CreateTransactionCase @@ -69,7 +69,7 @@ import javax.inject.Inject class ConfirmViewModel @Inject constructor( private val sessionRepository: SessionRepository, private val assetsRepository: AssetsRepository, - private val signerPreload: SignerPreload, + private val signerPreload: SignerPreloaderProxy, private val passwordStore: PasswordStore, private val loadPrivateKeyOperator: LoadPrivateKeyOperator, private val signClient: SignClientProxy, @@ -93,19 +93,7 @@ class ConfirmViewModel @Inject constructor( state.update { ConfirmState.Prepare } - ConfirmParams.unpack( - when (txType) { - TransactionType.Transfer -> ConfirmParams.TransferParams::class.java - TransactionType.Swap -> ConfirmParams.SwapParams::class.java - TransactionType.TokenApproval -> ConfirmParams.TokenApprovalParams::class.java - TransactionType.StakeDelegate -> ConfirmParams.DelegateParams::class.java - TransactionType.StakeUndelegate -> ConfirmParams.UndelegateParams::class.java - TransactionType.StakeRewards -> ConfirmParams.RewardsParams::class.java - TransactionType.StakeRedelegate -> ConfirmParams.RedeleateParams::class.java - TransactionType.StakeWithdraw -> ConfirmParams.WithdrawParams::class.java - }, - paramsPack, - ) + ConfirmParams.unpack(txType, paramsPack) } .stateIn(viewModelScope, SharingStarted.Eagerly, null) @@ -126,19 +114,20 @@ class ConfirmViewModel @Inject constructor( return@map null } - val preload = signerPreload(owner = owner, params = request).getOrNull() - if (preload == null) { - state.update { ConfirmState.Error(ConfirmError.CalculateFee) } + val preload = try { + signerPreload.preload(params = request) + } catch (err: Throwable) { + state.update { ConfirmState.Error(ConfirmError.PreloadError(err.message ?: "Preload error")) } return@map null } preload }.filterNotNull().combine(txSpeed) { params, txSpeed -> val finalAmount = when { - params.input is ConfirmParams.RewardsParams -> stakeRepository.getRewards(params.input.assetId, params.owner) + params.input is ConfirmParams.Stake.RewardsParams -> stakeRepository.getRewards(params.input.assetId, params.input.from.address) .map { BigInteger(it.base.rewards) } .fold(BigInteger.ZERO) { acc, value -> acc + value } - params.input.isMax() && params.input.assetId == params.info.fee(txSpeed).feeAssetId -> - params.input.amount - params.info.fee(txSpeed).amount + params.input.isMax() && params.input.assetId == params.chainData.fee(txSpeed).feeAssetId -> + params.input.amount - params.chainData.fee(txSpeed).amount else -> params.input.amount } state.update { ConfirmState.Ready } @@ -147,7 +136,7 @@ class ConfirmViewModel @Inject constructor( .stateIn(viewModelScope, SharingStarted.Eagerly, null) private val feeAssetInfo = signerParams.filterNotNull().flatMapLatest { signerParams -> - assetsRepository.getAssetInfo(signerParams.info.fee().feeAssetId) + assetsRepository.getAssetInfo(signerParams.chainData.fee().feeAssetId) } .stateIn(viewModelScope, SharingStarted.Eagerly, null) @@ -190,11 +179,11 @@ class ConfirmViewModel @Inject constructor( }.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) val feeUIModel = combine(signerParams, feeAssetInfo, state, txSpeed) { signerParams, feeAssetInfo, state, speed -> - val amount = signerParams?.info?.fee(speed)?.amount + val amount = signerParams?.chainData?.fee(speed)?.amount val result = if (amount == null || feeAssetInfo == null) { CellEntity( label = R.string.transfer_network_fee, - data = if ((state as? ConfirmState.Error)?.message == ConfirmError.CalculateFee) "-" else "", + data = if (state is ConfirmState.Error) "-" else "", trailing = { if (state !is ConfirmState.Error) { CircularProgressIndicator16() @@ -238,7 +227,7 @@ class ConfirmViewModel @Inject constructor( } .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) - val allFee = signerParams.filterNotNull().map { it.info.allFee() } + val allFee = signerParams.filterNotNull().map { it.chainData.allFee() } .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) fun init(params: ConfirmParams) { @@ -293,7 +282,7 @@ class ConfirmViewModel @Inject constructor( owner = assetInfo.owner, to = destinationAddress, state = TransactionState.Pending, - fee = signerParams.info.fee(), + fee = signerParams.chainData.fee(), amount = signerParams.finalAmount, memo = signerParams.input.memo() ?: "", type = signerParams.input.getTxType(), @@ -306,11 +295,7 @@ class ConfirmViewModel @Inject constructor( ) state.update { ConfirmState.Result(txHash = txHash) } val finishRoute = when (signerParams.input) { - is ConfirmParams.DelegateParams, - is ConfirmParams.RedeleateParams, - is ConfirmParams.RewardsParams, - is ConfirmParams.UndelegateParams, - is ConfirmParams.WithdrawParams -> stakeRoute + is ConfirmParams.Stake -> stakeRoute is ConfirmParams.SwapParams, is ConfirmParams.TokenApprovalParams -> swapRoute is ConfirmParams.TransferParams -> assetRoute @@ -343,22 +328,22 @@ class ConfirmViewModel @Inject constructor( is ConfirmParams.TransferParams, is ConfirmParams.SwapParams, is ConfirmParams.TokenApprovalParams, - is ConfirmParams.DelegateParams -> assetInfo.balance.balance.available.toBigInteger() - is ConfirmParams.RedeleateParams -> BigInteger(stakeRepository.getDelegation(params.srcValidatorId).firstOrNull()?.base?.balance ?: "0") - is ConfirmParams.UndelegateParams -> BigInteger(stakeRepository.getDelegation(params.validatorId, params.delegationId).firstOrNull()?.base?.balance ?: "0") - is ConfirmParams.WithdrawParams -> BigInteger(stakeRepository.getDelegation(params.validatorId, params.delegationId).firstOrNull()?.base?.balance ?: "0") - is ConfirmParams.RewardsParams -> stakeRepository.getRewards(assetInfo.asset.id, assetInfo.owner.address) + is ConfirmParams.Stake.DelegateParams -> assetInfo.balance.balance.available.toBigInteger() + is ConfirmParams.Stake.RedelegateParams -> BigInteger(stakeRepository.getDelegation(params.srcValidatorId).firstOrNull()?.base?.balance ?: "0") + is ConfirmParams.Stake.UndelegateParams -> BigInteger(stakeRepository.getDelegation(params.validatorId, params.delegationId).firstOrNull()?.base?.balance ?: "0") + is ConfirmParams.Stake.WithdrawParams -> BigInteger(stakeRepository.getDelegation(params.validatorId, params.delegationId).firstOrNull()?.base?.balance ?: "0") + is ConfirmParams.Stake.RewardsParams -> stakeRepository.getRewards(assetInfo.asset.id, assetInfo.owner.address) .fold(BigInteger.ZERO) { acc, delegation -> acc + BigInteger(delegation.base.balance) } } } private suspend fun getValidator(params: ConfirmParams): DelegationValidator? { val validatorId = when (params) { - is ConfirmParams.DelegateParams -> params.validatorId - is ConfirmParams.RedeleateParams -> params.dstValidatorId - is ConfirmParams.UndelegateParams -> params.validatorId - is ConfirmParams.WithdrawParams -> params.validatorId - is ConfirmParams.RewardsParams, + is ConfirmParams.Stake.DelegateParams -> params.validatorId + is ConfirmParams.Stake.RedelegateParams -> params.dstValidatorId + is ConfirmParams.Stake.UndelegateParams -> params.validatorId + is ConfirmParams.Stake.WithdrawParams -> params.validatorId + is ConfirmParams.Stake.RewardsParams, is ConfirmParams.SwapParams, is ConfirmParams.TokenApprovalParams, is ConfirmParams.TransferParams -> null @@ -373,11 +358,11 @@ class ConfirmViewModel @Inject constructor( private fun ConfirmParams.getRecipientCell(validator: DelegationValidator?): CellEntity? { return when (this) { - is ConfirmParams.RewardsParams -> null - is ConfirmParams.DelegateParams, - is ConfirmParams.RedeleateParams, - is ConfirmParams.UndelegateParams, - is ConfirmParams.WithdrawParams -> CellEntity(label = R.string.stake_validator, data = validator?.name ?: "") + is ConfirmParams.Stake.RewardsParams -> null + is ConfirmParams.Stake.DelegateParams, + is ConfirmParams.Stake.RedelegateParams, + is ConfirmParams.Stake.UndelegateParams, + is ConfirmParams.Stake.WithdrawParams -> CellEntity(label = R.string.stake_validator, data = validator?.name ?: "") is ConfirmParams.SwapParams -> CellEntity(label = R.string.swap_provider, data = provider) is ConfirmParams.TokenApprovalParams -> CellEntity(label = R.string.swap_provider, data = provider) is ConfirmParams.TransferParams -> { @@ -436,7 +421,7 @@ class ConfirmViewModel @Inject constructor( assetBalance: BigInteger, ) { val amount = signerParams.finalAmount - val feeAmount = signerParams.info.fee(txSpeed).amount + val feeAmount = signerParams.chainData.fee(txSpeed).amount val totalAmount = when (signerParams.input.getTxType()) { TransactionType.Transfer, diff --git a/app/src/main/java/com/gemwallet/android/features/confirm/views/ConfirmScreen.kt b/app/src/main/java/com/gemwallet/android/features/confirm/views/ConfirmScreen.kt index 3d2788f7e..80d7e5dda 100644 --- a/app/src/main/java/com/gemwallet/android/features/confirm/views/ConfirmScreen.kt +++ b/app/src/main/java/com/gemwallet/android/features/confirm/views/ConfirmScreen.kt @@ -14,7 +14,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -158,7 +157,7 @@ private fun ConfirmErrorInfo(state: ConfirmState) { text = when (state.message) { is ConfirmError.Init, is ConfirmError.TransactionIncorrect, - is ConfirmError.CalculateFee -> stringResource(R.string.confirm_fee_error) + is ConfirmError.PreloadError -> stringResource(R.string.confirm_fee_error) is ConfirmError.InsufficientBalance -> stringResource(R.string.transfer_insufficient_network_fee_balance, state.message.chainTitle) is ConfirmError.InsufficientFee -> stringResource(R.string.transfer_insufficient_network_fee_balance, state.message.chainTitle) is ConfirmError.BroadcastError -> "${stringResource(R.string.errors_transfer_error)}: ${state.message.message ?: stringResource(R.string.errors_unknown)}" diff --git a/app/src/main/java/com/gemwallet/android/features/stake/components/DelegationItem.kt b/app/src/main/java/com/gemwallet/android/features/stake/components/DelegationItem.kt index 4107b60d8..2a665e915 100644 --- a/app/src/main/java/com/gemwallet/android/features/stake/components/DelegationItem.kt +++ b/app/src/main/java/com/gemwallet/android/features/stake/components/DelegationItem.kt @@ -72,7 +72,7 @@ fun DelegationItem( trailing = { Column( verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.Start, + horizontalAlignment = Alignment.End, ) { ListItemTitleText(Crypto(delegation.base.balance).format(assetDecimals, assetSymbol, 2)) diff --git a/app/src/main/java/com/gemwallet/android/features/stake/stake/viewmodels/StakeViewModel.kt b/app/src/main/java/com/gemwallet/android/features/stake/stake/viewmodels/StakeViewModel.kt index 95efbd737..5298f2183 100644 --- a/app/src/main/java/com/gemwallet/android/features/stake/stake/viewmodels/StakeViewModel.kt +++ b/app/src/main/java/com/gemwallet/android/features/stake/stake/viewmodels/StakeViewModel.kt @@ -80,8 +80,9 @@ class StakeViewModel @Inject constructor( fun onRewards(onConfirm: (ConfirmParams) -> Unit) { val currentState = state.value onConfirm( - ConfirmParams.RewardsParams( + ConfirmParams.Stake.RewardsParams( assetId = currentState.asset?.asset?.id!!, + from = currentState.asset.owner, validatorsId = currentState.delegations .filter { BigInteger(it.base.rewards) > BigInteger.ZERO } .map { it.base.validatorId } diff --git a/app/src/main/java/com/gemwallet/android/features/swap/viewmodels/SwapViewModel.kt b/app/src/main/java/com/gemwallet/android/features/swap/viewmodels/SwapViewModel.kt index 832621661..86bfdabdc 100644 --- a/app/src/main/java/com/gemwallet/android/features/swap/viewmodels/SwapViewModel.kt +++ b/app/src/main/java/com/gemwallet/android/features/swap/viewmodels/SwapViewModel.kt @@ -291,6 +291,7 @@ class SwapViewModel @Inject constructor( onConfirm( ConfirmParams.TokenApprovalParams( assetId = if (from.asset.id.type() == AssetSubtype.TOKEN) from.asset.id else to.asset.id, + from = from.owner, data = encodeApprove(approvalData.v1.spender).toHexString(), provider = swapProviderNameToString(quote.data.provider), contract = approvalData.v1.token, @@ -302,6 +303,7 @@ class SwapViewModel @Inject constructor( swapScreenState.update { SwapState.Ready } onConfirm( ConfirmParams.SwapParams( + from = from.owner, fromAssetId = from.asset.id, toAssetId = to.asset.id, fromAmount = Crypto(fromAmount, from.asset.decimals).atomicValue, diff --git a/blockchain/build.gradle.kts b/blockchain/build.gradle.kts index f4d2deb30..5ed2c8a86 100644 --- a/blockchain/build.gradle.kts +++ b/blockchain/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { api(project(":gemcore")) // version catalog might not work //noinspection UseTomlInstead + api("net.java.dev.jna:jna:5.15.0@aar") api("com.gemwallet.gemstone:gemstone:1.0.0@aar") // Local wallet core api(files("../libs/wallet-core-4.1.19-sources.jar")) diff --git a/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/Const.kt b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/Const.kt new file mode 100644 index 000000000..dcef2c2c2 --- /dev/null +++ b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/Const.kt @@ -0,0 +1,3 @@ +package com.gemwallet.android.blockchain + +val testPhrase = "seminar cruel gown pause law tortoise step stairs size amused pond weapon" \ No newline at end of file diff --git a/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/IncludeLibs.kt b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/IncludeLibs.kt new file mode 100644 index 000000000..261d7b375 --- /dev/null +++ b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/IncludeLibs.kt @@ -0,0 +1,6 @@ +package com.gemwallet.android.blockchain + +fun includeLibs() { + System.loadLibrary("TrustWalletCore") + System.loadLibrary("gemstone") +} \ No newline at end of file diff --git a/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/aptos/TestAptosBalance.kt b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/aptos/TestAptosBalance.kt new file mode 100644 index 000000000..78ce768c3 --- /dev/null +++ b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/aptos/TestAptosBalance.kt @@ -0,0 +1,157 @@ +package com.gemwallet.android.blockchain.clients.aptos + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.gemwallet.android.blockchain.clients.aptos.services.AptosBalancesService +import com.gemwallet.android.blockchain.includeLibs +import com.gemwallet.android.ext.toIdentifier +import com.wallet.core.blockchain.aptos.models.AptosResource +import com.wallet.core.blockchain.aptos.models.AptosResourceBalance +import com.wallet.core.blockchain.aptos.models.AptosResourceCoin +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.Chain +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertNull +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class TestAptosBalance { + + companion object { + init { + includeLibs() + } + } + + @Test + fun testAptosBalance() { + val client = AptosBalanceClient( + chain = Chain.Aptos, + balanceService = object : AptosBalancesService { + override suspend fun balance(address: String): Result> { + assertEquals("0x80c3cca35602e4568a7ac88d4d91110f8efa6c45c659439c2b4ed04033059c6f", address) + return Result.success( + AptosResource( + type = "", + data = AptosResourceBalance( + coin = AptosResourceCoin( + value = "100000000" + ) + ) + ) + ) + } + } + ) + + val result = runBlocking { + client.getNativeBalance(Chain.Aptos, "0x80c3cca35602e4568a7ac88d4d91110f8efa6c45c659439c2b4ed04033059c6f") + } + assertNotNull(result) + assertEquals("100000000", result!!.balance.available) + assertEquals("0", result.balance.frozen) + assertEquals("0", result.balance.locked) + assertEquals("0", result.balance.staked) + assertEquals("0", result.balance.rewards) + assertEquals("0", result.balance.reserved) + assertEquals("0", result.balance.pending) + assertEquals(1.0, result.balanceAmount.available) + assertEquals(0.0, result.balanceAmount.frozen) + assertEquals(0.0, result.balanceAmount.locked) + assertEquals(0.0, result.balanceAmount.staked) + assertEquals(0.0, result.balanceAmount.rewards) + assertEquals(0.0, result.balanceAmount.reserved) + assertEquals(0.0, result.balanceAmount.pending) + assertEquals(1.0, result.totalAmount) + assertEquals(AssetId(Chain.Aptos).toIdentifier(), result.asset.id.toIdentifier()) + } + + @Test + fun testAptosBalanceFail() { + val client = AptosBalanceClient( + chain = Chain.Aptos, + balanceService = object : AptosBalancesService { + override suspend fun balance(address: String): Result> { + assertEquals("0x80c3cca35602e4568a7ac88d4d91110f8efa6c45c659439c2b4ed04033059c6f", address) + return Result.failure(Exception()) + } + } + ) + + runBlocking { + val result = client.getNativeBalance(Chain.Aptos, "0x80c3cca35602e4568a7ac88d4d91110f8efa6c45c659439c2b4ed04033059c6f") + assertNull(result) + } + } + + @Test + fun testAptosBalanceBadValue() { + val client = AptosBalanceClient( + chain = Chain.Aptos, + balanceService = object : AptosBalancesService { + override suspend fun balance(address: String): Result> { + return Result.success( + AptosResource( + type = "", + data = AptosResourceBalance( + coin = AptosResourceCoin( + value = "0abcde" + ) + ) + ) + ) + } + } + ) + + runBlocking { + val result = client.getNativeBalance(Chain.Aptos, "0x80c3cca35602e4568a7ac88d4d91110f8efa6c45c659439c2b4ed04033059c6f") + assertNull(result) + } + } + + @Test + fun testAptosBalanceEmpty() { + val client = AptosBalanceClient( + chain = Chain.Aptos, + balanceService = object : AptosBalancesService { + override suspend fun balance(address: String): Result> { + return Result.success( + AptosResource( + type = "", + data = AptosResourceBalance( + coin = AptosResourceCoin( + value = "0" + ) + ) + ) + ) + } + + } + ) + + runBlocking { + val result = client.getNativeBalance(Chain.Aptos, "0x80c3cca35602e4568a7ac88d4d91110f8efa6c45c659439c2b4ed04033059c6f") + assertNotNull(result) + assertEquals("0", result!!.balance.available) + assertEquals("0", result.balance.frozen) + assertEquals("0", result.balance.locked) + assertEquals("0", result.balance.staked) + assertEquals("0", result.balance.rewards) + assertEquals("0", result.balance.reserved) + assertEquals("0", result.balance.pending) + assertEquals(0.0, result.balanceAmount.available) + assertEquals(0.0, result.balanceAmount.frozen) + assertEquals(0.0, result.balanceAmount.locked) + assertEquals(0.0, result.balanceAmount.staked) + assertEquals(0.0, result.balanceAmount.rewards) + assertEquals(0.0, result.balanceAmount.reserved) + assertEquals(0.0, result.balanceAmount.pending) + assertEquals(0.0, result.totalAmount) + assertEquals(AssetId(Chain.Aptos).toIdentifier(), result.asset.id.toIdentifier()) + } + } +} \ No newline at end of file diff --git a/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/aptos/TestAptosSigner.kt b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/aptos/TestAptosSigner.kt new file mode 100644 index 000000000..d0937a9fa --- /dev/null +++ b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/aptos/TestAptosSigner.kt @@ -0,0 +1,64 @@ +package com.gemwallet.android.blockchain.clients.aptos + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.gemwallet.android.blockchain.includeLibs +import com.gemwallet.android.blockchain.testPhrase +import com.gemwallet.android.math.toHexString +import com.gemwallet.android.model.ConfirmParams +import com.gemwallet.android.model.DestinationAddress +import com.gemwallet.android.model.GasFee +import com.gemwallet.android.model.SignerParams +import com.gemwallet.android.model.TxSpeed +import com.wallet.core.primitives.Account +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.Chain +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import wallet.core.jni.CoinType +import wallet.core.jni.HDWallet +import java.math.BigInteger + +@RunWith(AndroidJUnit4::class) +class TestAptosSigner { + + companion object { + init { + includeLibs() + } + } + + @Test + fun testAptosNativeSign() { + val privateKey = HDWallet(testPhrase, "").getKeyForCoin(CoinType.APTOS) + val signer = AptosSignClient(Chain.Aptos) + + val sign = runBlocking { + signer.signTransfer( + SignerParams( + input = ConfirmParams.TransferParams.Native( + AssetId(Chain.Aptos), + Account(Chain.Aptos, "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", ""), + BigInteger.valueOf(10_000_000_000), + DestinationAddress("0x82111f2975a0f6080d178236369b7479f6aed1203ef4a23f8205e4b91716b783"), + ), + chainData = AptosSignerPreloader.AptosChainData( + 8L, + GasFee( + AssetId(Chain.Aptos), + speed = TxSpeed.Normal, + maxGasPrice = BigInteger.valueOf(150L), + limit = BigInteger.valueOf(18L) + ) + ), + finalAmount = BigInteger.valueOf(10_000_000_000) + ), + TxSpeed.Normal, + privateKey.data() + ) + } + + assertEquals("0x7b2265787069726174696f6e5f74696d657374616d705f73656373223a2233363634333930303832222c226761735f756e69745f7072696365223a22313530222c226d61785f6761735f616d6f756e74223a223138222c227061796c6f6164223a7b22617267756d656e7473223a5b22307838323131316632393735613066363038306431373832333633363962373437396636616564313230336566346132336638323035653462393137313662373833222c223130303030303030303030225d2c2266756e6374696f6e223a223078313a3a6170746f735f6163636f756e743a3a7472616e73666572222c2274797065223a22656e7472795f66756e6374696f6e5f7061796c6f6164222c22747970655f617267756d656e7473223a5b5d7d2c2273656e646572223a22307839623164623831313830633331623162343238353732626531303565323039623561363232326237222c2273657175656e63655f6e756d626572223a2238222c227369676e6174757265223a7b227075626c69635f6b6579223a22307863316334336435616464666531633233376164616436393732643566333866376239363135366163666336663765666438666234643533303765306365383861222c227369676e6174757265223a2230786630636664393736303962636566396366616462393062643865663635653134326438623930396665373061373835393934343233306562343065663530616232363061306239316639356336323263646365376366373138353233653565656235323664336661356438393635646431333966333637303930653562303031222c2274797065223a22656432353531395f7369676e6174757265227d7d", sign.toHexString()) + } +} \ No newline at end of file diff --git a/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/bitcoin/TestBitcoinBalance.kt b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/bitcoin/TestBitcoinBalance.kt new file mode 100644 index 000000000..83fce73ed --- /dev/null +++ b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/bitcoin/TestBitcoinBalance.kt @@ -0,0 +1,127 @@ +package com.gemwallet.android.blockchain.clients.bitcoin + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.gemwallet.android.blockchain.clients.bitcoin.services.BitcoinBalancesService +import com.gemwallet.android.blockchain.includeLibs +import com.gemwallet.android.ext.toIdentifier +import com.wallet.core.blockchain.bitcoin.models.BitcoinAccount +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.Chain +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertNull +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class TestBitcoinBalance { + + companion object { + init { + includeLibs() + } + } + + @Test + fun testBitcoinBalance() { + val client = BitcoinBalanceClient( + chain = Chain.Bitcoin, + balanceService = object : BitcoinBalancesService { + override suspend fun balance(address: String): Result { + assertEquals("bc1qqypy0q4uwk8h845j8qc5r76zyk2fwdtvqy4s4x", address) + return Result.success(BitcoinAccount(balance = "100000000")) + } + } + ) + + val result = runBlocking { + client.getNativeBalance(Chain.Bitcoin, "bc1qqypy0q4uwk8h845j8qc5r76zyk2fwdtvqy4s4x") + } + assertNotNull(result) + assertEquals("100000000", result!!.balance.available) + assertEquals("0", result.balance.frozen) + assertEquals("0", result.balance.locked) + assertEquals("0", result.balance.staked) + assertEquals("0", result.balance.rewards) + assertEquals("0", result.balance.reserved) + assertEquals("0", result.balance.pending) + assertEquals(1.0, result.balanceAmount.available) + assertEquals(0.0, result.balanceAmount.frozen) + assertEquals(0.0, result.balanceAmount.locked) + assertEquals(0.0, result.balanceAmount.staked) + assertEquals(0.0, result.balanceAmount.rewards) + assertEquals(0.0, result.balanceAmount.reserved) + assertEquals(0.0, result.balanceAmount.pending) + assertEquals(1.0, result.totalAmount) + assertEquals(AssetId(Chain.Bitcoin).toIdentifier(), result.asset.id.toIdentifier()) + } + + @Test + fun testBitcoinBalanceFail() { + val client = BitcoinBalanceClient( + chain = Chain.Bitcoin, + balanceService = object : BitcoinBalancesService { + override suspend fun balance(address: String): Result { + return Result.failure(Exception()) + } + } + ) + + runBlocking { + val result = client.getNativeBalance(Chain.Bitcoin, "bc1qqypy0q4uwk8h845j8qc5r76zyk2fwdtvqy4s4x") + assertNull(result) + } + } + + @Test + fun testBitcoinBalanceBadValue() { + val client = BitcoinBalanceClient( + chain = Chain.Bitcoin, + balanceService = object : BitcoinBalancesService { + override suspend fun balance(address: String): Result { + return Result.success(BitcoinAccount("0abcdesdf")) + } + } + ) + + runBlocking { + val result = client.getNativeBalance(Chain.Bitcoin, "bc1qqypy0q4uwk8h845j8qc5r76zyk2fwdtvqy4s4x") + assertNull(result) + } + } + + @Test + fun testBitcoinBalanceEmpty() { + val client = BitcoinBalanceClient( + chain = Chain.Bitcoin, + balanceService = object : BitcoinBalancesService { + override suspend fun balance(address: String): Result { + return Result.success(BitcoinAccount("0")) + } + + } + ) + + runBlocking { + val result = client.getNativeBalance(Chain.Bitcoin, "bc1qqypy0q4uwk8h845j8qc5r76zyk2fwdtvqy4s4x") + assertNotNull(result) + assertEquals("0", result!!.balance.available) + assertEquals("0", result.balance.frozen) + assertEquals("0", result.balance.locked) + assertEquals("0", result.balance.staked) + assertEquals("0", result.balance.rewards) + assertEquals("0", result.balance.reserved) + assertEquals("0", result.balance.pending) + assertEquals(0.0, result.balanceAmount.available) + assertEquals(0.0, result.balanceAmount.frozen) + assertEquals(0.0, result.balanceAmount.locked) + assertEquals(0.0, result.balanceAmount.staked) + assertEquals(0.0, result.balanceAmount.rewards) + assertEquals(0.0, result.balanceAmount.reserved) + assertEquals(0.0, result.balanceAmount.pending) + assertEquals(0.0, result.totalAmount) + assertEquals(AssetId(Chain.Bitcoin).toIdentifier(), result.asset.id.toIdentifier()) + } + } +} \ No newline at end of file diff --git a/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/bitcoin/TestBitcoinSIgnerPreloader.kt b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/bitcoin/TestBitcoinSIgnerPreloader.kt new file mode 100644 index 000000000..03700bb6e --- /dev/null +++ b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/bitcoin/TestBitcoinSIgnerPreloader.kt @@ -0,0 +1,122 @@ +package com.gemwallet.android.blockchain.clients.bitcoin + +import com.gemwallet.android.blockchain.clients.bitcoin.services.BitcoinFeeService +import com.gemwallet.android.blockchain.clients.bitcoin.services.BitcoinUTXOService +import com.gemwallet.android.blockchain.includeLibs +import com.gemwallet.android.ext.toIdentifier +import com.gemwallet.android.model.ConfirmParams +import com.gemwallet.android.model.DestinationAddress +import com.gemwallet.android.model.TxSpeed +import com.wallet.core.blockchain.bitcoin.models.BitcoinFeeResult +import com.wallet.core.blockchain.bitcoin.models.BitcoinUTXO +import com.wallet.core.primitives.Account +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.Chain +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.runBlocking +import org.junit.Test +import java.math.BigInteger + +class TestBitcoinSignerPreloader { + + companion object { + init { + includeLibs() + } + } + + @Test + fun testBitcoinMinimumByteFee() { + assertEquals(BitcoinFeeCalculator.getMinimumByteFee(Chain.Bitcoin), BigInteger.ONE) + assertEquals(BitcoinFeeCalculator.getMinimumByteFee(Chain.Litecoin), BigInteger.valueOf(5)) + assertEquals(BitcoinFeeCalculator.getMinimumByteFee(Chain.Doge), BigInteger.valueOf(1000)) + } + + @Test + fun testBitcoinPriority() { + assertEquals(BitcoinFeeCalculator.getFeePriority(Chain.Bitcoin, TxSpeed.Slow), "6") + assertEquals(BitcoinFeeCalculator.getFeePriority(Chain.Bitcoin, TxSpeed.Normal), "3") + assertEquals(BitcoinFeeCalculator.getFeePriority(Chain.Bitcoin, TxSpeed.Fast), "1") + + assertEquals(BitcoinFeeCalculator.getFeePriority(Chain.Litecoin, TxSpeed.Slow), "6") + assertEquals(BitcoinFeeCalculator.getFeePriority(Chain.Litecoin, TxSpeed.Normal), "3") + assertEquals(BitcoinFeeCalculator.getFeePriority(Chain.Litecoin, TxSpeed.Fast), "1") + + assertEquals(BitcoinFeeCalculator.getFeePriority(Chain.Doge, TxSpeed.Slow), "8") + assertEquals(BitcoinFeeCalculator.getFeePriority(Chain.Doge, TxSpeed.Normal), "4") + assertEquals(BitcoinFeeCalculator.getFeePriority(Chain.Doge, TxSpeed.Fast), "2") + } + + @Test + fun testBitcoinPreloader() { + var requestPubKey = "" + val preloader = BitcoinSignerPreloader( + Chain.Bitcoin, + object : BitcoinUTXOService { + override suspend fun getUTXO(address: String): Result> { + requestPubKey = address + + return Result.success( + listOf( + BitcoinUTXO( + txid = "9f47e5d5ae3dac0662766f95d0cdda9c242c08d6baac4ce5d82464cf948abd53", + vout = 1, + value = "86055170", + ) + ) + ) + } + + }, + object : BitcoinFeeService { + override suspend fun estimateFee(priority: String): Result { + val value = when (priority) { + "8" -> "0.17457567" + "4" -> "0.17457567" + "2" -> "0.60356369" + else -> return Result.failure(Exception("Incorrect priority")) + } + return Result.success(BitcoinFeeResult(value)) + } + } + ) + val result = runBlocking { + preloader.preloadNativeTransfer( + params = ConfirmParams.TransferParams.Native( + assetId = AssetId(Chain.Bitcoin), + from = Account( + Chain.Doge, + "DDyZeg24eU3csLa7LMWrZEoqnHXccz6c94", + "", + "dgub8rNuTi8ofZu1jVDKpBxW9VFo62kjjx3b6CcameEZnrNNHJ3sKCnWBxQSv6qAP6jrwZEpfT1ZdKsrcBFKGTMV8zgBtjZmvQt29VPnLzbHjjD" + ), + amount = BigInteger.valueOf(10_000_000_000), + destination = DestinationAddress("D8UBj4EfNfNWNCdnCSgpY48yZDqPdTZXWW"), + isMaxAmount = false + ) + ) + } + assertEquals("dgub8rNuTi8ofZu1jVDKpBxW9VFo62kjjx3b6CcameEZnrNNHJ3sKCnWBxQSv6qAP6jrwZEpfT1ZdKsrcBFKGTMV8zgBtjZmvQt29VPnLzbHjjD", requestPubKey) + assertEquals(BigInteger.valueOf(10_000_000_000), result.input.amount) + assertEquals("DDyZeg24eU3csLa7LMWrZEoqnHXccz6c94", result.input.from.address) + assertEquals(AssetId(Chain.Bitcoin).toIdentifier(), result.input.assetId.toIdentifier()) + assertEquals(false, result.input.isMax()) + assertEquals("D8UBj4EfNfNWNCdnCSgpY48yZDqPdTZXWW", result.input.destination()?.address) + assertEquals(null, result.input.memo()) + assertEquals(BigInteger.valueOf(3351936), result.chainData.fee().amount) + assertEquals(BigInteger.valueOf(17458), result.chainData.gasGee().maxGasPrice) + assertEquals(BigInteger.valueOf(192), result.chainData.gasGee().limit) + assertEquals(AssetId(Chain.Doge).toIdentifier(), result.chainData.fee().feeAssetId.toIdentifier()) + assertEquals(TxSpeed.Normal, result.chainData.fee().speed) + assertEquals("9f47e5d5ae3dac0662766f95d0cdda9c242c08d6baac4ce5d82464cf948abd53", (result.chainData as BitcoinSignerPreloader.BitcoinChainData).utxo[0].txid) + assertEquals(1, (result.chainData as BitcoinSignerPreloader.BitcoinChainData).utxo[0].vout) + assertEquals("86055170", (result.chainData as BitcoinSignerPreloader.BitcoinChainData).utxo[0].value) + assertEquals(3, result.chainData.allFee().size) + assertEquals(BigInteger.valueOf(192), result.chainData.gasGee(TxSpeed.Fast).limit) + assertEquals(BigInteger.valueOf(60357), result.chainData.gasGee(TxSpeed.Fast).maxGasPrice) + assertEquals(BigInteger.valueOf(11588544), result.chainData.gasGee(TxSpeed.Fast).amount) + assertEquals(BigInteger.valueOf(192), result.chainData.gasGee(TxSpeed.Slow).limit) + assertEquals(BigInteger.valueOf(17458), result.chainData.gasGee(TxSpeed.Slow).maxGasPrice) + assertEquals(BigInteger.valueOf(3351936), result.chainData.gasGee(TxSpeed.Slow).amount) + } +} \ No newline at end of file diff --git a/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/bitcoin/TestBitcoinSigner.kt b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/bitcoin/TestBitcoinSigner.kt new file mode 100644 index 000000000..a9f07c6ba --- /dev/null +++ b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/bitcoin/TestBitcoinSigner.kt @@ -0,0 +1,79 @@ +package com.gemwallet.android.blockchain.clients.bitcoin + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.gemwallet.android.blockchain.includeLibs +import com.gemwallet.android.blockchain.testPhrase +import com.gemwallet.android.math.toHexString +import com.gemwallet.android.model.ConfirmParams +import com.gemwallet.android.model.DestinationAddress +import com.gemwallet.android.model.GasFee +import com.gemwallet.android.model.SignerParams +import com.gemwallet.android.model.TxSpeed +import com.wallet.core.blockchain.bitcoin.models.BitcoinUTXO +import com.wallet.core.primitives.Account +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.Chain +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import wallet.core.jni.CoinType +import wallet.core.jni.HDWallet +import java.math.BigInteger + +@RunWith(AndroidJUnit4::class) +class TestBitcoinSigner { + + companion object { + const val SIGN_RESULT = "0x010000000153bd8a94cf6424d8e54cacbad6082c249cdacdd0956f766206a" + + "c3daed5e5479f010000006b483045022100ead2b7637532b167e66ddf76eafd032ada898c8719a" + + "680de8183b71fb3086a3e022055a073be9148cb8b9dc71de04755dbf11f140fee167ed0d4b44d6" + + "a54a829e75e012102fd6585adc0e86019abf00e83552d054cb5f4359ad4db8ca338099381f43e2" + + "5a5000000000182a82005000000001976a91424849c1d94eb9e6e002dd75fdcbce0a9673daba78" + + "8ac00000000" + init { + includeLibs() + } + } + + @Test + fun testBitcoinNativeSign() { + val privateKey = HDWallet(testPhrase, "").getKeyForCoin(CoinType.DOGECOIN) + val signer = BitcoinSignClient(Chain.Doge) + + val sign = runBlocking { + signer.signTransfer( + SignerParams( + input = ConfirmParams.TransferParams.Native( + AssetId(Chain.Doge), + Account(Chain.Doge, "D8UBj4EfNfNWNCdnCSgpY48yZDqPdTZXWW", "", "dgub8rNuTi8ofZu1jVDKpBxW9VFo62kjjx3b6CcameEZnrNNHJ3sKCnWBxQSv6qAP6jrwZEpfT1ZdKsrcBFKGTMV8zgBtjZmvQt29VPnLzbHjjD"), + BigInteger.valueOf(10_000_000_000), + DestinationAddress("D8UBj4EfNfNWNCdnCSgpY48yZDqPdTZXWW"), + ), + chainData = BitcoinSignerPreloader.BitcoinChainData( + listOf( + BitcoinUTXO( + txid = "9f47e5d5ae3dac0662766f95d0cdda9c242c08d6baac4ce5d82464cf948abd53", + vout = 1, + value = "86055170", + ) + ), + listOf( + GasFee( + AssetId(Chain.Doge), + speed = TxSpeed.Normal, + maxGasPrice = BigInteger.valueOf(150L), + limit = BigInteger.valueOf(18L) + ) + ) + ), + finalAmount = BigInteger.valueOf(10_000_000_000) + ), + TxSpeed.Normal, + privateKey.data() + ) + } + + assertEquals(SIGN_RESULT, sign.toHexString()) + } +} \ No newline at end of file diff --git a/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/cosmos/TestCosmosBalances.kt b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/cosmos/TestCosmosBalances.kt new file mode 100644 index 000000000..79a1aae2c --- /dev/null +++ b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/cosmos/TestCosmosBalances.kt @@ -0,0 +1,328 @@ +package com.gemwallet.android.blockchain.clients.cosmos + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.gemwallet.android.blockchain.clients.cosmos.services.CosmosBalancesService +import com.gemwallet.android.blockchain.clients.cosmos.services.CosmosStakeService +import com.gemwallet.android.ext.toIdentifier +import com.wallet.core.blockchain.cosmos.models.CosmosBalance +import com.wallet.core.blockchain.cosmos.models.CosmosBalances +import com.wallet.core.blockchain.cosmos.models.CosmosDelegation +import com.wallet.core.blockchain.cosmos.models.CosmosDelegationData +import com.wallet.core.blockchain.cosmos.models.CosmosDelegations +import com.wallet.core.blockchain.cosmos.models.CosmosReward +import com.wallet.core.blockchain.cosmos.models.CosmosRewards +import com.wallet.core.blockchain.cosmos.models.CosmosUnboudingDelegationEntry +import com.wallet.core.blockchain.cosmos.models.CosmosUnboundingDelegation +import com.wallet.core.blockchain.cosmos.models.CosmosUnboundingDelegations +import com.wallet.core.blockchain.cosmos.models.CosmosValidators +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.Chain +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class TestCosmosBalances { + + private class TestCosmosStakeService( + val delegations: List = emptyList(), + val unboundings: List = emptyList(), + val rewards: List = emptyList(), + ) : CosmosStakeService { + + var delegationsAddressRequest: String = "" + var unboundingAddressRequest: String = "" + var rewardsAddressRequest: String = "" + + override suspend fun validators(): Result { + return Result.success(CosmosValidators(emptyList())) + } + + override suspend fun delegations(address: String): Result { + delegationsAddressRequest = address + return Result.success(CosmosDelegations(delegations)) + } + + override suspend fun undelegations(address: String): Result { + unboundingAddressRequest = address + return Result.success(CosmosUnboundingDelegations(unboundings)) + } + + override suspend fun rewards(address: String): Result { + rewardsAddressRequest = address + return Result.success(CosmosRewards(rewards)) + } + + } + + @Test + fun testCosmosBalance() { + var ownerRequest: String = "" + val balanceService = object : CosmosBalancesService { + override suspend fun getBalance(owner: String): Result { + ownerRequest = owner + return Result.success( + CosmosBalances( + listOf( + CosmosBalance(denom = "uosmo", amount = "5272821") + ) + ) + ) + } + } + + val result = runBlocking { + CosmosBalanceClient( + Chain.Osmosis, + balancesService = balanceService, + stakeService = TestCosmosStakeService(), + ).getNativeBalance(Chain.Osmosis, "osmo1ac4ns740p4v78n4wr7j3t3s79jjjre6udx7n2v") + } + assertEquals("osmo1ac4ns740p4v78n4wr7j3t3s79jjjre6udx7n2v", ownerRequest) + assertNotNull(result) + assertEquals("5272821", result!!.balance.available) + assertEquals("0", result.balance.frozen) + assertEquals("0", result.balance.locked) + assertEquals("0", result.balance.staked) + assertEquals("0", result.balance.rewards) + assertEquals("0", result.balance.reserved) + assertEquals("0", result.balance.pending) + assertEquals(5.272821, result.balanceAmount.available) + assertEquals(0.0, result.balanceAmount.frozen) + assertEquals(0.0, result.balanceAmount.locked) + assertEquals(0.0, result.balanceAmount.staked) + assertEquals(0.0, result.balanceAmount.rewards) + assertEquals(0.0, result.balanceAmount.reserved) + assertEquals(0.0, result.balanceAmount.pending) + assertEquals(5.272821, result.totalAmount) + assertEquals(AssetId(Chain.Osmosis).toIdentifier(), result.asset.id.toIdentifier()) + } + + @Test + fun testCosmosBalanceStakeOnly() { + var ownerRequest: String = "" + val balanceService = object : CosmosBalancesService { + override suspend fun getBalance(owner: String): Result { + ownerRequest = owner + return Result.success(CosmosBalances(listOf())) + } + } + + val stakeService = TestCosmosStakeService( + delegations = listOf( + CosmosDelegation( + CosmosDelegationData("osmovaloper1pxphtfhqnx9ny27d53z4052e3r76e7qq495ehm"), + CosmosBalance("uosmo", "1000000") + ), + CosmosDelegation( + CosmosDelegationData("osmovaloper1z0sh4s80u99l6y9d3vfy582p8jejeeu6tcucs2"), + CosmosBalance("uosmo", "1000000") + ), + CosmosDelegation( + CosmosDelegationData("osmovaloper1wgmdcxzp49vjgrqusgcagq6qefk4mtjv5c0k7q"), + CosmosBalance("uosmo", "721000000") + ) + ) + ) + + val result = runBlocking { + CosmosBalanceClient( + Chain.Osmosis, + balancesService = balanceService, + stakeService = stakeService, + ).getNativeBalance(Chain.Osmosis, "osmo1ac4ns740p4v78n4wr7j3t3s79jjjre6udx7n2v") + } + assertEquals("osmo1ac4ns740p4v78n4wr7j3t3s79jjjre6udx7n2v", ownerRequest) + assertNotNull(result) + assertEquals("0", result!!.balance.available) + assertEquals("0", result.balance.frozen) + assertEquals("0", result.balance.locked) + assertEquals("723000000", result.balance.staked) + assertEquals("0", result.balance.rewards) + assertEquals("0", result.balance.reserved) + assertEquals("0", result.balance.pending) + assertEquals(0.0, result.balanceAmount.available) + assertEquals(0.0, result.balanceAmount.frozen) + assertEquals(0.0, result.balanceAmount.locked) + assertEquals(723.0, result.balanceAmount.staked) + assertEquals(0.0, result.balanceAmount.rewards) + assertEquals(0.0, result.balanceAmount.reserved) + assertEquals(0.0, result.balanceAmount.pending) + assertEquals(723.0, result.totalAmount) + assertEquals(AssetId(Chain.Osmosis).toIdentifier(), result.asset.id.toIdentifier()) + } + + @Test + fun testCosmosBalanceUnboundingOnly() { + var ownerRequest: String = "" + val balanceService = object : CosmosBalancesService { + override suspend fun getBalance(owner: String): Result { + ownerRequest = owner + return Result.success(CosmosBalances(listOf())) + } + } + val stakeService = TestCosmosStakeService( + unboundings = listOf( + CosmosUnboundingDelegation( + "osmovaloper1pxphtfhqnx9ny27d53z4052e3r76e7qq495ehm", + entries = listOf( + CosmosUnboudingDelegationEntry( + creation_height = "25053096", + completion_time = "2024-12-18T02:11:27.650773866Z", + balance = "2000000", + ) + ) + ), + ) + ) + + val result = runBlocking { + CosmosBalanceClient( + Chain.Osmosis, + balancesService = balanceService, + stakeService = stakeService, + ).getNativeBalance(Chain.Osmosis, "osmo1ac4ns740p4v78n4wr7j3t3s79jjjre6udx7n2v") + } + assertEquals("osmo1ac4ns740p4v78n4wr7j3t3s79jjjre6udx7n2v", ownerRequest) + assertNotNull(result) + assertEquals("0", result!!.balance.available) + assertEquals("0", result.balance.frozen) + assertEquals("0", result.balance.locked) + assertEquals("0", result.balance.staked) + assertEquals("0", result.balance.rewards) + assertEquals("0", result.balance.reserved) + assertEquals("2000000", result.balance.pending) + assertEquals(0.0, result.balanceAmount.available) + assertEquals(0.0, result.balanceAmount.frozen) + assertEquals(0.0, result.balanceAmount.locked) + assertEquals(0.0, result.balanceAmount.staked) + assertEquals(0.0, result.balanceAmount.rewards) + assertEquals(0.0, result.balanceAmount.reserved) + assertEquals(2.0, result.balanceAmount.pending) + assertEquals(2.0, result.totalAmount) + assertEquals(AssetId(Chain.Osmosis).toIdentifier(), result.asset.id.toIdentifier()) + } + + @Test + fun testCosmosBalanceRewardsOnly() { + var ownerRequest: String = "" + val balanceService = object : CosmosBalancesService { + override suspend fun getBalance(owner: String): Result { + ownerRequest = owner + return Result.success(CosmosBalances(listOf())) + } + } + val stakeService = TestCosmosStakeService( + rewards = listOf( + CosmosReward("osmovaloper1pxphtfhqnx9ny27d53z4052e3r76e7qq495ehm", listOf(CosmosBalance(denom = "uosmo", amount = "808.015913259210000000"))), + CosmosReward("osmovaloper1z0sh4s80u99l6y9d3vfy582p8jejeeu6tcucs2", listOf(CosmosBalance(denom = "uosmo", amount = "808.009961413523000000"))), + CosmosReward("osmovaloper1wgmdcxzp49vjgrqusgcagq6qefk4mtjv5c0k7q", listOf(CosmosBalance(denom = "uosmo", amount = "582579.692713670953000000"))), + ) + ) + + val result = runBlocking { + CosmosBalanceClient( + Chain.Osmosis, + balancesService = balanceService, + stakeService = stakeService, + ).getNativeBalance(Chain.Osmosis, "osmo1ac4ns740p4v78n4wr7j3t3s79jjjre6udx7n2v") + } + assertEquals("osmo1ac4ns740p4v78n4wr7j3t3s79jjjre6udx7n2v", ownerRequest) + assertNotNull(result) + assertEquals("0", result!!.balance.available) + assertEquals("0", result.balance.frozen) + assertEquals("0", result.balance.locked) + assertEquals("0", result.balance.staked) + assertEquals("584195", result.balance.rewards) + assertEquals("0", result.balance.reserved) + assertEquals("0", result.balance.pending) + assertEquals(0.0, result.balanceAmount.available) + assertEquals(0.0, result.balanceAmount.frozen) + assertEquals(0.0, result.balanceAmount.locked) + assertEquals(0.0, result.balanceAmount.staked) + assertEquals(0.584195, result.balanceAmount.rewards) + assertEquals(0.0, result.balanceAmount.reserved) + assertEquals(0.0, result.balanceAmount.pending) + assertEquals(0.584195, result.totalAmount) + assertEquals(AssetId(Chain.Osmosis).toIdentifier(), result.asset.id.toIdentifier()) + } + + @Test + fun testCosmosBalanceFull() { + var ownerRequest: String = "" + val balanceService = object : CosmosBalancesService { + override suspend fun getBalance(owner: String): Result { + ownerRequest = owner + return Result.success( + CosmosBalances( + listOf( + CosmosBalance(denom = "uosmo", amount = "5272821") + ) + ) + ) + } + } + + val stakeService = TestCosmosStakeService( + delegations = listOf( + CosmosDelegation( + CosmosDelegationData("osmovaloper1pxphtfhqnx9ny27d53z4052e3r76e7qq495ehm"), + CosmosBalance("uosmo", "1000000") + ), + CosmosDelegation( + CosmosDelegationData("osmovaloper1z0sh4s80u99l6y9d3vfy582p8jejeeu6tcucs2"), + CosmosBalance("uosmo", "1000000") + ), + CosmosDelegation( + CosmosDelegationData("osmovaloper1wgmdcxzp49vjgrqusgcagq6qefk4mtjv5c0k7q"), + CosmosBalance("uosmo", "721000000") + ) + ), + unboundings = listOf( + CosmosUnboundingDelegation( + "osmovaloper1pxphtfhqnx9ny27d53z4052e3r76e7qq495ehm", + entries = listOf( + CosmosUnboudingDelegationEntry( + creation_height = "25053096", + completion_time = "2024-12-18T02:11:27.650773866Z", + balance = "2000000", + ) + ) + ), + ), + rewards = listOf( + CosmosReward("osmovaloper1pxphtfhqnx9ny27d53z4052e3r76e7qq495ehm", listOf(CosmosBalance(denom = "uosmo", amount = "808.015913259210000000"))), + CosmosReward("osmovaloper1z0sh4s80u99l6y9d3vfy582p8jejeeu6tcucs2", listOf(CosmosBalance(denom = "uosmo", amount = "808.009961413523000000"))), + CosmosReward("osmovaloper1wgmdcxzp49vjgrqusgcagq6qefk4mtjv5c0k7q", listOf(CosmosBalance(denom = "uosmo", amount = "582579.692713670953000000"))), + ) + ) + + val result = runBlocking { + CosmosBalanceClient( + Chain.Osmosis, + balancesService = balanceService, + stakeService = stakeService, + ).getNativeBalance(Chain.Osmosis, "osmo1ac4ns740p4v78n4wr7j3t3s79jjjre6udx7n2v") + } + assertEquals("osmo1ac4ns740p4v78n4wr7j3t3s79jjjre6udx7n2v", ownerRequest) + assertNotNull(result) + assertEquals("5272821", result!!.balance.available) + assertEquals("0", result.balance.frozen) + assertEquals("0", result.balance.locked) + assertEquals("723000000", result.balance.staked) + assertEquals("584195", result.balance.rewards) + assertEquals("0", result.balance.reserved) + assertEquals("2000000", result.balance.pending) + assertEquals(5.272821, result.balanceAmount.available) + assertEquals(0.0, result.balanceAmount.frozen) + assertEquals(0.0, result.balanceAmount.locked) + assertEquals(723.0, result.balanceAmount.staked) + assertEquals(0.584195, result.balanceAmount.rewards) + assertEquals(0.0, result.balanceAmount.reserved) + assertEquals(2.0, result.balanceAmount.pending) + assertEquals(730.857016, result.totalAmount) + assertEquals(AssetId(Chain.Osmosis).toIdentifier(), result.asset.id.toIdentifier()) + } +} \ No newline at end of file diff --git a/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/cosmos/TestCosmosSigner.kt b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/cosmos/TestCosmosSigner.kt new file mode 100644 index 000000000..279d10d73 --- /dev/null +++ b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/cosmos/TestCosmosSigner.kt @@ -0,0 +1,303 @@ +package com.gemwallet.android.blockchain.clients.cosmos + +import com.gemwallet.android.blockchain.includeLibs +import com.gemwallet.android.blockchain.testPhrase +import com.gemwallet.android.math.toHexString +import com.gemwallet.android.model.ConfirmParams +import com.gemwallet.android.model.DestinationAddress +import com.gemwallet.android.model.GasFee +import com.gemwallet.android.model.SignerParams +import com.gemwallet.android.model.TxSpeed +import com.wallet.core.primitives.Account +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.Chain +import com.wallet.core.primitives.Delegation +import com.wallet.core.primitives.DelegationBase +import com.wallet.core.primitives.DelegationState +import com.wallet.core.primitives.DelegationValidator +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.runBlocking +import org.junit.Test +import wallet.core.jni.CoinType +import wallet.core.jni.HDWallet +import java.math.BigInteger + +class TestCosmosSigner { + companion object { + init { + includeLibs() + } + } + + val osmoAccount = Account(Chain.Osmosis, "osmo1kglemumu8mn658j6g4z9jzn3zef2qdyyvklwa3", "") + val signer = CosmosSignClient(Chain.Osmosis) + val privateKey = HDWallet(testPhrase, "").getKeyForCoin(CoinType.OSMOSIS).data() + + // TODO: Add Swap and token transfer + + @Test + fun testSignNativeTransfer() { + val transfer = ConfirmParams.Builder(AssetId(Chain.Osmosis), osmoAccount, BigInteger.TEN) + .transfer(DestinationAddress("osmo1rcjvzz8wzktqfz8qjf0l9q45kzxvd0z0n7l5cf")) + val signParams = SignerParams( + transfer, + CosmosSignerPreloader.CosmosChainData( + chainId = "osmosis-1", + accountNumber = 2913388L, + sequence = 10L, + fee = GasFee( + feeAssetId = AssetId(Chain.Osmosis), + maxGasPrice = BigInteger.valueOf(10000L), + limit = BigInteger.valueOf(200000L), + amount = BigInteger.valueOf(10000L), + speed = TxSpeed.Normal, + ) + ), + finalAmount = BigInteger.TEN, + ) + val result = runBlocking { + signer.signTransfer(signParams, TxSpeed.Normal, privateKey) + }.toHexString() + assertEquals( + "0x7b226d6f6465223a2242524f4144434153545f4d4f44455f53594e43222c2274785f6279746573223" + + "a22436f6f42436f6342436877765932397a6257397a4c6d4a68626d7375646a46695a585268" + + "4d53354e633264545a57356b456d634b4b32397a62573878613264735a57313162585534625" + + "734324e5468714e6d6330656a6c71656d347a656d566d4d6e466b65586c326132783359544d" + + "534b32397a62573878636d4e71646e70364f486436613352785a6e6f346357706d4d4777356" + + "354513161337034646d5177656a42754e3277315932596143776f466457397a62573853416a" + + "4577456d674b55417047436838765932397a6257397a4c6d4e79655842306279357a5a574e7" + + "74d6a5532617a4575554856695332563545694d4b49514d736c63596e374468506535622f38" + + "6c4d33466e50586847426a3553644331352b584931685a31675962424249454367494941526" + + "74b4568514b44676f466457397a62573853425445774d444177454d436144427041564a6b44" + + "786153355a6167686d4a365a7470433979696d374a413864754f384d774f4f44644a6548454" + + "87373483350514e2b34596c2b5356794c744e4557362b4944554b666b4731646649594f7670" + + "5269466c4f79673d3d227d", + result + ) + } + + @Test + fun testSignStake() { + val transfer = ConfirmParams.Builder(AssetId(Chain.Osmosis), osmoAccount, BigInteger.TEN) + .delegate("osmovaloper1pxphtfhqnx9ny27d53z4052e3r76e7qq495ehm") + val signParams = SignerParams( + transfer, + CosmosSignerPreloader.CosmosChainData( + chainId = "osmosis-1", + accountNumber = 2913388L, + sequence = 10L, + fee = GasFee( + feeAssetId = AssetId(Chain.Osmosis), + maxGasPrice = BigInteger.valueOf(10000L), + limit = BigInteger.valueOf(200000L), + amount = BigInteger.valueOf(10000L), + speed = TxSpeed.Normal, + ) + ), + finalAmount = BigInteger.TEN, + ) + val result = runBlocking { + signer.signTransfer(signParams, TxSpeed.Normal, privateKey) + }.toHexString() + assertEquals( + "0x7b226d6f6465223a2242524f4144434153545f4d4f44455f53594e43222c2274785f6279746573223" + + "a22437134424370554243694d765932397a6257397a4c6e4e3059577470626d6375646a4669" + + "5a5852684d53354e633264455a57786c5a3246305a524a7543697476633231764d57746e624" + + "75674645731314f4731754e6a5534616a5a6e4e486f35616e70754d33706c5a6a4a785a486c" + + "35646d74736432457a456a4a7663323176646d46736233426c636a46776548426f64475a6f6" + + "35735344f5735354d6a646b4e544e364e4441314d6d557a636a63325a546478635451354e57" + + "566f62526f4c4367563162334e74627849434d54415346464e305957746c49485a705953424" + + "85a5730675632467362475630456d674b55417047436838765932397a6257397a4c6d4e7965" + + "5842306279357a5a574e774d6a5532617a4575554856695332563545694d4b49514d736c635" + + "96e374468506535622f386c4d33466e50586847426a3553644331352b584931685a31675962" + + "42424945436749494152674b4568514b44676f466457397a62573853425445774d444177454" + + "d43614442704178683975774e5a76716c32664f444345417034586875634f3163785859727a" + + "326f4d456b61742b77764a45503156446c6169345a6e4c7a2b6e396d5262676a46313433456" + + "673616f6e6f4568333675514b594f5775513d3d227d", + result + ) + } + + @Test + fun testSignUndelegate() { + val transfer = ConfirmParams.Builder(AssetId(Chain.Osmosis), osmoAccount, BigInteger.TEN) + .undelegate( + Delegation( + base = DelegationBase( + assetId = AssetId(Chain.Osmosis), + state = DelegationState.Active, + balance = "10", + shares = "", + rewards = "", + completionDate = null, + delegationId = "25053096", + validatorId = "osmovaloper1pxphtfhqnx9ny27d53z4052e3r76e7qq495ehm" + ), + validator = DelegationValidator( + chain = Chain.Osmosis, + id = "osmovaloper1pxphtfhqnx9ny27d53z4052e3r76e7qq495ehm", + name = "", + isActive = true, + commision = 0.05, + apr = 5.11 + ) + ) + ) + val signParams = SignerParams( + transfer, + CosmosSignerPreloader.CosmosChainData( + chainId = "osmosis-1", + accountNumber = 2913388L, + sequence = 10L, + fee = GasFee( + feeAssetId = AssetId(Chain.Osmosis), + maxGasPrice = BigInteger.valueOf(10000L), + limit = BigInteger.valueOf(200000L), + amount = BigInteger.valueOf(10000L), + speed = TxSpeed.Normal, + ) + ), + finalAmount = BigInteger.TEN, + ) + val result = runBlocking { + signer.signTransfer(signParams, TxSpeed.Normal, privateKey) + }.toHexString() + assertEquals( + "0x7b226d6f6465223a2242524f4144434153545f4d4f44455f53594e43222c2274785f6279746573223" + + "a224372414243706342436955765932397a6257397a4c6e4e3059577470626d6375646a4669" + + "5a5852684d53354e63326456626d526c6247566e5958526c456d344b4b32397a62573878613" + + "264735a57313162585534625734324e5468714e6d6330656a6c71656d347a656d566d4d6e46" + + "6b65586c326132783359544d534d6d397a6257393259577876634756794d584234634768305" + + "a6d6878626e6735626e6b794e3251314d336f304d4455795a544e794e7a5a6c4e3346784e44" + + "6b315a5768744767734b4258567663323176456749784d4249555533526861325567646d6c6" + + "84945646c62534258595778735a58515361417051436b594b4879396a62334e7462334d7559" + + "334a35634852764c6e4e6c593341794e545a724d53355164574a4c5a586b5349776f6841797" + + "956786966734f4539376c762f79557a6357633965455947506c4a304c586e35636a57466e57" + + "426873454567514b4167674247416f5346416f4f4367563162334e74627849464d5441774d4" + + "44151774a6f4d476b436871792f4d33706248772f51766839796563536675706d3559525477" + + "654a513541706b433767384361383249626a2b574f7a59654858444f6276566d6f3144634d3" + + "27050652b2b6d4b426f3073686c424f43707869227d", + result + ) + } + + + @Test + fun testSignRedelegate() { + val transfer = ConfirmParams.Builder(AssetId(Chain.Osmosis), osmoAccount, BigInteger.TEN) + .redelegate( + "osmovaloper1z0sh4s80u99l6y9d3vfy582p8jejeeu6tcucs2", + Delegation( + base = DelegationBase( + assetId = AssetId(Chain.Osmosis), + state = DelegationState.Active, + balance = "10", + shares = "", + rewards = "", + completionDate = null, + delegationId = "25053096", + validatorId = "osmovaloper1pxphtfhqnx9ny27d53z4052e3r76e7qq495ehm" + ), + validator = DelegationValidator( + chain = Chain.Osmosis, + id = "osmovaloper1pxphtfhqnx9ny27d53z4052e3r76e7qq495ehm", + name = "", + isActive = true, + commision = 0.05, + apr = 5.11 + ) + ) + ) + val signParams = SignerParams( + transfer, + CosmosSignerPreloader.CosmosChainData( + chainId = "osmosis-1", + accountNumber = 2913388L, + sequence = 10L, + fee = GasFee( + feeAssetId = AssetId(Chain.Osmosis), + maxGasPrice = BigInteger.valueOf(10000L), + limit = BigInteger.valueOf(200000L), + amount = BigInteger.valueOf(10000L), + speed = TxSpeed.Normal, + ) + ), + finalAmount = BigInteger.TEN, + ) + val result = runBlocking { + signer.signTransfer(signParams, TxSpeed.Normal, privateKey) + }.toHexString() + assertEquals( + "0x7b226d6f6465223a2242524f4144434153545f4d4f44455f53594e43222c2274785f6279746573223" + + "a2243756f424374454243696f765932397a6257397a4c6e4e3059577470626d6375646a4669" + + "5a5852684d53354e633264435a576470626c4a6c5a4756735a576468644755536f67454b4b3" + + "2397a62573878613264735a57313162585534625734324e5468714e6d6330656a6c71656d34" + + "7a656d566d4d6e466b65586c326132783359544d534d6d397a6257393259577876634756794" + + "d584234634768305a6d6878626e6735626e6b794e3251314d336f304d4455795a544e794e7a" + + "5a6c4e3346784e446b315a576874476a4a7663323176646d46736233426c636a46364d484e6" + + "f4e484d344d4855354f57773265546c6b4d335a6d655455344d6e4134616d56715a5756314e" + + "6e526a64574e7a4d69494c4367563162334e74627849434d54415346464e305957746c49485" + + "a70595342485a5730675632467362475630456d674b55417047436838765932397a6257397a" + + "4c6d4e79655842306279357a5a574e774d6a5532617a4575554856695332563545694d4b495" + + "14d736c63596e374468506535622f386c4d33466e50586847426a3553644331352b58493168" + + "5a3167596242424945436749494152674b4568514b44676f466457397a62573853425445774" + + "d444177454d436144427041625637384c536c433373734c2f507a373335594d6b6c52614a4a" + + "4f54684a57645139324e33767a6730566c346f6e55513661666e7177312b70353937756d467" + + "837674a76364752414c566874722b4b6162395a3231673d3d227d", + result + ) + } + + @Test + fun testSignRewards() { + val transfer = ConfirmParams.Builder(AssetId(Chain.Osmosis), osmoAccount, BigInteger.TEN) + .rewards( + listOf( + "osmovaloper1pxphtfhqnx9ny27d53z4052e3r76e7qq495ehm", + "osmovaloper1pxphtfhqnx9ny27d53z4052e3r76e7qq495ehm", + ), + ) + val signParams = SignerParams( + transfer, + CosmosSignerPreloader.CosmosChainData( + chainId = "osmosis-1", + accountNumber = 2913388L, + sequence = 10L, + fee = GasFee( + feeAssetId = AssetId(Chain.Osmosis), + maxGasPrice = BigInteger.valueOf(10000L), + limit = BigInteger.valueOf(200000L), + amount = BigInteger.valueOf(10000L), + speed = TxSpeed.Normal, + ) + ), + finalAmount = BigInteger.TEN, + ) + val result = runBlocking { + signer.signTransfer(signParams, TxSpeed.Normal, privateKey) + }.toHexString() + assertEquals( + "0x7b226d6f6465223a2242524f4144434153545f4d4f44455f53594e43222c2274785f6279746573223" + + "a224374514343707742436a63765932397a6257397a4c6d52706333527961574a3164476c76" + + "626935324d574a6c644745784c6b317a5a3164706447686b636d4633524756735a576468644" + + "73979556d563359584a6b456d454b4b32397a62573878613264735a57313162585534625734" + + "324e5468714e6d6330656a6c71656d347a656d566d4d6e466b65586c326132783359544d534" + + "d6d397a6257393259577876634756794d584234634768305a6d6878626e6735626e6b794e32" + + "51314d336f304d4455795a544e794e7a5a6c4e3346784e446b315a57687443707742436a637" + + "65932397a6257397a4c6d52706333527961574a3164476c76626935324d574a6c644745784c" + + "6b317a5a3164706447686b636d4633524756735a57646864473979556d563359584a6b456d4" + + "54b4b32397a62573878613264735a57313162585534625734324e5468714e6d6330656a6c71" + + "656d347a656d566d4d6e466b65586c326132783359544d534d6d397a6257393259577876634" + + "756794d584234634768305a6d6878626e6735626e6b794e3251314d336f304d4455795a544e" + + "794e7a5a6c4e3346784e446b315a57687445685254644746725a53423261574567523256744" + + "94664686247786c64424a6f436c414b52676f664c324e76633231766379356a636e6c776447" + + "38756332566a634449314e6d73784c6c4231596b746c6552496a436945444c4a58474a2b773" + + "4543375572f2f4a544e785a7a31345267592b556e517465666c794e59576459474777515342" + + "416f4343414559436849554367344b4258567663323176456755784d4441774d42434174526" + + "76151482f553930754348307a78394164592b414c49484d35615a316372425377597a655a5a" + + "656a623572576a454d56585253636a4f66766e67333358466e464864493445707039796b4e4" + + "e745156557739424a6e5a7368553d227d", + result + ) + } +} \ No newline at end of file diff --git a/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/evm/TestEthSign.kt b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/evm/TestEthSign.kt new file mode 100644 index 000000000..3ae9192de --- /dev/null +++ b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/evm/TestEthSign.kt @@ -0,0 +1,279 @@ +package com.gemwallet.android.blockchain.clients.evm + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.gemwallet.android.blockchain.clients.ethereum.EvmSignClient +import com.gemwallet.android.blockchain.clients.ethereum.EvmSignerPreloader +import com.gemwallet.android.blockchain.includeLibs +import com.gemwallet.android.blockchain.testPhrase +import com.gemwallet.android.ext.asset +import com.gemwallet.android.math.toHexString +import com.gemwallet.android.model.ConfirmParams +import com.gemwallet.android.model.DestinationAddress +import com.gemwallet.android.model.GasFee +import com.gemwallet.android.model.SignerParams +import com.gemwallet.android.model.TxSpeed +import com.wallet.core.primitives.Account +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.Chain +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import wallet.core.jni.CoinType +import wallet.core.jni.HDWallet +import java.math.BigInteger + +@RunWith(AndroidJUnit4::class) +class TestEthSign { + + companion object { + init { + includeLibs() + } + } + + val signClient = EvmSignClient(Chain.Ethereum) + val privateKey = HDWallet(testPhrase, "").getKeyForCoin(CoinType.ETHEREUM).data() + + @Test + fun test_Evm_sign_native() { + val sign = runBlocking { + signClient.signTransfer( + params = SignerParams( + input = ConfirmParams.TransferParams.Native( + assetId = Chain.Ethereum.asset().id, + amount = BigInteger.TEN.pow(Chain.Ethereum.asset().decimals), + destination = DestinationAddress("0x9b1DB81180c31B1b428572Be105E209b5A6222b7"), + from = Account(Chain.Ethereum, "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", "") + ), + finalAmount = BigInteger.TEN.pow(Chain.Ethereum.asset().decimals), + chainData = EvmSignerPreloader.EvmChainData( + chainId = "1", + nonce = BigInteger.ONE, + fee = GasFee( + maxGasPrice = BigInteger.TEN, + limit = BigInteger("21000"), + minerFee = BigInteger.TEN, + relay = BigInteger.TEN, + speed = TxSpeed.Normal, + feeAssetId = Chain.Ethereum.asset().id, + ) + ) + ), + txSpeed = TxSpeed.Normal, + privateKey, + ) + } + assertEquals( + "0x02f86a01010a0a825208949b1db81180c31b1b428572be105e209b5a6222b7880de0b6b3a76400008" + + "0c001a04936670cff2d450a1375fb2c42cf9f97130f9f9365197e4e8461a8c43fe24786a041" + + "833ce7835a78604c8518ef4dcef4e6dcd2e2d031dce759cd89790b6054fa07", + sign.toHexString() + ) + } + + @Test + fun test_EvmTokenSign() { + val sign = runBlocking { + signClient.signTransfer( + params = SignerParams( + input = ConfirmParams.TransferParams.Token( + assetId = AssetId(Chain.Ethereum, "0xdAC17F958D2ee523a2206206994597C13D831ec7"), + amount = BigInteger.TEN.pow(Chain.Ethereum.asset().decimals), + destination = DestinationAddress("0x9b1DB81180c31B1b428572Be105E209b5A6222b7"), + from = Account(Chain.Ethereum, "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", "") + ), + finalAmount = BigInteger.TEN.pow(Chain.Ethereum.asset().decimals), + chainData = EvmSignerPreloader.EvmChainData( + chainId = "1", + nonce = BigInteger.ONE, + fee = GasFee( + maxGasPrice = BigInteger.TEN, + limit = BigInteger("91000"), + minerFee = BigInteger.TEN, + relay = BigInteger.TEN, + speed = TxSpeed.Normal, + feeAssetId = Chain.Ethereum.asset().id, + ) + ) + ), + txSpeed = TxSpeed.Normal, + privateKey, + ) + } + assertEquals( + "0x02f8a801010a0a8301637894dac17f958d2ee523a2206206994597c13d831ec780b844a9059cbb000" + + "0000000000000000000009b1db81180c31b1b428572be105e209b5a6222b700000000000000" + + "00000000000000000000000000000000000de0b6b3a7640000c001a02a975d0be8ce97d4518" + + "cd22407a21697f0177b8ca7c057b868eaba32aefd6887a00b20f74083ac147b7733c0b72d2f" + + "87cc96fc75be610da14b4f6265703421d273", + sign.toHexString() + ) + } + + @Test + fun test_Evm_sign_swap() { + val sign = runBlocking { + signClient.signTransfer( + params = SignerParams( + input = ConfirmParams.SwapParams( + fromAssetId = AssetId(Chain.Ethereum, "0xdAC17F958D2ee523a2206206994597C13D831ec7"), + toAssetId = AssetId(Chain.Ethereum, "0xdAC17F958D2ee523a2206206994597C13D831ec7"), + fromAmount = BigInteger.TEN.pow(Chain.Ethereum.asset().decimals), + toAmount = BigInteger.TEN.pow(Chain.Ethereum.asset().decimals), + swapData = "0xbc", + provider = "some_provide", + to = "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", + value = "10", + from = Account(Chain.Ethereum, "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", "") + ), + finalAmount = BigInteger.TEN.pow(Chain.Ethereum.asset().decimals), + chainData = EvmSignerPreloader.EvmChainData( + chainId = "1", + nonce = BigInteger.ONE, + fee = GasFee( + maxGasPrice = BigInteger.TEN, + limit = BigInteger("91000"), + minerFee = BigInteger.TEN, + relay = BigInteger.TEN, + speed = TxSpeed.Normal, + feeAssetId = Chain.Ethereum.asset().id, + ) + ) + ), + txSpeed = TxSpeed.Normal, + privateKey, + ) + } + assertEquals( + "0x02f86401010a0a83016378949b1db81180c31b1b428572be105e209b5a6222b70a81bcc001a084f6c" + + "708ff9bb9ef4f898860c38abf2293a9d34cf22e28047f5afa2af65b048ca06d436644b5fac5" + + "74f27c0caade1db8ee72a149897fcfe2f00797e78e8f58437a", + sign.toHexString() + ) + } + + @Test + fun test_Evm_sign_delegate() { + val sign = runBlocking { + signClient.signTransfer( + params = SignerParams( + input = ConfirmParams.Stake.DelegateParams( + assetId = AssetId(Chain.SmartChain), + amount = BigInteger.TEN, + from = Account(Chain.Ethereum, "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", ""), + validatorId = "0x9941BCe2601fC93478DF9f5F6Cc83F4FFC1D71d8" + ), + finalAmount = BigInteger.TEN.pow(Chain.Ethereum.asset().decimals), + chainData = EvmSignerPreloader.EvmChainData( + chainId = "1", + nonce = BigInteger.ONE, + fee = GasFee( + maxGasPrice = BigInteger.TEN, + limit = BigInteger("91000"), + minerFee = BigInteger.TEN, + relay = BigInteger.TEN, + speed = TxSpeed.Normal, + feeAssetId = Chain.Ethereum.asset().id, + ) + ) + ), + txSpeed = TxSpeed.Normal, + privateKey, + ) + } + assertEquals( + "0x02f8b031010a0a83016378940000000000000000000000000000000000002002880de0b6b3a764000" + + "0b844982ef0a70000000000000000000000009941bce2601fc93478df9f5f6cc83f4ffc1d71" + + "d80000000000000000000000000000000000000000000000000000000000000000c001a0708" + + "ce0a221cdfccdaa992fab54ebf65f0e6bd2a319b679f16bad820a332e76d5a071a1409772887" + + "9ebedcd62fc9bc2b1e5ae82b509d8c9da665123db2df895a280", + sign.toHexString() + ) + } + + + @Test + fun test_Evm_sign_undelegate() { + val sign = runBlocking { + signClient.signTransfer( + params = SignerParams( + input = ConfirmParams.Stake.UndelegateParams( + assetId = AssetId(Chain.SmartChain), + amount = BigInteger("1002901689671695193"), + from = Account(Chain.Ethereum, "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", ""), + validatorId = "0x9941BCe2601fC93478DF9f5F6Cc83F4FFC1D71d8", + delegationId = "", + share = "991645728829172501", + balance = "1002901689671695193", + ), + finalAmount = BigInteger.TEN.pow(Chain.Ethereum.asset().decimals), + chainData = EvmSignerPreloader.EvmChainData( + chainId = "1", + nonce = BigInteger.ONE, + fee = GasFee( + maxGasPrice = BigInteger.TEN, + limit = BigInteger("91000"), + minerFee = BigInteger.TEN, + relay = BigInteger.TEN, + speed = TxSpeed.Normal, + feeAssetId = Chain.Ethereum.asset().id, + ) + ) + ), + txSpeed = TxSpeed.Normal, + privateKey, + ) + } + assertEquals( + "0x02f8a831010a0a8301637894000000000000000000000000000000000000200280b8444d99dd16000" + + "0000000000000000000009941bce2601fc93478df9f5f6cc83f4ffc1d71d800000000000000" + + "00000000000000000000000000000000000dc3088951e56b15c001a0e3fcddc355556d317b4" + + "c67b3171a9565ab3973e20c6a468933a0491824b7dce4a00cf2947749cd16b758ae7db408a2" + + "43b86b31239a595fbfd283541ad12872c7ac", + sign.toHexString() + ) + } + + @Test + fun test_Evm_sign_approval() { + val sign = runBlocking { + signClient.signTransfer( + params = SignerParams( + input = ConfirmParams.TokenApprovalParams( + assetId = AssetId(Chain.SmartChain, "0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82"), + from = Account(Chain.SmartChain, "0x0Eb3a705fc54725037CC9e008bDede697f62F335", ""), + data = "0x095ea7b300000000000000000000000031c2f6fcff4f8759b3bd5bf0e1084a" + + "055615c7687ffffffffffffffffffffffffffffffffffffffffffffffffffff" + + "fffffffffff", + provider = "Uniswap v3", + contract = "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", + ), + finalAmount = BigInteger.ZERO, + chainData = EvmSignerPreloader.EvmChainData( + chainId = "1", + nonce = BigInteger.ONE, + fee = GasFee( + maxGasPrice = BigInteger.TEN, + limit = BigInteger("91000"), + minerFee = BigInteger.TEN, + relay = BigInteger.TEN, + speed = TxSpeed.Normal, + feeAssetId = Chain.SmartChain.asset().id, + ) + ) + ), + txSpeed = TxSpeed.Normal, + privateKey, + ) + } + assertEquals( + "0x02f8a801010a0a83016378940e09fabb73bd3ade0a17ecc321fd13a19e81ce8280b844095ea7b3000" + + "00000000000000000000031c2f6fcff4f8759b3bd5bf0e1084a055615c7687fffffffffffff" + + "ffffffffffffffffffffffffffffffffffffffffffffffffffc080a03a683902daf791be51b" + + "7354dbe5cef567c3825ce52808948ad00b195f79fb736a057f151181ea2898b4a6ef42e1f24" + + "0f64995e83fa19ade070e4d9ec7b0f66469c", + sign.toHexString() + ) + } +} \ No newline at end of file diff --git a/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/evm/TestEvmBalance.kt b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/evm/TestEvmBalance.kt new file mode 100644 index 000000000..386ae20dd --- /dev/null +++ b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/evm/TestEvmBalance.kt @@ -0,0 +1,212 @@ +package com.gemwallet.android.blockchain.clients.evm + +import com.gemwallet.android.blockchain.clients.ethereum.EvmBalanceClient +import com.gemwallet.android.blockchain.clients.ethereum.services.EvmRpcClient +import com.gemwallet.android.blockchain.clients.ethereum.SmartchainStakeClient +import com.gemwallet.android.blockchain.clients.ethereum.services.EvmBalancesService +import com.gemwallet.android.blockchain.clients.ethereum.services.EvmCallService +import com.gemwallet.android.blockchain.includeLibs +import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest +import com.gemwallet.android.blockchain.rpc.model.JSONRpcResponse +import com.gemwallet.android.model.getTotalAmount +import com.wallet.core.primitives.Asset +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.AssetType +import com.wallet.core.primitives.Chain +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertNull +import kotlinx.coroutines.runBlocking +import org.junit.Test +import java.math.BigInteger + +class TestEvmBalance { + + companion object { + init { + includeLibs() + } + } + + class CallService : EvmCallService { + override suspend fun callString(request: JSONRpcRequest>): Result> { + return Result.success(JSONRpcResponse("0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000")) + } + + override suspend fun callNumber(request: JSONRpcRequest>): Result> { + return Result.success(JSONRpcResponse(null)) + } + + } + class BalanceService : EvmBalancesService { + var nativeBalanceParam: String = "" + + override suspend fun getBalance(request: JSONRpcRequest>): Result> { + nativeBalanceParam = request.params[0] + return Result.success(JSONRpcResponse(EvmRpcClient.EvmNumber(BigInteger.TEN.pow(16)))) + } + + } + + @Test + fun testNativeBalance() { + val balanceService = BalanceService() + val balanceClient = EvmBalanceClient(Chain.Ethereum, CallService(), balanceService, SmartchainStakeClient(Chain.Ethereum, CallService())) + + val result = runBlocking { + balanceClient.getNativeBalance(Chain.Ethereum, "0x9b1DB81180c31B1b428572Be105E209b5A6222b7") + } + assertEquals("0x9b1DB81180c31B1b428572Be105E209b5A6222b7", balanceService.nativeBalanceParam) + assertNotNull(result) + assertEquals("10000000000000000", result!!.balance.available) + assertEquals("0", result.balance.rewards) + assertEquals("0", result.balance.staked) + assertEquals("0", result.balance.pending) + assertEquals("0", result.balance.frozen) + assertEquals("0", result.balance.locked) + assertEquals("0", result.balance.reserved) + assertEquals(BigInteger.valueOf(10_000_000_000_000_000), result.balance.getTotalAmount()) + assertEquals(0.01, result.balanceAmount.available) + assertEquals(0.0, result.balanceAmount.rewards) + assertEquals(0.0, result.balanceAmount.staked) + assertEquals(0.0, result.balanceAmount.pending) + assertEquals(0.0, result.balanceAmount.frozen) + assertEquals(0.0, result.balanceAmount.locked) + assertEquals(0.0, result.balanceAmount.reserved) + assertEquals(0.01, result.balanceAmount.getTotalAmount()) + } + + @Test + fun testNativeBalanceBadResponse() { + var nativeBalanceParam: String = "" + val balancesService = object : EvmBalancesService { + override suspend fun getBalance(request: JSONRpcRequest>): Result> { + nativeBalanceParam = request.params[0] + return Result.success(JSONRpcResponse(EvmRpcClient.EvmNumber(null))) + } + + } + val balanceClient = EvmBalanceClient(Chain.Ethereum, CallService(), balancesService, SmartchainStakeClient(Chain.Ethereum, CallService())) + + val result = runBlocking { + balanceClient.getNativeBalance(Chain.Ethereum, "0x9b1DB81180c31B1b428572Be105E209b5A6222b7") + } + assertEquals("0x9b1DB81180c31B1b428572Be105E209b5A6222b7", nativeBalanceParam) + assertNull(result) + } + + @Test + fun test_native_balance_on_SmartChain_without_delegations() { + val balanceService = BalanceService() + val balanceClient = EvmBalanceClient(Chain.SmartChain, CallService(), balanceService, SmartchainStakeClient(Chain.SmartChain, CallService())) + + val result = runBlocking { + balanceClient.getNativeBalance(Chain.SmartChain, "0x9b1DB81180c31B1b428572Be105E209b5A6222b7") + } + assertNotNull(result) + assertEquals("10000000000000000", result!!.balance.available) + assertEquals("0", result.balance.rewards) + assertEquals("0", result.balance.staked) + assertEquals("0", result.balance.pending) + assertEquals("0", result.balance.frozen) + assertEquals("0", result.balance.locked) + assertEquals("0", result.balance.reserved) + assertEquals(BigInteger.valueOf(10_000_000_000_000_000), result.balance.getTotalAmount()) + } + + @Test + fun test_native_balance_on_SmartChain_with_delegations() { + val balanceService = BalanceService() + val callService = object : EvmCallService { + override suspend fun callString(request: JSONRpcRequest>): Result> { + val data = when ((request.params[0] as Map<*, *>)["data"]) { + "0xced0e70e000000000000000000000000ee7e9ccfb529f2c1cc02c0aea8aced7ec7e98b5e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002d" -> { + "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000ee7e9ccfb529f2c1cc02c0aea8aced7ec7e98b5e0000000000000000000000009941bce2601fc93478df9f5f6cc83f4ffc1d71d80000000000000000000000000000000000000000000000000deacafbb23d1ee90000000000000000000000000000000000000000000000000dc3088951e56b15" + } + "0xd9d4c020000000000000000000000000ee7e9ccfb529f2c1cc02c0aea8aced7ec7e98b5e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002d" -> { + "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000" + } + "0xc473318f" -> "0x000000000000000000000000000000000000000000000000000000000000002d" + else -> "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000" + } + return Result.success(JSONRpcResponse(data)) + } + + override suspend fun callNumber(request: JSONRpcRequest>): Result> { + TODO("Not yet implemented") + } + } + val balanceClient = EvmBalanceClient( + Chain.SmartChain, + callService, + balanceService, + SmartchainStakeClient(Chain.SmartChain, callService) + ) + + val result = runBlocking { + balanceClient.getNativeBalance(Chain.SmartChain, "0xEe7E9CcFb529f2c1Cc02C0Aea8aCed7Ec7e98B5e") + } + assertNotNull(result) + assertEquals("10000000000000000", result!!.balance.available) + assertEquals("0", result.balance.rewards) + assertEquals("1002837049419308777", result.balance.staked) + assertEquals("0", result.balance.pending) + assertEquals("0", result.balance.frozen) + assertEquals("0", result.balance.locked) + assertEquals("0", result.balance.reserved) + assertEquals(BigInteger.valueOf(1_012_837_049_419_308_777), result.balance.getTotalAmount()) + } + + @Test + fun testTokenBalance() { + val balanceService = BalanceService() + val callService = object : EvmCallService { + override suspend fun callString(request: JSONRpcRequest>): Result> { + TODO("Not yet implemented") + } + + override suspend fun callNumber(request: JSONRpcRequest>): Result> { + val result = when ((request.params[0] as Map<*, *>)["to"]) { + "0x76A797A59Ba2C17726896976B7B3747BfD1d220f" -> "0x00000000000000000000000000000000000000000000000000000002eedef652" + "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c" -> "0x000000000000000000000000000000000000000000000000006a94d74f430000" + else -> "0x" + } + return Result.success(JSONRpcResponse(EvmRpcClient.EvmNumber(BigInteger(result.removePrefix("0x"), 16)))) + } + } + val balanceClient = EvmBalanceClient(Chain.SmartChain, callService, balanceService, SmartchainStakeClient(Chain.SmartChain, CallService())) + + val result = runBlocking { + balanceClient.getTokenBalances( + Chain.SmartChain, + "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", + listOf( + Asset( + AssetId(Chain.SmartChain, "0x76A797A59Ba2C17726896976B7B3747BfD1d220f"), + name = "ton", + symbol = "ton", + decimals = 9, + type = AssetType.TOKEN + ), + Asset( + AssetId(Chain.SmartChain, "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c"), + name = "Wrapped BNB", + symbol = "WBNB", + decimals = 18, + type = AssetType.TOKEN + ), + ) + ) + } + assertNotNull(result) + assertEquals("12597524050", result[0].balance.available) + assertEquals("0", result[0].balance.rewards) + assertEquals("0", result[0].balance.staked) + assertEquals("0", result[0].balance.pending) + assertEquals("0", result[0].balance.frozen) + assertEquals("0", result[0].balance.locked) + assertEquals("0", result[0].balance.reserved) + assertEquals(BigInteger.valueOf(12597524050), result[0].balance.getTotalAmount()) + assertEquals("30000000000000000", result[1].balance.available) + } +} \ No newline at end of file diff --git a/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/evm/TestEvmEncodeApprove.kt b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/evm/TestEvmEncodeApprove.kt new file mode 100644 index 000000000..1e2eef808 --- /dev/null +++ b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/evm/TestEvmEncodeApprove.kt @@ -0,0 +1,27 @@ +package com.gemwallet.android.blockchain.clients.evm + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.gemwallet.android.blockchain.clients.ethereum.encodeApprove +import com.gemwallet.android.blockchain.includeLibs +import com.gemwallet.android.math.toHexString +import junit.framework.TestCase.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class TestEvmEncodeApprove { + + companion object { + init { + includeLibs() + } + } + + @Test + fun testEncodeApprove() { + assertEquals( + "0x095ea7b30000000000000000000000009b1db81180c31b1b428572be105e209b5a6222b77fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + encodeApprove("0x9b1DB81180c31B1b428572Be105E209b5A6222b7").toHexString() + ) + } +} \ No newline at end of file diff --git a/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/evm/TestEvmExtentions.kt b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/evm/TestEvmExtentions.kt new file mode 100644 index 000000000..993bbc677 --- /dev/null +++ b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/evm/TestEvmExtentions.kt @@ -0,0 +1,64 @@ +package com.gemwallet.android.blockchain.clients.evm + +import com.gemwallet.android.blockchain.clients.ethereum.encodeTransactionData +import com.gemwallet.android.blockchain.clients.ethereum.getDestinationAddress +import com.gemwallet.android.blockchain.includeLibs +import com.gemwallet.android.math.toHexString +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.Chain +import com.wallet.core.primitives.EVMChain +import junit.framework.TestCase.assertEquals +import org.junit.Test +import java.math.BigInteger + +class TestEvmExtentions { + companion object { + init { + includeLibs() + } + } + + @Test + fun testEncodeTransactionData() { + val result = EVMChain.encodeTransactionData( + AssetId(Chain.SmartChain, "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c"), + null, + BigInteger.TEN, + "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", + ) + assertEquals( + "0xa9059cbb0000000000000000000000009b1db81180c31b1b428572be105e209b5a6222b7000000000" + + "000000000000000000000000000000000000000000000000000000a", + result.toHexString() + ) + } + + @Test + fun testEncodeTransactionData_with_memo() { + val result = EVMChain.encodeTransactionData( + AssetId(Chain.SmartChain, "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c"), + "0x00000000000000000000000000000000000000000000000000000002eedef652", + BigInteger.TEN, + "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", + ) + assertEquals("0x00000000000000000000000000000000000000000000000000000002eedef652", result.toHexString()) + } + + @Test + fun testGetDestinationAddress_token() { + val result = EVMChain.getDestinationAddress( + AssetId(Chain.SmartChain, "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c"), + "0x9b1DB81180c31B1b428572Be105E209b5A6222b7" + ) + assertEquals(result, "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c") + } + + @Test + fun testGetDestinationAddress_native() { + val result = EVMChain.getDestinationAddress( + AssetId(Chain.SmartChain), + "0x9b1DB81180c31B1b428572Be105E209b5A6222b7" + ) + assertEquals(result, "0x9b1DB81180c31B1b428572Be105E209b5A6222b7") + } +} \ No newline at end of file diff --git a/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/evm/TestEvmTransactionStatusClient.kt b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/evm/TestEvmTransactionStatusClient.kt new file mode 100644 index 000000000..c89c6e828 --- /dev/null +++ b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/evm/TestEvmTransactionStatusClient.kt @@ -0,0 +1,121 @@ +package com.gemwallet.android.blockchain.clients.evm + +import com.gemwallet.android.blockchain.clients.TransactionStatusClient +import com.gemwallet.android.blockchain.clients.ethereum.EvmTransactionStatusClient +import com.gemwallet.android.blockchain.clients.ethereum.services.EvmTransactionsService +import com.gemwallet.android.blockchain.includeLibs +import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest +import com.gemwallet.android.blockchain.rpc.model.JSONRpcResponse +import com.wallet.core.blockchain.ethereum.models.EthereumTransactionReciept +import com.wallet.core.primitives.Chain +import com.wallet.core.primitives.TransactionState +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertNull +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.experimental.theories.suppliers.TestedOn +import org.junit.runner.RunWith +import java.math.BigInteger +import kotlin.String + +class TestEvmTransactionStatusClient { + companion object { + init { + includeLibs() + } + } + + @Test + fun testEvm_transaction_tastus_confirm() { + var requestAddress = "" + val client = EvmTransactionStatusClient( + chain = Chain.SmartChain, + transactionsService = object : EvmTransactionsService { + override suspend fun transaction(request: JSONRpcRequest>): Result> { + requestAddress = request.params[0] + return Result.success( + JSONRpcResponse( + EthereumTransactionReciept( + status = "0x1", + gasUsed = "10", + effectiveGasPrice = "2", + l1Fee = "1" + ) + ) + ) + } + + } + ) + val resullt = runBlocking { + client.getStatus(Chain.SmartChain, "0x502aECFE253E6AA0e8D2A06E12438FFeD0Fe16a0", "0xe84b29d6f06aeb00ba071a409ff057649f19ebbc209114a6a8135a68af589e22") + .getOrNull() + } + assertNotNull(resullt) + assertEquals(TransactionState.Confirmed, resullt!!.state) + assertEquals(BigInteger("21"), resullt.fee) + } + + @Test + fun testEvm_transaction_tastus_fail() { + var requestAddress = "" + val client = EvmTransactionStatusClient( + chain = Chain.SmartChain, + transactionsService = object : EvmTransactionsService { + override suspend fun transaction(request: JSONRpcRequest>): Result> { + requestAddress = request.params[0] + return Result.success( + JSONRpcResponse( + EthereumTransactionReciept( + status = "0x0", + gasUsed = "10", + effectiveGasPrice = "2", + l1Fee = "1" + ) + ) + ) + } + + } + ) + val resullt = runBlocking { + client.getStatus(Chain.SmartChain, "0x502aECFE253E6AA0e8D2A06E12438FFeD0Fe16a0", "0xe84b29d6f06aeb00ba071a409ff057649f19ebbc209114a6a8135a68af589e22") + .getOrNull() + } + assertNotNull(resullt) + assertEquals(TransactionState.Reverted, resullt!!.state) + assertEquals(BigInteger("21"), resullt.fee) + } + + @Test + fun testEvm_transaction_tastus_pending() { + var requestAddress = "" + val client = EvmTransactionStatusClient( + chain = Chain.SmartChain, + transactionsService = object : EvmTransactionsService { + override suspend fun transaction(request: JSONRpcRequest>): Result> { + requestAddress = request.params[0] + return Result.success( + JSONRpcResponse( + EthereumTransactionReciept( + status = "0x2", + gasUsed = "10", + effectiveGasPrice = "2", + l1Fee = "1" + ) + ) + ) + } + + } + ) + val resullt = runBlocking { + client.getStatus(Chain.SmartChain, "0x502aECFE253E6AA0e8D2A06E12438FFeD0Fe16a0", "0xe84b29d6f06aeb00ba071a409ff057649f19ebbc209114a6a8135a68af589e22") + .getOrNull() + } + assertNotNull(resullt) + assertEquals(TransactionState.Pending, resullt!!.state) + assertNull(resullt.fee) + } +} \ No newline at end of file diff --git a/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/evm/TestFeeCalculator.kt b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/evm/TestFeeCalculator.kt new file mode 100644 index 000000000..28f6732d3 --- /dev/null +++ b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/evm/TestFeeCalculator.kt @@ -0,0 +1,399 @@ +package com.gemwallet.android.blockchain.clients.evm + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.gemwallet.android.blockchain.clients.ethereum.EvmFeeCalculator +import com.gemwallet.android.blockchain.clients.ethereum.services.EvmRpcClient +import com.gemwallet.android.blockchain.clients.ethereum.StakeHub +import com.gemwallet.android.blockchain.clients.ethereum.services.EvmCallService +import com.gemwallet.android.blockchain.clients.ethereum.services.EvmFeeService +import com.gemwallet.android.blockchain.includeLibs +import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest +import com.gemwallet.android.blockchain.rpc.model.JSONRpcResponse +import com.gemwallet.android.model.ConfirmParams +import com.gemwallet.android.model.DestinationAddress +import com.gemwallet.android.model.GasFee +import com.gemwallet.android.model.TxSpeed +import com.wallet.core.blockchain.ethereum.models.EthereumFeeHistory +import com.wallet.core.primitives.Account +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.Chain +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import uniffi.gemstone.Config +import wallet.core.jni.CoinType +import java.math.BigInteger + +@RunWith(AndroidJUnit4::class) +class TestFeeCalculator { + companion object { + init { + includeLibs() + } + } + + private class FeeService( + private val feeHistory: EthereumFeeHistory? = null, + private val gasLimit: BigInteger = BigInteger.valueOf(21_000), + ) : EvmFeeService { + + var feeHistoryRequest: List = emptyList() + var gasLimitRequest: List = emptyList() + + override suspend fun getFeeHistory(request: JSONRpcRequest>): Result> { + feeHistoryRequest = request.params + return Result.success(JSONRpcResponse(feeHistory ?: throw Exception("Fee history fail"))) + } + + override suspend fun getGasLimit(request: JSONRpcRequest>): Result> { + gasLimitRequest = request.params + return Result.success(JSONRpcResponse(EvmRpcClient.EvmNumber(gasLimit))) + } + + override suspend fun getNonce(request: JSONRpcRequest>): Result> { + return Result.success(JSONRpcResponse(EvmRpcClient.EvmNumber(BigInteger.ZERO))) + } + } + + private class CallService : EvmCallService { + override suspend fun callString(request: JSONRpcRequest>): Result> { + throw Exception("Call string fail") + } + + override suspend fun callNumber(request: JSONRpcRequest>): Result> { + return Result.success(JSONRpcResponse(EvmRpcClient.EvmNumber(BigInteger.ONE))) + } + } + + @Test + fun testEvm_transfer_fee_calculation_network_fail_gas_limit() { + val feeService = object : EvmFeeService { + override suspend fun getFeeHistory(request: JSONRpcRequest>): Result> { + return Result.failure(Exception("Fee history fail")) + } + + override suspend fun getGasLimit(request: JSONRpcRequest>): Result> { + return Result.failure(Exception("Gas limit fail")) + } + + override suspend fun getNonce(request: JSONRpcRequest>): Result> { + return Result.failure(Exception("Nonce fail")) + } + } + val callService = object : EvmCallService { + override suspend fun callString(request: JSONRpcRequest>): Result> { + throw Exception("Call string fail") + } + + override suspend fun callNumber(request: JSONRpcRequest>): Result> { + throw Exception("Call number fail") + } + } + try { + runBlocking { + EvmFeeCalculator( + feeService = feeService, + callService = callService, + coinType = CoinType.SMARTCHAIN, + ).calculate( + ConfirmParams.Builder( + AssetId(Chain.SmartChain), + Account(Chain.SmartChain, "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", ""), + amount = BigInteger.TEN, + ).transfer( + DestinationAddress("0x9b1DB81180c31B1b428572Be105E209b5A6222b7") + ), + AssetId(Chain.SmartChain), + "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", + outputAmount = BigInteger.TEN, + null, + chainId = Config().getChainConfig(Chain.SmartChain.string).networkId, + BigInteger.ZERO, + ) + } + assertTrue(false) + } catch (err: Throwable) { + assertTrue("Unable to calculate base fee" == err.message || "Fail calculate gas limit" == err.message) + } + } + + @Test + fun test_Evm_fee_transfer_calculation() { + val feeService = FeeService( + EthereumFeeHistory( + reward = listOf(listOf("0x3b9aca00")), + baseFeePerGas = listOf("0xa", "0xc") + ), + gasLimit = BigInteger.valueOf(25_000) + ) + val feeCalculator = EvmFeeCalculator( + feeService = feeService, + callService = CallService(), + coinType = CoinType.SMARTCHAIN, + ) + val result = runBlocking { + feeCalculator.calculate( + ConfirmParams.Builder( + AssetId(Chain.SmartChain), + Account(Chain.SmartChain, "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", ""), + amount = BigInteger.TEN, + ).transfer( + DestinationAddress("0x9b1DB81180c31B1b428572Be105E209b5A6222b7") + ), + AssetId(Chain.SmartChain), + "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", + outputAmount = BigInteger.TEN, + null, + chainId = Config().getChainConfig(Chain.SmartChain.string).networkId, + BigInteger.ZERO, + ) + } + assertEquals(TxSpeed.Normal, result.speed) + assertEquals(BigInteger("37500000450000"), result.amount) + assertEquals(BigInteger("1000000000"), (result as GasFee).minerFee) + assertEquals(BigInteger("1000000012"), result.maxGasPrice) + assertEquals(BigInteger("37500"), result.limit) + } + + @Test + fun test_Evm_fee_transfer_max_amount_calculation() { + val feeService = FeeService( + EthereumFeeHistory( + reward = listOf(listOf("0x3b9aca00")), + baseFeePerGas = listOf("0xa", "0xc") + ), + gasLimit = BigInteger.valueOf(25_000) + ) + val feeCalculator = EvmFeeCalculator( + feeService = feeService, + callService = CallService(), + coinType = CoinType.SMARTCHAIN, + ) + val result = runBlocking { + feeCalculator.calculate( + ConfirmParams.Builder( + AssetId(Chain.SmartChain), + Account(Chain.SmartChain, "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", ""), + amount = BigInteger.TEN, + ).transfer( + destination = DestinationAddress("0x9b1DB81180c31B1b428572Be105E209b5A6222b7"), + isMax = true + ), + AssetId(Chain.SmartChain), + "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", + outputAmount = BigInteger.TEN, + null, + chainId = Config().getChainConfig(Chain.SmartChain.string).networkId, + BigInteger.ZERO, + ) + } + assertEquals(TxSpeed.Normal, result.speed) + assertEquals(BigInteger("37500000450000"), result.amount) + assertEquals(BigInteger("1000000012"), (result as GasFee).minerFee) + assertEquals(BigInteger("1000000012"), result.maxGasPrice) + assertEquals(BigInteger("37500"), result.limit) + } + + + @Test + fun test_Evm_gas_limit_native_transfer_calculation() { + val feeService = FeeService( + EthereumFeeHistory( + reward = listOf(listOf("0x3b9aca00")), + baseFeePerGas = listOf("0x0", "0x0") + ), + gasLimit = BigInteger.valueOf(21_000) + ) + val feeCalculator = EvmFeeCalculator( + feeService = feeService, + callService = CallService(), + coinType = CoinType.SMARTCHAIN, + ) + val result = runBlocking { + feeCalculator.calculate( + ConfirmParams.Builder( + AssetId(Chain.SmartChain), + Account(Chain.SmartChain, "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", ""), + amount = BigInteger.TEN, + ).transfer( + DestinationAddress("0xa857a4E4B3f7C0eb7e132A7A4abcA287225dDB2A") + ), + AssetId(Chain.SmartChain), + "0xa857a4E4B3f7C0eb7e132A7A4abcA287225dDB2A", + outputAmount = BigInteger.TEN, + null, + chainId = Config().getChainConfig(Chain.SmartChain.string).networkId, + BigInteger.ZERO, + ) + } + assertEquals(TxSpeed.Normal, result.speed) + assertEquals(BigInteger("21000000000000"), result.amount) + assertEquals(BigInteger.valueOf(21_000), (result as GasFee).limit) + assertEquals("0x9b1DB81180c31B1b428572Be105E209b5A6222b7", (feeService.gasLimitRequest[0] as Map<*, *>)["from"]) + assertEquals("0xa857a4E4B3f7C0eb7e132A7A4abcA287225dDB2A", (feeService.gasLimitRequest[0] as Map<*, *>)["to"]) + assertEquals("0xa", (feeService.gasLimitRequest[0] as Map<*, *>)["value"]) + assertEquals("0x", (feeService.gasLimitRequest[0] as Map<*, *>)["data"]) + } + + @Test + fun test_Evm_Oracle() { + val feeService = FeeService( + EthereumFeeHistory( + reward = listOf(listOf("0x3b9aca00")), + baseFeePerGas = listOf("0x0", "0x0") + ), + gasLimit = BigInteger.valueOf(21_000) + ) + val feeCalculator = EvmFeeCalculator( + feeService = feeService, + callService = CallService(), + coinType = CoinType.SMARTCHAIN, + ) + val result = runBlocking { + feeCalculator.calculate( + ConfirmParams.Builder( + AssetId(Chain.OpBNB), + Account(Chain.OpBNB, "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", ""), + amount = BigInteger.TEN, + ).transfer( + DestinationAddress("0xa857a4E4B3f7C0eb7e132A7A4abcA287225dDB2A") + ), + AssetId(Chain.OpBNB), + "0xa857a4E4B3f7C0eb7e132A7A4abcA287225dDB2A", + outputAmount = BigInteger.TEN, + null, + chainId = Config().getChainConfig(Chain.OpBNB.string).networkId, + BigInteger.ZERO, + ) + } + assertEquals(TxSpeed.Normal, result.speed) + assertEquals(BigInteger("21000000000001"), result.amount) + assertEquals(BigInteger.valueOf(21_000), (result as GasFee).limit) + assertEquals("0x9b1DB81180c31B1b428572Be105E209b5A6222b7", (feeService.gasLimitRequest[0] as Map<*, *>)["from"]) + assertEquals("0xa857a4E4B3f7C0eb7e132A7A4abcA287225dDB2A", (feeService.gasLimitRequest[0] as Map<*, *>)["to"]) + assertEquals("0xa", (feeService.gasLimitRequest[0] as Map<*, *>)["value"]) + assertEquals("0x", (feeService.gasLimitRequest[0] as Map<*, *>)["data"]) + } + + @Test + fun test_Evm_gas_limit_token_transfer_calculation() { + val feeService = FeeService( + EthereumFeeHistory( + reward = listOf(listOf("0x3b9aca00")), + baseFeePerGas = listOf("0x0", "0x0") + ), + gasLimit = BigInteger.valueOf(21_000) + ) + val feeCalculator = EvmFeeCalculator( + feeService = feeService, + callService = CallService(), + coinType = CoinType.SMARTCHAIN, + ) + val result = runBlocking { + feeCalculator.calculate( + ConfirmParams.Builder( + AssetId(Chain.SmartChain, "0x2170Ed0880ac9A755fd29B2688956BD959F933F8"), + Account(Chain.SmartChain, "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", ""), + amount = BigInteger.TEN, + ).transfer( + DestinationAddress("0xa857a4E4B3f7C0eb7e132A7A4abcA287225dDB2A") + ), + AssetId(Chain.SmartChain, "0x2170Ed0880ac9A755fd29B2688956BD959F933F8"), + "0xa857a4E4B3f7C0eb7e132A7A4abcA287225dDB2A", + outputAmount = BigInteger.TEN, + null, + chainId = Config().getChainConfig(Chain.SmartChain.string).networkId, + BigInteger.ZERO, + ) + } + assertEquals(TxSpeed.Normal, result.speed) + assertEquals(BigInteger("21000000000000"), result.amount) + assertEquals(BigInteger.valueOf(21_000), (result as GasFee).limit) + assertEquals("0x9b1DB81180c31B1b428572Be105E209b5A6222b7", (feeService.gasLimitRequest[0] as Map<*, *>)["from"]) + assertEquals("0x2170ed0880ac9a755fd29b2688956bd959f933f8", (feeService.gasLimitRequest[0] as Map<*, *>)["to"]) + assertEquals("0x0", (feeService.gasLimitRequest[0] as Map<*, *>)["value"]) + assertEquals( + "0xa9059cbb000000000000000000000000a857a4e4b3f7c0eb7e132a7a4abca287225ddb2a000000000000000000000000000000000000000000000000000000000000000a", + (feeService.gasLimitRequest[0] as Map<*, *>)["data"] + ) + } + + @Test + fun test_Evm_gas_limit_delegate_calculation() { + val feeService = FeeService( + EthereumFeeHistory( + reward = listOf(listOf("0x3b9aca00")), + baseFeePerGas = listOf("0x0", "0x0") + ), + gasLimit = BigInteger.valueOf(21_000) + ) + val feeCalculator = EvmFeeCalculator( + feeService = feeService, + callService = CallService(), + coinType = CoinType.SMARTCHAIN, + ) + val params = ConfirmParams.Builder( + AssetId(Chain.SmartChain), + Account(Chain.SmartChain, "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", ""), + amount = BigInteger.TEN, + ).delegate("0xa857a4E4B3f7C0eb7e132A7A4abcA287225dDB2A") + val result = runBlocking { + feeCalculator.calculate( + params = params, + assetId = AssetId(Chain.SmartChain), + recipient = StakeHub.address, + outputAmount = BigInteger.TEN, + payload = "0xb", + chainId = Config().getChainConfig(Chain.SmartChain.string).networkId, + BigInteger.ZERO, + ) + } + assertEquals(TxSpeed.Normal, result.speed) + assertEquals(BigInteger("21000000000000"), result.amount) + assertEquals(BigInteger.valueOf(21_000), (result as GasFee).limit) + assertEquals("0x9b1DB81180c31B1b428572Be105E209b5A6222b7", (feeService.gasLimitRequest[0] as Map<*, *>)["from"]) + assertEquals("0x0000000000000000000000000000000000002002", (feeService.gasLimitRequest[0] as Map<*, *>)["to"]) + assertEquals("0xa", (feeService.gasLimitRequest[0] as Map<*, *>)["value"]) + assertEquals("0xb", (feeService.gasLimitRequest[0] as Map<*, *>)["data"]) + } + + @Test + fun testEvm_transfer_fee_calculation_network_fail_fee_history() { + val feeService = FeeService( + gasLimit = BigInteger.valueOf(21_000) + ) + try { + runBlocking { + EvmFeeCalculator( + feeService = feeService, + callService = CallService(), + coinType = CoinType.SMARTCHAIN, + ).calculate( + ConfirmParams.Builder( + AssetId(Chain.SmartChain), + Account(Chain.SmartChain, "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", ""), + amount = BigInteger.TEN, + ).transfer( + DestinationAddress("0x9b1DB81180c31B1b428572Be105E209b5A6222b7") + ), + AssetId(Chain.SmartChain), + "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", + outputAmount = BigInteger.TEN, + null, + chainId = Config().getChainConfig(Chain.SmartChain.string).networkId, + BigInteger.ZERO, + ) + } + assertTrue(false) + } catch (err: Throwable) { + assertEquals("Fee history fail", err.message) + } + } + + @Test + fun testEvm_transfer_fee_calculation_network_fail_L1Fee() { + + } +} \ No newline at end of file diff --git a/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/solana/TestSolanaAccountsService.kt b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/solana/TestSolanaAccountsService.kt new file mode 100644 index 000000000..bff0878af --- /dev/null +++ b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/solana/TestSolanaAccountsService.kt @@ -0,0 +1,28 @@ +package com.gemwallet.android.blockchain.clients.solana + +import com.gemwallet.android.blockchain.clients.solana.services.SolanaAccountsService +import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest +import com.gemwallet.android.blockchain.rpc.model.JSONRpcResponse +import com.wallet.core.blockchain.solana.models.SolanaTokenAccount +import com.wallet.core.blockchain.solana.models.SolanaValue + +internal class TestSolanaAccountsService( + private val tokenAddress: String? = null, +) : SolanaAccountsService { + var tokenAccountRequest: JSONRpcRequest>? = null + + override suspend fun getTokenAccountByOwner(request: JSONRpcRequest>): Result>>> { + tokenAccountRequest = request + return Result.success( + JSONRpcResponse( + SolanaValue( + listOf( + SolanaTokenAccount( + tokenAddress ?: (request.params[1] as Map<*, *>)["mint"].toString() + ), + ) + ) + ) + ) + } +} \ No newline at end of file diff --git a/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/solana/TestSolanaBalances.kt b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/solana/TestSolanaBalances.kt new file mode 100644 index 000000000..0c6d1c5b0 --- /dev/null +++ b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/solana/TestSolanaBalances.kt @@ -0,0 +1,301 @@ +package com.gemwallet.android.blockchain.clients.solana + +import com.gemwallet.android.blockchain.clients.solana.services.SolanaBalancesService +import com.gemwallet.android.blockchain.clients.solana.services.SolanaStakeService +import com.gemwallet.android.blockchain.includeLibs +import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest +import com.gemwallet.android.blockchain.rpc.model.JSONRpcResponse +import com.gemwallet.android.model.getTotalAmount +import com.wallet.core.blockchain.solana.models.SolanaBalance +import com.wallet.core.blockchain.solana.models.SolanaBalanceValue +import com.wallet.core.blockchain.solana.models.SolanaEpoch +import com.wallet.core.blockchain.solana.models.SolanaStakeAccount +import com.wallet.core.blockchain.solana.models.SolanaStakeAccountData +import com.wallet.core.blockchain.solana.models.SolanaStakeAccountDataParsed +import com.wallet.core.blockchain.solana.models.SolanaStakeAccountDataParsedInfo +import com.wallet.core.blockchain.solana.models.SolanaStakeAccountDataParsedInfoMeta +import com.wallet.core.blockchain.solana.models.SolanaStakeAccountDataParsedInfoStake +import com.wallet.core.blockchain.solana.models.SolanaStakeAccountDataParsedInfoStakeDelegation +import com.wallet.core.blockchain.solana.models.SolanaTokenAccountResult +import com.wallet.core.blockchain.solana.models.SolanaValidator +import com.wallet.core.blockchain.solana.models.SolanaValidators +import com.wallet.core.blockchain.solana.models.SolanaValue +import com.wallet.core.primitives.Asset +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.AssetType +import com.wallet.core.primitives.Chain +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import kotlinx.coroutines.runBlocking +import org.junit.Test +import kotlin.collections.Map + +class TestSolanaBalances { + + companion object { + init { + includeLibs() + } + } + + private class TestBalancesService : SolanaBalancesService { + var nativeRequest: JSONRpcRequest>? = null + var tokenRequest: JSONRpcRequest>? = null + + override suspend fun getBalance(request: JSONRpcRequest>): Result> { + nativeRequest = request + return Result.success( + JSONRpcResponse( + SolanaBalance(1_000_000) + ) + ) + } + + override suspend fun getTokenBalance(request: JSONRpcRequest>): Result>> { + tokenRequest = request + return Result.success( + JSONRpcResponse( + SolanaValue( + SolanaBalanceValue( + when (request.params[0]) { + "HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3" -> "5000000" + else -> "300000" + } + ) + ) + ) + ) + } + } + + private class TestStakeService( + private val delegationsResponse: JSONRpcResponse>> = JSONRpcResponse(emptyList()) + ) : SolanaStakeService { + var validatorsRequest: JSONRpcRequest>? = null + var delegationsRequest: JSONRpcRequest>? = null + + override suspend fun validators(request: JSONRpcRequest>): Result> { + validatorsRequest = request + return Result.success( + JSONRpcResponse( + SolanaValidators( + listOf( + SolanaValidator("validatorPubKey", 10, true) + ) + ) + ) + ) + } + + override suspend fun delegations(request: JSONRpcRequest>): Result>>> { + delegationsRequest = request + + return Result.success(delegationsResponse) + } + + override suspend fun epoch(request: JSONRpcRequest>): Result> { + return Result.success( + JSONRpcResponse( + SolanaEpoch( + epoch = 10, + slotsInEpoch = 1, + slotIndex = 1, + ) + ) + ) + } + + } + + @Test + fun testSolana_balance_native() { + val accountsService = TestSolanaAccountsService() + val balancesService = TestBalancesService() + val stakeService = TestStakeService() + + val balanceClient = SolanaBalanceClient( + chain = Chain.Solana, + accountsService = accountsService, + balancesService = balancesService, + stakeService = stakeService, + ) + val result = runBlocking { + balanceClient.getNativeBalance(Chain.Solana, "AGkXQZ9qm99xukisDUHvspWHESrcjs8Y4AmQQgef3BRh") + } + assertEquals("AGkXQZ9qm99xukisDUHvspWHESrcjs8Y4AmQQgef3BRh", balancesService.nativeRequest!!.params[0]) + assertEquals("AGkXQZ9qm99xukisDUHvspWHESrcjs8Y4AmQQgef3BRh", + ((((stakeService.delegationsRequest!!.params[1] as Map<*, *>)["filters"] as List<*>)[0] as Map<*, *>)["memcmp"] as Map<*, *>)["bytes"]) + assertNotNull(result) + assertEquals(AssetId(Chain.Solana), result!!.asset.id) + assertEquals("1000000", result.balance.available) + assertEquals("0", result.balance.staked) + assertEquals("0", result.balance.pending) + assertEquals("0", result.balance.reserved) + assertEquals("0", result.balance.locked) + assertEquals("0", result.balance.frozen) + assertEquals("0", result.balance.rewards) + } + + @Test + fun testSolana_balance_token() { + val accountsService = TestSolanaAccountsService() + val balancesService = TestBalancesService() + val stakeService = TestStakeService() + + val balanceClient = SolanaBalanceClient( + chain = Chain.Solana, + accountsService = accountsService, + balancesService = balancesService, + stakeService = stakeService, + ) + val result = runBlocking { + balanceClient.getTokenBalances( + Chain.Solana, + "AGkXQZ9qm99xukisDUHvspWHESrcjs8Y4AmQQgef3BRh", + listOf( + Asset( + id = AssetId(Chain.Solana, "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN"), + name = "Jupiter", + symbol = "JUP", + decimals = 6, + type = AssetType.TOKEN, + ), + Asset( + id = AssetId(Chain.Solana, "HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3"), + name = "Pyth Network ", + symbol = "PYTH", + decimals = 6, + type = AssetType.TOKEN, + ), + ) + ) + } + assertNotNull(result) + assertEquals(2, result.size) + assertEquals(AssetId(Chain.Solana, "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN"), result[0].asset.id) + assertEquals(AssetId(Chain.Solana, "HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3"), result[1].asset.id) + assertEquals("300000", result[0].balance.available) + assertEquals("5000000", result[1].balance.available) + } + + @Test + fun testSolana_balance_native_width_single_stake() { + val accountsService = TestSolanaAccountsService() + val balancesService = TestBalancesService() + val stakeService = TestStakeService( + delegationsResponse = JSONRpcResponse( + listOf( + SolanaTokenAccountResult( + SolanaStakeAccount( + 1000000, + 10, + SolanaStakeAccountData( + SolanaStakeAccountDataParsed( + SolanaStakeAccountDataParsedInfo( + SolanaStakeAccountDataParsedInfoStake( + SolanaStakeAccountDataParsedInfoStakeDelegation("", "", "", ""), + ), + SolanaStakeAccountDataParsedInfoMeta("") + ) + ) + ) + ), + pubkey = "", + ) + ) + + ) + ) + + val balanceClient = SolanaBalanceClient( + chain = Chain.Solana, + accountsService = accountsService, + balancesService = balancesService, + stakeService = stakeService, + ) + val result = runBlocking { + balanceClient.getNativeBalance(Chain.Solana, "AGkXQZ9qm99xukisDUHvspWHESrcjs8Y4AmQQgef3BRh") + } + assertNotNull(result) + assertEquals(AssetId(Chain.Solana), result!!.asset.id) + assertEquals("1000000", result.balance.available) + assertEquals("1000000", result.balance.staked) + assertEquals("0", result.balance.pending) + assertEquals("0", result.balance.reserved) + assertEquals("0", result.balance.locked) + assertEquals("0", result.balance.frozen) + assertEquals("0", result.balance.rewards) + assertEquals(0.001, result.balanceAmount.available) + assertEquals(0.001, result.balanceAmount.staked) + assertEquals(0.002, result.balanceAmount.getTotalAmount()) + } + + @Test + fun testSolana_balance_native_width_multi_stake() { + val accountsService = TestSolanaAccountsService() + val balancesService = TestBalancesService() + val stakeService = TestStakeService( + delegationsResponse = JSONRpcResponse( + listOf( + SolanaTokenAccountResult( + SolanaStakeAccount( + 1000000, + 10, + SolanaStakeAccountData( + SolanaStakeAccountDataParsed( + SolanaStakeAccountDataParsedInfo( + SolanaStakeAccountDataParsedInfoStake( + SolanaStakeAccountDataParsedInfoStakeDelegation("", "", "", ""), + ), + SolanaStakeAccountDataParsedInfoMeta("") + ) + ) + ) + ), + pubkey = "", + ), + SolanaTokenAccountResult( + SolanaStakeAccount( + 2000000, + 20, + SolanaStakeAccountData( + SolanaStakeAccountDataParsed( + SolanaStakeAccountDataParsedInfo( + SolanaStakeAccountDataParsedInfoStake( + SolanaStakeAccountDataParsedInfoStakeDelegation("", "", "", ""), + ), + SolanaStakeAccountDataParsedInfoMeta("") + ) + ) + ) + ), + pubkey = "", + ) + ) + + ) + ) + + val balanceClient = SolanaBalanceClient( + chain = Chain.Solana, + accountsService = accountsService, + balancesService = balancesService, + stakeService = stakeService, + ) + val result = runBlocking { + balanceClient.getNativeBalance(Chain.Solana, "AGkXQZ9qm99xukisDUHvspWHESrcjs8Y4AmQQgef3BRh") + } + assertNotNull(result) + assertEquals(AssetId(Chain.Solana), result!!.asset.id) + assertEquals("1000000", result.balance.available) + assertEquals("3000000", result.balance.staked) + assertEquals("0", result.balance.pending) + assertEquals("0", result.balance.reserved) + assertEquals("0", result.balance.locked) + assertEquals("0", result.balance.frozen) + assertEquals("0", result.balance.rewards) + assertEquals(0.001, result.balanceAmount.available) + assertEquals(0.003, result.balanceAmount.staked) + assertEquals(0.004, result.balanceAmount.getTotalAmount()) + } +} \ No newline at end of file diff --git a/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/solana/TestSolanaFeeCalculation.kt b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/solana/TestSolanaFeeCalculation.kt new file mode 100644 index 000000000..5d3ef9029 --- /dev/null +++ b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/solana/TestSolanaFeeCalculation.kt @@ -0,0 +1,100 @@ +package com.gemwallet.android.blockchain.clients.solana + +import com.gemwallet.android.blockchain.includeLibs +import com.gemwallet.android.model.ConfirmParams +import com.gemwallet.android.model.DestinationAddress +import com.wallet.core.primitives.Account +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.Chain +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.runBlocking +import org.junit.Test +import java.math.BigInteger + +class TestSolanaFeeCalculation { + companion object { + init { + includeLibs() + } + } + + @Test + fun testSolana_calculate_native_fee() { + val feeCalculator = SolanaFeeCalculator(TestSolanaFeeService()) + + val result = runBlocking { + feeCalculator.calculate( + ConfirmParams.Builder( + assetId = AssetId(Chain.Solana), + from = Account(Chain.Solana, "AGkXQZ9qm99xukisDUHvspWHESrcjs8Y4AmQQgef3BRh", ""), + amount = BigInteger.valueOf(10_000_000) + ).transfer(destination = DestinationAddress("AGkXQZ9qm99xukisDUHvspWHESrcjs8Y4AmQQgef3BRh")) + ) + } + assertEquals(BigInteger("105005000"), result.amount) + assertEquals(BigInteger("1050000000"), result.minerFee) + assertEquals(BigInteger("5000"), result.maxGasPrice) + assertEquals(BigInteger("100000"), result.limit) + assertEquals(BigInteger("30"), result.options["tokenAccountCreation"]) + } + + @Test + fun testSolana_calculate_native_fee_without_priority_fee() { + val feeCalculator = SolanaFeeCalculator(TestSolanaFeeService(fees = emptyList())) + + val result = runBlocking { + feeCalculator.calculate( + ConfirmParams.Builder( + assetId = AssetId(Chain.Solana), + from = Account(Chain.Solana, "AGkXQZ9qm99xukisDUHvspWHESrcjs8Y4AmQQgef3BRh", ""), + amount = BigInteger.valueOf(10_000_000) + ).transfer(destination = DestinationAddress("AGkXQZ9qm99xukisDUHvspWHESrcjs8Y4AmQQgef3BRh")) + ) + } + assertEquals(BigInteger("6000"), result.amount) + assertEquals(BigInteger("10000"), result.minerFee) + assertEquals(BigInteger("5000"), result.maxGasPrice) + assertEquals(BigInteger("100000"), result.limit) + assertEquals(BigInteger("30"), result.options["tokenAccountCreation"]) + } + + @Test + fun testSolana_calculate_native_fee_without_create() { + val feeCalculator = SolanaFeeCalculator(TestSolanaFeeService(fees = emptyList(), rentExemption = 0)) + + val result = runBlocking { + feeCalculator.calculate( + ConfirmParams.Builder( + assetId = AssetId(Chain.Solana), + from = Account(Chain.Solana, "AGkXQZ9qm99xukisDUHvspWHESrcjs8Y4AmQQgef3BRh", ""), + amount = BigInteger.valueOf(10_000_000) + ).transfer(destination = DestinationAddress("AGkXQZ9qm99xukisDUHvspWHESrcjs8Y4AmQQgef3BRh")) + ) + } + assertEquals(BigInteger("6000"), result.amount) + assertEquals(BigInteger("10000"), result.minerFee) + assertEquals(BigInteger("5000"), result.maxGasPrice) + assertEquals(BigInteger("100000"), result.limit) + assertEquals(BigInteger("0"), result.options["tokenAccountCreation"]) + } + + @Test + fun testSolana_calculate_token_fee_without_create() { + val feeCalculator = SolanaFeeCalculator(TestSolanaFeeService(listOf(100, 200))) + + val result = runBlocking { + feeCalculator.calculate( + ConfirmParams.Builder( + assetId = AssetId(Chain.Solana, "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"), + from = Account(Chain.Solana, "AGkXQZ9qm99xukisDUHvspWHESrcjs8Y4AmQQgef3BRh", ""), + amount = BigInteger.valueOf(10_000_000) + ).transfer(destination = DestinationAddress("AGkXQZ9qm99xukisDUHvspWHESrcjs8Y4AmQQgef3BRh")) + ) + } + assertEquals(BigInteger("15000"), result.amount) + assertEquals(BigInteger("100000"), result.minerFee) + assertEquals(BigInteger("5000"), result.maxGasPrice) + assertEquals(BigInteger("100000"), result.limit) + assertEquals(BigInteger("30"), result.options["tokenAccountCreation"]) + } +} \ No newline at end of file diff --git a/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/solana/TestSolanaFeeService.kt b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/solana/TestSolanaFeeService.kt new file mode 100644 index 000000000..34a6e8d48 --- /dev/null +++ b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/solana/TestSolanaFeeService.kt @@ -0,0 +1,20 @@ +package com.gemwallet.android.blockchain.clients.solana + +import com.gemwallet.android.blockchain.clients.solana.services.SolanaFeeService +import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest +import com.gemwallet.android.blockchain.rpc.model.JSONRpcResponse +import com.wallet.core.blockchain.solana.models.SolanaPrioritizationFee + +internal class TestSolanaFeeService( + private val fees: List = listOf(100000000, 2000000000), + private val rentExemption: Int = 30, +) : SolanaFeeService { + + override suspend fun rentExemption(request: JSONRpcRequest>): Result> { + return Result.success(JSONRpcResponse(rentExemption)) + } + + override suspend fun getPriorityFees(request: JSONRpcRequest>): Result>> { + return Result.success(JSONRpcResponse(fees.map { SolanaPrioritizationFee(it) })) + } +} \ No newline at end of file diff --git a/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/solana/TestSolanaSignPreloader.kt b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/solana/TestSolanaSignPreloader.kt new file mode 100644 index 000000000..bfe60a55d --- /dev/null +++ b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/solana/TestSolanaSignPreloader.kt @@ -0,0 +1,127 @@ +package com.gemwallet.android.blockchain.clients.solana + +import com.gemwallet.android.blockchain.clients.solana.models.SolanaTokenOwner +import com.gemwallet.android.blockchain.clients.solana.services.SolanaNetworkInfoService +import com.gemwallet.android.blockchain.includeLibs +import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest +import com.gemwallet.android.blockchain.rpc.model.JSONRpcResponse +import com.gemwallet.android.model.ConfirmParams +import com.gemwallet.android.model.DestinationAddress +import com.wallet.core.blockchain.solana.models.SolanaBlockhash +import com.wallet.core.blockchain.solana.models.SolanaBlockhashResult +import com.wallet.core.blockchain.solana.models.SolanaValue +import com.wallet.core.primitives.Account +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.Chain +import com.wallet.core.primitives.SolanaTokenProgramId +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.runBlocking +import org.junit.Test +import java.math.BigInteger + +class TestSolanaSignPreloader { + + companion object { + init { + includeLibs() + } + } + + private class TestSolanaNetworkInfoService( + val tokenOwner: String = "", + val blockhash: String = "", + ) : SolanaNetworkInfoService { + var tokenIdRequest: String = "" + + override suspend fun getTokenInfo(request: JSONRpcRequest>): Result>> { + tokenIdRequest = request.params[0].toString() + + return Result.success( + JSONRpcResponse(SolanaValue(SolanaTokenOwner(tokenOwner))) + ) + } + + override suspend fun getBlockhash(request: JSONRpcRequest>): Result> { + return Result.success(JSONRpcResponse(SolanaBlockhashResult(SolanaBlockhash(blockhash)))) + } + + } + + @Test + fun testSolana_netive_transfer_preload() { + val preloader = SolanaSignerPreloader( + chain = Chain.Solana, + feeService = TestSolanaFeeService(), + networkInfoService = TestSolanaNetworkInfoService(blockhash = "123"), + accountsService = TestSolanaAccountsService(), + ) + + val result = runBlocking { + preloader.preloadNativeTransfer( + ConfirmParams.Builder( + assetId = AssetId(Chain.Solana), + from = Account(Chain.Solana, "AGkXQZ9qm99xukisDUHvspWHESrcjs8Y4AmQQgef3BRh", ""), + amount = BigInteger.valueOf(10_000_000) + ) + .transfer(destination = DestinationAddress("AGkXQZ9qm99xukisDUHvspWHESrcjs8Y4AmQQgef3BRh")) as ConfirmParams.TransferParams.Native + ) + } + assertEquals(BigInteger("105005000"), result.chainData.gasGee().amount) + assertEquals("123", (result.chainData as SolanaSignerPreloader.SolanaChainData).blockhash) + assertEquals(SolanaTokenProgramId.Token, (result.chainData as SolanaSignerPreloader.SolanaChainData).tokenProgram) + assertEquals(null, (result.chainData as SolanaSignerPreloader.SolanaChainData).recipientTokenAddress) + assertEquals("", (result.chainData as SolanaSignerPreloader.SolanaChainData).senderTokenAddress) + } + + @Test + fun testSolana_transfer_token_preload() { + val preloader = SolanaSignerPreloader( + chain = Chain.Solana, + feeService = TestSolanaFeeService(), + networkInfoService = TestSolanaNetworkInfoService(blockhash = "123", tokenOwner = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"), + accountsService = TestSolanaAccountsService("DVWPV7brSbPDkA7a3qdn6UJsVc3J3DyhQhjNaZeZqwzo"), + ) + + val result = runBlocking { + preloader.preloadTokenTransfer( + ConfirmParams.Builder( + assetId = AssetId(Chain.Solana, "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"), + from = Account(Chain.Solana, "AGkXQZ9qm99xukisDUHvspWHESrcjs8Y4AmQQgef3BRh", ""), + amount = BigInteger.valueOf(10_000_000) + ) + .transfer(destination = DestinationAddress("AGkXQZ9qm99xukisDUHvspWHESrcjs8Y4AmQQgef3BRh")) as ConfirmParams.TransferParams.Token + ) + } + assertEquals(BigInteger("105005000"), result.chainData.gasGee().amount) + assertEquals("123", (result.chainData as SolanaSignerPreloader.SolanaChainData).blockhash) + assertEquals(SolanaTokenProgramId.Token, (result.chainData as SolanaSignerPreloader.SolanaChainData).tokenProgram) + assertEquals("DVWPV7brSbPDkA7a3qdn6UJsVc3J3DyhQhjNaZeZqwzo", (result.chainData as SolanaSignerPreloader.SolanaChainData).recipientTokenAddress) + assertEquals("DVWPV7brSbPDkA7a3qdn6UJsVc3J3DyhQhjNaZeZqwzo", (result.chainData as SolanaSignerPreloader.SolanaChainData).senderTokenAddress) + } + + @Test + fun testSolana_transfer_token2022_preload() { + val preloader = SolanaSignerPreloader( + chain = Chain.Solana, + feeService = TestSolanaFeeService(), + networkInfoService = TestSolanaNetworkInfoService(blockhash = "123", tokenOwner = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"), + accountsService = TestSolanaAccountsService("87vTugUvkkepa84mBRfENnvkPQRj5EZSkiG8XyFAhbQQ"), + ) + + val result = runBlocking { + preloader.preloadTokenTransfer( + ConfirmParams.Builder( + assetId = AssetId(Chain.Solana, "2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo"), + from = Account(Chain.Solana, "AGkXQZ9qm99xukisDUHvspWHESrcjs8Y4AmQQgef3BRh", ""), + amount = BigInteger.valueOf(10_000_000) + ) + .transfer(destination = DestinationAddress("AGkXQZ9qm99xukisDUHvspWHESrcjs8Y4AmQQgef3BRh")) as ConfirmParams.TransferParams.Token + ) + } + assertEquals(BigInteger("105005000"), result.chainData.gasGee().amount) + assertEquals("123", (result.chainData as SolanaSignerPreloader.SolanaChainData).blockhash) + assertEquals(SolanaTokenProgramId.Token2022, (result.chainData as SolanaSignerPreloader.SolanaChainData).tokenProgram) + assertEquals("87vTugUvkkepa84mBRfENnvkPQRj5EZSkiG8XyFAhbQQ", (result.chainData as SolanaSignerPreloader.SolanaChainData).recipientTokenAddress) + assertEquals("87vTugUvkkepa84mBRfENnvkPQRj5EZSkiG8XyFAhbQQ", (result.chainData as SolanaSignerPreloader.SolanaChainData).senderTokenAddress) + } +} \ No newline at end of file diff --git a/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/solana/TestSolanaSigner.kt b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/solana/TestSolanaSigner.kt new file mode 100644 index 000000000..32e116e6c --- /dev/null +++ b/blockchain/src/androidTest/kotlin/com/gemwallet/android/blockchain/clients/solana/TestSolanaSigner.kt @@ -0,0 +1,227 @@ +package com.gemwallet.android.blockchain.clients.solana + +import com.gemwallet.android.blockchain.includeLibs +import com.gemwallet.android.blockchain.testPhrase +import com.gemwallet.android.math.toHexString +import com.gemwallet.android.model.ConfirmParams +import com.gemwallet.android.model.DestinationAddress +import com.gemwallet.android.model.GasFee +import com.gemwallet.android.model.SignerParams +import com.gemwallet.android.model.TxSpeed +import com.wallet.core.primitives.Account +import com.wallet.core.primitives.Asset +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.AssetType +import com.wallet.core.primitives.Chain +import com.wallet.core.primitives.SolanaTokenProgramId +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.runBlocking +import org.junit.Test +import wallet.core.jni.CoinType +import wallet.core.jni.HDWallet +import java.math.BigInteger + +class TestSolanaSigner { + companion object { + init { + includeLibs() + } + } + + val privateKey = HDWallet(testPhrase, "").getKeyForCoin(CoinType.SOLANA).data() + + @Test + fun testSolana_native_transfer() { + val signer = SolanaSignClient( + chain = Chain.Solana, + getAsset = { + Asset( + it, + name = "sol_asset", + symbol = "sol_asset", + decimals = 6, + type = AssetType.SPL, + ) + } + ) + val input = SignerParams( + input = ConfirmParams.Builder( + assetId = AssetId(Chain.Solana), + from = Account(Chain.Solana, "4Yu2e1Wz5T1Ci2hAPswDqvMgSnJ1Ftw7ZZh8x7xKLx7S", ""), + amount = BigInteger.valueOf(10_000_000) + ) + .transfer(destination = DestinationAddress("4Yu2e1Wz5T1Ci2hAPswDqvMgSnJ1Ftw7ZZh8x7xKLx7S")) as ConfirmParams.TransferParams.Native, + chainData = SolanaSignerPreloader.SolanaChainData( + blockhash = "kiEPF6aKvEsj5nbi4FBvgRRm9ha36Y3cgDU9qnUKt32", + fee = GasFee( + amount = BigInteger("105005000"), + minerFee = BigInteger("1050000000"), + maxGasPrice = BigInteger("5000"), + limit = BigInteger("100000"), + feeAssetId = AssetId(Chain.Solana), + speed = TxSpeed.Normal, + ), + recipientTokenAddress = null, + senderTokenAddress = "", + tokenProgram = SolanaTokenProgramId.Token + ) + ) + val result = runBlocking { signer.signTransfer(input, TxSpeed.Normal, privateKey) } + assertEquals("0x4159436b734f696556323339774436566c6841614a41464844544a647a632f61577a3331" + + "6f693676686e783978676d4c544a5372643165504634454e73734a704867667575424e6e55556b3" + + "159304a52784e504a59513442414149455365626b44466a2b415242396b4b486b394f4167745057" + + "456e614370426a475a3869707a61372f4e43577330767554324f647746546758414767305a61753" + + "17474703157354f7153437a77434c53384e5738357a71514d47526d2f6c495263792f2b7974756e" + + "4c446d2b65386a4f573778666353617978446d7a704141414141414141414141414141414141414" + + "141414141414141414141414141414141414141414141414141414141414c4d706730536d427863" + + "684e486e756872566468464259677863634c722b5370616932436959444a4b51514d4341416b446" + + "74c71565067414141414143414155436f49594241414d434141454d416741414141414141414141" + + "41414141", result.toHexString()) + } + + @Test + fun testSolana_token_transfer() { + val signer = SolanaSignClient( + chain = Chain.Solana, + getAsset = { + Asset( + it, + name = "sol_asset", + symbol = "sol_asset", + decimals = 6, + type = AssetType.SPL, + ) + } + ) + val input = SignerParams( + input = ConfirmParams.Builder( + assetId = AssetId(Chain.Solana, "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"), + from = Account(Chain.Solana, "4Yu2e1Wz5T1Ci2hAPswDqvMgSnJ1Ftw7ZZh8x7xKLx7S", ""), + amount = BigInteger.valueOf(10_000_000) + ) + .transfer(destination = DestinationAddress("AGkXQZ9qm99xukisDUHvspWHESrcjs8Y4AmQQgef3BRh")) as ConfirmParams.TransferParams.Token, + chainData = SolanaSignerPreloader.SolanaChainData( + blockhash = "kiEPF6aKvEsj5nbi4FBvgRRm9ha36Y3cgDU9qnUKt32", + fee = GasFee( + amount = BigInteger("105005000"), + minerFee = BigInteger("1050000000"), + maxGasPrice = BigInteger("5000"), + limit = BigInteger("100000"), + feeAssetId = AssetId(Chain.Solana), + speed = TxSpeed.Normal, + ), + recipientTokenAddress = "DVWPV7brSbPDkA7a3qdn6UJsVc3J3DyhQhjNaZeZqwzo", + senderTokenAddress = "DVWPV7brSbPDkA7a3qdn6UJsVc3J3DyhQhjNaZeZqwzo", + tokenProgram = SolanaTokenProgramId.Token + ) + ) + val result = runBlocking { signer.signTransfer(input, TxSpeed.Normal, privateKey) } + assertEquals("0x416238786c7268464374766e72677357477874416f6b6852423654737057545862643066" + + "6e31557835466c736b734b317838714d6668326e313662454d384d6b6a4a4d467450664b7046684" + + "25a376576716557713051514241414d465365626b44466a2b415242396b4b486b394f4167745057" + + "456e614370426a475a3869707a61372f4e435775356d6277492b744e3454586473757149654b725" + + "254647a734b644444707a385843635179334a766a50304d3442446d43763762496e4637316a4753" + + "395546466f2f6c6c6f7a75344c5378774b657373346549494a6b41775a47622b5568467a4c2f374" + + "b323663734f623537794d3562764639784a724c454f624f6b41414141414733666268313257686b" + + "396e4c3455624f36336d73484c5346375639624e3545366a50574666763841715173796d44524b5" + + "948467945306565364774563245554669444678777576354b6c714c594b4a674d6b704241774d41" + + "43514f417570552b4141414141414d4142514b67686745414241514241674541436777414141414" + + "1414141414141593d", result.toHexString()) + } + + @Test + fun testSolana_token2022_transfer() { + val signer = SolanaSignClient( + chain = Chain.Solana, + getAsset = { + Asset( + it, + name = "sol_asset", + symbol = "sol_asset", + decimals = 6, + type = AssetType.SPL, + ) + } + ) + val input = SignerParams( + input = ConfirmParams.Builder( + assetId = AssetId(Chain.Solana, "2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo"), + from = Account(Chain.Solana, "4Yu2e1Wz5T1Ci2hAPswDqvMgSnJ1Ftw7ZZh8x7xKLx7S", ""), + amount = BigInteger.valueOf(10_000_000) + ) + .transfer(destination = DestinationAddress("AGkXQZ9qm99xukisDUHvspWHESrcjs8Y4AmQQgef3BRh")) as ConfirmParams.TransferParams.Token, + chainData = SolanaSignerPreloader.SolanaChainData( + blockhash = "kiEPF6aKvEsj5nbi4FBvgRRm9ha36Y3cgDU9qnUKt32", + fee = GasFee( + amount = BigInteger("105005000"), + minerFee = BigInteger("1050000000"), + maxGasPrice = BigInteger("5000"), + limit = BigInteger("100000"), + feeAssetId = AssetId(Chain.Solana), + speed = TxSpeed.Normal, + ), + recipientTokenAddress = "87vTugUvkkepa84mBRfENnvkPQRj5EZSkiG8XyFAhbQQ", + senderTokenAddress = "87vTugUvkkepa84mBRfENnvkPQRj5EZSkiG8XyFAhbQQ", + tokenProgram = SolanaTokenProgramId.Token2022 + ) + ) + val result = runBlocking { signer.signTransfer(input, TxSpeed.Normal, privateKey) } + assertEquals("0x4152744d346e59365642416d427563754c675a7759623165304764516d6b782b6251764f" + + "6241657558466e6a4b5a4744507930653972305249444f384757705056384439453345754b33416" + + "e492b354b6b6d72485667414241414d465365626b44466a2b415242396b4b486b394f4167745057" + + "456e614370426a475a3869707a61372f4e43577470783737467363375a7773776d4234324a65393" + + "04538487a622b59486131534a74465451777a385351705265535344747369697148743063646755" + + "2b566b666b355849514b6e4f505a394e57366654704c696e536541775a47622b5568467a4c2f374" + + "b323663734f623537794d3562764639784a724c454f624f6b41414141414733666268376e575033" + + "68684358627a6b624d33617468723854594f354453662b76666b6f324b474c2f4173796d44524b5" + + "948467945306565364774563245554669444678777576354b6c714c594b4a674d6b704241774d41" + + "43514f417570552b4141414141414d4142514b67686745414241514241674541436777414141414" + + "1414141414141593d", result.toHexString()) + } + +// @Test +// fun testSolana_swap() { +// val signer = SolanaSignClient( +// chain = Chain.Solana, +// getAsset = { +// Asset( +// it, +// name = "sol_asset", +// symbol = "sol_asset", +// decimals = 6, +// type = AssetType.SPL, +// ) +// } +// ) +// val input = SignerParams( +// input = ConfirmParams.SwapParams( +// from = Account(Chain.Solana, "4Yu2e1Wz5T1Ci2hAPswDqvMgSnJ1Ftw7ZZh8x7xKLx7S", ""), +// fromAmount = BigInteger("1000000000"), +// fromAssetId = AssetId(Chain.Solana), +// provider = "Jupiter", +// swapData = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAHDjS+5PY53AVOBcAaDRlq7W22nVbk6pILPAItLw1bznOpVnc0WHX0iBa76mPlR5DMysds1h9/FNOv+q/dzEjiSqRmf8tThVI2sRkmD2EmMS9/AyOY0sK8kOkTIbTMftjXJHEzfZHfK+df8cHOiMH18Ck85+5FYvbuPgRfoH+q0xHdfiDLDpwNUu6Ja8lBL3sFhPDWgrrr6cu9Ez6m7wuAwlSpqhQZW2JKxscg7kplpPWG6E/FUSFLmKyo8J5QdY2PRdG4FzZMmQ0vu3MSk+s026EyyxDNxq/8Jesxeb8z3xsbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADBkZv5SEXMv/srbpyw5vnvIzlu8X3EmssQ5s6QAAAAAR51VvyMcBu7nTFbs5oFQf9sbLeo/SOUQKxzaJWvBOPBt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKmMlyWPTiSJ8bs9ECkUjg2DC1oTmdr/EIQEjnvY2+n4WZqAC/9MhzaIlsIPwUBz6/HLWqN1/oH+Tb3IK6Tft154tD/6J/XX9kp0wJsfKVh53ksJqzbfyd1RSzIap7OM5ejXbCeYNe17/CeC+wmOilA+gAKngqcevwZzbvcwTpb9tggIAAUCwFwVAAgACQMWcwUAAAAAAAsGAAQAFQcKAQEHAgAEDAIAAAAAypo7AAAAAAoBBAERCwYABgAbBwoBAQkjCgwABAMBBhUbBQkNCRwMEhQTAwIKHRcMAgEQDw4RGhYZGAopwSCbM0HWnIECAgAAAD0AZAABOGQBAgDKmjsAAAAArghIDQAAAAAKADIKAwQAAAEJAgANKUgaezLclq6i6/fo4RGyOuqlNL8whAoKN2dzwqWgBMnIzccHAXbLxXfK206Eki7kKWIPQ9PpE4DMKW/o9pK4tDRe+0dG6+DVAZB1AwMFAgIBAA==", +// to = "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4", +// toAmount = BigInteger("222824622"), +// toAssetId = AssetId(Chain.Solana, "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"), +// value = "", +// ), +// chainData = SolanaSignerPreloader.SolanaChainData( +// blockhash = "489EdRewv3Yk17rwq4HoVwfwPS3h291TmsVgdxDgFbhg", +// fee = GasFee( +// amount = BigInteger("355000"), +// minerFee = BigInteger("250000"), +// maxGasPrice = BigInteger("5000"), +// limit = BigInteger("1400000"), +// feeAssetId = AssetId(Chain.Solana), +// speed = TxSpeed.Normal, +// options = mapOf("tokenAccountCreation" to BigInteger("2039280")) +// ), +// recipientTokenAddress = null, +// senderTokenAddress = "", +// tokenProgram = SolanaTokenProgramId.Token +// ) +// ) +// val result = runBlocking { signer.signTransfer(input, TxSpeed.Normal, privateKey) } +// assertEquals("", result.toHexString()) +// } +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/SignerPreload.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/SignerPreload.kt index 7458c3676..a8602aa3d 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/SignerPreload.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/SignerPreload.kt @@ -4,9 +4,22 @@ import com.gemwallet.android.model.ConfirmParams import com.gemwallet.android.model.SignerParams import com.wallet.core.primitives.Account -interface SignerPreload : BlockchainClient { - suspend operator fun invoke( - owner: Account, - params: ConfirmParams, - ): Result +interface NativeTransferPreloader : BlockchainClient { + suspend fun preloadNativeTransfer(params: ConfirmParams.TransferParams.Native): SignerParams +} + +interface TokenTransferPreloader : BlockchainClient { + suspend fun preloadTokenTransfer(params: ConfirmParams.TransferParams.Token): SignerParams +} + +interface SwapTransactionPreloader : BlockchainClient { + suspend fun preloadSwap(params: ConfirmParams.SwapParams): SignerParams +} + +interface ApprovalTransactionPreloader : BlockchainClient { + suspend fun preloadApproval(params: ConfirmParams.TokenApprovalParams): SignerParams +} + +interface StakeTransactionPreloader : BlockchainClient { + suspend fun preloadStake(params: ConfirmParams.Stake): SignerParams } \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/SignerPreloaderProxy.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/SignerPreloaderProxy.kt index 22b758fb2..545271b53 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/SignerPreloaderProxy.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/SignerPreloaderProxy.kt @@ -2,29 +2,56 @@ package com.gemwallet.android.blockchain.clients import com.gemwallet.android.model.ConfirmParams import com.gemwallet.android.model.SignerParams -import com.wallet.core.primitives.Account import com.wallet.core.primitives.Chain -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlin.math.acos class SignerPreloaderProxy( - private val clients: List -) : SignerPreload { - override suspend fun invoke( - owner: Account, - params: ConfirmParams, - ): Result = withContext(Dispatchers.IO) { - try { - clients.getClient(owner.chain)?.invoke(owner = owner, params = params) - ?: Result.failure(IllegalArgumentException("Chain isn't support") - ) - } catch (err: Throwable) { - Result.failure(err) + private val nativeTransferClients: List, + private val tokenTransferClients: List, + private val stakeTransactionClients: List, + private val swapTransactionClients: List, + private val approvalTransactionClients: List, +) : NativeTransferPreloader, TokenTransferPreloader, StakeTransactionPreloader, SwapTransactionPreloader, ApprovalTransactionPreloader { + + suspend fun preload(params: ConfirmParams): SignerParams { + return when (params) { + is ConfirmParams.Stake -> preloadStake(params) + is ConfirmParams.SwapParams -> preloadSwap(params) + is ConfirmParams.TokenApprovalParams -> preloadApproval(params) + is ConfirmParams.TransferParams.Native -> preloadNativeTransfer(params) + is ConfirmParams.TransferParams.Token -> preloadTokenTransfer(params) } } override fun supported(chain: Chain): Boolean { - return clients.getClient(chain) != null + return (nativeTransferClients + + tokenTransferClients + + stakeTransactionClients + + swapTransactionClients + + approvalTransactionClients).getClient(chain) != null + } + + override suspend fun preloadNativeTransfer(params: ConfirmParams.TransferParams.Native): SignerParams { + return nativeTransferClients.getClient(params.from.chain)?.preloadNativeTransfer(params = params) + ?: throw IllegalArgumentException("Chain isn't support") + } + + override suspend fun preloadTokenTransfer(params: ConfirmParams.TransferParams.Token): SignerParams { + return tokenTransferClients.getClient(params.from.chain)?.preloadTokenTransfer(params = params) + ?: throw IllegalArgumentException("Chain isn't support") + } + + override suspend fun preloadStake(params: ConfirmParams.Stake): SignerParams { + return stakeTransactionClients.getClient(params.from.chain)?.preloadStake(params = params) + ?: throw IllegalArgumentException("Chain isn't support") + } + + override suspend fun preloadSwap(params: ConfirmParams.SwapParams): SignerParams { + return swapTransactionClients.getClient(params.from.chain)?.preloadSwap(params = params) + ?: throw IllegalArgumentException("Chain isn't support") + } + + override suspend fun preloadApproval(params: ConfirmParams.TokenApprovalParams): SignerParams { + return approvalTransactionClients.getClient(params.from.chain)?.preloadApproval(params = params) + ?: throw IllegalArgumentException("Chain isn't support") } } \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosBalanceClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosBalanceClient.kt index 50e78bc02..a783bb316 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosBalanceClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosBalanceClient.kt @@ -1,6 +1,7 @@ package com.gemwallet.android.blockchain.clients.aptos import com.gemwallet.android.blockchain.clients.BalanceClient +import com.gemwallet.android.blockchain.clients.aptos.services.AptosBalancesService import com.gemwallet.android.ext.asset import com.gemwallet.android.model.AssetBalance import com.wallet.core.primitives.Asset @@ -8,13 +9,16 @@ import com.wallet.core.primitives.Chain class AptosBalanceClient( private val chain: Chain, - private val rpcClient: AptosRpcClient, + private val balanceService: AptosBalancesService, ) : BalanceClient { override suspend fun getNativeBalance(chain: Chain, address: String): AssetBalance? { - return rpcClient.balance(address) - .fold({ - AssetBalance.create(chain.asset(), available = it.data.coin.value) - }) { null } + val result = balanceService.balance(address).getOrNull()?.data?.coin?.value ?: return null + return try { // String to number + AssetBalance.create(chain.asset(), available = result) + } catch (err: Throwable) { + print(err) + null + } } override suspend fun getTokenBalances(chain: Chain, address: String, tokens: List): List = emptyList() diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosBroadcastClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosBroadcastClient.kt index e02a53d61..d2f57be2a 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosBroadcastClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosBroadcastClient.kt @@ -2,6 +2,8 @@ package com.gemwallet.android.blockchain.clients.aptos import com.gemwallet.android.blockchain.Mime import com.gemwallet.android.blockchain.clients.BroadcastClient +import com.gemwallet.android.blockchain.clients.aptos.services.AptosBroadcastService +import com.gemwallet.android.blockchain.clients.aptos.services.AptosService import com.wallet.core.primitives.Account import com.wallet.core.primitives.Chain import com.wallet.core.primitives.TransactionType @@ -9,7 +11,7 @@ import okhttp3.RequestBody.Companion.toRequestBody class AptosBroadcastClient( private val chain: Chain, - private val rpcClient: AptosRpcClient, + private val rpcClient: AptosBroadcastService, ) : BroadcastClient { override suspend fun send(account: Account, signedMessage: ByteArray, type: TransactionType): Result = try { val hash = rpcClient.broadcast(String(signedMessage).toRequestBody(Mime.Json.value)).getOrThrow().hash diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosFee.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosFeeCalculator.kt similarity index 65% rename from blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosFee.kt rename to blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosFeeCalculator.kt index 4df1da916..2ab179766 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosFee.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosFeeCalculator.kt @@ -1,5 +1,7 @@ package com.gemwallet.android.blockchain.clients.aptos +import com.gemwallet.android.blockchain.clients.aptos.services.AptosAccountsService +import com.gemwallet.android.blockchain.clients.aptos.services.AptosFeeService import com.gemwallet.android.model.Fee import com.gemwallet.android.model.GasFee import com.gemwallet.android.model.TxSpeed @@ -11,16 +13,16 @@ import kotlinx.coroutines.async import kotlinx.coroutines.withContext import java.math.BigInteger -internal class AptosFee { - suspend operator fun invoke( - chain: Chain, - destination: String, - rpcClient: AptosRpcClient, - ): Fee = withContext(Dispatchers.IO) { +internal class AptosFeeCalculator( + private val chain: Chain, + private val feeRpcClient: AptosFeeService, + private val accountsRpcClient: AptosAccountsService, +) { + suspend fun calculate(destination: String): Fee = withContext(Dispatchers.IO) { val gasPriceJob = - async { rpcClient.feePrice().getOrThrow().prioritized_gas_estimate.toBigInteger() } + async { feeRpcClient.feePrice().getOrThrow().prioritized_gas_estimate.toBigInteger() } val isNewJob = async { - val result = rpcClient.accounts(destination).getOrThrow() + val result = accountsRpcClient.accounts(destination).getOrThrow() if (result.sequence_number != null) { false } else { diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosNodeStatusClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosNodeStatusClient.kt index f79e5a640..702cd7db4 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosNodeStatusClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosNodeStatusClient.kt @@ -1,6 +1,8 @@ package com.gemwallet.android.blockchain.clients.aptos import com.gemwallet.android.blockchain.clients.NodeStatusClient +import com.gemwallet.android.blockchain.clients.aptos.services.AptosService +import com.gemwallet.android.blockchain.clients.aptos.services.getLedger import com.gemwallet.android.blockchain.rpc.getLatency import com.gemwallet.android.model.NodeStatus import com.wallet.core.primitives.Chain @@ -10,7 +12,7 @@ import kotlin.String class AptosNodeStatusClient( private val chain: Chain, - private val rpcClient: AptosRpcClient, + private val rpcClient: AptosService, ) : NodeStatusClient { override suspend fun getNodeStatus(chain: Chain, url: String): NodeStatus? = withContext(Dispatchers.IO) { diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosRpcClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosRpcClient.kt deleted file mode 100644 index d80c99f09..000000000 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosRpcClient.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.gemwallet.android.blockchain.clients.aptos - -import com.gemwallet.android.blockchain.clients.aptos.model.AptosAccount -import com.wallet.core.blockchain.aptos.models.AptosGasFee -import com.wallet.core.blockchain.aptos.models.AptosLedger -import com.wallet.core.blockchain.aptos.models.AptosResource -import com.wallet.core.blockchain.aptos.models.AptosResourceBalance -import com.wallet.core.blockchain.aptos.models.AptosTransaction -import com.wallet.core.blockchain.aptos.models.AptosTransactionBroacast -import okhttp3.RequestBody -import retrofit2.Response -import retrofit2.http.Body -import retrofit2.http.GET -import retrofit2.http.Headers -import retrofit2.http.POST -import retrofit2.http.Path -import retrofit2.http.Url - -interface AptosRpcClient { - - @GET - suspend fun ledger(@Url url: String): Response - - @GET("/v1/accounts/{address}") - suspend fun accounts(@Path("address") address: String): Result - - @GET("/v1/accounts/{address}/resource/0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>") - suspend fun balance(@Path("address") address: String): Result> - - @GET("/v1/transactions/by_hash/{id}") - suspend fun transactions(@Path("id") txId: String): Result - - @GET("/v1/estimate_gas_price") - suspend fun feePrice(): Result - - @Headers("Content-type: application/json") - @POST("/v1/transactions") - suspend fun broadcast(@Body request: RequestBody): Result -} - -suspend fun AptosRpcClient.getLedger(url: String): Response { - return ledger("$url/v1") -} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosSignClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosSignClient.kt index 3b9135e7b..d1977877e 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosSignClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosSignClient.kt @@ -2,7 +2,6 @@ package com.gemwallet.android.blockchain.clients.aptos import com.gemwallet.android.blockchain.clients.SignClient import com.gemwallet.android.blockchain.operators.walletcore.WCChainTypeProxy -import com.gemwallet.android.model.GasFee import com.gemwallet.android.model.SignerParams import com.gemwallet.android.model.TxSpeed import com.google.protobuf.ByteString @@ -15,8 +14,8 @@ class AptosSignClient( ) : SignClient { override suspend fun signTransfer(params: SignerParams, txSpeed: TxSpeed, privateKey: ByteArray): ByteArray { val coinType = WCChainTypeProxy().invoke(chain) - val metadata = params.info as AptosSignerPreloader.Info - val fee = (metadata.fee() as? GasFee) ?: throw Exception("Fee error") + val metadata = params.chainData as AptosSignerPreloader.AptosChainData + val fee = metadata.gasGee() val signInput = Aptos.SigningInput.newBuilder().apply { this.chainId = 1 this.transfer = Aptos.TransferMessage.newBuilder().apply { @@ -27,11 +26,15 @@ class AptosSignClient( this.gasUnitPrice = fee.maxGasPrice.toLong() this.maxGasAmount = fee.limit.toLong() this.sequenceNumber = metadata.sequence - this.sender = params.owner + this.sender = params.input.from.address this.privateKey = ByteString.copyFrom(privateKey) }.build() val output = AnySigner.sign(signInput, coinType, Aptos.SigningOutput.parser()) - return output.json.toByteArray() + if (output.errorMessage.isNullOrEmpty()) { + return output.json.toByteArray() + } else { + throw Exception(output.errorMessage) + } } override fun supported(chain: Chain): Boolean = this.chain == chain diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosSignerPreloader.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosSignerPreloader.kt index 88fb99a0e..9b3b2c5ac 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosSignerPreloader.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosSignerPreloader.kt @@ -1,43 +1,42 @@ package com.gemwallet.android.blockchain.clients.aptos -import com.gemwallet.android.blockchain.clients.SignerPreload +import com.gemwallet.android.blockchain.clients.NativeTransferPreloader +import com.gemwallet.android.blockchain.clients.aptos.services.AptosAccountsService +import com.gemwallet.android.blockchain.clients.aptos.services.AptosFeeService +import com.gemwallet.android.model.ChainSignData import com.gemwallet.android.model.ConfirmParams import com.gemwallet.android.model.Fee -import com.gemwallet.android.model.SignerInputInfo +import com.gemwallet.android.model.GasFee import com.gemwallet.android.model.SignerParams import com.gemwallet.android.model.TxSpeed -import com.wallet.core.primitives.Account import com.wallet.core.primitives.Chain class AptosSignerPreloader( private val chain: Chain, - private val rpcClient: AptosRpcClient, -) : SignerPreload { - override suspend fun invoke( - owner: Account, - params: ConfirmParams, - ): Result { + private val accountsService: AptosAccountsService, + feeService: AptosFeeService, +) : NativeTransferPreloader { + + private val feeCalculator = AptosFeeCalculator(chain, feeService, accountsService) + + override suspend fun preloadNativeTransfer(params: ConfirmParams.TransferParams.Native): SignerParams { val sequence = try { - rpcClient.accounts(owner.address).getOrThrow().sequence_number?.toLong() ?: 0L + val response = accountsService.accounts(params.from.address).getOrThrow() + response.sequence_number?.toLong() ?: 0L } catch (_: Throwable) { - 0 + 0L } - val fee = AptosFee().invoke(chain, params.destination()?.address!!, rpcClient) - val input = SignerParams( - input = params, - owner = owner.address, - info = Info(sequence = sequence, fee) - ) - return Result.success(input) + val fee = feeCalculator.calculate(params.destination().address) + val input = SignerParams(params, AptosChainData(sequence, fee)) + return input } override fun supported(chain: Chain): Boolean = this.chain == chain - data class Info( + data class AptosChainData( val sequence: Long, val fee: Fee, - ) : SignerInputInfo { + ) : ChainSignData { override fun fee(speed: TxSpeed): Fee = fee - } } \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosTransactionStatusClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosTransactionStatusClient.kt index 45a153489..5bd18853e 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosTransactionStatusClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/AptosTransactionStatusClient.kt @@ -1,6 +1,8 @@ package com.gemwallet.android.blockchain.clients.aptos import com.gemwallet.android.blockchain.clients.TransactionStatusClient +import com.gemwallet.android.blockchain.clients.aptos.services.AptosService +import com.gemwallet.android.blockchain.clients.aptos.services.AptosTransactionsService import com.gemwallet.android.model.TransactionChages import com.wallet.core.primitives.Chain import com.wallet.core.primitives.TransactionState @@ -9,7 +11,7 @@ import java.math.BigInteger class AptosTransactionStatusClient( private val chain: Chain, - private val rpcClient: AptosRpcClient, + private val rpcClient: AptosTransactionsService, ) : TransactionStatusClient { override suspend fun getStatus(chain: Chain, owner: String, txId: String): Result { diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/services/AptosAccountsService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/services/AptosAccountsService.kt new file mode 100644 index 000000000..5b45abc1b --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/services/AptosAccountsService.kt @@ -0,0 +1,10 @@ +package com.gemwallet.android.blockchain.clients.aptos.services + +import com.gemwallet.android.blockchain.clients.aptos.model.AptosAccount +import retrofit2.http.GET +import retrofit2.http.Path + +interface AptosAccountsService { + @GET("/v1/accounts/{address}") + suspend fun accounts(@Path("address") address: String): Result +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/services/AptosBalancesService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/services/AptosBalancesService.kt new file mode 100644 index 000000000..268c01d47 --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/services/AptosBalancesService.kt @@ -0,0 +1,11 @@ +package com.gemwallet.android.blockchain.clients.aptos.services + +import com.wallet.core.blockchain.aptos.models.AptosResource +import com.wallet.core.blockchain.aptos.models.AptosResourceBalance +import retrofit2.http.GET +import retrofit2.http.Path + +interface AptosBalancesService { + @GET("/v1/accounts/{address}/resource/0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>") + suspend fun balance(@Path("address") address: String): Result> +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/services/AptosBroadcastService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/services/AptosBroadcastService.kt new file mode 100644 index 000000000..c96cb4efc --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/services/AptosBroadcastService.kt @@ -0,0 +1,13 @@ +package com.gemwallet.android.blockchain.clients.aptos.services + +import com.wallet.core.blockchain.aptos.models.AptosTransactionBroacast +import okhttp3.RequestBody +import retrofit2.http.Body +import retrofit2.http.Headers +import retrofit2.http.POST + +interface AptosBroadcastService { + @Headers("Content-type: application/json") + @POST("/v1/transactions") + suspend fun broadcast(@Body request: RequestBody): Result +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/services/AptosFeeService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/services/AptosFeeService.kt new file mode 100644 index 000000000..a41660447 --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/services/AptosFeeService.kt @@ -0,0 +1,9 @@ +package com.gemwallet.android.blockchain.clients.aptos.services + +import com.wallet.core.blockchain.aptos.models.AptosGasFee +import retrofit2.http.GET + +interface AptosFeeService { + @GET("/v1/estimate_gas_price") + suspend fun feePrice(): Result +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/services/AptosNodeStatusService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/services/AptosNodeStatusService.kt new file mode 100644 index 000000000..6ca3854c1 --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/services/AptosNodeStatusService.kt @@ -0,0 +1,19 @@ +package com.gemwallet.android.blockchain.clients.aptos.services + +import com.wallet.core.blockchain.aptos.models.AptosLedger +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Url + +interface AptosNodeStatusService { + @GET + suspend fun ledger(@Url url: String): Response + + suspend fun AptosService.getLedger(url: String): Response { + return ledger("$url/v1") + } +} + +suspend fun AptosNodeStatusService.getLedger(url: String): Response { + return ledger("$url/v1") +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/services/AptosService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/services/AptosService.kt new file mode 100644 index 000000000..dd5dada2b --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/services/AptosService.kt @@ -0,0 +1,9 @@ +package com.gemwallet.android.blockchain.clients.aptos.services + +interface AptosService : + AptosAccountsService, + AptosBalancesService, + AptosBroadcastService, + AptosFeeService, + AptosNodeStatusService, + AptosTransactionsService \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/services/AptosTransactionsService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/services/AptosTransactionsService.kt new file mode 100644 index 000000000..b7dbdec89 --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/aptos/services/AptosTransactionsService.kt @@ -0,0 +1,10 @@ +package com.gemwallet.android.blockchain.clients.aptos.services + +import com.wallet.core.blockchain.aptos.models.AptosTransaction +import retrofit2.http.GET +import retrofit2.http.Path + +interface AptosTransactionsService { + @GET("/v1/transactions/by_hash/{id}") + suspend fun transactions(@Path("id") txId: String): Result +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinBalanceClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinBalanceClient.kt index 6852413f2..bc7faa182 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinBalanceClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinBalanceClient.kt @@ -1,27 +1,23 @@ package com.gemwallet.android.blockchain.clients.bitcoin import com.gemwallet.android.blockchain.clients.BalanceClient +import com.gemwallet.android.blockchain.clients.bitcoin.services.BitcoinBalancesService import com.gemwallet.android.ext.asset import com.gemwallet.android.model.AssetBalance -import com.wallet.core.primitives.Asset import com.wallet.core.primitives.Chain class BitcoinBalanceClient( private val chain: Chain, - private val rpcClient: BitcoinRpcClient, + private val balanceService: BitcoinBalancesService, ) : BalanceClient { override suspend fun getNativeBalance(chain: Chain, address: String): AssetBalance? { - return rpcClient.getBalance(address) - .fold( - { - if (it.balance != null) { - AssetBalance.create(chain.asset(), available = it.balance) - } else { - null - } - } - ) { null } + val result = balanceService.balance(address).getOrNull() + return try { + AssetBalance.create(chain.asset(), available = result?.balance ?: return null) + } catch (_: Throwable) { + null + } } override fun supported(chain: Chain): Boolean = this.chain == chain diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinBroadcastClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinBroadcastClient.kt index 99dd819cc..8709675d4 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinBroadcastClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinBroadcastClient.kt @@ -2,6 +2,7 @@ package com.gemwallet.android.blockchain.clients.bitcoin import com.gemwallet.android.blockchain.Mime import com.gemwallet.android.blockchain.clients.BroadcastClient +import com.gemwallet.android.blockchain.clients.bitcoin.services.BitcoinBroadcastService import com.gemwallet.android.blockchain.rpc.RpcError import com.gemwallet.android.math.toHexString import com.wallet.core.primitives.Account @@ -11,12 +12,12 @@ import okhttp3.RequestBody.Companion.toRequestBody class BitcoinBroadcastClient( private val chain: Chain, - private val rpcClient: BitcoinRpcClient, + private val broadcastService: BitcoinBroadcastService, ) : BroadcastClient { override suspend fun send(account: Account, signedMessage: ByteArray, type: TransactionType): Result { val requestBody = signedMessage.toHexString("").toRequestBody(Mime.Plain.value) - return rpcClient.broadcast(requestBody).mapCatching { + return broadcastService.broadcast(requestBody).mapCatching { it.result ?: throw RpcError.BroadcastFail(it.error?.message ?: "Unknown error") } } diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinFee.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinFeeCalculator.kt similarity index 56% rename from blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinFee.kt rename to blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinFeeCalculator.kt index c82c35ea2..43297516f 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinFee.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinFeeCalculator.kt @@ -1,6 +1,8 @@ package com.gemwallet.android.blockchain.clients.bitcoin +import com.gemwallet.android.blockchain.clients.bitcoin.services.BitcoinFeeService import com.gemwallet.android.blockchain.operators.walletcore.WCChainTypeProxy +import com.gemwallet.android.ext.toBitcoinChain import com.gemwallet.android.model.Crypto import com.gemwallet.android.model.Fee import com.gemwallet.android.model.GasFee @@ -11,7 +13,12 @@ import com.gemwallet.android.model.TxSpeed.Slow import com.wallet.core.blockchain.bitcoin.models.BitcoinUTXO import com.wallet.core.primitives.Account import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.BitcoinChain import com.wallet.core.primitives.Chain +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.withContext import wallet.core.java.AnySigner import wallet.core.jni.BitcoinSigHashType import wallet.core.jni.CoinTypeConfiguration @@ -21,33 +28,36 @@ import java.math.BigDecimal import java.math.BigInteger import java.math.RoundingMode -class BitcoinFee { - suspend operator fun invoke( - rpcClient: BitcoinRpcClient, +class BitcoinFeeCalculator( + private val feeService: BitcoinFeeService, +) { + suspend fun calculate( utxos: List, account: Account, recipient: String, amount: BigInteger, - ): List { + ): List = withContext(Dispatchers.IO) { val ownerAddress = account.address val chain = account.chain - val fee = TxSpeed.entries.map { - val price = estimateFeePrice(chain, it, rpcClient) - val limit = calcFee(chain, ownerAddress, recipient, amount.toLong(), price.toLong(), utxos) - GasFee( - feeAssetId = AssetId(chain), - speed = it, - maxGasPrice = price, - limit = limit - ) - } - return fee + TxSpeed.entries.map { + async { + val price = estimateFeePrice(chain, it) + val limit = + calcFee(chain, ownerAddress, recipient, amount.toLong(), price.toLong(), utxos) + GasFee( + feeAssetId = AssetId(chain), + speed = it, + maxGasPrice = price, + limit = limit + ) + } + }.awaitAll() } - private suspend fun estimateFeePrice(chain: Chain, speed: TxSpeed, rpcClient: BitcoinRpcClient): BigInteger { + private suspend fun estimateFeePrice(chain: Chain, speed: TxSpeed): BigInteger { val decimals = CoinTypeConfiguration.getDecimals(WCChainTypeProxy().invoke(chain)) val minimumByteFee = getMinimumByteFee(chain) - return rpcClient.estimateFee(getFeePriority(chain, speed)).fold( + return feeService.estimateFee(getFeePriority(chain, speed)).fold( { val networkFeePerKb = Crypto(it.result, decimals).atomicValue val feePerByte = networkFeePerKb.toBigDecimal().divide(BigDecimal(1000), RoundingMode.CEILING).toBigInteger() @@ -67,7 +77,7 @@ class BitcoinFee { utxos: List, ): BigInteger { val coinType = WCChainTypeProxy().invoke(chain) - val total = utxos.map { it.value.toLong() }.reduce { x, y -> x + y } + val total = utxos.map { it.value.toLong() }.fold(0L) { x, y -> x + y } if (total == 0L) { return BigInteger.ZERO // empty balance } @@ -83,10 +93,12 @@ class BitcoinFee { }.build() val plan = AnySigner.plan(input, coinType, Bitcoin.TransactionPlan.parser()) - if (plan.error == Common.SigningError.Error_not_enough_utxos || plan.error == Common.SigningError.Error_missing_input_utxos) { - throw IllegalStateException("Dust Error: $bytePrice") - } else if (plan.error != Common.SigningError.OK) { - throw IllegalStateException(plan.error.name) + when (plan.error) { + Common.SigningError.OK -> { /* continue */ } + Common.SigningError.Error_not_enough_utxos, + Common.SigningError.Error_dust_amount_requested, + Common.SigningError.Error_missing_input_utxos -> throw IllegalStateException("Dust Error: $bytePrice") + else -> throw IllegalStateException(plan.error.name) } val selectedUtxos: MutableList = mutableListOf() @@ -101,27 +113,29 @@ class BitcoinFee { return BigInteger.valueOf(plan.fee / bytePrice) } - private fun getMinimumByteFee(chain: Chain) = when (chain) { - Chain.Litecoin -> BigInteger("5") - Chain.Doge -> BigInteger("1000") - else -> BigInteger.ONE - } - - private fun getFeePriority(chain: Chain, speed: TxSpeed) = when (chain) { - Chain.Litecoin -> when (speed) { - Fast -> 1 - Normal -> 3 - Slow -> 6 - } - Chain.Doge -> when (speed) { - Fast -> 2 - Normal -> 4 - Slow -> 8 - } - else -> when (speed) { - Fast -> 1 - Normal -> 3 - Slow -> 6 + companion object { + fun getMinimumByteFee(chain: Chain) = when (chain.toBitcoinChain()) { + BitcoinChain.Litecoin -> BigInteger("5") + BitcoinChain.Doge -> BigInteger("1000") + BitcoinChain.Bitcoin -> BigInteger.ONE } - }.toString() + + fun getFeePriority(chain: Chain, speed: TxSpeed) = when (chain.toBitcoinChain()) { + BitcoinChain.Litecoin -> when (speed) { + Fast -> 1 + Normal -> 3 + Slow -> 6 + } + BitcoinChain.Doge -> when (speed) { + Fast -> 2 + Normal -> 4 + Slow -> 8 + } + BitcoinChain.Bitcoin -> when (speed) { + Fast -> 1 + Normal -> 3 + Slow -> 6 + } + }.toString() + } } \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinNodeStatusClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinNodeStatusClient.kt index fcd397c03..d329d8a1f 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinNodeStatusClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinNodeStatusClient.kt @@ -1,6 +1,9 @@ package com.gemwallet.android.blockchain.clients.bitcoin import com.gemwallet.android.blockchain.clients.NodeStatusClient +import com.gemwallet.android.blockchain.clients.bitcoin.services.BitcoinNodeStatusService +import com.gemwallet.android.blockchain.clients.bitcoin.services.getBlock +import com.gemwallet.android.blockchain.clients.bitcoin.services.getNodeInfo import com.gemwallet.android.blockchain.rpc.getLatency import com.gemwallet.android.model.NodeStatus import com.wallet.core.primitives.Chain @@ -10,12 +13,12 @@ import kotlinx.coroutines.withContext class BitcoinNodeStatusClient( private val chain: Chain, - private val rpcClient: BitcoinRpcClient + private val nodeStatusService: BitcoinNodeStatusService, ) : NodeStatusClient { override suspend fun getNodeStatus(chain: Chain, url: String): NodeStatus? = withContext(Dispatchers.IO) { - val nodeInfoJob = async { rpcClient.getNodeInfo(url).getOrNull() } - val chainIdJob = async { rpcClient.getBlock(url) } + val nodeInfoJob = async { nodeStatusService.getNodeInfo(url).getOrNull() } + val chainIdJob = async { nodeStatusService.getBlock(url) } val nodeInfo = nodeInfoJob.await() ?: return@withContext null val chainId = chainIdJob.await() diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinRpcClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinRpcClient.kt deleted file mode 100644 index 84317e718..000000000 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinRpcClient.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.gemwallet.android.blockchain.clients.bitcoin - -import com.wallet.core.blockchain.bitcoin.models.BitcoinAccount -import com.wallet.core.blockchain.bitcoin.models.BitcoinBlock -import com.wallet.core.blockchain.bitcoin.models.BitcoinFeeResult -import com.wallet.core.blockchain.bitcoin.models.BitcoinNodeInfo -import com.wallet.core.blockchain.bitcoin.models.BitcoinTransaction -import com.wallet.core.blockchain.bitcoin.models.BitcoinTransactionBroacastResult -import com.wallet.core.blockchain.bitcoin.models.BitcoinUTXO -import okhttp3.RequestBody -import retrofit2.Response -import retrofit2.http.Body -import retrofit2.http.GET -import retrofit2.http.POST -import retrofit2.http.Path -import retrofit2.http.Url - -interface BitcoinRpcClient { - @GET("/api/v2/address/{address}") - suspend fun getBalance(@Path("address") address: String): Result - - @GET("/api/v2/utxo/{address}") - suspend fun getUTXO(@Path("address") address: String): Result> - - @GET("/api/v2/estimatefee/{priority}") - suspend fun estimateFee(@Path("priority") priority: String): Result - - @POST("/api/v2/sendtx/") - suspend fun broadcast(@Body body: RequestBody): Result - - @GET("/api/v2/tx/{txId}") - suspend fun transaction(@Path("txId") txId: String): Result - - @GET//("/api/v2/") - suspend fun nodeInfo(@Url url: String): Result - - @GET//("/api/v2/block/{block}") - suspend fun block(@Url url: String): Response -} - -suspend fun BitcoinRpcClient.getBlock(url: String): Response { - return block("$url/api/v2/block/1") -} - -suspend fun BitcoinRpcClient.getNodeInfo(url: String): Result { - return nodeInfo("$url/api/v2") -} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinSignClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinSignClient.kt index 04b587082..a8fa19a4c 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinSignClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinSignClient.kt @@ -27,7 +27,7 @@ class BitcoinSignClient( txSpeed: TxSpeed, privateKey: ByteArray ): ByteArray { - val metadata = params.info as BitcoinSignerPreloader.Info + val metadata = params.chainData as BitcoinSignerPreloader.BitcoinChainData val coinType = WCChainTypeProxy().invoke(chain) val gasFee = metadata.fee(txSpeed) as GasFee val input = params.input @@ -37,16 +37,16 @@ class BitcoinSignClient( this.amount = params.finalAmount.toLong() this.byteFee = gasFee.maxGasPrice.toLong() this.toAddress = params.input.destination()?.address - this.changeAddress = params.owner + this.changeAddress = params.input.from.address this.useMaxAmount = params.input.isMax() when (input) { is ConfirmParams.SwapParams -> this.outputOpReturn = ByteString.copyFrom(input.swapData.toByteArray()) else -> {} } this.addPrivateKey(ByteString.copyFrom(privateKey)) - this.addAllUtxo(metadata.utxo.getUtxoTransactions(params.owner, coinType)) + this.addAllUtxo(metadata.utxo.getUtxoTransactions(params.input.from.address, coinType)) metadata.utxo.forEach { - val redeemScript = BitcoinScript.lockScriptForAddress(params.owner, coinType) + val redeemScript = BitcoinScript.lockScriptForAddress(params.input.from.address, coinType) val scriptData = redeemScript.data() if (coinType == CoinType.BITCOIN || scriptData?.isNotEmpty() == true) { return@forEach diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinSignerPreloader.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinSignerPreloader.kt index 2073489fb..0e2583d8c 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinSignerPreloader.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinSignerPreloader.kt @@ -1,40 +1,38 @@ package com.gemwallet.android.blockchain.clients.bitcoin -import com.gemwallet.android.blockchain.clients.SignerPreload +import com.gemwallet.android.blockchain.clients.NativeTransferPreloader +import com.gemwallet.android.blockchain.clients.bitcoin.services.BitcoinFeeService +import com.gemwallet.android.blockchain.clients.bitcoin.services.BitcoinUTXOService +import com.gemwallet.android.model.ChainSignData import com.gemwallet.android.model.ConfirmParams import com.gemwallet.android.model.Fee -import com.gemwallet.android.model.SignerInputInfo import com.gemwallet.android.model.SignerParams import com.gemwallet.android.model.TxSpeed import com.wallet.core.blockchain.bitcoin.models.BitcoinUTXO -import com.wallet.core.primitives.Account import com.wallet.core.primitives.Chain +import java.lang.Exception class BitcoinSignerPreloader( private val chain: Chain, - private val rpcClient: BitcoinRpcClient, -) : SignerPreload { + private val utxoService: BitcoinUTXOService, + feeService: BitcoinFeeService, +) : NativeTransferPreloader { - override suspend fun invoke( - owner: Account, - params: ConfirmParams, - ): Result { - return rpcClient.getUTXO(owner.extendedPublicKey!!).mapCatching { - val fee = BitcoinFee().invoke(rpcClient, it, owner, params.destination()?.address!!, params.amount) - SignerParams( - input = params, - owner = owner.address, - info = Info(it, fee) - ) - } + private val feeCalculator = BitcoinFeeCalculator(feeService) + + override suspend fun preloadNativeTransfer(params: ConfirmParams.TransferParams.Native): SignerParams { + val extPubKey = params.from.extendedPublicKey ?: throw IllegalArgumentException("No extended public key") + val utxo = utxoService.getUTXO(extPubKey).getOrNull() ?: throw Exception("Can't load UTXO") + val fee = feeCalculator.calculate(utxo, params.from, params.destination().address, params.amount) + return SignerParams(params, BitcoinChainData(utxo, fee)) } override fun supported(chain: Chain): Boolean = this.chain == chain - data class Info( + data class BitcoinChainData( val utxo: List, val fee: List, - ) : SignerInputInfo { + ) : ChainSignData { override fun fee(speed: TxSpeed): Fee = fee.firstOrNull { it.speed == speed } ?: fee.first() override fun allFee(): List = fee diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinTransactionStatusClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinTransactionStatusClient.kt index d8d797e42..3dc5ee5b4 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinTransactionStatusClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/BitcoinTransactionStatusClient.kt @@ -1,13 +1,15 @@ package com.gemwallet.android.blockchain.clients.bitcoin import com.gemwallet.android.blockchain.clients.TransactionStatusClient +import com.gemwallet.android.blockchain.clients.bitcoin.services.BitcoinRpcClient +import com.gemwallet.android.blockchain.clients.bitcoin.services.BitcoinTransactionsService import com.gemwallet.android.model.TransactionChages import com.wallet.core.primitives.Chain import com.wallet.core.primitives.TransactionState class BitcoinTransactionStatusClient( private val chain: Chain, - private val rpcClient: BitcoinRpcClient + private val rpcClient: BitcoinTransactionsService ) : TransactionStatusClient { override suspend fun getStatus(chain: Chain, owner: String, txId: String): Result { diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/services/BitcoinBalancesService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/services/BitcoinBalancesService.kt new file mode 100644 index 000000000..5b061079d --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/services/BitcoinBalancesService.kt @@ -0,0 +1,10 @@ +package com.gemwallet.android.blockchain.clients.bitcoin.services + +import com.wallet.core.blockchain.bitcoin.models.BitcoinAccount +import retrofit2.http.GET +import retrofit2.http.Path + +interface BitcoinBalancesService { + @GET("/api/v2/address/{address}") + suspend fun balance(@Path("address") address: String): Result +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/services/BitcoinBroadcastService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/services/BitcoinBroadcastService.kt new file mode 100644 index 000000000..ac2dced48 --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/services/BitcoinBroadcastService.kt @@ -0,0 +1,11 @@ +package com.gemwallet.android.blockchain.clients.bitcoin.services + +import com.wallet.core.blockchain.bitcoin.models.BitcoinTransactionBroacastResult +import okhttp3.RequestBody +import retrofit2.http.Body +import retrofit2.http.POST + +interface BitcoinBroadcastService { + @POST("/api/v2/sendtx/") + suspend fun broadcast(@Body body: RequestBody): Result +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/services/BitcoinFeeService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/services/BitcoinFeeService.kt new file mode 100644 index 000000000..f9c2ff80b --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/services/BitcoinFeeService.kt @@ -0,0 +1,10 @@ +package com.gemwallet.android.blockchain.clients.bitcoin.services + +import com.wallet.core.blockchain.bitcoin.models.BitcoinFeeResult +import retrofit2.http.GET +import retrofit2.http.Path + +interface BitcoinFeeService { + @GET("/api/v2/estimatefee/{priority}") + suspend fun estimateFee(@Path("priority") priority: String): Result +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/services/BitcoinNodeStatusService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/services/BitcoinNodeStatusService.kt new file mode 100644 index 000000000..dc9ce5b80 --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/services/BitcoinNodeStatusService.kt @@ -0,0 +1,15 @@ +package com.gemwallet.android.blockchain.clients.bitcoin.services + +import com.wallet.core.blockchain.bitcoin.models.BitcoinBlock +import com.wallet.core.blockchain.bitcoin.models.BitcoinNodeInfo +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Url + +interface BitcoinNodeStatusService { + @GET//("/api/v2/") + suspend fun nodeInfo(@Url url: String): Result + + @GET//("/api/v2/block/{block}") + suspend fun block(@Url url: String): Response +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/services/BitcoinRpcClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/services/BitcoinRpcClient.kt new file mode 100644 index 000000000..daa06ac12 --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/services/BitcoinRpcClient.kt @@ -0,0 +1,21 @@ +package com.gemwallet.android.blockchain.clients.bitcoin.services + +import com.wallet.core.blockchain.bitcoin.models.BitcoinBlock +import com.wallet.core.blockchain.bitcoin.models.BitcoinNodeInfo +import retrofit2.Response + +interface BitcoinRpcClient : + BitcoinBalancesService, + BitcoinUTXOService, + BitcoinFeeService, + BitcoinBroadcastService, + BitcoinTransactionsService, + BitcoinNodeStatusService + +suspend fun BitcoinNodeStatusService.getBlock(url: String): Response { + return block("$url/api/v2/block/1") +} + +suspend fun BitcoinNodeStatusService.getNodeInfo(url: String): Result { + return nodeInfo("$url/api/v2") +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/services/BitcoinTransactionsService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/services/BitcoinTransactionsService.kt new file mode 100644 index 000000000..15c771417 --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/services/BitcoinTransactionsService.kt @@ -0,0 +1,10 @@ +package com.gemwallet.android.blockchain.clients.bitcoin.services + +import com.wallet.core.blockchain.bitcoin.models.BitcoinTransaction +import retrofit2.http.GET +import retrofit2.http.Path + +interface BitcoinTransactionsService { + @GET("/api/v2/tx/{txId}") + suspend fun transaction(@Path("txId") txId: String): Result +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/services/BitcoinUTXOService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/services/BitcoinUTXOService.kt new file mode 100644 index 000000000..59d9490bf --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/bitcoin/services/BitcoinUTXOService.kt @@ -0,0 +1,10 @@ +package com.gemwallet.android.blockchain.clients.bitcoin.services + +import com.wallet.core.blockchain.bitcoin.models.BitcoinUTXO +import retrofit2.http.GET +import retrofit2.http.Path + +interface BitcoinUTXOService { + @GET("/api/v2/utxo/{address}") + suspend fun getUTXO(@Path("address") address: String): Result> +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosBalanceClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosBalanceClient.kt index db5968755..8b2551ed0 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosBalanceClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosBalanceClient.kt @@ -1,6 +1,8 @@ package com.gemwallet.android.blockchain.clients.cosmos import com.gemwallet.android.blockchain.clients.BalanceClient +import com.gemwallet.android.blockchain.clients.cosmos.services.CosmosBalancesService +import com.gemwallet.android.blockchain.clients.cosmos.services.CosmosStakeService import com.gemwallet.android.ext.asset import com.gemwallet.android.model.AssetBalance import com.wallet.core.primitives.Asset @@ -13,13 +15,14 @@ import java.math.BigInteger class CosmosBalanceClient( private val chain: Chain, - private val rpcClient: CosmosRpcClient, + private val balancesService: CosmosBalancesService, + private val stakeService: CosmosStakeService, ) : BalanceClient { override suspend fun getNativeBalance(chain: Chain, address: String): AssetBalance? = withContext(Dispatchers.IO) { val denom = CosmosDenom.from(chain) - val getBalances = async { rpcClient.getBalance(address).getOrNull()?.balances } + val getBalances = async { balancesService.getBalance(address).getOrNull()?.balances } val balance = getBalances.await() ?.filter { it.denom == denom } ?.map { it.amount.toBigDecimal().toBigInteger() } @@ -30,9 +33,9 @@ class CosmosBalanceClient( AssetBalance.create(chain.asset(), available = balance.toString()) } else -> { - val getDelegations = async { rpcClient.delegations(address).getOrNull()?.delegation_responses } - val getUnboundingDelegations = async { rpcClient.undelegations(address).getOrNull()?.unbonding_responses } - val getRewards = async { rpcClient.rewards(address).getOrNull()?.rewards } + val getDelegations = async { stakeService.delegations(address).getOrNull()?.delegation_responses } + val getUnboundingDelegations = async { stakeService.undelegations(address).getOrNull()?.unbonding_responses } + val getRewards = async { stakeService.rewards(address).getOrNull()?.rewards } val delegations = getDelegations.await() ?.filter { it.balance.denom == denom } @@ -61,7 +64,7 @@ class CosmosBalanceClient( override suspend fun getTokenBalances(chain: Chain, address: String, tokens: List): List { val balances = try { - rpcClient.getBalance(address).getOrNull() ?: return emptyList() + balancesService.getBalance(address).getOrNull() ?: return emptyList() } catch (_: Throwable) { return emptyList() } diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosBroadcastClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosBroadcastClient.kt index 791296fab..758fd7e39 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosBroadcastClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosBroadcastClient.kt @@ -2,6 +2,7 @@ package com.gemwallet.android.blockchain.clients.cosmos import com.gemwallet.android.blockchain.Mime import com.gemwallet.android.blockchain.clients.BroadcastClient +import com.gemwallet.android.blockchain.clients.cosmos.services.CosmosBroadcastService import com.wallet.core.primitives.Account import com.wallet.core.primitives.Chain import com.wallet.core.primitives.TransactionType @@ -9,13 +10,13 @@ import okhttp3.RequestBody.Companion.toRequestBody class CosmosBroadcastClient( private val chain: Chain, - private val client: CosmosRpcClient, + private val broadcastService: CosmosBroadcastService, ) : BroadcastClient { override suspend fun send(account: Account, signedMessage: ByteArray, type: TransactionType): Result { val requestData = signedMessage.toString(Charsets.UTF_8) val requestBody = requestData.toRequestBody(Mime.Json.value) - return client.broadcast(requestBody).mapCatching { + return broadcastService.broadcast(requestBody).mapCatching { if (it.tx_response.code != 0) { throw IllegalStateException(it.tx_response.raw_log) } diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosFee.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosFeeCalculator.kt similarity index 92% rename from blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosFee.kt rename to blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosFeeCalculator.kt index 20a7359bc..684696e53 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosFee.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosFeeCalculator.kt @@ -8,10 +8,10 @@ import com.wallet.core.primitives.Chain import com.wallet.core.primitives.TransactionType import java.math.BigInteger -class CosmosFee( - private val txType: TransactionType, +class CosmosFeeCalculator( + private val chain: Chain, ) { - operator fun invoke(chain: Chain): Fee { + fun calculate(txType: TransactionType): Fee { val assetId = AssetId(chain) val maxGasFee = when (chain) { Chain.Cosmos -> when (txType) { @@ -37,7 +37,7 @@ class CosmosFee( else -> BigInteger.valueOf(200_000L) } Chain.Noble -> BigInteger.valueOf(25_000) - else -> throw IllegalArgumentException() + else -> throw IllegalArgumentException("Unsupported chain") } val limit = when (txType) { TransactionType.Transfer, TransactionType.Swap -> BigInteger.valueOf(200_000L) diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosNodeStatusClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosNodeStatusClient.kt index cdaf3c9de..1c3e55fd2 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosNodeStatusClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosNodeStatusClient.kt @@ -1,6 +1,7 @@ package com.gemwallet.android.blockchain.clients.cosmos import com.gemwallet.android.blockchain.clients.NodeStatusClient +import com.gemwallet.android.blockchain.clients.cosmos.services.CosmosNodeStatusService import com.gemwallet.android.blockchain.rpc.getLatency import com.gemwallet.android.model.NodeStatus import com.wallet.core.primitives.Chain @@ -10,11 +11,11 @@ import kotlinx.coroutines.withContext class CosmosNodeStatusClient( private val chain: Chain, - private val rpcClient: CosmosRpcClient + private val nodeStatusService: CosmosNodeStatusService, ) : NodeStatusClient { override suspend fun getNodeStatus(chain: Chain, url: String): NodeStatus? = withContext(Dispatchers.IO) { - val inSyncJob = async { rpcClient.syncing("$url/cosmos/base/tendermint/v1beta1/syncing") } //.getOrNull()?.syncing == false } - val nodeInfoJob = async { rpcClient.getNodeInfo("$url/cosmos/base/tendermint/v1beta1/blocks/latest").getOrNull()?.block?.header } + val inSyncJob = async { nodeStatusService.syncing("$url/cosmos/base/tendermint/v1beta1/syncing") } + val nodeInfoJob = async { nodeStatusService.getNodeInfo("$url/cosmos/base/tendermint/v1beta1/blocks/latest").getOrNull()?.block?.header } val inSync = inSyncJob.await() val nodeInfo = nodeInfoJob.await() ?: return@withContext null diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosRpcClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosRpcClient.kt deleted file mode 100644 index 193c40595..000000000 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosRpcClient.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.gemwallet.android.blockchain.clients.cosmos - -import com.wallet.core.blockchain.cosmos.models.CosmosAccount -import com.wallet.core.blockchain.cosmos.models.CosmosAccountResponse -import com.wallet.core.blockchain.cosmos.models.CosmosBalances -import com.wallet.core.blockchain.cosmos.models.CosmosBlockResponse -import com.wallet.core.blockchain.cosmos.models.CosmosBroadcastResponse -import com.wallet.core.blockchain.cosmos.models.CosmosDelegations -import com.wallet.core.blockchain.cosmos.models.CosmosInjectiveAccount -import com.wallet.core.blockchain.cosmos.models.CosmosRewards -import com.wallet.core.blockchain.cosmos.models.CosmosSyncing -import com.wallet.core.blockchain.cosmos.models.CosmosTransactionResponse -import com.wallet.core.blockchain.cosmos.models.CosmosUnboundingDelegations -import com.wallet.core.blockchain.cosmos.models.CosmosValidators -import okhttp3.RequestBody -import retrofit2.Response -import retrofit2.http.Body -import retrofit2.http.GET -import retrofit2.http.POST -import retrofit2.http.Path -import retrofit2.http.Url - -interface CosmosRpcClient { - @GET("/cosmos/bank/v1beta1/balances/{owner}") - suspend fun getBalance(@Path("owner") owner: String): Result - - @GET("/cosmos/auth/v1beta1/accounts/{owner}") - suspend fun getAccountData(@Path("owner") owner: String): Result> - - @GET("/cosmos/auth/v1beta1/accounts/{owner}") - suspend fun getInjectiveAccountData(@Path("owner") owner: String): Result> - - @GET("/cosmos/base/tendermint/v1beta1/blocks/latest") - suspend fun getNodeInfo(): Result - - @POST("/cosmos/tx/v1beta1/txs") - suspend fun broadcast(@Body body: RequestBody): Result - - @GET("/cosmos/tx/v1beta1/txs/{txId}") - suspend fun transaction(@Path("txId") txId: String): Result - - @GET("/cosmos/staking/v1beta1/validators?pagination.limit=1000") - suspend fun validators(): Result - - @GET("/cosmos/staking/v1beta1/delegations/{address}") - suspend fun delegations(@Path("address") address: String): Result - - @GET("/cosmos/staking/v1beta1/delegators/{address}/unbonding_delegations") - suspend fun undelegations(@Path("address") address: String): Result - - @GET("/cosmos/distribution/v1beta1/delegators/{address}/rewards") - suspend fun rewards(@Path("address") address: String): Result - - @GET//("/cosmos/base/tendermint/v1beta1/syncing") - suspend fun syncing(@Url url: String): Response - - @GET//("/cosmos/base/tendermint/v1beta1/blocks/latest") - suspend fun getNodeInfo(@Url url: String): Result - -} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosSignClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosSignClient.kt index 17b9496c7..f258a985f 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosSignClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosSignClient.kt @@ -32,7 +32,7 @@ class CosmosSignClient( txSpeed: TxSpeed, privateKey: ByteArray, ): ByteArray { - val from = params.owner + val from = params.input.from.address val coin = WCChainTypeProxy().invoke(chain) val input = params.input val denom = if (input.assetId.type() == AssetSubtype.NATIVE) CosmosDenom.from(chain) else input.assetId.tokenId!! @@ -43,15 +43,15 @@ class CosmosSignClient( coin = coin, amount = getAmount(params.finalAmount, denom = denom) ) - is ConfirmParams.DelegateParams -> getStakeMessage(from, input.validatorId, getAmount(params.finalAmount, denom)) - is ConfirmParams.RedeleateParams -> getRedelegateMessage( + is ConfirmParams.Stake.DelegateParams -> getStakeMessage(from, input.validatorId, getAmount(params.finalAmount, denom)) + is ConfirmParams.Stake.RedelegateParams -> getRedelegateMessage( delegatorAddress = from, validatorSrcAddress = input.srcValidatorId, validatorDstAddress = input.dstValidatorId, amount = getAmount(params.input.amount, denom), ) - is ConfirmParams.RewardsParams -> getRewardsMessage(from, input.validatorsId) - is ConfirmParams.UndelegateParams -> getUnstakeMessage(from, input.validatorId, getAmount(params.input.amount, denom)) + is ConfirmParams.Stake.RewardsParams -> getRewardsMessage(from, input.validatorsId) + is ConfirmParams.Stake.UndelegateParams -> getUnstakeMessage(from, input.validatorId, getAmount(params.input.amount, denom)) is ConfirmParams.SwapParams -> when (chain) { Chain.Thorchain -> listOf(getThorChainSwapMessage(params, coin)) else -> getTransferMessage( @@ -62,7 +62,7 @@ class CosmosSignClient( ) } is ConfirmParams.TokenApprovalParams, - is ConfirmParams.WithdrawParams -> throw IllegalArgumentException() + is ConfirmParams.Stake.WithdrawParams -> throw IllegalArgumentException() } return sign(params, privateKey, message) } @@ -83,7 +83,7 @@ class CosmosSignClient( } ) this.memo = swapParams.swapData - this.signer = ByteString.copyFrom(AnyAddress(params.owner, coinType).data()) + this.signer = ByteString.copyFrom(AnyAddress(params.input.from.address, coinType).data()) } ) }.build() @@ -150,7 +150,7 @@ class CosmosSignClient( return listOf(message) } - suspend fun getRewardsMessage(delegatorAddress: String, validators: List): List { + fun getRewardsMessage(delegatorAddress: String, validators: List): List { return validators.map { validator -> Message.newBuilder().apply { withdrawStakeRewardMessage = WithdrawDelegationReward.newBuilder().apply { @@ -197,7 +197,7 @@ class CosmosSignClient( } private fun sign(input: SignerParams, privateKey: ByteArray, messages: List): ByteArray { - val meta = input.info as CosmosSignerPreloader.Info + val meta = input.chainData as CosmosSignerPreloader.CosmosChainData val fee = meta.fee() as GasFee val feeAmount = fee.amount val gas = fee.limit.toLong() * messages.size diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosSignerPreloader.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosSignerPreloader.kt index 8d8049732..f4b5e3a97 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosSignerPreloader.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosSignerPreloader.kt @@ -1,12 +1,15 @@ package com.gemwallet.android.blockchain.clients.cosmos -import com.gemwallet.android.blockchain.clients.SignerPreload +import com.gemwallet.android.blockchain.clients.NativeTransferPreloader +import com.gemwallet.android.blockchain.clients.StakeTransactionPreloader +import com.gemwallet.android.blockchain.clients.SwapTransactionPreloader +import com.gemwallet.android.blockchain.clients.TokenTransferPreloader +import com.gemwallet.android.blockchain.clients.cosmos.services.CosmosAccountsService +import com.gemwallet.android.model.ChainSignData import com.gemwallet.android.model.ConfirmParams import com.gemwallet.android.model.Fee -import com.gemwallet.android.model.SignerInputInfo import com.gemwallet.android.model.SignerParams import com.gemwallet.android.model.TxSpeed -import com.wallet.core.primitives.Account import com.wallet.core.primitives.Chain import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -14,53 +17,59 @@ import kotlinx.coroutines.withContext class CosmosSignerPreloader( private val chain: Chain, - private val rpcClient: CosmosRpcClient, -) : SignerPreload { - override suspend fun invoke( - owner: Account, - params: ConfirmParams, - ): Result = withContext(Dispatchers.IO) { + private val accountsService: CosmosAccountsService, +) : NativeTransferPreloader, TokenTransferPreloader, StakeTransactionPreloader, SwapTransactionPreloader { + + private val feeCalculator = CosmosFeeCalculator(chain) + + override suspend fun preloadTokenTransfer(params: ConfirmParams.TransferParams.Token): SignerParams { + return preload(params) + } + + override suspend fun preloadNativeTransfer(params: ConfirmParams.TransferParams.Native): SignerParams { + return preload(params) + } + override suspend fun preloadStake(params: ConfirmParams.Stake): SignerParams { + return preload(params) + } + + override suspend fun preloadSwap(params: ConfirmParams.SwapParams): SignerParams { + return preload(params) + } + + override fun supported(chain: Chain): Boolean = this.chain == chain + + private suspend fun preload(params: ConfirmParams) = withContext(Dispatchers.IO) { val accountJob = async { - if (chain == Chain.Injective) { - rpcClient.getInjectiveAccountData(owner.address).getOrNull()?.account?.base_account - } else { - rpcClient.getAccountData(owner.address).getOrNull()?.account + when (chain) { + Chain.Injective -> accountsService.getInjectiveAccountData(params.from.address).getOrNull()?.account?.base_account + else -> accountsService.getAccountData(params.from.address).getOrNull()?.account } } - val nodeInfoJob = async { rpcClient.getNodeInfo() } - val feeJob = async { CosmosFee(txType = params.getTxType()).invoke(chain) } + val nodeInfoJob = async { accountsService.getNodeInfo().getOrNull()?.block?.header?.chain_id } + val fee = feeCalculator.calculate(params.getTxType()) - val (account, nodeInfo, fee) = Triple( - accountJob.await(), - nodeInfoJob.await().getOrNull(), - feeJob.await() + val (account, nodeInfo) = Pair( + accountJob.await() ?: throw Exception("Can't get data (account) for sign"), + nodeInfoJob.await() ?: throw Exception("Can't get data (node info) for sign") ) - if (account != null && nodeInfo != null) { - Result.success( - SignerParams( - input = params, - owner = owner.address, - info = Info( - chainId = nodeInfo.block.header.chain_id, - accountNumber = account.account_number.toLong(), - sequence = account.sequence.toLong(), - fee = fee, - ) - ) + SignerParams( + input = params, + chainData = CosmosChainData( + chainId = nodeInfo, + accountNumber = account.account_number.toLong(), + sequence = account.sequence.toLong(), + fee = fee, ) - } else { - Result.failure(Exception("Can't get data for sign")) - } + ) } - override fun supported(chain: Chain): Boolean = this.chain == chain - - data class Info( + data class CosmosChainData( val chainId: String, val accountNumber: Long, val sequence: Long, val fee: Fee, - ) : SignerInputInfo { + ) : ChainSignData { override fun fee(speed: TxSpeed): Fee = fee } } \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosStakeClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosStakeClient.kt index 8282e6044..c13647cd1 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosStakeClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosStakeClient.kt @@ -2,6 +2,7 @@ package com.gemwallet.android.blockchain.clients.cosmos import android.annotation.SuppressLint import com.gemwallet.android.blockchain.clients.StakeClient +import com.gemwallet.android.blockchain.clients.cosmos.services.CosmosStakeService import com.wallet.core.primitives.AssetId import com.wallet.core.primitives.Chain import com.wallet.core.primitives.CosmosDenom @@ -16,14 +17,14 @@ import java.text.SimpleDateFormat class CosmosStakeClient( private val chain: Chain, - private val rpcClient: CosmosRpcClient, + private val stakeService: CosmosStakeService, ) : StakeClient { @SuppressLint("SimpleDateFormat") private val completionDateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX") override suspend fun getValidators(chain: Chain, apr: Double): List { - return rpcClient.validators().getOrNull()?.validators?.map { + return stakeService.validators().getOrNull()?.validators?.map { val commission = it.commission.commission_rates.rate.toDouble() val isActive = !it.jailed && it.status == "BOND_STATUS_BONDED" DelegationValidator( @@ -38,10 +39,10 @@ class CosmosStakeClient( } override suspend fun getStakeDelegations(chain: Chain, address: String, apr: Double): List = withContext(Dispatchers.IO) { - val getDelegations = async { rpcClient.delegations(address).getOrNull()?.delegation_responses } - val getUnboundingDelegations = async { rpcClient.undelegations(address).getOrNull()?.unbonding_responses } + val getDelegations = async { stakeService.delegations(address).getOrNull()?.delegation_responses } + val getUnboundingDelegations = async { stakeService.undelegations(address).getOrNull()?.unbonding_responses } val getRewards = async { - rpcClient.rewards(address).getOrNull()?.rewards + stakeService.rewards(address).getOrNull()?.rewards ?.associateBy { it.validator_address } ?.mapValues { entry -> entry.value.reward diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosTransactionStatusClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosTransactionStatusClient.kt index eb02be06c..853cb290e 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosTransactionStatusClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/CosmosTransactionStatusClient.kt @@ -1,16 +1,17 @@ package com.gemwallet.android.blockchain.clients.cosmos import com.gemwallet.android.blockchain.clients.TransactionStatusClient +import com.gemwallet.android.blockchain.clients.cosmos.services.CosmosTransactionsService import com.gemwallet.android.model.TransactionChages import com.wallet.core.primitives.Chain import com.wallet.core.primitives.TransactionState class CosmosTransactionStatusClient( private val chain: Chain, - private val rpcClient: CosmosRpcClient, + private val transactionsService: CosmosTransactionsService, ) : TransactionStatusClient { override suspend fun getStatus(chain: Chain, owner: String, txId: String): Result { - return rpcClient.transaction(txId).mapCatching { + return transactionsService.transaction(txId).mapCatching { TransactionChages( if (it.tx_response == null || it.tx_response.txhash.isEmpty()) { TransactionState.Pending diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/services/CosmosAccountsService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/services/CosmosAccountsService.kt new file mode 100644 index 000000000..0cd5a278e --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/services/CosmosAccountsService.kt @@ -0,0 +1,19 @@ +package com.gemwallet.android.blockchain.clients.cosmos.services + +import com.wallet.core.blockchain.cosmos.models.CosmosAccount +import com.wallet.core.blockchain.cosmos.models.CosmosAccountResponse +import com.wallet.core.blockchain.cosmos.models.CosmosBlockResponse +import com.wallet.core.blockchain.cosmos.models.CosmosInjectiveAccount +import retrofit2.http.GET +import retrofit2.http.Path + +interface CosmosAccountsService { + @GET("/cosmos/auth/v1beta1/accounts/{owner}") + suspend fun getAccountData(@Path("owner") owner: String): Result> + + @GET("/cosmos/auth/v1beta1/accounts/{owner}") + suspend fun getInjectiveAccountData(@Path("owner") owner: String): Result> + + @GET("/cosmos/base/tendermint/v1beta1/blocks/latest") + suspend fun getNodeInfo(): Result +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/services/CosmosBalancesService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/services/CosmosBalancesService.kt new file mode 100644 index 000000000..be0991cc3 --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/services/CosmosBalancesService.kt @@ -0,0 +1,10 @@ +package com.gemwallet.android.blockchain.clients.cosmos.services + +import com.wallet.core.blockchain.cosmos.models.CosmosBalances +import retrofit2.http.GET +import retrofit2.http.Path + +interface CosmosBalancesService { + @GET("/cosmos/bank/v1beta1/balances/{owner}") + suspend fun getBalance(@Path("owner") owner: String): Result +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/services/CosmosBroadcastService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/services/CosmosBroadcastService.kt new file mode 100644 index 000000000..4683e1d5e --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/services/CosmosBroadcastService.kt @@ -0,0 +1,11 @@ +package com.gemwallet.android.blockchain.clients.cosmos.services + +import com.wallet.core.blockchain.cosmos.models.CosmosBroadcastResponse +import okhttp3.RequestBody +import retrofit2.http.Body +import retrofit2.http.POST + +interface CosmosBroadcastService { + @POST("/cosmos/tx/v1beta1/txs") + suspend fun broadcast(@Body body: RequestBody): Result +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/services/CosmosNodeStatusService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/services/CosmosNodeStatusService.kt new file mode 100644 index 000000000..b18a9284b --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/services/CosmosNodeStatusService.kt @@ -0,0 +1,15 @@ +package com.gemwallet.android.blockchain.clients.cosmos.services + +import com.wallet.core.blockchain.cosmos.models.CosmosBlockResponse +import com.wallet.core.blockchain.cosmos.models.CosmosSyncing +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Url + +interface CosmosNodeStatusService { + @GET//("/cosmos/base/tendermint/v1beta1/syncing") + suspend fun syncing(@Url url: String): Response + + @GET//("/cosmos/base/tendermint/v1beta1/blocks/latest") + suspend fun getNodeInfo(@Url url: String): Result +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/services/CosmosRpcClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/services/CosmosRpcClient.kt new file mode 100644 index 000000000..81914b9d9 --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/services/CosmosRpcClient.kt @@ -0,0 +1,9 @@ +package com.gemwallet.android.blockchain.clients.cosmos.services + +interface CosmosRpcClient : + CosmosBalancesService, + CosmosAccountsService, + CosmosStakeService, + CosmosTransactionsService, + CosmosBroadcastService, + CosmosNodeStatusService \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/services/CosmosStakeService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/services/CosmosStakeService.kt new file mode 100644 index 000000000..6e290823a --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/services/CosmosStakeService.kt @@ -0,0 +1,23 @@ +package com.gemwallet.android.blockchain.clients.cosmos.services + +import com.wallet.core.blockchain.cosmos.models.CosmosDelegations +import com.wallet.core.blockchain.cosmos.models.CosmosRewards +import com.wallet.core.blockchain.cosmos.models.CosmosUnboundingDelegations +import com.wallet.core.blockchain.cosmos.models.CosmosValidators +import retrofit2.http.GET +import retrofit2.http.Path + +interface CosmosStakeService { + + @GET("/cosmos/staking/v1beta1/validators?pagination.limit=1000") + suspend fun validators(): Result + + @GET("/cosmos/staking/v1beta1/delegations/{address}") + suspend fun delegations(@Path("address") address: String): Result + + @GET("/cosmos/staking/v1beta1/delegators/{address}/unbonding_delegations") + suspend fun undelegations(@Path("address") address: String): Result + + @GET("/cosmos/distribution/v1beta1/delegators/{address}/rewards") + suspend fun rewards(@Path("address") address: String): Result +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/services/CosmosTransactionsService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/services/CosmosTransactionsService.kt new file mode 100644 index 000000000..9c08fb23d --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/cosmos/services/CosmosTransactionsService.kt @@ -0,0 +1,10 @@ +package com.gemwallet.android.blockchain.clients.cosmos.services + +import com.wallet.core.blockchain.cosmos.models.CosmosTransactionResponse +import retrofit2.http.GET +import retrofit2.http.Path + +interface CosmosTransactionsService { + @GET("/cosmos/tx/v1beta1/txs/{txId}") + suspend fun transaction(@Path("txId") txId: String): Result +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmBalanceClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmBalanceClient.kt index d3da75147..d13d1ce7e 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmBalanceClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmBalanceClient.kt @@ -1,6 +1,9 @@ package com.gemwallet.android.blockchain.clients.ethereum import com.gemwallet.android.blockchain.clients.BalanceClient +import com.gemwallet.android.blockchain.clients.ethereum.services.EvmBalancesService +import com.gemwallet.android.blockchain.clients.ethereum.services.EvmCallService +import com.gemwallet.android.blockchain.clients.ethereum.services.getBalance import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest import com.gemwallet.android.ext.asset import com.gemwallet.android.model.AssetBalance @@ -9,15 +12,18 @@ import com.wallet.core.primitives.Chain class EvmBalanceClient( private val chain: Chain, - private val rpcClient: EvmRpcClient, + private val callService: EvmCallService, + private val balancesService: EvmBalancesService, + private val smartChainStakeClient: SmartchainStakeClient, ) : BalanceClient { override suspend fun getNativeBalance(chain: Chain, address: String): AssetBalance? { - val available = rpcClient.getBalance(address) - .fold({ AssetBalance.create(chain.asset(), available = it.result?.value?.toString() ?: return null) }) { null } + val availableValue = balancesService.getBalance(address) + .getOrNull()?.result?.value?.toString() + return when (chain) { - Chain.SmartChain -> SmartchainStakeClient(chain, rpcClient).getBalance(address, availableBalance = available) - else -> available + Chain.SmartChain -> smartChainStakeClient.getBalance(address, availableValue) + else -> AssetBalance.create(chain.asset(), available = availableValue ?: return null) } } @@ -33,7 +39,7 @@ class EvmBalanceClient( "to" to contract, "data" to data, ) - val balance = rpcClient.callNumber(JSONRpcRequest.create(EvmMethod.Call, listOf(params, "latest"))) + val balance = callService.callNumber(JSONRpcRequest.create(EvmMethod.Call, listOf(params, "latest"))) .getOrNull()?.result?.value ?: continue result.add(AssetBalance.create(token, available = balance.toString())) } diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmBroadcastClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmBroadcastClient.kt index d8a24a635..ad7f424fb 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmBroadcastClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmBroadcastClient.kt @@ -1,6 +1,7 @@ package com.gemwallet.android.blockchain.clients.ethereum import com.gemwallet.android.blockchain.clients.BroadcastClient +import com.gemwallet.android.blockchain.clients.ethereum.services.EvmBroadcastService import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest import com.gemwallet.android.math.toHexString import com.wallet.core.primitives.Account @@ -9,11 +10,11 @@ import com.wallet.core.primitives.TransactionType class EvmBroadcastClient( private val chain: Chain, - private val client: EvmRpcClient, + private val broadcastService: EvmBroadcastService, ) : BroadcastClient { override suspend fun send(account: Account, signedMessage: ByteArray, type: TransactionType): Result { val request = JSONRpcRequest.create(EvmMethod.Broadcast, listOf(signedMessage.toHexString())) - return client.broadcast(request).mapCatching { + return broadcastService.broadcast(request).mapCatching { if (it.error != null) throw Exception(it.error.message) else it.result } } diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmChainExt.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmChainExt.kt index 4b2cb4e22..d721568c4 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmChainExt.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmChainExt.kt @@ -7,6 +7,7 @@ import com.wallet.core.primitives.AssetId import com.wallet.core.primitives.AssetSubtype import com.wallet.core.primitives.Chain import com.wallet.core.primitives.EVMChain +import uniffi.gemstone.Config import wallet.core.jni.AnyAddress import wallet.core.jni.EthereumAbi import wallet.core.jni.EthereumAbiFunction @@ -39,11 +40,6 @@ fun EVMChain.Companion.getDestinationAddress( } } -fun EVMChain.Companion.isOpStack(chain: Chain): Boolean { - return when (chain) { - Chain.Optimism, - Chain.Base, - Chain.OpBNB -> true - else -> false - } +fun EVMChain.isOpStack(): Boolean { + return Config().getEvmChainConfig(string).isOpstack } \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmFee.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmFee.kt deleted file mode 100644 index ac344dfd7..000000000 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmFee.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.gemwallet.android.blockchain.clients.ethereum - -import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest -import com.gemwallet.android.ext.type -import com.gemwallet.android.math.hexToBigInteger -import com.gemwallet.android.model.ConfirmParams -import com.gemwallet.android.model.Fee -import com.gemwallet.android.model.GasFee -import com.gemwallet.android.model.TxSpeed -import com.wallet.core.primitives.AssetId -import com.wallet.core.primitives.AssetSubtype -import com.wallet.core.primitives.Chain -import com.wallet.core.primitives.EVMChain -import com.wallet.core.primitives.TransactionType -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import uniffi.gemstone.Config -import wallet.core.jni.CoinType -import java.math.BigInteger - -class EvmFee { - suspend operator fun invoke( - rpcClient: EvmRpcClient, - params: ConfirmParams, - chainId: BigInteger, - nonce: BigInteger, - gasLimit: BigInteger, - coinType: CoinType, - ): Fee = withContext(Dispatchers.IO) { - val assetId = params.assetId - val isMaxAmount = params.isMax() - val feeAssetId = AssetId(assetId.chain) - - if (EVMChain.isOpStack(params.assetId.chain)) { - return@withContext OptimismGasOracle().invoke( - params = params, - chainId = chainId, - nonce = nonce, - gasLimit = gasLimit, - coin = coinType, - rpcClient = rpcClient, - ) - } - val (baseFee, priorityFee) = getBasePriorityFee(chain = assetId.chain, rpcClient = rpcClient) - - val maxGasPrice = baseFee.plus(priorityFee) - val minerFee = when (params.getTxType()) { - TransactionType.Transfer -> if (assetId.type() == AssetSubtype.NATIVE && isMaxAmount) maxGasPrice else priorityFee - TransactionType.StakeUndelegate, - TransactionType.StakeWithdraw, - TransactionType.StakeRedelegate, - TransactionType.StakeDelegate, - TransactionType.Swap, - TransactionType.TokenApproval -> priorityFee - else -> throw IllegalAccessException("Operation doesn't available") - } - GasFee(feeAssetId = feeAssetId, speed = TxSpeed.Normal, limit = gasLimit, maxGasPrice = maxGasPrice, minerFee = minerFee) - } - - companion object { - internal suspend fun getBasePriorityFee( - chain: Chain, - rpcClient: EvmRpcClient - ): Pair { - val feeHistory = rpcClient.getFeeHistory( - JSONRpcRequest.create(EvmMethod.GetFeeHistory, listOf("10", "latest", listOf(25))) - ).getOrNull()?.result ?: throw Exception("Unable to calculate base fee") - val reward = feeHistory.reward - .mapNotNull { it.firstOrNull() } - .mapNotNull { it.hexToBigInteger() } - .maxByOrNull { it } - ?: throw Exception("Unable to calculate priority fee") - val baseFee = - feeHistory.baseFeePerGas.mapNotNull { it.hexToBigInteger() }.maxByOrNull { it } - ?: throw Exception("Unable to calculate base fee") - val defaultPriorityFee = - BigInteger(Config().getEvmChainConfig(chain.string).minPriorityFee.toString()) - val priorityFee = if (reward < defaultPriorityFee) defaultPriorityFee else reward - return Pair(baseFee, priorityFee) - } - } -} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmFeeCalculator.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmFeeCalculator.kt new file mode 100644 index 000000000..d104105da --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmFeeCalculator.kt @@ -0,0 +1,120 @@ +package com.gemwallet.android.blockchain.clients.ethereum + +import com.gemwallet.android.blockchain.clients.ethereum.services.EvmCallService +import com.gemwallet.android.blockchain.clients.ethereum.services.EvmFeeService +import com.gemwallet.android.blockchain.clients.ethereum.services.getFeeHistory +import com.gemwallet.android.blockchain.clients.ethereum.services.getGasLimit +import com.gemwallet.android.ext.toEVM +import com.gemwallet.android.ext.type +import com.gemwallet.android.math.hexToBigInteger +import com.gemwallet.android.math.toHexString +import com.gemwallet.android.model.ConfirmParams +import com.gemwallet.android.model.Fee +import com.gemwallet.android.model.GasFee +import com.gemwallet.android.model.TxSpeed +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.AssetSubtype +import com.wallet.core.primitives.Chain +import com.wallet.core.primitives.EVMChain +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.withContext +import uniffi.gemstone.Config +import wallet.core.jni.CoinType +import java.math.BigDecimal +import java.math.BigInteger +import java.util.Locale + +class EvmFeeCalculator( + private val feeService: EvmFeeService, + callService: EvmCallService, + coinType: CoinType +) { + + private val optimismGasOracle = OptimismGasOracle(callService, coinType) + + private val nativeGasLimit = BigInteger.valueOf(21_000L) + + suspend fun calculate( + params: ConfirmParams, + assetId: AssetId, + recipient: String, + outputAmount: BigInteger, + payload: String?, + chainId: String, + nonce: BigInteger, + ): Fee = withContext(Dispatchers.IO) { + val getGasLimit = async { getGasLimit(assetId, params.from.address, recipient, outputAmount, payload) } + val getBasePriorityFee = async { getBasePriorityFee(params.assetId.chain, feeService) } + val gasLimit = getGasLimit.await() + val (baseFee, priorityFee) = getBasePriorityFee.await() + + if (params.assetId.chain.toEVM()?.isOpStack() == true) { + return@withContext optimismGasOracle.estimate( + params = params, + chainId = chainId, + nonce = nonce, + gasLimit = gasLimit, + baseFee = baseFee, + priorityFee = priorityFee, + ) + } + + val maxGasPrice = baseFee.plus(priorityFee) + val minerFee = when (params) { + is ConfirmParams.Stake, + is ConfirmParams.SwapParams, + is ConfirmParams.TokenApprovalParams -> priorityFee + is ConfirmParams.TransferParams -> if (params.assetId.type() == AssetSubtype.NATIVE && params.isMax()) { + maxGasPrice + } else { + priorityFee + } + } + GasFee( + feeAssetId = AssetId(params.assetId.chain), + speed = TxSpeed.Normal, + limit = gasLimit, + maxGasPrice = maxGasPrice, + minerFee = minerFee, + ) + } + + private suspend fun getGasLimit( + assetId: AssetId, + from: String, + recipient: String, + outputAmount: BigInteger, + payload: String?, + ): BigInteger { + val (amount, to, data) = when (assetId.type()) { + AssetSubtype.NATIVE -> Triple(outputAmount, recipient, payload) + AssetSubtype.TOKEN -> Triple( + BigInteger.ZERO, // Amount + assetId.tokenId!!.lowercase(Locale.ROOT), // token + EVMChain.encodeTransactionData(assetId, payload, outputAmount, recipient).toHexString() + ) + } + + val gasLimit = feeService.getGasLimit(from, to, amount, data) + return if (gasLimit == nativeGasLimit) { + gasLimit + } else { + gasLimit.add(gasLimit.toBigDecimal().multiply(BigDecimal.valueOf(0.5)).toBigInteger()) + } + } + + internal suspend fun getBasePriorityFee(chain: Chain, feeService: EvmFeeService): Pair { + val feeHistory = feeService.getFeeHistory() ?: throw Exception("Unable to calculate base fee") + + val reward = feeHistory.reward.mapNotNull { it.firstOrNull()?.hexToBigInteger() }.maxOrNull() + ?: throw Exception("Unable to calculate priority fee") + + val baseFee = feeHistory.baseFeePerGas.mapNotNull { it.hexToBigInteger() }.maxOrNull() + ?: throw Exception("Unable to calculate base fee") + + val defaultPriorityFee = BigInteger(Config().getEvmChainConfig(chain.string).minPriorityFee.toString()) + val priorityFee = if (reward < defaultPriorityFee) defaultPriorityFee else reward + return Pair(baseFee, priorityFee) + } +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmGetTokenClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmGetTokenClient.kt index d3be67bce..44ee24985 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmGetTokenClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmGetTokenClient.kt @@ -1,6 +1,7 @@ package com.gemwallet.android.blockchain.clients.ethereum import com.gemwallet.android.blockchain.clients.GetTokenClient +import com.gemwallet.android.blockchain.clients.ethereum.services.EvmCallService import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest import com.gemwallet.android.math.decodeHex import com.gemwallet.android.math.has0xPrefix @@ -19,7 +20,7 @@ import java.math.BigInteger class EvmGetTokenClient( private val chain: Chain, - private val rpcClient: EvmRpcClient, + private val callService: EvmCallService, ) : GetTokenClient { override suspend fun getTokenData(tokenId: String): Asset? = withContext(Dispatchers.IO) { val getNameJob = async { getERC20Name(tokenId) } @@ -56,7 +57,7 @@ class EvmGetTokenClient( "latest", ), ) - return rpcClient.callNumber(request).getOrNull()?.result?.value + return callService.callNumber(request).getOrNull()?.result?.value } private suspend fun getERC20Name(contract: String): String? { @@ -72,7 +73,7 @@ class EvmGetTokenClient( "latest" ) ) - val response = rpcClient.callString(request).getOrNull()?.result ?: return null + val response = callService.callString(request).getOrNull()?.result ?: return null return decodeAbi(response) } @@ -89,7 +90,7 @@ class EvmGetTokenClient( "latest" ) ) - val response = rpcClient.callString(request).getOrNull()?.result ?: return null + val response = callService.callString(request).getOrNull()?.result ?: return null return decodeAbi(response) } diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmMethod.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmMethod.kt index 3ef58644c..676b41804 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmMethod.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmMethod.kt @@ -5,16 +5,12 @@ import com.gemwallet.android.blockchain.rpc.model.JSONRpcMethod enum class EvmMethod(val value: String) : JSONRpcMethod { GetBalance("eth_getBalance"), GetGasLimit("eth_estimateGas"), - GetGasPrice("eth_gasPrice"), GetFeeHistory("eth_feeHistory"), GetChainId("eth_chainId"), - GetNetVersion("eth_chainId"), GetNonce("eth_getTransactionCount"), Broadcast("eth_sendRawTransaction"), Call("eth_call"), GetTransaction("eth_getTransactionReceipt"), - GetTransactionByHash("eth_getTransactionByHash"), - GetMaxPriorityFeePerGas("eth_maxPriorityFeePerGas"), Sync("eth_syncing"), GetBlockNumber("eth_blockNumber"), ; diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmNodeStatusClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmNodeStatusClient.kt index 0c3e64ea5..c61a4d94a 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmNodeStatusClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmNodeStatusClient.kt @@ -1,6 +1,10 @@ package com.gemwallet.android.blockchain.clients.ethereum import com.gemwallet.android.blockchain.clients.NodeStatusClient +import com.gemwallet.android.blockchain.clients.ethereum.services.EvmNodeStatusService +import com.gemwallet.android.blockchain.clients.ethereum.services.getChainId +import com.gemwallet.android.blockchain.clients.ethereum.services.latestBlock +import com.gemwallet.android.blockchain.clients.ethereum.services.sync import com.gemwallet.android.blockchain.rpc.getLatency import com.gemwallet.android.model.NodeStatus import com.wallet.core.primitives.Chain @@ -10,13 +14,13 @@ import kotlinx.coroutines.withContext class EvmNodeStatusClient( private val chain: Chain, - private val rpcClient: EvmRpcClient, + private val nodeStatusService: EvmNodeStatusService, ) : NodeStatusClient { override suspend fun getNodeStatus(chain: Chain, url: String): NodeStatus? = withContext(Dispatchers.IO) { - val getChainId = async { rpcClient.getChainId(url) } - val getLatestBlock = async { rpcClient.latestBlock(url).getOrNull()?.result?.value?.toString() } - val getSync = async { rpcClient.sync(url).getOrNull()?.result } + val getChainId = async { nodeStatusService.getChainId(url) } + val getLatestBlock = async { nodeStatusService.latestBlock(url).getOrNull()?.result?.value?.toString() } + val getSync = async { nodeStatusService.sync(url).getOrNull()?.result } val chainId = getChainId.await() val blockNumber = getLatestBlock.await() ?: return@withContext null diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmRpcClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmRpcClient.kt deleted file mode 100644 index 340481793..000000000 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmRpcClient.kt +++ /dev/null @@ -1,164 +0,0 @@ -package com.gemwallet.android.blockchain.clients.ethereum - -import com.gemwallet.android.blockchain.clients.ethereum.EvmRpcClient.EvmNumber -import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest -import com.gemwallet.android.blockchain.rpc.model.JSONRpcResponse -import com.gemwallet.android.math.decodeHex -import com.gemwallet.android.math.hexToBigInteger -import com.google.gson.JsonDeserializationContext -import com.google.gson.JsonDeserializer -import com.google.gson.JsonElement -import com.wallet.core.blockchain.ethereum.models.EthereumFeeHistory -import com.wallet.core.blockchain.ethereum.models.EthereumTransactionReciept -import retrofit2.Response -import retrofit2.http.Body -import retrofit2.http.POST -import retrofit2.http.Url -import wallet.core.jni.EthereumAbiValue -import java.lang.reflect.Type -import java.math.BigInteger - -interface EvmRpcClient { - @POST("/") - suspend fun getBalance(@Body request: JSONRpcRequest>): Result> - - @POST("/") - suspend fun getFeeHistory(@Body request: JSONRpcRequest>): Result> - - @POST("/") - suspend fun getGasLimit(@Body request: JSONRpcRequest>): Result> - - @POST("/") - suspend fun getNetVersion(@Body request: JSONRpcRequest>): Result> - - @POST("/") - suspend fun getNonce(@Body request: JSONRpcRequest>): Result> - - @POST("/") - suspend fun broadcast(@Body request: JSONRpcRequest>): Result> - - @POST("/") - suspend fun callNumber(@Body request: JSONRpcRequest>): Result> - - @POST("/") - suspend fun callString(@Body request: JSONRpcRequest>): Result> - - @POST("/") - suspend fun getTransactionByHash(@Body request: JSONRpcRequest>): Result> - - @POST("/") - suspend fun transaction(@Body request: JSONRpcRequest>): Result> - - @POST - suspend fun chainId(@Url url: String, @Body request: JSONRpcRequest>): Response> - - @POST - suspend fun sync(@Url url: String, @Body request: JSONRpcRequest>): Result> - - @POST - suspend fun latestBlock(@Url url: String, @Body request: JSONRpcRequest>): Result> - - class EvmNumber( - val value: BigInteger?, - ) - - class EvmCallResult( - val value: T? - ) - - class TokenBalance( - val value: BigInteger?, - ) - - class Transaction( - val from: String, - val to: String, - val value: String?, - val data: String?, - ) - - class ButchItem( - val from: String, - val to: String, - val data: String, - ) - - class AllowanceCall( - val from: String, - val to: String, - val data: String, - ) - - class EthereumTransactionByHash( - val blockNumber: String, - ) - - class BalanceDeserializer : JsonDeserializer { - override fun deserialize( - json: JsonElement?, - typeOfT: Type?, - context: JsonDeserializationContext? - ): EvmNumber { - return EvmNumber( - try { - json?.asString?.hexToBigInteger() - } catch (err: Throwable) { - null - } - ) - } - } - - class TokenBalanceDeserializer : JsonDeserializer { - override fun deserialize( - json: JsonElement?, - typeOfT: Type?, - context: JsonDeserializationContext? - ): TokenBalance { - return TokenBalance( - try { - EthereumAbiValue.decodeUInt256(json?.asString?.decodeHex()).toBigIntegerOrNull() - } catch (err: Throwable) { - null - } - ) - } - - } -} - -internal suspend fun EvmRpcClient.getBalance(address: String): Result> { - return getBalance( - JSONRpcRequest.create( - method = EvmMethod.GetBalance, - params = listOf(address, "latest") - ) - ) -} - -internal suspend fun EvmRpcClient.callString(contract: String, hexData: String): String? { - val params = mapOf( - "to" to contract, - "data" to hexData - ) - val request = JSONRpcRequest.create( - EvmMethod.Call, - listOf( - params, - "latest" - ) - ) - return callString(request).getOrNull()?.result -} - -internal suspend fun EvmRpcClient.getChainId(url: String): Response> { - return chainId(url, JSONRpcRequest.create(EvmMethod.GetChainId, emptyList())) -} - -internal suspend fun EvmRpcClient.latestBlock(url: String): Result> { - return latestBlock(url, JSONRpcRequest.create(EvmMethod.GetBlockNumber, emptyList())) -} - -internal suspend fun EvmRpcClient.sync(url: String): Result> { - return sync(url, JSONRpcRequest.create(EvmMethod.Sync, emptyList())) -} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmSignClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmSignClient.kt index a3d2f46f4..9173725b8 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmSignClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmSignClient.kt @@ -44,11 +44,11 @@ class EvmSignClient( privateKey: ByteArray, ): ByteArray { when (params.input) { - is ConfirmParams.RedeleateParams, - is ConfirmParams.DelegateParams, - is ConfirmParams.UndelegateParams, - is ConfirmParams.RewardsParams, - is ConfirmParams.WithdrawParams -> when (params.input.assetId.chain) { + is ConfirmParams.Stake.RedelegateParams, + is ConfirmParams.Stake.DelegateParams, + is ConfirmParams.Stake.UndelegateParams, + is ConfirmParams.Stake.RewardsParams, + is ConfirmParams.Stake.WithdrawParams -> when (params.input.assetId.chain) { Chain.SmartChain -> { return stakeSmartchain(params, privateKey) } @@ -56,8 +56,7 @@ class EvmSignClient( } else -> {} } - val meta = params.info as EvmSignerPreloader.Info - val fee = meta.fee() as? GasFee ?: throw IllegalArgumentException() + val meta = params.chainData as EvmSignerPreloader.EvmChainData val coinType = WCChainTypeProxy().invoke(chain) val input = params.input val amount = when (input) { @@ -76,8 +75,8 @@ class EvmSignClient( }, amount = amount, tokenAmount = params.finalAmount, - fee = fee, - chainId = meta.chainId, + fee = meta.gasGee(), + chainId = meta.chainId.toBigInteger(), nonce = meta.nonce, destinationAddress = params.input.destination()?.address ?: "", memo = when (input) { @@ -93,13 +92,13 @@ class EvmSignClient( } private fun stakeSmartchain(params: SignerParams, privateKey: ByteArray): ByteArray { - val meta = params.info as EvmSignerPreloader.Info + val meta = params.chainData as EvmSignerPreloader.EvmChainData val fee = meta.fee as? GasFee ?: throw IllegalArgumentException() val valueData = when (params.input) { - is ConfirmParams.DelegateParams -> params.finalAmount.toByteArray() + is ConfirmParams.Stake.DelegateParams -> params.finalAmount.toByteArray() else -> BigInteger.ZERO.toByteArray() } - val callData = StakeHub().encodeStake(params.input) + val callData = StakeHub.encodeStake(params.input) val signInput = Ethereum.SigningInput.newBuilder().apply { when (chain.eip1559Support()) { true -> { diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmSignerPreloader.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmSignerPreloader.kt index f3e59c8fb..a3f95566b 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmSignerPreloader.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmSignerPreloader.kt @@ -1,180 +1,94 @@ package com.gemwallet.android.blockchain.clients.ethereum -import android.util.Log -import com.gemwallet.android.blockchain.clients.SignerPreload +import com.gemwallet.android.blockchain.clients.ApprovalTransactionPreloader +import com.gemwallet.android.blockchain.clients.NativeTransferPreloader +import com.gemwallet.android.blockchain.clients.StakeTransactionPreloader +import com.gemwallet.android.blockchain.clients.SwapTransactionPreloader +import com.gemwallet.android.blockchain.clients.TokenTransferPreloader +import com.gemwallet.android.blockchain.clients.ethereum.services.EvmCallService +import com.gemwallet.android.blockchain.clients.ethereum.services.EvmFeeService +import com.gemwallet.android.blockchain.clients.ethereum.services.getNonce import com.gemwallet.android.blockchain.operators.walletcore.WCChainTypeProxy -import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest -import com.gemwallet.android.ext.type -import com.gemwallet.android.math.append0x -import com.gemwallet.android.math.decodeHex -import com.gemwallet.android.math.toHexString +import com.gemwallet.android.ext.getNetworkId +import com.gemwallet.android.model.ChainSignData import com.gemwallet.android.model.ConfirmParams import com.gemwallet.android.model.Fee -import com.gemwallet.android.model.SignerInputInfo import com.gemwallet.android.model.SignerParams import com.gemwallet.android.model.TxSpeed import com.wallet.core.primitives.Account import com.wallet.core.primitives.AssetId -import com.wallet.core.primitives.AssetSubtype import com.wallet.core.primitives.Chain -import com.wallet.core.primitives.EVMChain -import com.wallet.core.primitives.TransactionType import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async import kotlinx.coroutines.withContext -import java.math.BigDecimal import java.math.BigInteger -import java.util.Locale class EvmSignerPreloader( private val chain: Chain, - private val rpcClient: EvmRpcClient, -) : SignerPreload { - override suspend fun invoke(owner: Account, params: ConfirmParams): Result = withContext(Dispatchers.IO) { - val assetId = if (params.getTxType() == TransactionType.Swap) { - AssetId(params.assetId.chain) - } else { - params.assetId - } - val coinType = WCChainTypeProxy().invoke(chain) - val chainIdJob = async { - try { - rpcClient.getNetVersion(JSONRpcRequest.create(EvmMethod.GetNetVersion, emptyList())) - .fold({ it.result?.value }) { null } ?: BigInteger(coinType.chainId()) - } catch (err: Throwable) { - Log.d("ERROR", "Err: ", err) - BigInteger(coinType.chainId()) - } - } - val nonceJob = async { - try { - val nonceParams = listOf(owner.address, "latest") - rpcClient.getNonce(JSONRpcRequest.create(EvmMethod.GetNonce, nonceParams)) - .fold({ it.result?.value }) { null } ?: BigInteger.ZERO - } catch (err: Throwable) { - Log.d("ERROR", "Err: ", err) - BigInteger.ZERO - } - } - val gasLimitJob = async { - try { - getGasLimit( - assetId = assetId, - rpcClient = rpcClient, - from = owner.address, - recipient = when (params) { - is ConfirmParams.SwapParams -> params.to - is ConfirmParams.TokenApprovalParams -> params.contract - is ConfirmParams.TransferParams -> params.destination().address - is ConfirmParams.RedeleateParams, - is ConfirmParams.WithdrawParams, - is ConfirmParams.UndelegateParams, - is ConfirmParams.RewardsParams, - is ConfirmParams.DelegateParams -> StakeHub.address + private val feeService: EvmFeeService, + callService: EvmCallService, +) : NativeTransferPreloader, TokenTransferPreloader, SwapTransactionPreloader, StakeTransactionPreloader, ApprovalTransactionPreloader { - else -> throw IllegalArgumentException() - }, - outputAmount = when (params) { - is ConfirmParams.SwapParams -> BigInteger(params.value) - is ConfirmParams.TokenApprovalParams -> BigInteger.ZERO - is ConfirmParams.TransferParams, - is ConfirmParams.DelegateParams -> params.amount + private val wcCoinType = WCChainTypeProxy().invoke(chain) + private val feeCalculator = EvmFeeCalculator(feeService, callService, wcCoinType) - is ConfirmParams.RedeleateParams, - is ConfirmParams.WithdrawParams, - is ConfirmParams.UndelegateParams -> BigInteger.ZERO + override suspend fun preloadNativeTransfer(params: ConfirmParams.TransferParams.Native): SignerParams = + preload(params.assetId, params.from, params.destination().address, params.amount, params.memo, params) - else -> throw IllegalArgumentException() - }, - payload = when (params) { - is ConfirmParams.SwapParams -> params.swapData - is ConfirmParams.TokenApprovalParams -> params.data - is ConfirmParams.RedeleateParams, - is ConfirmParams.WithdrawParams, - is ConfirmParams.UndelegateParams, - is ConfirmParams.DelegateParams -> when (params.assetId.chain) { - Chain.SmartChain -> StakeHub().encodeStake(params) - else -> throw IllegalArgumentException() - } + override suspend fun preloadTokenTransfer(params: ConfirmParams.TransferParams.Token): SignerParams = + preload(params.assetId, params.from, params.destination().address, params.amount, params.memo, params) - else -> params.memo() - }, - ) - } catch (err: Throwable) { - throw err - } - } - val chainId = chainIdJob.await() - val nonce = nonceJob.await() - val gasLimit = gasLimitJob.await() - val fee = try { - EvmFee().invoke( - rpcClient, - params, - chainId, - nonce, - gasLimit, - WCChainTypeProxy().invoke(chain) - ) - } catch (err: Throwable) { - return@withContext Result.failure(err) - } - Result.success( - SignerParams( - input = params, - owner = owner.address, - info = Info(chainId, nonce, fee), - ) + override suspend fun preloadStake(params: ConfirmParams.Stake): SignerParams = + preload( + assetId = params.assetId, + from = params.from, + recipient = StakeHub.address, + outputAmount = when (params) { + is ConfirmParams.Stake.DelegateParams -> params.amount + else -> BigInteger.ZERO + }, + payload = when (params.assetId.chain) { + Chain.SmartChain -> StakeHub.encodeStake(params) + else -> throw IllegalArgumentException("Stake doesn't suppoted for chain ${params.assetId.chain} ") + }, + params = params, ) - } - override fun supported(chain: Chain): Boolean = this.chain == chain + override suspend fun preloadSwap(params: ConfirmParams.SwapParams): SignerParams = + preload(AssetId(params.assetId.chain), params.from, params.to, params.value.toBigInteger(), params.swapData, params) + + override suspend fun preloadApproval(params: ConfirmParams.TokenApprovalParams): SignerParams = + preload(params.assetId, params.from, params.contract, BigInteger.ZERO, params.data, params) - private suspend fun getGasLimit( + private suspend fun preload( assetId: AssetId, - rpcClient: EvmRpcClient, - from: String, + from: Account, recipient: String, outputAmount: BigInteger, payload: String?, - ): BigInteger { - val (amount, to, data) = when (assetId.type()) { - AssetSubtype.NATIVE -> Triple(outputAmount, recipient, payload) - AssetSubtype.TOKEN -> Triple( - BigInteger.ZERO, // Amount - assetId.tokenId!!.lowercase(Locale.ROOT), // token - EVMChain.encodeTransactionData(assetId, payload, outputAmount, recipient) - .toHexString() - ) - else -> throw IllegalArgumentException() - } - val transaction = EvmRpcClient.Transaction( - from = from, - to = to, - value = "0x${amount.toString(16)}", - data = if (data.isNullOrEmpty()) "0x" else data.append0x(), + params: ConfirmParams, + ) = withContext(Dispatchers.IO) { + val nonce = feeService.getNonce(from.address) + val chainId = chain.getNetworkId() + val fee = feeCalculator.calculate( + params = params, + assetId = assetId, + recipient = recipient, + outputAmount = outputAmount, + payload = payload, + chainId = chainId, + nonce = nonce ) - val request = JSONRpcRequest.create(EvmMethod.GetGasLimit, listOf(transaction)) - val gasLimitResult = rpcClient.getGasLimit(request) - val gasLimit = gasLimitResult.fold({ it.result?.value ?: BigInteger.ZERO}) { - BigInteger.ZERO - } - return if (gasLimit == BigInteger.valueOf(21_000L)) { - gasLimit - } else { - gasLimit.add( - gasLimit.toBigDecimal().multiply( - BigDecimal.valueOf(0.5) - ).toBigInteger() - ) - } + + SignerParams(params, EvmChainData(chainId, nonce, fee)) } - data class Info( - val chainId: BigInteger, + override fun supported(chain: Chain): Boolean = this.chain == chain + + data class EvmChainData( + val chainId: String, val nonce: BigInteger, val fee: Fee, - ) : SignerInputInfo { + ) : ChainSignData { override fun fee(speed: TxSpeed): Fee = fee } } \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmTransactionStatusClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmTransactionStatusClient.kt index b0839decf..2a4678a1c 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmTransactionStatusClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/EvmTransactionStatusClient.kt @@ -1,6 +1,7 @@ package com.gemwallet.android.blockchain.clients.ethereum import com.gemwallet.android.blockchain.clients.TransactionStatusClient +import com.gemwallet.android.blockchain.clients.ethereum.services.EvmTransactionsService import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest import com.gemwallet.android.ext.eip1559Support import com.gemwallet.android.math.hexToBigInteger @@ -11,14 +12,15 @@ import java.math.BigInteger class EvmTransactionStatusClient( private val chain: Chain, - private val rpcClient: EvmRpcClient, + private val transactionsService: EvmTransactionsService, ) : TransactionStatusClient { + override suspend fun getStatus(chain: Chain, owner: String, txId: String): Result { return Result.success(getStatus(txId)) } private suspend fun getStatus(txId: String): TransactionChages { - return rpcClient.transaction(JSONRpcRequest.create(EvmMethod.GetTransaction, listOf(txId))) + return transactionsService.transaction(JSONRpcRequest.create(EvmMethod.GetTransaction, listOf(txId))) .fold( { if (it.result?.status != "0x0" && it.result?.status != "0x1") { diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/OptimismGasOracle.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/OptimismGasOracle.kt index 81bf14d08..094ca24fa 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/OptimismGasOracle.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/OptimismGasOracle.kt @@ -1,5 +1,6 @@ package com.gemwallet.android.blockchain.clients.ethereum +import com.gemwallet.android.blockchain.clients.ethereum.services.EvmCallService import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest import com.gemwallet.android.ext.type import com.gemwallet.android.math.toHexString @@ -10,7 +11,6 @@ import com.wallet.core.primitives.AssetId import com.wallet.core.primitives.AssetSubtype import com.wallet.core.primitives.TransactionType import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async import kotlinx.coroutines.withContext import wallet.core.java.AnySigner import wallet.core.jni.CoinType @@ -20,20 +20,21 @@ import wallet.core.jni.PrivateKey import wallet.core.jni.proto.Ethereum import java.math.BigInteger -class OptimismGasOracle { +class OptimismGasOracle( + private val callService: EvmCallService, + private val coinType: CoinType, +) { - suspend operator fun invoke( + suspend fun estimate( params: ConfirmParams, - chainId: BigInteger, + chainId: String, nonce: BigInteger, + baseFee: BigInteger, + priorityFee: BigInteger, gasLimit: BigInteger, - coin: CoinType, - rpcClient: EvmRpcClient ): GasFee = withContext(Dispatchers.IO) { val assetId = params.assetId val feeAssetId = AssetId(assetId.chain) - val basePriorityFee = async { EvmFee.getBasePriorityFee(assetId.chain, rpcClient) } - val (baseFee, priorityFee) = basePriorityFee.await() val gasPrice = baseFee + priorityFee val minerFee = when (params.getTxType()) { TransactionType.Transfer -> if (assetId.type() == AssetSubtype.NATIVE && params.isMax()) gasPrice else priorityFee @@ -47,7 +48,7 @@ class OptimismGasOracle { } val encoded = encode( assetId = assetId, - coin = coin, + coin = coinType, amount = amount, destinationAddress = when (params) { is ConfirmParams.SwapParams -> params.to @@ -71,7 +72,7 @@ class OptimismGasOracle { ), ) val l2Fee = gasPrice * gasLimit - val l1Fee = getL1Fee(encoded, rpcClient) ?: throw IllegalStateException("Can't get L1 Fee") + val l1Fee = getL1Fee(encoded) ?: throw IllegalStateException("Can't get L1 Fee") GasFee( feeAssetId = feeAssetId, speed = TxSpeed.Normal, @@ -82,9 +83,7 @@ class OptimismGasOracle { ) } - class BaseFeeRequest(val to: String, val data: String) - - private suspend fun getL1Fee(data: ByteArray, rpcClient: EvmRpcClient): BigInteger? { + private suspend fun getL1Fee(data: ByteArray): BigInteger? { val abiFn = EthereumAbiFunction("getL1Fee").apply { this.addParamBytes(data, false) } @@ -92,13 +91,14 @@ class OptimismGasOracle { val request = JSONRpcRequest.create( EvmMethod.Call, listOf( - BaseFeeRequest( - to = "0x420000000000000000000000000000000000000F", encodedFn.toHexString(), + mapOf( + "to" to "0x420000000000000000000000000000000000000F", + "data" to encodedFn.toHexString(), ), "latest", ) ) - return rpcClient.callNumber(request).getOrNull()?.result?.value + return callService.callNumber(request).getOrNull()?.result?.value } private fun encode( @@ -107,7 +107,7 @@ class OptimismGasOracle { destinationAddress: String, amount: BigInteger, meta: String?, - chainId: BigInteger, + chainId: String, nonce: BigInteger, gasFee: GasFee, ): ByteArray { @@ -116,7 +116,7 @@ class OptimismGasOracle { amount = amount, tokenAmount = amount, fee = gasFee, - chainId = chainId, + chainId = chainId.toBigInteger(), nonce = nonce, destinationAddress = destinationAddress, memo = meta, diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/SmartchainStakeClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/SmartchainStakeClient.kt index a7c6a6a49..da6b18af5 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/SmartchainStakeClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/SmartchainStakeClient.kt @@ -1,6 +1,8 @@ package com.gemwallet.android.blockchain.clients.ethereum import com.gemwallet.android.blockchain.clients.StakeClient +import com.gemwallet.android.blockchain.clients.ethereum.services.EvmCallService +import com.gemwallet.android.blockchain.clients.ethereum.services.callString import com.gemwallet.android.ext.asset import com.gemwallet.android.math.decodeHex import com.gemwallet.android.model.AssetBalance @@ -16,14 +18,14 @@ import java.math.BigInteger class SmartchainStakeClient( private val chain: Chain, - private val evmRpcClient: EvmRpcClient, - private val stakeHub: StakeHub = StakeHub(), + private val callService: EvmCallService, ) : StakeClient { + override suspend fun getValidators(chain: Chain, apr: Double): List { val limit = getMaxElectedValidators() - val data = stakeHub.encodeValidatorsCall(0, limit) - val result = evmRpcClient.callString(StakeHub.reader, data) ?: return emptyList() - val validators = stakeHub.decodeValidatorsReturn(result) + val data = StakeHub.encodeValidatorsCall(0, limit) + val result = callService.callString(StakeHub.reader, data) ?: return emptyList() + val validators = StakeHub.decodeValidatorsReturn(result) return validators } @@ -36,10 +38,8 @@ class SmartchainStakeClient( delegations + undelegations } - suspend fun getBalance(address: String, availableBalance: AssetBalance?): AssetBalance? = withContext(Dispatchers.IO) { - if (availableBalance == null) { - return@withContext null - } + suspend fun getBalance(address: String, availableValue: String?): AssetBalance? = withContext(Dispatchers.IO) { + availableValue ?: return@withContext null val limit = getMaxElectedValidators() val getDelegationCall = async { getDelegations(address, limit) } val getUndelegationsCall = async { getUndelegations(address, limit) } @@ -52,7 +52,7 @@ class SmartchainStakeClient( AssetBalance.create( asset = Chain.SmartChain.asset(), - available = availableBalance.balance.available, + available = availableValue, staked = staked.toString(), pending = pending.toString() ) @@ -61,24 +61,24 @@ class SmartchainStakeClient( override fun supported(chain: Chain): Boolean = this.chain == chain private suspend fun getDelegations(address: String, limit: Int): List { - val data = evmRpcClient.callString(StakeHub.reader, stakeHub.encodeDelegationsCall(address, limit)) - ?: return emptyList() - return stakeHub.decodeDelegationsResult(data) + val dataRequest = StakeHub.encodeDelegationsCall(address, limit) + val data = callService.callString(StakeHub.reader, dataRequest) ?: return emptyList() + return StakeHub.decodeDelegationsResult(data) } private suspend fun getUndelegations(address: String, limit: Int): List { - val data = evmRpcClient.callString(StakeHub.reader, stakeHub.encodeUndelegationsCall(address, limit)) + val data = callService.callString(StakeHub.reader, StakeHub.encodeUndelegationsCall(address, limit)) ?: return emptyList() - return stakeHub.decodeUnelegationsResult(data) + return StakeHub.decodeUnelegationsResult(data) } - private fun List.sumBalances(): BigInteger = - map { it.balance.toBigIntegerOrNull() ?: BigInteger.ZERO } - .fold(BigInteger.ZERO) {acc, value -> acc + value } - private suspend fun getMaxElectedValidators(): Int { - val result = evmRpcClient.callString(StakeHub.address, stakeHub.encodeMaxElectedValidators()) + val result = callService.callString(StakeHub.address, StakeHub.encodeMaxElectedValidators()) ?: throw IllegalStateException("Unable to get validators") return EthereumAbiValue.decodeUInt256(result.decodeHex()).toUShort().toInt() } + + private fun List.sumBalances(): BigInteger = + map { it.balance.toBigIntegerOrNull() ?: BigInteger.ZERO } + .fold(BigInteger.ZERO) {acc, value -> acc + value } } \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/StakeHub.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/StakeHub.kt index 9e8082499..d0edd2639 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/StakeHub.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/StakeHub.kt @@ -11,7 +11,10 @@ import com.wallet.core.primitives.DelegationValidator import uniffi.gemstone.BscDelegation import uniffi.gemstone.BscDelegationStatus -class StakeHub { +object StakeHub { + val offset: UShort = (0).toUShort() + const val address = "0x0000000000000000000000000000000000002002" + const val reader = "0x830295c0abe7358f7e24bc38408095621474280b" fun encodeMaxElectedValidators(): String { // cast calldata "maxElectedValidators()" @@ -20,10 +23,14 @@ class StakeHub { fun encodeStake(params: ConfirmParams): String { return when (params) { - is ConfirmParams.DelegateParams -> encodeDelegateCall(params.validatorId, false) - is ConfirmParams.RedeleateParams -> encodeRedelegateCall(params, false) - is ConfirmParams.UndelegateParams -> encodeUndelegateCall(params) - is ConfirmParams.WithdrawParams -> encodeClaim(params, (0).toULong()) + is ConfirmParams.Stake.DelegateParams -> encodeDelegateCall( + params.validatorId, + false + ) + + is ConfirmParams.Stake.RedelegateParams -> encodeRedelegateCall(params, false) + is ConfirmParams.Stake.UndelegateParams -> encodeUndelegateCall(params) + is ConfirmParams.Stake.WithdrawParams -> encodeClaim(params, (0).toULong()) else -> throw IllegalArgumentException() }.toHexString() } @@ -33,14 +40,15 @@ class StakeHub { } fun encodeValidatorsCall(offset: Int, limit: Int): String { - return uniffi.gemstone.bscEncodeValidatorsCall(offset.toUShort(), limit.toUShort()).toHexString() + return uniffi.gemstone.bscEncodeValidatorsCall(offset.toUShort(), limit.toUShort()) + .toHexString() } fun decodeValidatorsReturn(hexData: String): List { return uniffi.gemstone.bscDecodeValidatorsReturn(hexData.decodeHex()).map { DelegationValidator( chain = Chain.SmartChain, - id = it.operatorAddress, + id = it.operatorAddress, name = it.moniker, isActive = !it.jailed, commision = it.commission.toDouble() / 100, @@ -50,7 +58,8 @@ class StakeHub { } fun encodeDelegationsCall(address: String, limit: Int): String { - return uniffi.gemstone.bscEncodeDelegationsCall(address, offset, limit.toUShort()).toHexString() + return uniffi.gemstone.bscEncodeDelegationsCall(address, offset, limit.toUShort()) + .toHexString() } fun decodeDelegationsResult(data: String): List { @@ -58,20 +67,29 @@ class StakeHub { } fun encodeUndelegationsCall(address: String, limit: Int): String { - return uniffi.gemstone.bscEncodeUndelegationsCall(address, offset, limit.toUShort()).toHexString() + return uniffi.gemstone.bscEncodeUndelegationsCall(address, offset, limit.toUShort()) + .toHexString() } fun decodeUnelegationsResult(data: String): List { return uniffi.gemstone.bscDecodeUndelegationsReturn(data.decodeHex()).map { it.into() } } - fun encodeUndelegateCall(params: ConfirmParams.UndelegateParams): ByteArray { - val amountShare = params.amount * params.share!!.toBigInteger() / params.balance!!.toBigInteger() - return uniffi.gemstone.bscEncodeUndelegateCall(operatorAddress = params.validatorId, shares = amountShare.toString()) + fun encodeUndelegateCall(params: ConfirmParams.Stake.UndelegateParams): ByteArray { + val amountShare = + params.amount * params.share!!.toBigInteger() / params.balance!!.toBigInteger() + return uniffi.gemstone.bscEncodeUndelegateCall( + operatorAddress = params.validatorId, + shares = amountShare.toString() + ) } - fun encodeRedelegateCall(params: ConfirmParams.RedeleateParams, votePower: Boolean): ByteArray { - val amountShare = params.amount * params.share!!.toBigInteger() / params.balance!!.toBigInteger() + fun encodeRedelegateCall( + params: ConfirmParams.Stake.RedelegateParams, + votePower: Boolean + ): ByteArray { + val amountShare = + params.amount * params.share!!.toBigInteger() / params.balance!!.toBigInteger() return uniffi.gemstone.bscEncodeRedelegateCall( srcValidator = params.srcValidatorId, dstValidator = params.dstValidatorId, @@ -80,14 +98,14 @@ class StakeHub { ) } - fun encodeClaim(params: ConfirmParams.WithdrawParams, requestNumber: ULong): ByteArray { - return uniffi.gemstone.bscEncodeClaimCall(operatorAddress = params.validatorId, requestNumber = requestNumber) - } - - companion object { - val offset: UShort = (0).toUShort() - const val address = "0x0000000000000000000000000000000000002002" - const val reader = "0x830295c0abe7358f7e24bc38408095621474280b" + fun encodeClaim( + params: ConfirmParams.Stake.WithdrawParams, + requestNumber: ULong + ): ByteArray { + return uniffi.gemstone.bscEncodeClaimCall( + operatorAddress = params.validatorId, + requestNumber = requestNumber + ) } } diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/services/EvmBalancesService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/services/EvmBalancesService.kt new file mode 100644 index 000000000..856a7c419 --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/services/EvmBalancesService.kt @@ -0,0 +1,22 @@ +package com.gemwallet.android.blockchain.clients.ethereum.services + +import com.gemwallet.android.blockchain.clients.ethereum.EvmMethod +import com.gemwallet.android.blockchain.clients.ethereum.services.EvmRpcClient.EvmNumber +import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest +import com.gemwallet.android.blockchain.rpc.model.JSONRpcResponse +import retrofit2.http.Body +import retrofit2.http.POST + +interface EvmBalancesService { + @POST("/") + suspend fun getBalance(@Body request: JSONRpcRequest>): Result> +} + +suspend fun EvmBalancesService.getBalance(address: String): Result> { + return getBalance( + JSONRpcRequest.create( + method = EvmMethod.GetBalance, + params = listOf(address, "latest") + ) + ) +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/services/EvmBroadcastService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/services/EvmBroadcastService.kt new file mode 100644 index 000000000..62d14fb71 --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/services/EvmBroadcastService.kt @@ -0,0 +1,11 @@ +package com.gemwallet.android.blockchain.clients.ethereum.services + +import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest +import com.gemwallet.android.blockchain.rpc.model.JSONRpcResponse +import retrofit2.http.Body +import retrofit2.http.POST + +interface EvmBroadcastService { + @POST("/") + suspend fun broadcast(@Body request: JSONRpcRequest>): Result> +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/services/EvmCallService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/services/EvmCallService.kt new file mode 100644 index 000000000..6e4606288 --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/services/EvmCallService.kt @@ -0,0 +1,31 @@ +package com.gemwallet.android.blockchain.clients.ethereum.services + +import com.gemwallet.android.blockchain.clients.ethereum.EvmMethod +import com.gemwallet.android.blockchain.clients.ethereum.services.EvmRpcClient.EvmNumber +import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest +import com.gemwallet.android.blockchain.rpc.model.JSONRpcResponse +import retrofit2.http.Body +import retrofit2.http.POST + +interface EvmCallService { + @POST("/") + suspend fun callString(@Body request: JSONRpcRequest>): Result> + + @POST("/") + suspend fun callNumber(@Body request: JSONRpcRequest>): Result> +} + +suspend fun EvmCallService.callString(contract: String, hexData: String): String? { + val params = mapOf( + "to" to contract, + "data" to hexData + ) + val request = JSONRpcRequest.create( + EvmMethod.Call, + listOf( + params, + "latest" + ) + ) + return callString(request).getOrNull()?.result +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/services/EvmFeeService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/services/EvmFeeService.kt new file mode 100644 index 000000000..3db0f9e6b --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/services/EvmFeeService.kt @@ -0,0 +1,45 @@ +package com.gemwallet.android.blockchain.clients.ethereum.services + +import com.gemwallet.android.blockchain.clients.ethereum.EvmMethod +import com.gemwallet.android.blockchain.clients.ethereum.services.EvmRpcClient.EvmNumber +import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest +import com.gemwallet.android.blockchain.rpc.model.JSONRpcResponse +import com.gemwallet.android.math.append0x +import com.wallet.core.blockchain.ethereum.models.EthereumFeeHistory +import retrofit2.http.Body +import retrofit2.http.POST +import java.math.BigInteger + +interface EvmFeeService { + @POST("/") + suspend fun getFeeHistory(@Body request: JSONRpcRequest>): Result> + + @POST("/") + suspend fun getGasLimit(@Body request: JSONRpcRequest>): Result> + + @POST("/") + suspend fun getNonce(@Body request: JSONRpcRequest>): Result> +} + +internal suspend fun EvmFeeService.getFeeHistory(): EthereumFeeHistory? { + return getFeeHistory(JSONRpcRequest.create(EvmMethod.GetFeeHistory, listOf("10", "latest", listOf(25)))) + .getOrNull()?.result +} + +internal suspend fun EvmFeeService.getGasLimit(from: String, to: String, amount: BigInteger, data: String?): BigInteger { + val transaction = mapOf( + "from" to from, + "to" to to, + "value" to "0x${amount.toString(16)}", + "data" to if (data.isNullOrEmpty()) "0x" else data.append0x(), + ) + val request = JSONRpcRequest.create(EvmMethod.GetGasLimit, listOf(transaction)) + return getGasLimit(request).getOrNull()?.result?.value + ?: throw Exception("Fail calculate gas limit") +} + +internal suspend fun EvmFeeService.getNonce(fromAddress: String): BigInteger { + val nonceParams = listOf(fromAddress, "latest") + return getNonce(JSONRpcRequest.create(EvmMethod.GetNonce, nonceParams)) + .getOrNull()?.result?.value ?: throw Exception("Fail get current nonce") +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/services/EvmNodeStatusService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/services/EvmNodeStatusService.kt new file mode 100644 index 000000000..c76b5a395 --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/services/EvmNodeStatusService.kt @@ -0,0 +1,33 @@ +package com.gemwallet.android.blockchain.clients.ethereum.services + +import com.gemwallet.android.blockchain.clients.ethereum.EvmMethod +import com.gemwallet.android.blockchain.clients.ethereum.services.EvmRpcClient.EvmNumber +import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest +import com.gemwallet.android.blockchain.rpc.model.JSONRpcResponse +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.POST +import retrofit2.http.Url + +interface EvmNodeStatusService { + @POST + suspend fun chainId(@Url url: String, @Body request: JSONRpcRequest>): Response> + + @POST + suspend fun sync(@Url url: String, @Body request: JSONRpcRequest>): Result> + + @POST + suspend fun latestBlock(@Url url: String, @Body request: JSONRpcRequest>): Result> +} + +internal suspend fun EvmNodeStatusService.getChainId(url: String): Response> { + return chainId(url, JSONRpcRequest.create(EvmMethod.GetChainId, emptyList())) +} + +internal suspend fun EvmNodeStatusService.latestBlock(url: String): Result> { + return latestBlock(url, JSONRpcRequest.create(EvmMethod.GetBlockNumber, emptyList())) +} + +internal suspend fun EvmNodeStatusService.sync(url: String): Result> { + return sync(url, JSONRpcRequest.create(EvmMethod.Sync, emptyList())) +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/services/EvmRpcClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/services/EvmRpcClient.kt new file mode 100644 index 000000000..21c3e9621 --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/services/EvmRpcClient.kt @@ -0,0 +1,61 @@ +package com.gemwallet.android.blockchain.clients.ethereum.services + +import com.gemwallet.android.blockchain.clients.ethereum.services.EvmRpcClient.EvmNumber +import com.gemwallet.android.math.decodeHex +import com.gemwallet.android.math.hexToBigInteger +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import wallet.core.jni.EthereumAbiValue +import java.lang.reflect.Type +import java.math.BigInteger + +interface EvmRpcClient : + EvmCallService, + EvmBalancesService, + EvmFeeService, + EvmNodeStatusService, + EvmBroadcastService, + EvmTransactionsService +{ + class EvmNumber( + val value: BigInteger?, + ) + + class TokenBalance( + val value: BigInteger?, + ) + + class BalanceDeserializer : JsonDeserializer { + override fun deserialize( + json: JsonElement?, + typeOfT: Type?, + context: JsonDeserializationContext? + ): EvmNumber { + return EvmNumber( + try { + json?.asString?.hexToBigInteger() + } catch (_: Throwable) { + null + } + ) + } + } + + class TokenBalanceDeserializer : JsonDeserializer { + override fun deserialize( + json: JsonElement?, + typeOfT: Type?, + context: JsonDeserializationContext? + ): TokenBalance { + return TokenBalance( + try { + EthereumAbiValue.decodeUInt256(json?.asString?.decodeHex()).toBigIntegerOrNull() + } catch (_: Throwable) { + null + } + ) + } + + } +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/services/EvmTransactionsService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/services/EvmTransactionsService.kt new file mode 100644 index 000000000..78438111d --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ethereum/services/EvmTransactionsService.kt @@ -0,0 +1,12 @@ +package com.gemwallet.android.blockchain.clients.ethereum.services + +import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest +import com.gemwallet.android.blockchain.rpc.model.JSONRpcResponse +import com.wallet.core.blockchain.ethereum.models.EthereumTransactionReciept +import retrofit2.http.Body +import retrofit2.http.POST + +interface EvmTransactionsService { + @POST("/") + suspend fun transaction(@Body request: JSONRpcRequest>): Result> +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/near/NearRpcClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/near/NearRpcClient.kt index 051e6820d..5e32578db 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/near/NearRpcClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/near/NearRpcClient.kt @@ -2,6 +2,7 @@ package com.gemwallet.android.blockchain.clients.near import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest import com.gemwallet.android.blockchain.rpc.model.JSONRpcResponse +import com.gemwallet.android.math.decodeHex import com.wallet.core.blockchain.near.models.NearAccount import com.wallet.core.blockchain.near.models.NearAccountAccessKey import com.wallet.core.blockchain.near.models.NearBlock @@ -11,6 +12,7 @@ import retrofit2.Response import retrofit2.http.Body import retrofit2.http.POST import retrofit2.http.Url +import wallet.core.jni.Base58 interface NearRpcClient { @POST("/") @@ -33,4 +35,39 @@ interface NearRpcClient { @POST suspend fun latestBlock(@Url url: String, @Body params: JSONRpcRequest): Response> +} + +suspend fun NearRpcClient.accountAccessKey(from: String, ): NearAccountAccessKey { + val publicKey = "ed25519:" + Base58.encodeNoCheck(from.decodeHex()) + return accountAccessKey( + JSONRpcRequest( + method = NearMethod.Query.value, + params = mapOf( + "request_type" to "view_access_key", + "finality" to "final", + "account_id" to from, + "public_key" to publicKey, + ) + ) + ).getOrNull()?.result ?: throw IllegalStateException("Can't get account") +} + +suspend fun NearRpcClient.latestBlock(): NearBlock { + return latestBlock( + JSONRpcRequest( + method = NearMethod.LatestBlock.value, + params = mapOf( + "finality" to "final", + ) + ) + ).getOrNull()?.result ?: throw IllegalStateException("Can't get block") +} + +suspend fun NearRpcClient.getGasPrice(): NearGasPrice { + return getGasPrice( + JSONRpcRequest( + method = NearMethod.GasPrice.value, + params = listOf(null), + ) + ).getOrNull()?.result ?: throw IllegalStateException("Can't get gas price") } \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/near/NearSignClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/near/NearSignClient.kt index 9d970a321..b8c84142d 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/near/NearSignClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/near/NearSignClient.kt @@ -17,10 +17,10 @@ class NearSignClient( private val chain: Chain, ) : SignClient { override suspend fun signTransfer(params: SignerParams, txSpeed: TxSpeed, privateKey: ByteArray): ByteArray { - val metadata = params.info as NearSignerPreloader.Info + val metadata = params.chainData as NearSignerPreloader.NearChainData val input = NEAR.SigningInput.newBuilder().apply { - this.signerId = params.owner + this.signerId = params.input.from.address this.nonce = metadata.sequence this.receiverId = params.input.destination()?.address this.addAllActions( diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/near/NearSignerPreloader.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/near/NearSignerPreloader.kt index 353b438d5..7311121df 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/near/NearSignerPreloader.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/near/NearSignerPreloader.kt @@ -1,77 +1,41 @@ package com.gemwallet.android.blockchain.clients.near -import com.gemwallet.android.blockchain.clients.SignerPreload -import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest -import com.gemwallet.android.math.decodeHex +import com.gemwallet.android.blockchain.clients.NativeTransferPreloader +import com.gemwallet.android.model.ChainSignData import com.gemwallet.android.model.ConfirmParams import com.gemwallet.android.model.Fee -import com.gemwallet.android.model.SignerInputInfo import com.gemwallet.android.model.SignerParams import com.gemwallet.android.model.TxSpeed -import com.wallet.core.primitives.Account import com.wallet.core.primitives.AssetId import com.wallet.core.primitives.Chain import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.withContext -import wallet.core.jni.Base58 import java.math.BigInteger class NearSignerPreloader( private val chain: Chain, private val rpcClient: NearRpcClient, -) : SignerPreload { - override suspend fun invoke(owner: Account, params: ConfirmParams): Result = withContext(Dispatchers.IO) { - val getAccountJob = async { - val publicKey = "ed25519:" + Base58.encodeNoCheck(owner.address.decodeHex()) - rpcClient.accountAccessKey( - JSONRpcRequest( - method = NearMethod.Query.value, - params = mapOf( - "request_type" to "view_access_key", - "finality" to "final", - "account_id" to owner.address, - "public_key" to publicKey, - ) - ) - ).getOrNull() - } - val blockJob = async { - rpcClient.latestBlock( - JSONRpcRequest( - method = NearMethod.LatestBlock.value, - params = mapOf( - "finality" to "final", - ) - ) - ).getOrNull() - } - val gasPriceJob = async { - rpcClient.getGasPrice( - JSONRpcRequest( - method = NearMethod.GasPrice.value, - params = listOf(null), - ) - ).getOrNull() - } - val account = getAccountJob.await()?.result ?: throw IllegalStateException("Can't get account") - val block = blockJob.await() ?: throw IllegalStateException("Can't get block") - val gasPrice = gasPriceJob.await() ?: throw IllegalStateException("Can't get gas price") +) : NativeTransferPreloader { + + override suspend fun preloadNativeTransfer(params: ConfirmParams.TransferParams.Native): SignerParams = withContext(Dispatchers.IO) { + val getAccountJob = async { rpcClient.accountAccessKey(params.from.address) } + val blockJob = async { rpcClient.latestBlock() } + + val account = getAccountJob.await() + val block = blockJob.await() val fee = BigInteger("900000000000000000000") - Result.success( - SignerParams( - input = params, - owner = owner.address, - info = Info( - sequence = account.nonce + 1L, - block = block.result.header.hash, - fee = Fee( - feeAssetId = AssetId(chain), - speed = TxSpeed.Normal, - amount = fee, - ) + SignerParams( + input = params, + chainData = NearChainData( + sequence = account.nonce + 1L, + block = block.header.hash, + fee = Fee( + feeAssetId = AssetId(chain), + speed = TxSpeed.Normal, + amount = fee, ) ) ) @@ -79,11 +43,11 @@ class NearSignerPreloader( override fun supported(chain: Chain): Boolean = this.chain == chain - data class Info( + data class NearChainData( val block: String, val sequence: Long, val fee: Fee, - ) : SignerInputInfo { + ) : ChainSignData { override fun fee(speed: TxSpeed): Fee = fee } } \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaBalanceClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaBalanceClient.kt index e7c9e6c53..872ab2926 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaBalanceClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaBalanceClient.kt @@ -1,7 +1,13 @@ package com.gemwallet.android.blockchain.clients.solana import com.gemwallet.android.blockchain.clients.BalanceClient -import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest +import com.gemwallet.android.blockchain.clients.solana.services.SolanaAccountsService +import com.gemwallet.android.blockchain.clients.solana.services.SolanaBalancesService +import com.gemwallet.android.blockchain.clients.solana.services.SolanaStakeService +import com.gemwallet.android.blockchain.clients.solana.services.getBalance +import com.gemwallet.android.blockchain.clients.solana.services.getDelegationsBalance +import com.gemwallet.android.blockchain.clients.solana.services.getTokenAccountByOwner +import com.gemwallet.android.blockchain.clients.solana.services.getTokenBalance import com.gemwallet.android.ext.asset import com.gemwallet.android.model.AssetBalance import com.wallet.core.primitives.Asset @@ -13,28 +19,20 @@ import java.math.BigInteger class SolanaBalanceClient( val chain: Chain, - val rpcClient: SolanaRpcClient, + val accountsService: SolanaAccountsService, + val balancesService: SolanaBalancesService, + val stakeService: SolanaStakeService, ) : BalanceClient { override suspend fun getNativeBalance(chain: Chain, address: String): AssetBalance? = withContext(Dispatchers.IO) { - val getAvailable = async { - rpcClient.getBalance(JSONRpcRequest.create(SolanaMethod.GetBalance, listOf(address))) - .getOrNull()?.result?.value - } - val getStaked = async { - rpcClient.delegations(address) - .getOrNull()?.result?.map { it.account.lamports } - ?.fold(0L) { acc, value -> acc + value } ?: 0L - } + val getAvailable = async { balancesService.getBalance(address) } + val getStaked = async { stakeService.getDelegationsBalance(address) } + val (available, staked) = Pair(getAvailable.await(), getStaked.await()) - if (available == null) { - return@withContext null - } - AssetBalance.create( - asset = chain.asset(), - available = available.toString(), - staked = staked.toString(), - ) + + available ?: return@withContext null + + AssetBalance.create(chain.asset(), available.toString(), staked = staked.toString()) } override suspend fun getTokenBalances(chain: Chain, address: String, tokens: List): List { @@ -48,20 +46,8 @@ class SolanaBalanceClient( } private suspend fun getTokenBalance(owner: String, tokenId: String): BigInteger { - val accountRequest = JSONRpcRequest.create( - method = SolanaMethod.GetTokenAccountByOwner, - params = listOf( - owner, - mapOf("mint" to tokenId), - mapOf("encoding" to "jsonParsed"), - ) - ) - val tokenAccount = rpcClient.getTokenAccountByOwner(accountRequest) - .getOrNull()?.result?.value?.firstOrNull()?.pubkey ?: return BigInteger.ZERO - - val balanceRequest = JSONRpcRequest.create(SolanaMethod.GetTokenBalance, listOf(tokenAccount)) - return rpcClient.getTokenBalance(balanceRequest).getOrNull() - ?.result?.value?.amount?.toBigInteger() ?: return BigInteger.ZERO + val tokenAccount = accountsService.getTokenAccountByOwner(owner, tokenId) ?: return BigInteger.ZERO + return balancesService.getTokenBalance(tokenAccount) ?: BigInteger.ZERO } override fun supported(chain: Chain): Boolean = this.chain == chain diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaFee.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaFee.kt deleted file mode 100644 index f0383a1d7..000000000 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaFee.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.gemwallet.android.blockchain.clients.solana - -import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest -import com.gemwallet.android.model.GasFee -import com.gemwallet.android.model.TxSpeed -import com.wallet.core.primitives.AssetId -import com.wallet.core.primitives.AssetSubtype -import com.wallet.core.primitives.Chain -import com.wallet.core.primitives.TransactionType -import kotlin.math.max - -class SolanaFee { - - private val staticBaseFee = 5_000L - private val tokenAccountSize = 165 - - suspend operator fun invoke(rpcClient: SolanaRpcClient, transactionType: TransactionType, assetType: AssetSubtype): GasFee { - val gasLimit = when (transactionType) { - TransactionType.TokenApproval -> throw IllegalArgumentException("Solana doesn't support token approval") - TransactionType.StakeDelegate, - TransactionType.StakeUndelegate, - TransactionType.StakeRewards, - TransactionType.StakeRedelegate, - TransactionType.StakeWithdraw, - TransactionType.Transfer -> 100_000L - TransactionType.Swap -> 1_400_000 - } - val priorityFees = rpcClient.getPriorityFees() - val multipleOf = when (transactionType) { - TransactionType.Transfer -> if (assetType == AssetSubtype.NATIVE) 10_000L else 100_000L - TransactionType.TokenApproval -> throw IllegalArgumentException("Solana doesn't support token approval") - TransactionType.StakeDelegate, - TransactionType.StakeUndelegate, - TransactionType.StakeRewards, - TransactionType.StakeRedelegate, - TransactionType.StakeWithdraw -> 10_000 - TransactionType.Swap -> 250_000 - } - val minerFee = if (priorityFees.isEmpty()) { - multipleOf - } else { - val averagePriorityFee = priorityFees.map { it.prioritizationFee }.fold(0) { acc, i -> acc + i } / priorityFees.size - max(((averagePriorityFee + multipleOf - 1) / multipleOf) * multipleOf, multipleOf) - } - - val tokenAccountCreation = rpcClient.rentExemption(JSONRpcRequest(id = 1, method = SolanaMethod.RentExemption.value, params = listOf(tokenAccountSize))) - .getOrNull()?.result?.toBigInteger() ?: throw Exception("Can't get fee") - - val totalFee = staticBaseFee + (minerFee * gasLimit / 1_000_000) - return GasFee( - feeAssetId = AssetId(Chain.Solana), - speed = TxSpeed.Normal, - minerFee = minerFee.toBigInteger(), - maxGasPrice = staticBaseFee.toBigInteger(), - limit = gasLimit.toBigInteger(), - amount = totalFee.toBigInteger(), - options = mapOf("tokenAccountCreation" to tokenAccountCreation) - ) - } - -} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaFeeCalculator.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaFeeCalculator.kt new file mode 100644 index 000000000..97908bf0f --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaFeeCalculator.kt @@ -0,0 +1,74 @@ +package com.gemwallet.android.blockchain.clients.solana + +import com.gemwallet.android.blockchain.clients.solana.services.SolanaFeeService +import com.gemwallet.android.blockchain.clients.solana.services.getPriorityFees +import com.gemwallet.android.blockchain.clients.solana.services.rentExemption +import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest +import com.gemwallet.android.ext.type +import com.gemwallet.android.model.ConfirmParams +import com.gemwallet.android.model.GasFee +import com.gemwallet.android.model.SignerParams +import com.gemwallet.android.model.TxSpeed +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.AssetSubtype +import com.wallet.core.primitives.Chain +import kotlin.math.max + +class SolanaFeeCalculator( + private val feeService: SolanaFeeService +) { + + private val staticBaseFee = 5_000L + private val tokenAccountSize = 165 + + suspend fun calculate(params: ConfirmParams): GasFee = when (params) { + is ConfirmParams.Stake -> calculate(params) + is ConfirmParams.SwapParams -> calculate(params) + is ConfirmParams.TokenApprovalParams -> throw IllegalArgumentException("Token approval doesn't support") + is ConfirmParams.TransferParams -> calculate(params) + } + + suspend fun calculate(params: ConfirmParams.Stake): GasFee { + return calculate( + gasLimit = 100_000L, + multipleOf = 10_000L, + ) + } + + suspend fun calculate(params: ConfirmParams.TransferParams): GasFee { + return calculate( + gasLimit = 100_000L, + multipleOf = if (params.assetId.type() == AssetSubtype.NATIVE) 10_000L else 100_000L, + ) + } + + suspend fun calculate(params: ConfirmParams.SwapParams): GasFee { + return calculate( + gasLimit = 1_400_000L, + multipleOf = 250_000, + + ) + } + + private suspend fun calculate(gasLimit: Long, multipleOf: Long): GasFee { + val priorityFees = feeService.getPriorityFees() + val minerFee = if (priorityFees.isEmpty()) { + multipleOf + } else { + val averagePriorityFee = priorityFees.map { it.prioritizationFee }.fold(0) { acc, i -> acc + i } / priorityFees.size + max(((averagePriorityFee + multipleOf - 1) / multipleOf) * multipleOf, multipleOf) + } + val tokenAccountCreation = feeService.rentExemption(tokenAccountSize) + val totalFee = staticBaseFee + (minerFee * gasLimit / 1_000_000) + return GasFee( + feeAssetId = AssetId(Chain.Solana), + speed = TxSpeed.Normal, + minerFee = minerFee.toBigInteger(), + maxGasPrice = staticBaseFee.toBigInteger(), + limit = gasLimit.toBigInteger(), + amount = totalFee.toBigInteger(), + options = mapOf("tokenAccountCreation" to tokenAccountCreation) + ) + } + +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaMethod.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaMethod.kt index 1add404c7..5b2d947d7 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaMethod.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaMethod.kt @@ -6,7 +6,6 @@ enum class SolanaMethod(val value: String) : JSONRpcMethod { GetBalance("getBalance"), GetTokenBalance("getTokenAccountBalance"), GetTokenAccountByOwner("getTokenAccountsByOwner"), - GetFees("getFees"), RentExemption("getMinimumBalanceForRentExemption"), GetLatestBlockhash("getLatestBlockhash"), GetPriorityFee("getRecentPrioritizationFees"), diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaRpcClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaRpcClient.kt index 71a13712c..079549bec 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaRpcClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaRpcClient.kt @@ -4,9 +4,14 @@ import com.gemwallet.android.blockchain.clients.solana.models.SolanaArrayData import com.gemwallet.android.blockchain.clients.solana.models.SolanaInfo import com.gemwallet.android.blockchain.clients.solana.models.SolanaParsedData import com.gemwallet.android.blockchain.clients.solana.models.SolanaParsedSplTokenInfo +import com.gemwallet.android.blockchain.clients.solana.models.SolanaTokenOwner +import com.gemwallet.android.blockchain.clients.solana.services.SolanaAccountsService +import com.gemwallet.android.blockchain.clients.solana.services.SolanaBalancesService +import com.gemwallet.android.blockchain.clients.solana.services.SolanaFeeService +import com.gemwallet.android.blockchain.clients.solana.services.SolanaNetworkInfoService +import com.gemwallet.android.blockchain.clients.solana.services.SolanaStakeService import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest import com.gemwallet.android.blockchain.rpc.model.JSONRpcResponse -import com.wallet.core.blockchain.solana.models.SolanaBalance import com.wallet.core.blockchain.solana.models.SolanaBalanceValue import com.wallet.core.blockchain.solana.models.SolanaBlockhashResult import com.wallet.core.blockchain.solana.models.SolanaEpoch @@ -22,49 +27,25 @@ import retrofit2.http.Body import retrofit2.http.POST import retrofit2.http.Url -interface SolanaRpcClient { - @POST("/") - suspend fun getBalance(@Body request: JSONRpcRequest>): Result> - - @POST("/") - suspend fun getTokenAccountByOwner(@Body request: JSONRpcRequest>): Result>>> - +interface SolanaRpcClient : + SolanaAccountsService, + SolanaBalancesService, + SolanaStakeService, + SolanaFeeService, + SolanaNetworkInfoService +{ @POST("/") suspend fun getAccountInfoSpl(@Body request: JSONRpcRequest>): Result>>>> - @POST("/") - suspend fun getTokenInfo(@Body request: JSONRpcRequest>): Result>> - @POST("/") suspend fun getAccountInfoMpl(@Body request: JSONRpcRequest>): Result>>> - @POST("/") - suspend fun getTokenBalance(@Body request: JSONRpcRequest>): Result>> - - @POST("/") - suspend fun rentExemption(@Body request: JSONRpcRequest>): Result> - - @POST("/") - suspend fun getBlockhash(@Body request: JSONRpcRequest>): Result> - - @POST("/") - suspend fun getPriorityFees(@Body request: JSONRpcRequest>): Result>> - @POST("/") suspend fun broadcast(@Body request: JSONRpcRequest>): Result> @POST("/") suspend fun transaction(@Body request: JSONRpcRequest>): Result> - @POST("/") - suspend fun validators(@Body request: JSONRpcRequest>): Result> - - @POST("/") - suspend fun delegations(@Body request: JSONRpcRequest>): Result>>> - - @POST("/") - suspend fun epoch(@Body request: JSONRpcRequest>): Result> - @POST suspend fun health(@Url url: String,@Body request: JSONRpcRequest>): Response> @@ -75,52 +56,6 @@ interface SolanaRpcClient { suspend fun genesisHash(@Url url: String, @Body request: JSONRpcRequest>): Result> } -class SolanaTokenOwner(val owner: String) - -suspend fun SolanaRpcClient.getTokenAccountByOwner( - owner: String, - tokenId: String, -): Result>>> { - val accountRequest = JSONRpcRequest.create( - method = SolanaMethod.GetTokenAccountByOwner, - params = listOf( - owner, - mapOf("mint" to tokenId), - mapOf("encoding" to "jsonParsed"), - ) - ) - return getTokenAccountByOwner(accountRequest) -} - -suspend fun SolanaRpcClient.delegations( - owner: String, -): Result>>> { - val request = JSONRpcRequest.create( - SolanaMethod.GetDelegations, - listOf( - "Stake11111111111111111111111111111111111111", - mapOf( - "encoding" to "jsonParsed", - "commitment" to "finalized", - "filters" to listOf( - mapOf( - "memcmp" to mapOf( - "bytes" to owner, - "offset" to 44, - ) - ) - ) - ) - ) - ) - return delegations(request) -} - -suspend fun SolanaRpcClient.getPriorityFees(): List { - val request = JSONRpcRequest.create(SolanaMethod.GetPriorityFee, listOf()) - return getPriorityFees(request).getOrNull()?.result ?: throw Exception() -} - suspend fun SolanaRpcClient.health(url: String): Response> { return health(url, JSONRpcRequest.create(SolanaMethod.GetHealth, emptyList())) } @@ -131,4 +66,5 @@ suspend fun SolanaRpcClient.slot(url: String): Result> { suspend fun SolanaRpcClient.genesisHash(url: String): Result> { return genesisHash(url, JSONRpcRequest.create(SolanaMethod.GetGenesisHash, emptyList())) -} \ No newline at end of file +} + diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaSignClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaSignClient.kt index 4d8cb901e..b7a4c8515 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaSignClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaSignClient.kt @@ -47,8 +47,8 @@ class SolanaSignClient( txSpeed: TxSpeed, privateKey: ByteArray ): ByteArray { - val fee = params.info.fee(txSpeed) as GasFee - val recentBlockhash = (params.info as SolanaSignerPreloader.Info).blockhash + val fee = params.chainData.fee(txSpeed) as GasFee + val recentBlockhash = (params.chainData as SolanaSignerPreloader.SolanaChainData).blockhash return when(params.input.getTxType()) { TransactionType.Swap -> swap(params, txSpeed, privateKey).toByteArray() @@ -64,7 +64,7 @@ class SolanaSignClient( val signInput = Solana.SigningInput.newBuilder().apply { this.recentBlockhash = recentBlockhash this.delegateStakeTransaction = Solana.DelegateStake.newBuilder().apply { - this.validatorPubkey = (params.input as ConfirmParams.DelegateParams).validatorId + this.validatorPubkey = (params.input as ConfirmParams.Stake.DelegateParams).validatorId this.value = params.finalAmount.toLong() }.build() this.privateKey = ByteString.copyFrom(privateKey) @@ -75,7 +75,7 @@ class SolanaSignClient( val signInput = Solana.SigningInput.newBuilder().apply { this.recentBlockhash = recentBlockhash this.deactivateStakeTransaction = Solana.DeactivateStake.newBuilder().apply { - this.stakeAccount = (params.input as ConfirmParams.UndelegateParams).delegationId + this.stakeAccount = (params.input as ConfirmParams.Stake.UndelegateParams).delegationId }.build() this.privateKey = ByteString.copyFrom(privateKey) } @@ -85,7 +85,7 @@ class SolanaSignClient( val signInput = Solana.SigningInput.newBuilder().apply { this.recentBlockhash = recentBlockhash this.withdrawTransaction = Solana.WithdrawStake.newBuilder().apply { - stakeAccount = (params.input as ConfirmParams.WithdrawParams).delegationId + stakeAccount = (params.input as ConfirmParams.Stake.WithdrawParams).delegationId value = params.finalAmount.toLong() }.build() this.privateKey = ByteString.copyFrom(privateKey) @@ -99,7 +99,7 @@ class SolanaSignClient( } private fun signNative(input: SignerParams): Solana.SigningInput.Builder { - val blockhash = (input.info as SolanaSignerPreloader.Info).blockhash + val blockhash = (input.chainData as SolanaSignerPreloader.SolanaChainData).blockhash return Solana.SigningInput.newBuilder().apply { this.transferTransaction = Solana.Transfer.newBuilder().apply { @@ -118,7 +118,7 @@ class SolanaSignClient( val tokenId = input.input.assetId.tokenId val amount = input.finalAmount.toLong() val recipient = input.input.destination()?.address - val metadata = input.info as SolanaSignerPreloader.Info + val metadata = input.chainData as SolanaSignerPreloader.SolanaChainData val tokenProgramId = when (metadata.tokenProgram) { SolanaTokenProgramId.Token -> Solana.TokenProgramId.TokenProgram SolanaTokenProgramId.Token2022 -> Solana.TokenProgramId.Token2022Program @@ -160,7 +160,7 @@ class SolanaSignClient( } private fun swap(input: SignerParams, txSpeed: TxSpeed, privateKey: ByteArray): String { - val fee = input.info.fee(txSpeed) as? GasFee ?: throw java.lang.IllegalArgumentException("Incorrect fee data") + val fee = input.chainData.fee(txSpeed) as? GasFee ?: throw java.lang.IllegalArgumentException("Incorrect fee data") val feePrice = fee.minerFee val feeLimit = fee.limit val swapParams = input.input as ConfirmParams.SwapParams @@ -181,6 +181,9 @@ class SolanaSignClient( this.txEncoding = Solana.Encoding.Base64 }.build() val output: Solana.SigningOutput = AnySigner.sign(signingInput, CoinType.SOLANA, Solana.SigningOutput.parser()) + if (!output.errorMessage.isNullOrEmpty()) { + throw Exception(output.errorMessage) + } return output.encoded } diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaSignerPreloader.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaSignerPreloader.kt index 5918c2535..24c413f50 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaSignerPreloader.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaSignerPreloader.kt @@ -1,16 +1,22 @@ package com.gemwallet.android.blockchain.clients.solana -import com.gemwallet.android.blockchain.clients.SignerPreload -import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest -import com.gemwallet.android.ext.type +import com.gemwallet.android.blockchain.clients.NativeTransferPreloader +import com.gemwallet.android.blockchain.clients.StakeTransactionPreloader +import com.gemwallet.android.blockchain.clients.SwapTransactionPreloader +import com.gemwallet.android.blockchain.clients.TokenTransferPreloader +import com.gemwallet.android.blockchain.clients.solana.SolanaSignerPreloader.SolanaChainData +import com.gemwallet.android.blockchain.clients.solana.services.SolanaAccountsService +import com.gemwallet.android.blockchain.clients.solana.services.SolanaFeeService +import com.gemwallet.android.blockchain.clients.solana.services.SolanaNetworkInfoService +import com.gemwallet.android.blockchain.clients.solana.services.getBlockhash +import com.gemwallet.android.blockchain.clients.solana.services.getTokenAccountByOwner +import com.gemwallet.android.blockchain.clients.solana.services.getTokenInfo +import com.gemwallet.android.model.ChainSignData import com.gemwallet.android.model.ConfirmParams import com.gemwallet.android.model.Fee import com.gemwallet.android.model.GasFee -import com.gemwallet.android.model.SignerInputInfo import com.gemwallet.android.model.SignerParams import com.gemwallet.android.model.TxSpeed -import com.wallet.core.primitives.Account -import com.wallet.core.primitives.AssetSubtype import com.wallet.core.primitives.Chain import com.wallet.core.primitives.SolanaTokenProgramId import kotlinx.coroutines.Dispatchers @@ -20,108 +26,76 @@ import uniffi.gemstone.Config class SolanaSignerPreloader( private val chain: Chain, - private val rpcClient: SolanaRpcClient, -) : SignerPreload { + feeService: SolanaFeeService, + private val networkInfoService: SolanaNetworkInfoService, + private val accountsService: SolanaAccountsService, +) : NativeTransferPreloader, TokenTransferPreloader, SwapTransactionPreloader, StakeTransactionPreloader { - override suspend fun invoke( - owner: Account, - params: ConfirmParams, - ): Result = withContext(Dispatchers.IO) { - val blockhashJob = async { - rpcClient.getBlockhash(JSONRpcRequest.create(SolanaMethod.GetLatestBlockhash, emptyList())) - .getOrNull()?.result?.value?.blockhash - } - val feeJob = async { SolanaFee().invoke(rpcClient, params.getTxType(), params.assetId.type()) } - val (fee, blockhash) = Pair(feeJob.await(), blockhashJob.await()) + private val feeCalculator = SolanaFeeCalculator(feeService) - if (blockhash.isNullOrEmpty()) { - return@withContext Result.failure(Exception("Can't get latest blockhash")) - } - val info = when (params) { - is ConfirmParams.TransferParams -> when (params.assetId.type()) { - AssetSubtype.NATIVE -> { - Info(blockhash, "", null, SolanaTokenProgramId.Token, fee) - } - AssetSubtype.TOKEN -> { - val (senderTokenAddress, recipientTokenAddress, tokenProgram) = getTokenAccounts(params.assetId.tokenId!!, owner.address, params.destination.address) - - if (senderTokenAddress.isNullOrEmpty()) { - return@withContext Result.failure(Exception("Sender token address is empty")) - } - Info( - blockhash = blockhash, - senderTokenAddress = senderTokenAddress, - recipientTokenAddress = recipientTokenAddress, - tokenProgram = tokenProgram, - fee = if (recipientTokenAddress.isNullOrEmpty()) { - fee.withOptions("tokenAccountCreation") - } else { - fee - }, - ) - } - } - is ConfirmParams.SwapParams -> Info(blockhash, "", null, SolanaTokenProgramId.Token, fee) - is ConfirmParams.DelegateParams, - is ConfirmParams.RedeleateParams, - is ConfirmParams.RewardsParams, - is ConfirmParams.TokenApprovalParams, - is ConfirmParams.UndelegateParams, - is ConfirmParams.WithdrawParams -> Info(blockhash, "", null, SolanaTokenProgramId.Token, fee) - } - Result.success( - SignerParams( - input = params, - owner = owner.address, - info = info, - ) - ) + override suspend fun preloadNativeTransfer(params: ConfirmParams.TransferParams.Native): SignerParams { + return preload(params, "", null, SolanaTokenProgramId.Token) } - private suspend fun getTokenAccounts( - tokenId: String, - senderAddress: String, - recipientAddress: String, - ): Triple = withContext(Dispatchers.IO) { - val senderTokenAddressJob = async { - rpcClient.getTokenAccountByOwner(senderAddress, tokenId) - .getOrNull()?.result?.value?.firstOrNull()?.pubkey - } - val recipientTokenAddressJob = async { - rpcClient.getTokenAccountByOwner(recipientAddress, tokenId) - .getOrNull()?.result?.value?.firstOrNull()?.pubkey - } + override suspend fun preloadTokenTransfer(params: ConfirmParams.TransferParams.Token): SignerParams = withContext(Dispatchers.IO) { + val tokenId = params.assetId.tokenId!! + val senderTokenAddressJob = async { accountsService.getTokenAccountByOwner(params.from.address, tokenId) } + val recipientTokenAddressJob = async { accountsService.getTokenAccountByOwner(params.destination.address, tokenId) } val tokenProgramJob = async { - val owner = rpcClient.getTokenInfo( - JSONRpcRequest( - SolanaMethod.GetAccountInfo.value, - params = listOf( - tokenId, - mapOf( - "encoding" to "jsonParsed" - ), - ) - ) - ).getOrNull()?.result?.value?.owner + val owner = networkInfoService.getTokenInfo(tokenId) if (owner != null) { - SolanaTokenProgramId.entries.firstOrNull { it.string == Config().getSolanaTokenProgramId(owner) } ?: SolanaTokenProgramId.Token + SolanaTokenProgramId.entries.firstOrNull { + it.string == Config().getSolanaTokenProgramId(owner) + } ?: SolanaTokenProgramId.Token } else { SolanaTokenProgramId.Token } } - Triple(senderTokenAddressJob.await(), recipientTokenAddressJob.await(), tokenProgramJob.await()) + + val senderTokenAddress = senderTokenAddressJob.await() + val recipientTokenAddress = recipientTokenAddressJob.await() + val tokenProgram = tokenProgramJob.await() + + if (senderTokenAddress.isNullOrEmpty()) { + throw Exception("Sender token address is empty") + } + + preload(params, senderTokenAddress, recipientTokenAddress, tokenProgram) + } + + override suspend fun preloadSwap(params: ConfirmParams.SwapParams): SignerParams { + return preload(params, "", null, SolanaTokenProgramId.Token) + } + + override suspend fun preloadStake(params: ConfirmParams.Stake): SignerParams { + return preload(params, "", null, SolanaTokenProgramId.Token) + } + + private suspend fun preload( + params: ConfirmParams, + senderTokenAddress: String = "", + recipientTokenAddress: String? = null, + tokenProgram: SolanaTokenProgramId = SolanaTokenProgramId.Token + ): SignerParams = withContext(Dispatchers.IO) { + val blockHashJob = async { networkInfoService.getBlockhash() } + val feeJob = async { feeCalculator.calculate(params) } + + val (fee, blockHash) = Pair(feeJob.await(), blockHashJob.await()) + + val chainData = SolanaChainData(blockHash, senderTokenAddress, recipientTokenAddress, tokenProgram, fee) + SignerParams(params, chainData) } override fun supported(chain: Chain): Boolean = this.chain == chain - data class Info( + data class SolanaChainData( val blockhash: String, val senderTokenAddress: String, val recipientTokenAddress: String?, val tokenProgram: SolanaTokenProgramId, val fee: GasFee, - ) : SignerInputInfo { + ) : ChainSignData { override fun fee(speed: TxSpeed): Fee = fee } } \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaStakeClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaStakeClient.kt index fc52bf28e..e9f0f1505 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaStakeClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/SolanaStakeClient.kt @@ -1,7 +1,10 @@ package com.gemwallet.android.blockchain.clients.solana import com.gemwallet.android.blockchain.clients.StakeClient -import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest +import com.gemwallet.android.blockchain.clients.solana.services.SolanaStakeService +import com.gemwallet.android.blockchain.clients.solana.services.delegations +import com.gemwallet.android.blockchain.clients.solana.services.epoch +import com.gemwallet.android.blockchain.clients.solana.services.validators import com.gemwallet.android.ext.asset import com.wallet.core.primitives.Chain import com.wallet.core.primitives.DelegationBase @@ -14,20 +17,10 @@ import java.math.BigInteger class SolanaStakeClient( private val chain: Chain, - private val rpcClient: SolanaRpcClient + private val stakeService: SolanaStakeService, ): StakeClient { override suspend fun getValidators(chain: Chain, apr: Double): List { - return rpcClient.validators( - JSONRpcRequest.create( - SolanaMethod.GetValidators, - listOf( - mapOf( - "commitment" to "finalized", - "keepUnstakedDelinquents" to false - ) - ) - ) - ).getOrNull()?.result?.current?.map { + return stakeService.validators()?.map { val isActive = it.epochVoteAccount DelegationValidator( chain = chain, @@ -41,11 +34,8 @@ class SolanaStakeClient( } override suspend fun getStakeDelegations(chain: Chain, address: String, apr: Double): List = withContext(Dispatchers.IO) { - val getEpoch = async { - rpcClient.epoch(JSONRpcRequest.create(SolanaMethod.GetEpoch, emptyList())) - .getOrNull()?.result - } - val getDelegations = async { rpcClient.delegations(address).getOrNull()?.result } + val getEpoch = async { stakeService.epoch() } + val getDelegations = async { stakeService.delegations(address) } val epoch = getEpoch.await() ?: return@withContext emptyList() val delegations = getDelegations.await() ?: return@withContext emptyList() val nextEpoch = System.currentTimeMillis() + ((epoch.slotsInEpoch - epoch.slotIndex) * 420) diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/models/SolanaTokenOwner.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/models/SolanaTokenOwner.kt new file mode 100644 index 000000000..955023de8 --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/models/SolanaTokenOwner.kt @@ -0,0 +1,3 @@ +package com.gemwallet.android.blockchain.clients.solana.models + +class SolanaTokenOwner(val owner: String) \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/services/SolanaAccountsService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/services/SolanaAccountsService.kt new file mode 100644 index 000000000..9f1491b84 --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/services/SolanaAccountsService.kt @@ -0,0 +1,27 @@ +package com.gemwallet.android.blockchain.clients.solana.services + +import com.gemwallet.android.blockchain.clients.solana.SolanaMethod +import com.gemwallet.android.blockchain.clients.solana.SolanaRpcClient +import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest +import com.gemwallet.android.blockchain.rpc.model.JSONRpcResponse +import com.wallet.core.blockchain.solana.models.SolanaTokenAccount +import com.wallet.core.blockchain.solana.models.SolanaValue +import retrofit2.http.Body +import retrofit2.http.POST + +interface SolanaAccountsService { + @POST("/") + suspend fun getTokenAccountByOwner(@Body request: JSONRpcRequest>): Result>>> +} + +suspend fun SolanaAccountsService.getTokenAccountByOwner(owner: String, tokenId: String): String? { + val accountRequest = JSONRpcRequest.create( + method = SolanaMethod.GetTokenAccountByOwner, + params = listOf( + owner, + mapOf("mint" to tokenId), + mapOf("encoding" to "jsonParsed"), + ) + ) + return getTokenAccountByOwner(accountRequest).getOrNull()?.result?.value?.firstOrNull()?.pubkey +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/services/SolanaBalancesService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/services/SolanaBalancesService.kt new file mode 100644 index 000000000..739538b70 --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/services/SolanaBalancesService.kt @@ -0,0 +1,34 @@ +package com.gemwallet.android.blockchain.clients.solana.services + +import com.gemwallet.android.blockchain.clients.solana.SolanaMethod +import com.gemwallet.android.blockchain.clients.solana.SolanaRpcClient +import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest +import com.gemwallet.android.blockchain.rpc.model.JSONRpcResponse +import com.wallet.core.blockchain.solana.models.SolanaBalance +import com.wallet.core.blockchain.solana.models.SolanaBalanceValue +import com.wallet.core.blockchain.solana.models.SolanaValue +import retrofit2.http.Body +import retrofit2.http.POST +import java.math.BigInteger + +interface SolanaBalancesService { + @POST("/") + suspend fun getBalance(@Body request: JSONRpcRequest>): Result> + + @POST("/") + suspend fun getTokenBalance(@Body request: JSONRpcRequest>): Result>> +} + +suspend fun SolanaBalancesService.getBalance(address: String): Long? { + return getBalance(JSONRpcRequest.create(SolanaMethod.GetBalance, listOf(address))) + .getOrNull()?.result?.value +} + +suspend fun SolanaBalancesService.getTokenBalance(tokenAccount: String): BigInteger? { + val balanceRequest = JSONRpcRequest.create(SolanaMethod.GetTokenBalance, listOf(tokenAccount)) + return try { + getTokenBalance(balanceRequest).getOrNull()?.result?.value?.amount?.toBigInteger() + } catch (_: Throwable) { + null + } +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/services/SolanaFeeService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/services/SolanaFeeService.kt new file mode 100644 index 000000000..c1402cba3 --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/services/SolanaFeeService.kt @@ -0,0 +1,33 @@ +package com.gemwallet.android.blockchain.clients.solana.services + +import com.gemwallet.android.blockchain.clients.solana.SolanaMethod +import com.gemwallet.android.blockchain.clients.solana.SolanaRpcClient +import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest +import com.gemwallet.android.blockchain.rpc.model.JSONRpcResponse +import com.wallet.core.blockchain.solana.models.SolanaPrioritizationFee +import retrofit2.http.Body +import retrofit2.http.POST +import java.math.BigInteger + +interface SolanaFeeService { + @POST("/") + suspend fun rentExemption(@Body request: JSONRpcRequest>): Result> + + @POST("/") + suspend fun getPriorityFees(@Body request: JSONRpcRequest>): Result>> +} + +suspend fun SolanaFeeService.getPriorityFees(): List { + val request = JSONRpcRequest.create(SolanaMethod.GetPriorityFee, listOf()) + return getPriorityFees(request).getOrNull()?.result ?: throw Exception("Can't load fee price") +} + +suspend fun SolanaFeeService.rentExemption(tokenAccountSize: Int): BigInteger { + return rentExemption( + JSONRpcRequest( + id = 1, + method = SolanaMethod.RentExemption.value, + params = listOf(tokenAccountSize) + ) + ).getOrNull()?.result?.toBigInteger() ?: throw Exception("Can't load token creation fee") +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/services/SolanaNetworkInfoService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/services/SolanaNetworkInfoService.kt new file mode 100644 index 000000000..3254caa8c --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/services/SolanaNetworkInfoService.kt @@ -0,0 +1,41 @@ +package com.gemwallet.android.blockchain.clients.solana.services + +import com.gemwallet.android.blockchain.clients.solana.SolanaMethod +import com.gemwallet.android.blockchain.clients.solana.models.SolanaTokenOwner +import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest +import com.gemwallet.android.blockchain.rpc.model.JSONRpcResponse +import com.wallet.core.blockchain.solana.models.SolanaBlockhashResult +import com.wallet.core.blockchain.solana.models.SolanaValue +import retrofit2.http.Body +import retrofit2.http.POST + +interface SolanaNetworkInfoService { + @POST("/") + suspend fun getTokenInfo(@Body request: JSONRpcRequest>): Result>> + + @POST("/") + suspend fun getBlockhash(@Body request: JSONRpcRequest>): Result> +} + +suspend fun SolanaNetworkInfoService.getTokenInfo(tokenId: String): String? { + return getTokenInfo( + JSONRpcRequest( + SolanaMethod.GetAccountInfo.value, + params = listOf( + tokenId, + mapOf( + "encoding" to "jsonParsed" + ), + ) + ) + ).getOrNull()?.result?.value?.owner +} + +suspend fun SolanaNetworkInfoService.getBlockhash(): String { + val blockhash = getBlockhash(JSONRpcRequest.create(SolanaMethod.GetLatestBlockhash, emptyList())) + .getOrNull()?.result?.value?.blockhash + if (blockhash.isNullOrEmpty()) { + throw Exception("Can't get latest blockhash") + } + return blockhash +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/services/SolanaStakeService.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/services/SolanaStakeService.kt new file mode 100644 index 000000000..7e01f571a --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/solana/services/SolanaStakeService.kt @@ -0,0 +1,68 @@ +package com.gemwallet.android.blockchain.clients.solana.services + +import com.gemwallet.android.blockchain.clients.solana.SolanaMethod +import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest +import com.gemwallet.android.blockchain.rpc.model.JSONRpcResponse +import com.wallet.core.blockchain.solana.models.SolanaEpoch +import com.wallet.core.blockchain.solana.models.SolanaStakeAccount +import com.wallet.core.blockchain.solana.models.SolanaTokenAccountResult +import com.wallet.core.blockchain.solana.models.SolanaValidator +import com.wallet.core.blockchain.solana.models.SolanaValidators +import retrofit2.http.Body +import retrofit2.http.POST + +interface SolanaStakeService { + @POST("/") + suspend fun validators(@Body request: JSONRpcRequest>): Result> + + @POST("/") + suspend fun delegations(@Body request: JSONRpcRequest>): Result>>> + + @POST("/") + suspend fun epoch(@Body request: JSONRpcRequest>): Result> +} + +suspend fun SolanaStakeService.delegations( + owner: String, +): List>? { + val request = JSONRpcRequest.create( + SolanaMethod.GetDelegations, + listOf( + "Stake11111111111111111111111111111111111111", + mapOf( + "encoding" to "jsonParsed", + "commitment" to "finalized", + "filters" to listOf( + mapOf( + "memcmp" to mapOf( + "bytes" to owner, + "offset" to 44, + ) + ) + ) + ) + ) + ) + return delegations(request).getOrNull()?.result +} + +suspend fun SolanaStakeService.getDelegationsBalance(owner: String): Long { + return delegations(owner)?.map { it.account.lamports }?.fold(0L) { acc, v -> acc + v } ?: 0L +} + +suspend fun SolanaStakeService.validators(): List? { + val request = JSONRpcRequest.create( + SolanaMethod.GetValidators, + listOf( + mapOf( + "commitment" to "finalized", + "keepUnstakedDelinquents" to false + ) + ) + ) + return validators(request).getOrNull()?.result?.current +} + +suspend fun SolanaStakeService.epoch(): SolanaEpoch? { + return epoch(JSONRpcRequest.create(SolanaMethod.GetEpoch, emptyList())).getOrNull()?.result +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/sui/SuiBroadcastClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/sui/SuiBroadcastClient.kt index b02a37f6a..b8cad93bb 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/sui/SuiBroadcastClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/sui/SuiBroadcastClient.kt @@ -9,6 +9,7 @@ class SuiBroadcastClient( private val chain: Chain, private val rpcClient: SuiRpcClient, ) : BroadcastClient { + override suspend fun send(account: Account, signedMessage: ByteArray, type: TransactionType): Result { val parts = String(signedMessage).split("_") val data = parts.first() diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/sui/SuiFee.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/sui/SuiFeeCalculator.kt similarity index 70% rename from blockchain/src/main/java/com/gemwallet/android/blockchain/clients/sui/SuiFee.kt rename to blockchain/src/main/java/com/gemwallet/android/blockchain/clients/sui/SuiFeeCalculator.kt index 5be37d8be..03868a0fd 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/sui/SuiFee.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/sui/SuiFeeCalculator.kt @@ -6,15 +6,12 @@ import com.gemwallet.android.model.TxSpeed import com.wallet.core.primitives.Account import com.wallet.core.primitives.AssetId -class SuiFee { - suspend operator fun invoke( - rpcClient: SuiRpcClient, - account: Account, - data: String, - ): Fee { +class SuiFeeCalculator( + private val rpcClient: SuiRpcClient, +) { + suspend fun calculate(account: Account, data: String): Fee { val chain = account.chain - val gasUsed = rpcClient.dryRun(JSONRpcRequest.create(SuiMethod.DryRun, listOf(data))) - .getOrThrow().result.effects.gasUsed + val gasUsed = rpcClient.dryRun(data) val computationCost = gasUsed.computationCost.toBigInteger() val storageCost = gasUsed.storageCost.toBigInteger() val storageRebate = gasUsed.storageRebate.toBigInteger() diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/sui/SuiRpcClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/sui/SuiRpcClient.kt index 9ff23692e..632083334 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/sui/SuiRpcClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/sui/SuiRpcClient.kt @@ -7,6 +7,7 @@ import com.wallet.core.blockchain.sui.SuiBroadcastTransaction import com.wallet.core.blockchain.sui.SuiCoin import com.wallet.core.blockchain.sui.SuiCoinBalance import com.wallet.core.blockchain.sui.SuiData +import com.wallet.core.blockchain.sui.SuiGasUsed import com.wallet.core.blockchain.sui.SuiStakeDelegation import com.wallet.core.blockchain.sui.SuiTransaction import com.wallet.core.blockchain.sui.SuiValidators @@ -16,6 +17,7 @@ import retrofit2.Response import retrofit2.http.Body import retrofit2.http.POST import retrofit2.http.Url +import java.lang.Exception interface SuiRpcClient { @@ -112,4 +114,9 @@ internal suspend fun SuiRpcClient.chainId(url: String): Response> { return latestBlock(url, JSONRpcRequest.create(SuiMethod.LatestCheckpoint, emptyList())) +} + +internal suspend fun SuiRpcClient.dryRun(data: String): SuiGasUsed { + return dryRun(JSONRpcRequest.create(SuiMethod.DryRun, listOf(data))).getOrNull()?.result?.effects?.gasUsed + ?: throw Exception("Can't load SUI gas") } \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/sui/SuiSignClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/sui/SuiSignClient.kt index 537514e6f..581fa7342 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/sui/SuiSignClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/sui/SuiSignClient.kt @@ -13,7 +13,7 @@ class SuiSignClient( private val chain: Chain, ) : SignClient { override suspend fun signTransfer(params: SignerParams, txSpeed: TxSpeed, privateKey: ByteArray): ByteArray { - val metadata = params.info as SuiSignerPreloader.Info + val metadata = params.chainData as SuiSignerPreloader.SuiChainData return signTxDataDigest(metadata.messageBytes, privateKey) } diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/sui/SuiSignerPreloader.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/sui/SuiSignerPreloader.kt index 8d9a8b160..b47b3fdb7 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/sui/SuiSignerPreloader.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/sui/SuiSignerPreloader.kt @@ -1,16 +1,16 @@ package com.gemwallet.android.blockchain.clients.sui -import com.gemwallet.android.blockchain.clients.SignerPreload +import com.gemwallet.android.blockchain.clients.NativeTransferPreloader +import com.gemwallet.android.blockchain.clients.StakeTransactionPreloader +import com.gemwallet.android.blockchain.clients.SwapTransactionPreloader +import com.gemwallet.android.blockchain.clients.TokenTransferPreloader import com.gemwallet.android.blockchain.rpc.model.JSONRpcRequest -import com.gemwallet.android.ext.type import com.gemwallet.android.math.toHexString +import com.gemwallet.android.model.ChainSignData import com.gemwallet.android.model.ConfirmParams import com.gemwallet.android.model.Fee -import com.gemwallet.android.model.SignerInputInfo import com.gemwallet.android.model.SignerParams import com.gemwallet.android.model.TxSpeed -import com.wallet.core.primitives.Account -import com.wallet.core.primitives.AssetSubtype import com.wallet.core.primitives.Chain import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -34,102 +34,40 @@ import java.math.BigInteger class SuiSignerPreloader( private val chain: Chain, private val rpcClient: SuiRpcClient, -) : SignerPreload { +) : NativeTransferPreloader, TokenTransferPreloader, StakeTransactionPreloader, SwapTransactionPreloader { - private val coinId = "0x2::sui::SUI" - - override suspend fun invoke(owner: Account, params: ConfirmParams): Result { - val txData = when (params) { - is ConfirmParams.TransferParams -> when (params.assetId.type()) { - AssetSubtype.NATIVE -> encodeTransfer( - sender = owner.address, - recipient = params.destination.address, - value = params.amount, - coinType = coinId, - sendMax = params.isMax(), - ) - AssetSubtype.TOKEN -> encodeTokenTransfer( - sender = owner.address, - recipient = params.destination.address, - value = params.amount, - coinType = params.assetId.tokenId!!, - gasCoinType = coinId - ) - } - is ConfirmParams.DelegateParams -> encodeStake( - sender = owner.address, - validator = params.validatorId, - coinType = coinId, - value = params.amount, - ) - is ConfirmParams.UndelegateParams -> encodeUnstake( - sender = owner.address, - coinType = coinId, - stakeId = params.delegationId, - ) - is ConfirmParams.SwapParams -> encodeSwap(params) - is ConfirmParams.RewardsParams, - is ConfirmParams.RedeleateParams, - - is ConfirmParams.TokenApprovalParams, - is ConfirmParams.WithdrawParams -> throw java.lang.IllegalArgumentException() - } + private val feeCalculator = SuiFeeCalculator(rpcClient) - val fee = SuiFee().invoke(rpcClient, owner, Base64.encode(txData.txData)) - return Result.success( - SignerParams( - input = params, - owner = owner.address, - info = Info( - messageBytes = "${Base64.encode(txData.txData)}_${txData.hash.toHexString()}", - fee = fee, - ) - ) - ) - } - - override fun supported(chain: Chain): Boolean = this.chain == chain - - private fun gasBudget(coinType: String): BigInteger = BigInteger.valueOf(25_000_000) + private val coinId = "0x2::sui::SUI" - private suspend fun encodeTransfer( - sender: String, - recipient: String, - coinType: String, - value: BigInteger, - sendMax: Boolean - ) = withContext(Dispatchers.IO) { - val getCoins = async { rpcClient.coins(sender, coinType).getOrThrow().result.data } + override suspend fun preloadNativeTransfer(params: ConfirmParams.TransferParams.Native): SignerParams = withContext(Dispatchers.IO) { + val sender = params.from.address + val getCoins = async { rpcClient.coins(sender, coinId).getOrThrow().result.data } val getGasPrice = async { rpcClient.gasPrice(JSONRpcRequest.create(SuiMethod.GasPrice, emptyList())).getOrNull()?.result ?: "750" } val coins = getCoins.await() val gasPrice = getGasPrice.await() val input = SuiTransferInput( sender = sender, - recipient = recipient, - amount = value.toLong().toULong(), + recipient = params.destination.address, + amount = params.amount.toLong().toULong(), coins = coins.map { it.togemstone() }, - sendMax = sendMax, + sendMax = params.isMax(), gas = SuiGas( - budget = gasBudget(coinType).toLong().toULong(), + budget = gasBudget(coinId).toLong().toULong(), price = gasPrice.toULong(), ) ) - suiEncodeTransfer(input) + val data = suiEncodeTransfer(input) + build(params, data) } - - private suspend fun encodeTokenTransfer( - sender: String, - recipient: String, - coinType: String, - gasCoinType: String, - value: BigInteger, - ) = withContext(Dispatchers.IO) { + override suspend fun preloadTokenTransfer(params: ConfirmParams.TransferParams.Token): SignerParams = withContext(Dispatchers.IO) { + val sender = params.from.address val getCoins = async { - rpcClient.coins(sender, coinType).getOrThrow().result.data + rpcClient.coins(sender, params.assetId.tokenId!!).getOrThrow().result.data } val getGasCoins = async { - rpcClient.coins(sender, gasCoinType).getOrThrow().result.data + rpcClient.coins(sender, coinId).getOrThrow().result.data } val getGasPrice = async { rpcClient.gasPrice(JSONRpcRequest.create(SuiMethod.GasPrice, emptyList())).getOrNull()?.result ?: "750" } val coins = getCoins.await() @@ -138,18 +76,60 @@ class SuiSignerPreloader( val gas = gasCoins.firstOrNull() ?: throw IllegalStateException("no gas coin") val input = SuiTokenTransferInput( sender = sender, - recipient = recipient, - amount = value.toLong().toULong(), + recipient = params.destination.address, + amount = params.amount.toLong().toULong(), tokens = coins.map { it.togemstone() }, gas = SuiGas( - budget = gasBudget(gasCoinType).toLong().toULong(), + budget = gasBudget(coinId).toLong().toULong(), price = gasPrice.toULong(), ), gasCoin = gas.togemstone(), ) - suiEncodeTokenTransfer(input) + val data = suiEncodeTokenTransfer(input) + build(params, data) + } + + override suspend fun preloadStake(params: ConfirmParams.Stake): SignerParams { + val data = when (params) { + is ConfirmParams.Stake.DelegateParams -> encodeStake( + sender = params.from.address, + validator = params.validatorId, + coinType = coinId, + value = params.amount, + ) + is ConfirmParams.Stake.UndelegateParams -> encodeUnstake( + sender = params.from.address, + coinType = coinId, + stakeId = params.delegationId, + ) + is ConfirmParams.Stake.RewardsParams, + is ConfirmParams.Stake.RedelegateParams, + is ConfirmParams.Stake.WithdrawParams -> throw IllegalArgumentException("Not supported") + } + return build(params, data) } + override suspend fun preloadSwap(params: ConfirmParams.SwapParams): SignerParams { + val data = suiValidateAndHash(params.swapData) + return build(params, data) + } + + private suspend fun build(params: ConfirmParams, data: SuiTxOutput): SignerParams { + val fee = feeCalculator.calculate(params.from, Base64.encode(data.txData)) + + return SignerParams( + input = params, + chainData = SuiChainData( + messageBytes = "${Base64.encode(data.txData)}_${data.hash.toHexString()}", + fee = fee, + ) + ) + } + + override fun supported(chain: Chain): Boolean = this.chain == chain + + private fun gasBudget(coinType: String): BigInteger = BigInteger.valueOf(25_000_000) + private suspend fun encodeStake( sender: String, coinType: String, @@ -201,17 +181,13 @@ class SuiSignerPreloader( suiEncodeUnstake(input) } - private fun encodeSwap(input: ConfirmParams.SwapParams): SuiTxOutput { - return suiValidateAndHash(input.swapData) - } - - data class Info( + data class SuiChainData( val messageBytes: String, val fee: Fee, - ) : SignerInputInfo { + ) : ChainSignData { override fun fee(speed: TxSpeed): Fee = fee } - + private fun com.wallet.core.blockchain.sui.SuiCoin.togemstone() = SuiCoin( coinType = coinType, balance = balance.toULong(), diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ton/TonFee.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ton/TonFee.kt deleted file mode 100644 index b32f03212..000000000 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ton/TonFee.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.gemwallet.android.blockchain.clients.ton - -import com.gemwallet.android.ext.type -import com.gemwallet.android.model.Fee -import com.gemwallet.android.model.TxSpeed -import com.wallet.core.primitives.AssetId -import com.wallet.core.primitives.AssetSubtype -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.math.BigInteger - -class TonFee { - suspend operator fun invoke( - rpcClient: TonRpcClient, - assetId: AssetId, - destinationAddress: String, - memo: String?, - ): Fee = withContext(Dispatchers.IO) { - when (assetId.type()) { - AssetSubtype.NATIVE -> Fee(TxSpeed.Normal, AssetId(assetId.chain), BigInteger("10000000")) - AssetSubtype.TOKEN -> { - val tokenId = assetId.tokenId!! - val jetonAddress = jettonAddress(rpcClient, tokenId, destinationAddress) - ?: throw Exception("can't get jetton address") - val state = rpcClient.addressState(jetonAddress).getOrNull()?.result == "active" - val tokenAccountFee = if (state) { - if (memo.isNullOrEmpty()) { - BigInteger.valueOf(100_000_000) - } else { - BigInteger.valueOf(60_000_000) // 0.06 - } - } else { - BigInteger.valueOf(300_000_000) - } - Fee(TxSpeed.Normal, AssetId(assetId.chain), BigInteger("10000000"), options = mapOf( - tokenAccountCreationKey to tokenAccountFee - )).withOptions(tokenAccountCreationKey) - } - } - } -} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ton/TonFeeCalculator.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ton/TonFeeCalculator.kt new file mode 100644 index 000000000..9a412ba7f --- /dev/null +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ton/TonFeeCalculator.kt @@ -0,0 +1,40 @@ +package com.gemwallet.android.blockchain.clients.ton + +import com.gemwallet.android.model.Fee +import com.gemwallet.android.model.TxSpeed +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.Chain +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.math.BigInteger + +class TonFeeCalculator( + private val chain: Chain, + private val rpcClient: TonRpcClient, +) { + fun calculateNative() = Fee(TxSpeed.Normal, AssetId(chain), BigInteger("10000000")) + + suspend fun calculateToken(assetId: AssetId, destinationAddress: String, memo: String?): Fee = withContext(Dispatchers.IO) { + val tokenId = assetId.tokenId!! + val jetonAddress = jettonAddress(rpcClient, tokenId, destinationAddress) + ?: throw Exception("can't get jetton address") + val state = rpcClient.addressState(jetonAddress).getOrNull()?.result == "active" + val tokenAccountFee = if (state) { + if (memo.isNullOrEmpty()) { + BigInteger.valueOf(100_000_000) + } else { + BigInteger.valueOf(60_000_000) // 0.06 + } + } else { + BigInteger.valueOf(300_000_000) + } + Fee( + TxSpeed.Normal, + AssetId(assetId.chain), + BigInteger("10000000"), + options = mapOf( + tokenAccountCreationKey to tokenAccountFee + ) + ).withOptions(tokenAccountCreationKey) + } +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ton/TonSignClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ton/TonSignClient.kt index 02f6474b9..7f0af9fd2 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ton/TonSignClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ton/TonSignClient.kt @@ -24,7 +24,7 @@ class TonSignClient( return signToken(params, privateKey) } val signingInput = TheOpenNetwork.SigningInput.newBuilder().apply { - sequenceNumber = (params.info as TonSignerPreloader.Info).sequence + sequenceNumber = (params.chainData as TonSignerPreloader.TonChainData).sequence expireAt = (System.currentTimeMillis() / 1000).toInt() + 600 this.addMessages( TheOpenNetwork.Transfer.newBuilder().apply { @@ -45,12 +45,12 @@ class TonSignClient( override fun supported(chain: Chain): Boolean = this.chain == chain private fun signToken(params: SignerParams, privateKey: ByteArray): ByteArray { - val meta = params.info as TonSignerPreloader.Info + val meta = params.chainData as TonSignerPreloader.TonChainData val jettonTransfer = TheOpenNetwork.JettonTransfer.newBuilder().apply { this.jettonAmount = params.finalAmount.toLong() this.toOwner = params.input.destination()?.address - this.responseAddress = params.owner + this.responseAddress = params.input.from.address this.forwardAmount = 1 }.build() diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ton/TonSignerPreloader.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ton/TonSignerPreloader.kt index 139917f82..8c0ecc06e 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ton/TonSignerPreloader.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/ton/TonSignerPreloader.kt @@ -1,14 +1,12 @@ package com.gemwallet.android.blockchain.clients.ton -import com.gemwallet.android.blockchain.clients.SignerPreload -import com.gemwallet.android.ext.type +import com.gemwallet.android.blockchain.clients.NativeTransferPreloader +import com.gemwallet.android.blockchain.clients.TokenTransferPreloader +import com.gemwallet.android.model.ChainSignData import com.gemwallet.android.model.ConfirmParams import com.gemwallet.android.model.Fee -import com.gemwallet.android.model.SignerInputInfo import com.gemwallet.android.model.SignerParams import com.gemwallet.android.model.TxSpeed -import com.wallet.core.primitives.Account -import com.wallet.core.primitives.AssetSubtype import com.wallet.core.primitives.Chain import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -19,56 +17,41 @@ internal const val tokenAccountCreationKey: String = "tokenAccountCreation" class TonSignerPreloader( private val chain: Chain, private val rpcClient: TonRpcClient, -) : SignerPreload { +) : NativeTransferPreloader, TokenTransferPreloader { - override suspend fun invoke(owner: Account, params: ConfirmParams): Result = withContext(Dispatchers.IO) { - when (params.assetId.type()) { - AssetSubtype.NATIVE -> coinSign(owner, params) - AssetSubtype.TOKEN -> tokenSign(owner, params) - } + private val feeCalculator = TonFeeCalculator(chain, rpcClient) + override suspend fun preloadNativeTransfer(params: ConfirmParams.TransferParams.Native): SignerParams { + val fee = feeCalculator.calculateNative() + val seqno = rpcClient.walletInfo(params.from.address).getOrNull()?.result?.seqno ?: 0 + return SignerParams( + input = params, + chainData = TonChainData(seqno, fee) + ) } - override fun supported(chain: Chain): Boolean = this.chain == chain - - private suspend fun coinSign(owner: Account, params: ConfirmParams): Result { - val fee = TonFee().invoke(rpcClient, params.assetId, params.destination()?.address!!, params.memo()) - return rpcClient.walletInfo(owner.address).mapCatching { - SignerParams( - input = params, - owner = owner.address, - info = Info(sequence = it.result.seqno ?: 0, fee = fee) - ) - } - } + override suspend fun preloadTokenTransfer(params: ConfirmParams.TransferParams.Token): SignerParams = withContext(Dispatchers.IO) { + val getWalletInfo = async { rpcClient.walletInfo(params.from.address).getOrNull() } + val getJettonAddress = async { jettonAddress(rpcClient, params.assetId.tokenId!!, params.from.address) } + val getFee = async { feeCalculator.calculateToken(params.assetId, params.destination().address, params.memo()) } - private suspend fun tokenSign(owner: Account, params: ConfirmParams): Result = withContext(Dispatchers.IO) { - val getWalletInfo = async { rpcClient.walletInfo(owner.address).getOrNull() } - val getJettonAddress = async { jettonAddress(rpcClient, params.assetId.tokenId!!, owner.address) } - val feeJob = async { TonFee().invoke(rpcClient, params.assetId, params.destination()?.address!!, params.memo()) } - val walletInfo = getWalletInfo.await() - ?: return@withContext Result.failure(Exception("can't get wallet info. check internet.")) - val jettonAddress = getJettonAddress.await() ?: return@withContext Result.failure(Exception("can't get jetton address. check internet.")) - val fee = feeJob.await() + val seqno = getWalletInfo.await()?.result?.seqno ?: throw Exception("can't get wallet info. check internet.") + val jettonAddress = getJettonAddress.await() ?: throw Exception("can't get jetton address. check internet.") + val fee = getFee.await() - val signerParams = SignerParams( + SignerParams( input = params, - owner = owner.address, - info = Info( - sequence = walletInfo.result.seqno ?: 0, - jettonAddress = jettonAddress, - fee = fee, - ) + chainData = TonChainData(seqno, fee, jettonAddress) ) - - Result.success(signerParams) } - data class Info( + override fun supported(chain: Chain): Boolean = this.chain == chain + + data class TonChainData( val sequence: Int, - val jettonAddress: String? = null, val fee: Fee, - ) : SignerInputInfo { + val jettonAddress: String? = null, + ) : ChainSignData { override fun fee(speed: TxSpeed): Fee = fee } } \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/tron/TronFee.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/tron/TronFeeCalculator.kt similarity index 61% rename from blockchain/src/main/java/com/gemwallet/android/blockchain/clients/tron/TronFee.kt rename to blockchain/src/main/java/com/gemwallet/android/blockchain/clients/tron/TronFeeCalculator.kt index fe343262e..a69291c02 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/tron/TronFee.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/tron/TronFeeCalculator.kt @@ -1,12 +1,13 @@ package com.gemwallet.android.blockchain.clients.tron +import com.gemwallet.android.ext.type import com.gemwallet.android.math.toHexString +import com.gemwallet.android.model.ConfirmParams import com.gemwallet.android.model.Fee import com.gemwallet.android.model.TxSpeed -import com.wallet.core.blockchain.tron.models.TronAccountRequest -import com.wallet.core.primitives.Account import com.wallet.core.primitives.AssetId import com.wallet.core.primitives.AssetSubtype +import com.wallet.core.primitives.Chain import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.withContext @@ -14,47 +15,31 @@ import wallet.core.jni.Base58 import java.math.BigDecimal import java.math.BigInteger -class TronFee { - suspend operator fun invoke( - rpcClient: TronRpcClient, - account: Account, - contractAddress: String?, - recipientAddress: String, - value: BigInteger, - type: AssetSubtype, - ): Fee = withContext(Dispatchers.IO) { - val isNewAccountJob = async { - rpcClient.getAccount( - TronAccountRequest( - address = Base58.decode(account.address).toHexString(""), - visible = false - ) - ).fold({ it.address.isNullOrEmpty() }) { true } - } - val accountUsageJob = async { - rpcClient.getAccountUsage( - TronAccountRequest( - address = Base58.decode(account.address).toHexString(""), - visible = false - ) - ).getOrNull() - } +class TronFeeCalculator( + private val chain: Chain, + private val rpcClient: TronRpcClient, + +) { + suspend fun calculate(inParams: ConfirmParams.TransferParams) = withContext(Dispatchers.IO) { + val getIsNewAccount = async { rpcClient.getAccount(inParams.from.address) } + val getAccountUsage = async { rpcClient.getAccountUsage(inParams.from.address) } + val getParams = async { rpcClient.getChainParameters().fold({ it.chainParameter }) { null } } - val paramsJob = async { rpcClient.getChainParameters().fold({ it.chainParameter }) {null} } - val isNewAccount = isNewAccountJob.await() - val params = paramsJob.await() - val accountUsage = accountUsageJob.await() + val isNewAccount = getIsNewAccount.await() + val params = getParams.await() + val accountUsage = getAccountUsage.await() val newAccountFeeInSmartContract = params?.firstOrNull { it.key == "getCreateNewAccountFeeInSystemContract" }?.value val newAccountFee = params?.firstOrNull{ it.key == "getCreateAccountFee" }?.value val energyFee = params?.firstOrNull { it.key == "getEnergyFee" }?.value + if (newAccountFeeInSmartContract == null || newAccountFee == null || energyFee == null) { - throw Exception("unknown key") + throw Exception("Tron unknown key") } - val fee = when (type) { + + val fee = when (inParams.assetId.type()) { AssetSubtype.NATIVE -> { - val availableBandwidth = - accountUsage?.freeNetLimit ?: (0 - (accountUsage?.freeNetUsed ?: 0)) + val availableBandwidth = accountUsage?.freeNetLimit ?: (0 - (accountUsage?.freeNetUsed ?: 0)) val coinTransferFee = if (availableBandwidth >= 300) BigInteger.ZERO else BigInteger.valueOf(280_000) if (isNewAccount) coinTransferFee + BigInteger.valueOf(newAccountFee) else coinTransferFee } @@ -62,16 +47,16 @@ class TronFee { // https://developers.tron.network/docs/set-feelimit#how-to-estimate-energy-consumption val gasLimit = estimateTRC20Transfer( rpcClient = rpcClient, - ownerAddress = account.address, - recipientAddress = recipientAddress, - contractAddress = contractAddress ?: throw IllegalArgumentException("Incorrect contract on fee calculation"), - value = value, + ownerAddress = inParams.from.address, + recipientAddress = inParams.destination.address, + contractAddress = inParams.assetId.tokenId ?: throw IllegalArgumentException("Incorrect contract on fee calculation"), + value = inParams.amount, ) val tokenTransfer = BigInteger.valueOf(energyFee) * gasLimit.add(gasLimit.multiply(BigDecimal("0.2"))).toBigInteger() if (isNewAccount) tokenTransfer + BigInteger.valueOf(newAccountFeeInSmartContract) else tokenTransfer } } - Fee(TxSpeed.Normal, AssetId(account.chain), fee) + Fee(TxSpeed.Normal, AssetId(chain), fee) } // https://developers.tron.network/docs/set-feelimit#how-to-estimate-energy-consumption diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/tron/TronRpcClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/tron/TronRpcClient.kt index 8f0aa5502..5eb83e72c 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/tron/TronRpcClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/tron/TronRpcClient.kt @@ -1,5 +1,6 @@ package com.gemwallet.android.blockchain.clients.tron +import com.gemwallet.android.math.toHexString import com.wallet.core.blockchain.tron.models.TronAccount import com.wallet.core.blockchain.tron.models.TronAccountRequest import com.wallet.core.blockchain.tron.models.TronAccountUsage @@ -14,6 +15,7 @@ import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST import retrofit2.http.Url +import wallet.core.jni.Base58 interface TronRpcClient { @POST("/wallet/getnowblock") @@ -63,3 +65,25 @@ suspend fun TronRpcClient.triggerSmartContract( ) return triggerSmartContract(call) } + +suspend fun TronRpcClient.getAccount(address: String): Boolean { + return try { + getAccount( + TronAccountRequest( + address = Base58.decode(address).toHexString(""), + visible = false + ) + ).getOrNull()?.address.isNullOrEmpty() + } catch (_: Throwable) { + true + } +} + +suspend fun TronRpcClient.getAccountUsage(address: String): TronAccountUsage? { + return getAccountUsage( + TronAccountRequest( + address = Base58.decode(address).toHexString(""), + visible = false + ) + ).getOrNull() +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/tron/TronSignClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/tron/TronSignClient.kt index ed51e59d6..578a55ce7 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/tron/TronSignClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/tron/TronSignClient.kt @@ -22,7 +22,7 @@ class TronSignClient( txSpeed: TxSpeed, privateKey: ByteArray ): ByteArray { - val blockInfo = params.info as TronSignerPreloader.Info + val blockInfo = params.chainData as TronSignerPreloader.TronChainData val transaction = Tron.Transaction.newBuilder().apply { this.blockHeader = Tron.BlockHeader.newBuilder().apply { this.number = blockInfo.number @@ -33,11 +33,11 @@ class TronSignClient( this.txTrieRoot = ByteString.copyFrom(blockInfo.txTrieRoot.decodeHex()) }.build() when (params.input.assetId.type()) { - AssetSubtype.NATIVE -> this.transfer = getTransferContract(params.finalAmount, params.owner, params.input.destination()?.address ?: "") + AssetSubtype.NATIVE -> this.transfer = getTransferContract(params.finalAmount, params.input.from.address, params.input.destination()?.address ?: "") AssetSubtype.TOKEN -> this.transferTrc20Contract = getTransferTRC20Contract( params.input.assetId.tokenId!!, params.finalAmount, - params.owner, + params.input.from.address, params.input.destination()?.address ?: "" ) else -> throw IllegalArgumentException("Unsupported type") diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/tron/TronSignerPreloader.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/tron/TronSignerPreloader.kt index 7751a23a5..a145b9c46 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/tron/TronSignerPreloader.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/tron/TronSignerPreloader.kt @@ -1,13 +1,12 @@ package com.gemwallet.android.blockchain.clients.tron -import com.gemwallet.android.blockchain.clients.SignerPreload -import com.gemwallet.android.ext.type +import com.gemwallet.android.blockchain.clients.NativeTransferPreloader +import com.gemwallet.android.blockchain.clients.TokenTransferPreloader +import com.gemwallet.android.model.ChainSignData import com.gemwallet.android.model.ConfirmParams import com.gemwallet.android.model.Fee -import com.gemwallet.android.model.SignerInputInfo import com.gemwallet.android.model.SignerParams import com.gemwallet.android.model.TxSpeed -import com.wallet.core.primitives.Account import com.wallet.core.primitives.Chain import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -16,47 +15,41 @@ import kotlinx.coroutines.withContext class TronSignerPreloader( private val chain: Chain, private val rpcClient: TronRpcClient, -) : SignerPreload { - override suspend fun invoke(owner: Account, params: ConfirmParams): Result = withContext(Dispatchers.IO) { - val feeJob = async { - try { - TronFee().invoke( - rpcClient = rpcClient, - account = owner, - recipientAddress = params.destination()?.address!!, - value = params.amount, - contractAddress = params.assetId.tokenId, - type = params.assetId.type(), - ) - } catch (err: Throwable) { - null - } - } - val nowBlockJob = async { rpcClient.nowBlock() } +) : NativeTransferPreloader, TokenTransferPreloader { + val feeCalculator = TronFeeCalculator(chain, rpcClient) - val fee = feeJob.await() ?: return@withContext Result.failure(Exception("Fee calculation error")) - val nowBlock = nowBlockJob.await() + override suspend fun preloadNativeTransfer(params: ConfirmParams.TransferParams.Native): SignerParams { + return preloadTransfer(params) + } - nowBlock.mapCatching { - SignerParams( - input = params, - owner = owner.address, - info = Info( - number = it.block_header.raw_data.number, - version = it.block_header.raw_data.version, - txTrieRoot = it.block_header.raw_data.txTrieRoot, - witnessAddress = it.block_header.raw_data.witness_address, - parentHash = it.block_header.raw_data.parentHash, - timestamp = it.block_header.raw_data.timestamp, - fee = fee, - ) - ) - } + override suspend fun preloadTokenTransfer(params: ConfirmParams.TransferParams.Token): SignerParams { + return preloadTransfer(params) } override fun supported(chain: Chain): Boolean = this.chain == chain - data class Info( + private suspend fun preloadTransfer(params: ConfirmParams.TransferParams): SignerParams = withContext(Dispatchers.IO) { + val feeJob = async { feeCalculator.calculate(params) } + val nowBlockJob = async { rpcClient.nowBlock() } + + val fee = feeJob.await() + val nowBlock = nowBlockJob.await().getOrThrow() + + SignerParams( + input = params, + chainData = TronChainData( + number = nowBlock.block_header.raw_data.number, + version = nowBlock.block_header.raw_data.version, + txTrieRoot = nowBlock.block_header.raw_data.txTrieRoot, + witnessAddress = nowBlock.block_header.raw_data.witness_address, + parentHash = nowBlock.block_header.raw_data.parentHash, + timestamp = nowBlock.block_header.raw_data.timestamp, + fee = fee, + ) + ) + } + + data class TronChainData( val number: Long, val version: Long, val txTrieRoot: String, @@ -64,7 +57,7 @@ class TronSignerPreloader( val parentHash: String, val timestamp: Long, val fee: Fee, - ) : SignerInputInfo { + ) : ChainSignData { override fun fee(speed: TxSpeed): Fee = fee } } \ No newline at end of file diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/xrp/XrpFee.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/xrp/XrpFeeCalculator.kt similarity index 75% rename from blockchain/src/main/java/com/gemwallet/android/blockchain/clients/xrp/XrpFee.kt rename to blockchain/src/main/java/com/gemwallet/android/blockchain/clients/xrp/XrpFeeCalculator.kt index 63ff7ce61..1088b6cd6 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/xrp/XrpFee.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/xrp/XrpFeeCalculator.kt @@ -5,11 +5,11 @@ import com.gemwallet.android.model.TxSpeed import com.wallet.core.primitives.AssetId import com.wallet.core.primitives.Chain -class XrpFee { - suspend operator fun invoke( - chain: Chain, - rpcClient: XrpRpcClient, - ): Fee { +class XrpFeeCalculator( + private val chain: Chain, + private val rpcClient: XrpRpcClient, +) { + suspend fun calculate(): Fee { val median = rpcClient.fee().getOrThrow().result.drops.median_fee.toBigInteger() return Fee(feeAssetId = AssetId(chain), speed = TxSpeed.Normal, amount = median) } diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/xrp/XrpSignClient.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/xrp/XrpSignClient.kt index de50baa2a..03e44bc1f 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/xrp/XrpSignClient.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/xrp/XrpSignClient.kt @@ -13,16 +13,16 @@ class XrpSignClient( private val chain: Chain, ) : SignClient { override suspend fun signTransfer(params: SignerParams, txSpeed: TxSpeed, privateKey: ByteArray): ByteArray { - val metadata = params.info as XrpSignerPreloader.Info + val metadata = params.chainData as XrpSignerPreloader.XrpChainData val signInput = Ripple.SigningInput.newBuilder().apply { this.fee = metadata.fee().amount.toLong() this.sequence = metadata.sequence - this.account = params.owner + this.account = params.input.from.address this.privateKey = ByteString.copyFrom(privateKey) this.opPayment = Ripple.OperationPayment.newBuilder().apply { this.destination = params.input.destination()?.address ?: "" this.amount = params.finalAmount.toLong() - this.destinationTag = try { params.input.memo()?.toLong() ?: 0L } catch (err: Throwable) { 0L } + this.destinationTag = try { params.input.memo()?.toLong() ?: 0L } catch (_: Throwable) { 0L } }.build() }.build() val output = AnySigner.sign(signInput, WCChainTypeProxy().invoke(chain), Ripple.SigningOutput.parser()) diff --git a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/xrp/XrpSignerPreloader.kt b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/xrp/XrpSignerPreloader.kt index f880fcb1b..512634e66 100644 --- a/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/xrp/XrpSignerPreloader.kt +++ b/blockchain/src/main/java/com/gemwallet/android/blockchain/clients/xrp/XrpSignerPreloader.kt @@ -1,12 +1,11 @@ package com.gemwallet.android.blockchain.clients.xrp -import com.gemwallet.android.blockchain.clients.SignerPreload +import com.gemwallet.android.blockchain.clients.NativeTransferPreloader import com.gemwallet.android.model.ConfirmParams import com.gemwallet.android.model.Fee -import com.gemwallet.android.model.SignerInputInfo +import com.gemwallet.android.model.ChainSignData import com.gemwallet.android.model.SignerParams import com.gemwallet.android.model.TxSpeed -import com.wallet.core.primitives.Account import com.wallet.core.primitives.Chain import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -15,33 +14,27 @@ import kotlinx.coroutines.withContext class XrpSignerPreloader( private val chain: Chain, private val rpcClient: XrpRpcClient, -) : SignerPreload { - override suspend fun invoke(owner: Account, params: ConfirmParams): Result = withContext(Dispatchers.IO) { - val (sequenceJob, feeJob) = Pair ( - async { rpcClient.account(owner.address) }, - async { XrpFee().invoke(chain, rpcClient) } +) : NativeTransferPreloader { + + private val feeCalculator = XrpFeeCalculator(chain, rpcClient) + + override suspend fun preloadNativeTransfer(params: ConfirmParams.TransferParams.Native): SignerParams = withContext(Dispatchers.IO) { + val (getSequence, getFee) = Pair ( + async { rpcClient.account(params.from.address).getOrNull()?.result?.account_data?.Sequence }, + async { feeCalculator.calculate() } ) - val (sequenceResult, fee) = Pair(sequenceJob.await(), feeJob.await()) - sequenceResult.mapCatching { - val sequence = it.result.account_data.Sequence - SignerParams( - input = params, - owner = owner.address, - info = Info( - sequence = sequence, - fee = fee, - ) - ) + val sequence = getSequence.await() ?: throw Exception("Sequence doesn't available") + val fee = getFee.await() - } + SignerParams(params, XrpChainData(sequence, fee)) } override fun supported(chain: Chain): Boolean = this.chain == chain - data class Info( + data class XrpChainData( val sequence: Int, val fee: Fee, - ) : SignerInputInfo { + ) : ChainSignData { override fun fee(speed: TxSpeed): Fee = fee } } \ No newline at end of file diff --git a/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/aptos/Data.kt b/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/aptos/Data.kt new file mode 100644 index 000000000..ad078cdb5 --- /dev/null +++ b/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/aptos/Data.kt @@ -0,0 +1,15 @@ +package com.gemwallet.android.blockchain.clients.aptos + +import com.gemwallet.android.blockchain.clients.aptos.model.AptosAccount +import com.wallet.core.blockchain.aptos.models.AptosGasFee + +internal val aptosAccountResponse = AptosAccount( + sequence_number = "8", + message = null, + error_code = null, +) + +internal val aptosFeeResponse = AptosGasFee( + gas_estimate = 100, + prioritized_gas_estimate = 150, +) \ No newline at end of file diff --git a/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/aptos/TestAptosBroadcast.kt b/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/aptos/TestAptosBroadcast.kt new file mode 100644 index 000000000..16b31d0e9 --- /dev/null +++ b/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/aptos/TestAptosBroadcast.kt @@ -0,0 +1,77 @@ +package com.gemwallet.android.blockchain.clients.aptos + +import com.gemwallet.android.blockchain.Mime +import com.gemwallet.android.blockchain.clients.aptos.services.AptosBroadcastService +import com.gemwallet.android.math.decodeHex +import com.wallet.core.blockchain.aptos.models.AptosTransactionBroacast +import com.wallet.core.primitives.Account +import com.wallet.core.primitives.Chain +import com.wallet.core.primitives.TransactionType +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.runBlocking +import okhttp3.RequestBody +import okio.Buffer +import org.junit.Test + +class TestAptosBroadcast { + + val sign: ByteArray = ("0x7b2265787069726174696f6e5f74696d657374616d705f73656373223a223336363" + + "4333930303832222c226761735f756e69745f7072696365223a22313530222c226d61785f6761735f616" + + "d6f756e74223a223138222c227061796c6f6164223a7b22617267756d656e7473223a5b2230783832313" + + "131663239373561306636303830643137383233363336396237343739663661656431323033656634613" + + "2336638323035653462393137313662373833222c223130303030303030303030225d2c2266756e63746" + + "96f6e223a223078313a3a6170746f735f6163636f756e743a3a7472616e73666572222c2274797065223" + + "a22656e7472795f66756e6374696f6e5f7061796c6f6164222c22747970655f617267756d656e7473223" + + "a5b5d7d2c2273656e646572223a223078396231646238313138306333316231623432383537326265313" + + "03565323039623561363232326237222c2273657175656e63655f6e756d626572223a2238222c2273696" + + "76e6174757265223a7b227075626c69635f6b6579223a223078633163343364356164646665316332333" + + "761646164363937326435663338663762393631353661636663366637656664386662346435333037653" + + "06365383861222c227369676e6174757265223a223078663063666439373630396263656639636661646" + + "239306264386566363565313432643862393039666537306137383539393434323330656234306566353" + + "061623236306130623931663935633632326364636537636637313835323365356565623532366433666" + + "1356438393635646431333966333637303930653562303031222c2274797065223a22656432353531395" + + "f7369676e6174757265227d7d").decodeHex() + val actualSendingData = "{" + + "\"expiration_timestamp_secs\":\"3664390082\"," + + "\"gas_unit_price\":\"150\"," + + "\"max_gas_amount\":\"18\"," + + "\"payload\":{" + + "\"arguments\":[\"0x82111f2975a0f6080d178236369b7479f6aed1203ef4a23f8205e4b91716b783\",\"10000000000\"]," + + "\"function\":\"0x1::aptos_account::transfer\",\"type\":\"entry_function_payload\",\"type_arguments\":[]}," + + "\"sender\":\"0x9b1db81180c31b1b428572be105e209b5a6222b7\"," + + "\"sequence_number\":\"8\"," + + "\"signature\":{" + + "\"public_key\":\"0xc1c43d5addfe1c237adad6972d5f38f7b96156acfc6f7efd8fb4d5307e0ce88a\"," + + "\"signature\":\"0xf0cfd97609bcef9cfadb90bd8ef65e142d8b909fe70a7859944230eb40ef50ab260a0b91f95c622cdce7cf718523e5eeb526d3fa5d8965dd139f367090e5b001\"," + + "\"type\":\"ed25519_signature\"}}" + + @Test + fun testAptosBroadcast() { + var sendingData: String = "" + val broadcastClient = AptosBroadcastClient( + Chain.Aptos, + object : AptosBroadcastService { + override suspend fun broadcast(request: RequestBody): Result { + assertEquals(Mime.Json.value, request.contentType()) + val buffer = Buffer() + request.writeTo(buffer) + sendingData = String(buffer.inputStream().readAllBytes()) + return Result.success( + AptosTransactionBroacast(hash = "some hash") + ) + } + } + + ) + val result = runBlocking { + broadcastClient.send( + Account(Chain.Aptos, "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", ""), + sign, + TransactionType.Transfer, + ) + } + + assertEquals(actualSendingData, sendingData) + assertEquals("some hash", result.getOrNull()) + } +} \ No newline at end of file diff --git a/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/aptos/TestAptosFeeCalculator.kt b/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/aptos/TestAptosFeeCalculator.kt new file mode 100644 index 000000000..644d2f0af --- /dev/null +++ b/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/aptos/TestAptosFeeCalculator.kt @@ -0,0 +1,52 @@ +package com.gemwallet.android.blockchain.clients.aptos + +import com.gemwallet.android.blockchain.clients.aptos.model.AptosAccount +import com.gemwallet.android.blockchain.clients.aptos.services.AptosAccountsService +import com.gemwallet.android.blockchain.clients.aptos.services.AptosFeeService +import com.gemwallet.android.model.ConfirmParams +import com.gemwallet.android.model.DestinationAddress +import com.wallet.core.blockchain.aptos.models.AptosGasFee +import com.wallet.core.primitives.Account +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.Chain +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.runBlocking +import org.junit.Test +import java.math.BigInteger + +class TestAptosFeeCalculator { + @Test + fun testAptosFeeAccountError() { + val preloader = AptosSignerPreloader( + chain = Chain.Aptos, + accountsService = object : AptosAccountsService { + override suspend fun accounts(address: String): Result { + return Result.success(aptosAccountResponse.copy(sequence_number = null, error_code = null, message = "Error message")) + } + + }, + feeService = object : AptosFeeService { + override suspend fun feePrice(): Result { + return Result.success(aptosFeeResponse) + } + } + ) + try { + runBlocking { + preloader.preloadNativeTransfer( + params = ConfirmParams.TransferParams.Native( + assetId = AssetId(Chain.Aptos), + from = Account(Chain.Aptos, "0x80c3cca35602e4568a7ac88d4d91110f8efa6c45c659439c2b4ed04033059c6f", ""), + amount = BigInteger.valueOf(10_000_000_000), + destination = DestinationAddress("0xd7257c62806cea85fc8eaf947377b672fe062b81e6c0b19b6d8a3f408e59cf8c"), + isMaxAmount = false + ) + ) + } + assertTrue(false) + } catch (err: Throwable) { + assertEquals("Error message", err.message) + } + } +} \ No newline at end of file diff --git a/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/aptos/TestAptosSignerPreloader.kt b/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/aptos/TestAptosSignerPreloader.kt new file mode 100644 index 000000000..d0bc5d730 --- /dev/null +++ b/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/aptos/TestAptosSignerPreloader.kt @@ -0,0 +1,136 @@ +package com.gemwallet.android.blockchain.clients.aptos + +import com.gemwallet.android.blockchain.clients.aptos.model.AptosAccount +import com.gemwallet.android.blockchain.clients.aptos.services.AptosAccountsService +import com.gemwallet.android.blockchain.clients.aptos.services.AptosFeeService +import com.gemwallet.android.ext.toIdentifier +import com.gemwallet.android.model.ConfirmParams +import com.gemwallet.android.model.DestinationAddress +import com.gemwallet.android.model.GasFee +import com.gemwallet.android.model.TxSpeed +import com.wallet.core.blockchain.aptos.models.AptosGasFee +import com.wallet.core.primitives.Account +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.Chain +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.runBlocking +import org.junit.Test +import java.math.BigInteger + +class TestAptosSignerPreloader { + + @Test + fun testAptosPreload() { + var serviceAddressParam = mutableListOf() + val preloader = AptosSignerPreloader( + chain = Chain.Aptos, + accountsService = object : AptosAccountsService { + override suspend fun accounts(address: String): Result { + serviceAddressParam.add(address) + return Result.success(aptosAccountResponse) + } + + }, + feeService = object : AptosFeeService { + override suspend fun feePrice(): Result { + return Result.success(aptosFeeResponse) + } + } + ) + val result = runBlocking { + preloader.preloadNativeTransfer( + params = ConfirmParams.TransferParams.Native( + assetId = AssetId(Chain.Aptos), + from = Account(Chain.Aptos, "0x80c3cca35602e4568a7ac88d4d91110f8efa6c45c659439c2b4ed04033059c6f", ""), + amount = BigInteger.valueOf(10_000_000_000), + destination = DestinationAddress("0xd7257c62806cea85fc8eaf947377b672fe062b81e6c0b19b6d8a3f408e59cf8c"), + isMaxAmount = false + ) + ) + } + assertEquals("0x80c3cca35602e4568a7ac88d4d91110f8efa6c45c659439c2b4ed04033059c6f", serviceAddressParam[0]) + assertEquals("0xd7257c62806cea85fc8eaf947377b672fe062b81e6c0b19b6d8a3f408e59cf8c", serviceAddressParam[1]) + assertEquals(BigInteger.valueOf(10_000_000_000), result.input.amount) + assertEquals("0x80c3cca35602e4568a7ac88d4d91110f8efa6c45c659439c2b4ed04033059c6f", result.input.from.address) + assertEquals(AssetId(Chain.Aptos).toIdentifier(), result.input.assetId.toIdentifier()) + assertEquals(false, result.input.isMax()) + assertEquals("0xd7257c62806cea85fc8eaf947377b672fe062b81e6c0b19b6d8a3f408e59cf8c", result.input.destination()?.address) + assertEquals(null, result.input.memo()) + assertEquals(BigInteger.valueOf(2700L), result.chainData.fee().amount) + assertEquals(BigInteger.valueOf(150L), (result.chainData.fee() as GasFee).maxGasPrice) + assertEquals(BigInteger.valueOf(18L), (result.chainData.fee() as GasFee).limit) + assertEquals(AssetId(Chain.Aptos).toIdentifier(), result.chainData.fee().feeAssetId.toIdentifier()) + assertEquals(TxSpeed.Normal, result.chainData.fee().speed) + assertEquals(8L, (result.chainData as AptosSignerPreloader.AptosChainData).sequence) + } + + @Test + fun testAptosEmptyAccountPreload() { + val preloader = AptosSignerPreloader( + chain = Chain.Aptos, + accountsService = object : AptosAccountsService { + override suspend fun accounts(address: String): Result { + return Result.success(aptosAccountResponse.copy(sequence_number = null, error_code = "account_not_found")) + } + + }, + feeService = object : AptosFeeService { + override suspend fun feePrice(): Result { + return Result.success(aptosFeeResponse) + } + } + ) + val result = runBlocking { + preloader.preloadNativeTransfer( + params = ConfirmParams.TransferParams.Native( + assetId = AssetId(Chain.Aptos), + from = Account(Chain.Aptos, "0x80c3cca35602e4568a7ac88d4d91110f8efa6c45c659439c2b4ed04033059c6f", ""), + amount = BigInteger.valueOf(10_000_000_000), + destination = DestinationAddress("0xd7257c62806cea85fc8eaf947377b672fe062b81e6c0b19b6d8a3f408e59cf8c"), + isMaxAmount = false + ) + ) + } + assertEquals(BigInteger.valueOf(202800L), result.chainData.fee().amount) + assertEquals(BigInteger.valueOf(150L), (result.chainData.fee() as GasFee).maxGasPrice) + assertEquals(BigInteger.valueOf(1352L), (result.chainData.fee() as GasFee).limit) + assertEquals(AssetId(Chain.Aptos).toIdentifier(), result.chainData.fee().feeAssetId.toIdentifier()) + assertEquals(TxSpeed.Normal, result.chainData.fee().speed) + assertEquals(0L, (result.chainData as AptosSignerPreloader.AptosChainData).sequence) + } + + @Test + fun testAptosFeeFail() { + val preloader = AptosSignerPreloader( + chain = Chain.Aptos, + accountsService = object : AptosAccountsService { + override suspend fun accounts(address: String): Result { + return Result.success(aptosAccountResponse.copy(sequence_number = null, error_code = null, message = "Message")) + } + + }, + feeService = object : AptosFeeService { + override suspend fun feePrice(): Result { + return Result.success(aptosFeeResponse) + } + } + ) + try { + runBlocking { + preloader.preloadNativeTransfer( + params = ConfirmParams.TransferParams.Native( + assetId = AssetId(Chain.Aptos), + from = Account(Chain.Aptos, "0x80c3cca35602e4568a7ac88d4d91110f8efa6c45c659439c2b4ed04033059c6f", ""), + amount = BigInteger.valueOf(10_000_000_000), + destination = DestinationAddress("0xd7257c62806cea85fc8eaf947377b672fe062b81e6c0b19b6d8a3f408e59cf8c"), + isMaxAmount = false + ) + ) + } + assertTrue(false) + } catch (err: Throwable) { + assertEquals("Message", err.message) + } + } +} \ No newline at end of file diff --git a/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/aptos/TestAptosTransactions.kt b/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/aptos/TestAptosTransactions.kt new file mode 100644 index 000000000..082094844 --- /dev/null +++ b/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/aptos/TestAptosTransactions.kt @@ -0,0 +1,74 @@ +package com.gemwallet.android.blockchain.clients.aptos + +import com.gemwallet.android.blockchain.clients.aptos.services.AptosTransactionsService +import com.wallet.core.blockchain.aptos.models.AptosTransaction +import com.wallet.core.primitives.Chain +import com.wallet.core.primitives.TransactionState +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import kotlinx.coroutines.runBlocking +import org.junit.Test +import java.math.BigInteger + +class TestAptosTransactions { + + @Test + fun testAptosTransaction() { + var requestId: String = "" + val transactionsClient = AptosTransactionStatusClient( + Chain.Aptos, + object : AptosTransactionsService { + override suspend fun transactions(txId: String): Result { + requestId = txId + return Result.success( + AptosTransaction( + success = true, + gas_used = "100", + gas_unit_price = "10" + ) + ) + } + } + ) + val result = runBlocking { + transactionsClient.getStatus( + Chain.Aptos, + "some_address", + "some_id" + ) + }.getOrNull() + assertNotNull(result) + assertEquals("some_id", requestId) + assertEquals(TransactionState.Confirmed, result!!.state) + assertEquals(BigInteger("1000"), result.fee) + } + + @Test + fun testAptosTransactionFail() { + var requestId: String = "" + val transactionsClient = AptosTransactionStatusClient( + Chain.Aptos, + object : AptosTransactionsService { + override suspend fun transactions(txId: String): Result { + requestId = txId + return Result.success( + AptosTransaction( + success = false, + gas_used = "100", + gas_unit_price = "10" + ) + ) + } + } + ) + val result = runBlocking { + transactionsClient.getStatus( + Chain.Aptos, + "some_address", + "some_id" + ) + }.getOrNull() + assertNotNull(result) + assertEquals(TransactionState.Reverted, result!!.state) + } +} \ No newline at end of file diff --git a/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/bitcoin/TestBitcoinBroadcast.kt b/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/bitcoin/TestBitcoinBroadcast.kt new file mode 100644 index 000000000..8f45dbbff --- /dev/null +++ b/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/bitcoin/TestBitcoinBroadcast.kt @@ -0,0 +1,54 @@ +package com.gemwallet.android.blockchain.clients.bitcoin + +import com.gemwallet.android.blockchain.Mime +import com.gemwallet.android.blockchain.clients.bitcoin.services.BitcoinBroadcastService +import com.gemwallet.android.math.decodeHex +import com.wallet.core.blockchain.bitcoin.models.BitcoinTransactionBroacastResult +import com.wallet.core.primitives.Account +import com.wallet.core.primitives.Chain +import com.wallet.core.primitives.TransactionType +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.runBlocking +import okhttp3.RequestBody +import okio.Buffer +import org.junit.Test + +class TestBitcoinBroadcast { + + val sign = "010000000153bd8a94cf6424d8e54cacbad6082c249cdacdd0956f766206a" + + "c3daed5e5479f010000006b483045022100ead2b7637532b167e66ddf76eafd032ada898c8719a" + + "680de8183b71fb3086a3e022055a073be9148cb8b9dc71de04755dbf11f140fee167ed0d4b44d6" + + "a54a829e75e012102fd6585adc0e86019abf00e83552d054cb5f4359ad4db8ca338099381f43e2" + + "5a5000000000182a82005000000001976a91424849c1d94eb9e6e002dd75fdcbce0a9673daba78" + + "8ac00000000" + + @Test + fun testBitcoinBroadcast() { + var sendingData: String = "" + val broadcastClient = BitcoinBroadcastClient( + Chain.Bitcoin, + object : BitcoinBroadcastService { + override suspend fun broadcast(request: RequestBody): Result { + assertEquals(Mime.Plain.value, request.contentType()) + val buffer = Buffer() + request.writeTo(buffer) + sendingData = String(buffer.inputStream().readAllBytes()) + return Result.success( + BitcoinTransactionBroacastResult(result = "some hash") + ) + } + } + + ) + val result = runBlocking { + broadcastClient.send( + Account(Chain.Bitcoin, "0x9b1DB81180c31B1b428572Be105E209b5A6222b7", ""), + sign.decodeHex(), + TransactionType.Transfer, + ) + } + + assertEquals(sign, sendingData) + assertEquals("some hash", result.getOrNull()) + } +} \ No newline at end of file diff --git a/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/bitcoin/TestBitcoinTransactions.kt b/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/bitcoin/TestBitcoinTransactions.kt new file mode 100644 index 000000000..3b7c9fae1 --- /dev/null +++ b/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/bitcoin/TestBitcoinTransactions.kt @@ -0,0 +1,52 @@ +package com.gemwallet.android.blockchain.clients.bitcoin + +import com.gemwallet.android.blockchain.clients.bitcoin.services.BitcoinTransactionsService +import com.wallet.core.blockchain.bitcoin.models.BitcoinTransaction +import com.wallet.core.primitives.Chain +import com.wallet.core.primitives.TransactionState +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import kotlinx.coroutines.runBlocking +import org.junit.Test + +class TestBitcoinTransactions { + + @Test + fun testBitcoinTransaction() { + var requestId: String = "" + val transactionsClient = BitcoinTransactionStatusClient( + Chain.Bitcoin, + object : BitcoinTransactionsService { + override suspend fun transaction(txId: String): Result { + requestId = txId + return Result.success(BitcoinTransaction(0)) + } + } + ) + val result = runBlocking { + transactionsClient.getStatus(Chain.Bitcoin, "some_address", "some_id") + }.getOrNull() + assertNotNull(result) + assertEquals("some_id", requestId) + assertEquals(TransactionState.Pending, result!!.state) + } + + @Test + fun testBitcoinTransactionConfirm() { + var requestId: String = "" + val transactionsClient = BitcoinTransactionStatusClient( + Chain.Bitcoin, + object : BitcoinTransactionsService { + override suspend fun transaction(txId: String): Result { + requestId = txId + return Result.success(BitcoinTransaction(1)) + } + } + ) + val result = runBlocking { + transactionsClient.getStatus(Chain.Bitcoin, "some_address", "some_id") + }.getOrNull() + assertNotNull(result) + assertEquals(TransactionState.Confirmed, result!!.state) + } +} \ No newline at end of file diff --git a/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/cosmos/TestCosmosBroadcast.kt b/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/cosmos/TestCosmosBroadcast.kt new file mode 100644 index 000000000..2d8e74dde --- /dev/null +++ b/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/cosmos/TestCosmosBroadcast.kt @@ -0,0 +1,100 @@ +package com.gemwallet.android.blockchain.clients.cosmos + +import com.gemwallet.android.blockchain.Mime +import com.gemwallet.android.blockchain.clients.cosmos.services.CosmosBroadcastService +import com.gemwallet.android.math.decodeHex +import com.wallet.core.blockchain.cosmos.models.CosmosBroadcastResponse +import com.wallet.core.blockchain.cosmos.models.CosmosBroadcastResult +import com.wallet.core.primitives.Account +import com.wallet.core.primitives.Chain +import com.wallet.core.primitives.TransactionType +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.runBlocking +import okhttp3.RequestBody +import okio.Buffer +import org.junit.Test + +class TestCosmosBroadcast { + + val sign = "0x7b226d6f6465223a2242524f4144434153545f4d4f44455f53594e43222c2274785f6279746573223" + + "a22436f6f42436f6342436877765932397a6257397a4c6d4a68626d7375646a46695a585268" + + "4d53354e633264545a57356b456d634b4b32397a62573878613264735a57313162585534625" + + "734324e5468714e6d6330656a6c71656d347a656d566d4d6e466b65586c326132783359544d" + + "534b32397a62573878636d4e71646e70364f486436613352785a6e6f346357706d4d4777356" + + "354513161337034646d5177656a42754e3277315932596143776f466457397a62573853416a" + + "4577456d674b55417047436838765932397a6257397a4c6d4e79655842306279357a5a574e7" + + "74d6a5532617a4575554856695332563545694d4b49514d736c63596e374468506535622f38" + + "6c4d33466e50586847426a3553644331352b584931685a31675962424249454367494941526" + + "74b4568514b44676f466457397a62573853425445774d444177454d436144427041564a6b44" + + "786153355a6167686d4a365a7470433979696d374a413864754f384d774f4f44644a6548454" + + "87373483350514e2b34596c2b5356794c744e4557362b4944554b666b4731646649594f7670" + + "5269466c4f79673d3d227d" + + @Test + fun testCosmosBroadcast() { + var sendingData: String = "" + val broadcastClient = CosmosBroadcastClient( + Chain.Cosmos, + object : CosmosBroadcastService { + override suspend fun broadcast(request: RequestBody): Result { + assertEquals(Mime.Json.value, request.contentType()) + val buffer = Buffer() + request.writeTo(buffer) + sendingData = String(buffer.inputStream().readAllBytes()) + return Result.success( + CosmosBroadcastResponse(CosmosBroadcastResult(txhash = "some hash", code = 0, raw_log = "")) + ) + } + } + + ) + val result = runBlocking { + broadcastClient.send( + Account(Chain.Cosmos, "cosmos1kglemumu8mn658j6g4z9jzn3zef2qdyyydv7tr", ""), + sign.decodeHex(), + TransactionType.Transfer, + ) + } + + assertEquals( + "{\"mode\":\"BROADCAST_MODE_SYNC\",\"tx_bytes\":\"CooBCocBChwvY29zbW9zLmJhb" + + "msudjFiZXRhMS5Nc2dTZW5kEmcKK29zbW8xa2dsZW11bXU4bW42NThqNmc0ejlqem4zemVmMnFkeXl2" + + "a2x3YTMSK29zbW8xcmNqdnp6OHd6a3RxZno4cWpmMGw5cTQ1a3p4dmQwejBuN2w1Y2YaCwoFdW9zbW8" + + "SAjEwEmgKUApGCh8vY29zbW9zLmNyeXB0by5zZWNwMjU2azEuUHViS2V5EiMKIQMslcYn7DhPe5b/8l" + + "M3FnPXhGBj5SdC15+XI1hZ1gYbBBIECgIIARgKEhQKDgoFdW9zbW8SBTEwMDAwEMCaDBpAVJkDxaS5Z" + + "aghmJ6ZtpC9yim7JA8duO8MwOODdJeHEHssH3PQN+4Yl+SVyLtNEW6+IDUKfkG1dfIYOvpRiFlOyg==\"}", + sendingData + ) + assertEquals("some hash", result.getOrNull()) + } + + @Test + fun testCosmosBroadcastFail() { + var sendingData: String = "" + val broadcastClient = CosmosBroadcastClient( + Chain.Cosmos, + object : CosmosBroadcastService { + override suspend fun broadcast(request: RequestBody): Result { + assertEquals(Mime.Json.value, request.contentType()) + val buffer = Buffer() + request.writeTo(buffer) + sendingData = String(buffer.inputStream().readAllBytes()) + return Result.success( + CosmosBroadcastResponse(CosmosBroadcastResult(txhash = "some hash", code = 1, raw_log = "Some error")) + ) + } + } + + ) + val result = runBlocking { + broadcastClient.send( + Account(Chain.Cosmos, "cosmos1kglemumu8mn658j6g4z9jzn3zef2qdyyydv7tr", ""), + sign.decodeHex(), + TransactionType.Transfer, + ) + } + + assertEquals("Some error", result.exceptionOrNull()?.message) + } +} \ No newline at end of file diff --git a/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/cosmos/TestCosmosPreload.kt b/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/cosmos/TestCosmosPreload.kt new file mode 100644 index 000000000..570021e09 --- /dev/null +++ b/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/cosmos/TestCosmosPreload.kt @@ -0,0 +1,181 @@ +package com.gemwallet.android.blockchain.clients.cosmos + +import com.gemwallet.android.blockchain.clients.cosmos.services.CosmosAccountsService +import com.gemwallet.android.ext.toIdentifier +import com.gemwallet.android.model.ConfirmParams +import com.gemwallet.android.model.DestinationAddress +import com.gemwallet.android.model.GasFee +import com.gemwallet.android.model.TxSpeed +import com.wallet.core.blockchain.cosmos.models.CosmosAccount +import com.wallet.core.blockchain.cosmos.models.CosmosAccountResponse +import com.wallet.core.blockchain.cosmos.models.CosmosBlock +import com.wallet.core.blockchain.cosmos.models.CosmosBlockResponse +import com.wallet.core.blockchain.cosmos.models.CosmosHeader +import com.wallet.core.blockchain.cosmos.models.CosmosInjectiveAccount +import com.wallet.core.primitives.Account +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.Chain +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.runBlocking +import org.junit.Test +import java.math.BigInteger + +class TestCosmosPreload { + + private val accountsService = object : CosmosAccountsService { + + var addressRequest: String = "" + + override suspend fun getAccountData(owner: String): Result> { + addressRequest = owner + return Result.success(CosmosAccountResponse(CosmosAccount(account_number = "2913388", sequence = "10"))) + } + + override suspend fun getInjectiveAccountData(owner: String): Result> { + throw Exception() + } + + override suspend fun getNodeInfo(): Result { + return Result.success(CosmosBlockResponse(CosmosBlock(CosmosHeader(chain_id = "osmosis-1", height = "25181150")))) + } + } + + + @Test + fun testCosmosPreload() { + val preload = CosmosSignerPreloader(Chain.Osmosis, accountsService) + val result = runBlocking { + preload.preloadNativeTransfer( + ConfirmParams.TransferParams.Native( + from = Account(Chain.Osmosis, "osmo1q0d0q8w8y8t6h4l9w5u8p8s8h8f8e8r8t8y8u8i8o8p8", ""), + amount = BigInteger.ONE, + assetId = AssetId(Chain.Osmosis), + destination = DestinationAddress("osmo1rcjvzz8wzktqfz8qjf0l9q45kzxvd0z0n7l5cf"), + ) + ) + } + assertEquals("osmo1q0d0q8w8y8t6h4l9w5u8p8s8h8f8e8r8t8y8u8i8o8p8", accountsService.addressRequest) + + assertEquals(BigInteger.valueOf(1), result.input.amount) + assertEquals("osmo1q0d0q8w8y8t6h4l9w5u8p8s8h8f8e8r8t8y8u8i8o8p8", result.input.from.address) + assertEquals(AssetId(Chain.Osmosis).toIdentifier(), result.input.assetId.toIdentifier()) + assertEquals(false, result.input.isMax()) + assertEquals("osmo1rcjvzz8wzktqfz8qjf0l9q45kzxvd0z0n7l5cf", result.input.destination()?.address) + assertEquals(null, result.input.memo()) + + assertEquals(BigInteger.valueOf(10000L), result.chainData.fee().amount) + assertEquals(BigInteger.valueOf(10000L), (result.chainData.fee() as GasFee).maxGasPrice) + assertEquals(BigInteger.valueOf(200000L), (result.chainData.fee() as GasFee).limit) + assertEquals(AssetId(Chain.Osmosis).toIdentifier(), result.chainData.fee().feeAssetId.toIdentifier()) + assertEquals(TxSpeed.Normal, result.chainData.fee().speed) + assertEquals(10L, (result.chainData as CosmosSignerPreloader.CosmosChainData).sequence) + assertEquals(2913388L, (result.chainData as CosmosSignerPreloader.CosmosChainData).accountNumber) + assertEquals("osmosis-1", (result.chainData as CosmosSignerPreloader.CosmosChainData).chainId) + } + + @Test + fun testCosmosDelegatePreload() { + val preload = CosmosSignerPreloader(Chain.Osmosis, accountsService) + val result = runBlocking { + preload.preloadStake( + ConfirmParams.Stake.DelegateParams( + from = Account(Chain.Osmosis, "osmo1q0d0q8w8y8t6h4l9w5u8p8s8h8f8e8r8t8y8u8i8o8p8", ""), + amount = BigInteger.ONE, + assetId = AssetId(Chain.Osmosis), + validatorId = "osmovaloper1pxphtfhqnx9ny27d53z4052e3r76e7qq495ehm" + ) + ) + } + + assertEquals("osmo1q0d0q8w8y8t6h4l9w5u8p8s8h8f8e8r8t8y8u8i8o8p8", accountsService.addressRequest) + assertEquals(BigInteger.valueOf(100000L), result.chainData.fee().amount) + assertEquals(BigInteger.valueOf(100000L), (result.chainData.fee() as GasFee).maxGasPrice) + assertEquals(BigInteger.valueOf(1000000L), (result.chainData.fee() as GasFee).limit) + assertEquals(AssetId(Chain.Osmosis).toIdentifier(), result.chainData.fee().feeAssetId.toIdentifier()) + assertEquals(TxSpeed.Normal, result.chainData.fee().speed) + assertEquals(10L, (result.chainData as CosmosSignerPreloader.CosmosChainData).sequence) + assertEquals(2913388L, (result.chainData as CosmosSignerPreloader.CosmosChainData).accountNumber) + assertEquals("osmosis-1", (result.chainData as CosmosSignerPreloader.CosmosChainData).chainId) + } + + @Test + fun testCosmosUndelegatePreload() { + val preload = CosmosSignerPreloader(Chain.Osmosis, accountsService) + val result = runBlocking { + preload.preloadStake( + ConfirmParams.Stake.UndelegateParams( + from = Account(Chain.Osmosis, "osmo1q0d0q8w8y8t6h4l9w5u8p8s8h8f8e8r8t8y8u8i8o8p8", ""), + amount = BigInteger.ONE, + assetId = AssetId(Chain.Osmosis), + validatorId = "osmovaloper1pxphtfhqnx9ny27d53z4052e3r76e7qq495ehm", + delegationId = "25053096", + share = "", + balance = "2000000", + ) + ) + } + assertEquals("osmo1q0d0q8w8y8t6h4l9w5u8p8s8h8f8e8r8t8y8u8i8o8p8", accountsService.addressRequest) + assertEquals(BigInteger.valueOf(100000L), result.chainData.fee().amount) + assertEquals(BigInteger.valueOf(100000L), (result.chainData.fee() as GasFee).maxGasPrice) + assertEquals(BigInteger.valueOf(1000000L), (result.chainData.fee() as GasFee).limit) + assertEquals(AssetId(Chain.Osmosis).toIdentifier(), result.chainData.fee().feeAssetId.toIdentifier()) + assertEquals(TxSpeed.Normal, result.chainData.fee().speed) + assertEquals(10L, (result.chainData as CosmosSignerPreloader.CosmosChainData).sequence) + assertEquals(2913388L, (result.chainData as CosmosSignerPreloader.CosmosChainData).accountNumber) + assertEquals("osmosis-1", (result.chainData as CosmosSignerPreloader.CosmosChainData).chainId) + } + + @Test + fun testCosmosRedelegatePreload() { + val preload = CosmosSignerPreloader(Chain.Osmosis, accountsService) + val result = runBlocking { + preload.preloadStake( + ConfirmParams.Stake.RedelegateParams( + from = Account(Chain.Osmosis, "osmo1q0d0q8w8y8t6h4l9w5u8p8s8h8f8e8r8t8y8u8i8o8p8", ""), + amount = BigInteger.ONE, + assetId = AssetId(Chain.Osmosis), + srcValidatorId = "osmovaloper1pxphtfhqnx9ny27d53z4052e3r76e7qq495ehm", + dstValidatorId = "osmovaloper1z0sh4s80u99l6y9d3vfy582p8jejeeu6tcucs2", + share = "", + balance = "2000000", + ) + ) + } + assertEquals("osmo1q0d0q8w8y8t6h4l9w5u8p8s8h8f8e8r8t8y8u8i8o8p8", accountsService.addressRequest) + assertEquals(BigInteger.valueOf(100000L), result.chainData.fee().amount) + assertEquals(BigInteger.valueOf(100000L), (result.chainData.fee() as GasFee).maxGasPrice) + assertEquals(BigInteger.valueOf(1250000L), (result.chainData.fee() as GasFee).limit) + assertEquals(AssetId(Chain.Osmosis).toIdentifier(), result.chainData.fee().feeAssetId.toIdentifier()) + assertEquals(TxSpeed.Normal, result.chainData.fee().speed) + assertEquals(10L, (result.chainData as CosmosSignerPreloader.CosmosChainData).sequence) + assertEquals(2913388L, (result.chainData as CosmosSignerPreloader.CosmosChainData).accountNumber) + assertEquals("osmosis-1", (result.chainData as CosmosSignerPreloader.CosmosChainData).chainId) + } + + @Test + fun testCosmosRewardsPreload() { + val preload = CosmosSignerPreloader(Chain.Osmosis, accountsService) + val result = runBlocking { + preload.preloadStake( + ConfirmParams.Stake.RewardsParams( + from = Account(Chain.Osmosis, "osmo1q0d0q8w8y8t6h4l9w5u8p8s8h8f8e8r8t8y8u8i8o8p8", ""), + amount = BigInteger.ONE, + assetId = AssetId(Chain.Osmosis), + validatorsId = listOf( + "osmovaloper1pxphtfhqnx9ny27d53z4052e3r76e7qq495ehm", + "osmovaloper1z0sh4s80u99l6y9d3vfy582p8jejeeu6tcucs2", + ), + ) + ) + } + assertEquals("osmo1q0d0q8w8y8t6h4l9w5u8p8s8h8f8e8r8t8y8u8i8o8p8", accountsService.addressRequest) + assertEquals(BigInteger.valueOf(100000L), result.chainData.fee().amount) + assertEquals(BigInteger.valueOf(100000L), (result.chainData.fee() as GasFee).maxGasPrice) + assertEquals(BigInteger.valueOf(900000L), (result.chainData.fee() as GasFee).limit) + assertEquals(AssetId(Chain.Osmosis).toIdentifier(), result.chainData.fee().feeAssetId.toIdentifier()) + assertEquals(TxSpeed.Normal, result.chainData.fee().speed) + assertEquals(10L, (result.chainData as CosmosSignerPreloader.CosmosChainData).sequence) + assertEquals(2913388L, (result.chainData as CosmosSignerPreloader.CosmosChainData).accountNumber) + assertEquals("osmosis-1", (result.chainData as CosmosSignerPreloader.CosmosChainData).chainId) + } +} \ No newline at end of file diff --git a/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/cosmos/TestCosmosStake.kt b/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/cosmos/TestCosmosStake.kt new file mode 100644 index 000000000..f0f40826c --- /dev/null +++ b/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/cosmos/TestCosmosStake.kt @@ -0,0 +1,179 @@ +package com.gemwallet.android.blockchain.clients.cosmos + +import com.gemwallet.android.blockchain.clients.cosmos.services.CosmosStakeService +import com.wallet.core.blockchain.cosmos.models.CosmosBalance +import com.wallet.core.blockchain.cosmos.models.CosmosDelegation +import com.wallet.core.blockchain.cosmos.models.CosmosDelegationData +import com.wallet.core.blockchain.cosmos.models.CosmosDelegations +import com.wallet.core.blockchain.cosmos.models.CosmosReward +import com.wallet.core.blockchain.cosmos.models.CosmosRewards +import com.wallet.core.blockchain.cosmos.models.CosmosUnboudingDelegationEntry +import com.wallet.core.blockchain.cosmos.models.CosmosUnboundingDelegation +import com.wallet.core.blockchain.cosmos.models.CosmosUnboundingDelegations +import com.wallet.core.blockchain.cosmos.models.CosmosValidator +import com.wallet.core.blockchain.cosmos.models.CosmosValidatorCommission +import com.wallet.core.blockchain.cosmos.models.CosmosValidatorCommissionRates +import com.wallet.core.blockchain.cosmos.models.CosmosValidatorMoniker +import com.wallet.core.blockchain.cosmos.models.CosmosValidators +import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.Chain +import com.wallet.core.primitives.DelegationState +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertNull +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.runBlocking +import org.junit.Test + +class TestCosmosStake { + + private class TestCosmosStakeService( + val validators: List = listOf( + CosmosValidator( + operator_address = "osmovaloper1pxphtfhqnx9ny27d53z4052e3r76e7qq495ehm", + jailed = false, + status = "BOND_STATUS_BONDED", + description = CosmosValidatorMoniker("stakebeast"), + commission = CosmosValidatorCommission(CosmosValidatorCommissionRates(rate = "0.050000000000000000")) + ), + CosmosValidator( + operator_address = "osmovaloper1z0sh4s80u99l6y9d3vfy582p8jejeeu6tcucs2", + jailed = false, + status = "BOND_STATUS_UBONDED", + description = CosmosValidatorMoniker("Redline Validation"), + commission = CosmosValidatorCommission(CosmosValidatorCommissionRates(rate = "1.000000000000000000")) + ), + CosmosValidator( + operator_address = "osmovaloper1wgmdcxzp49vjgrqusgcagq6qefk4mtjv5c0k7q", + jailed = true, + status = "BOND_STATUS_UNBONDED", + description = CosmosValidatorMoniker("Dystopia Labs Validator"), + commission = CosmosValidatorCommission(CosmosValidatorCommissionRates(rate = "0.040000000000000000")) + ), + CosmosValidator( + operator_address = "osmovaloper1pxphtfhqnx9ny27d53z4052e3r76e7qq495ehm", + jailed = false, + status = "BOND_STATUS_BONDED", + description = CosmosValidatorMoniker("Cypher Core"), + commission = CosmosValidatorCommission(CosmosValidatorCommissionRates(rate = "0.100000000000000000")) + ), + ), + val delegations: List = emptyList(), + val unboundings: List = emptyList(), + val rewards: List = emptyList(), + ) : CosmosStakeService { + + var delegationsAddressRequest: String = "" + var unboundingAddressRequest: String = "" + var rewardsAddressRequest: String = "" + + override suspend fun validators(): Result { + return Result.success(CosmosValidators(validators)) + } + + override suspend fun delegations(address: String): Result { + delegationsAddressRequest = address + return Result.success(CosmosDelegations(delegations)) + } + + override suspend fun undelegations(address: String): Result { + unboundingAddressRequest = address + return Result.success(CosmosUnboundingDelegations(unboundings)) + } + + override suspend fun rewards(address: String): Result { + rewardsAddressRequest = address + return Result.success(CosmosRewards(rewards)) + } + + } + + @Test + fun testGetValidators() { + val client = CosmosStakeClient( + chain = Chain.Osmosis, + stakeService = TestCosmosStakeService() + ) + val result = runBlocking { client.getValidators(Chain.Osmosis, 5.68) } + assertFalse(result.isEmpty()) + assertEquals(4, result.size) + for (validator in result) { + assertEquals(Chain.Osmosis, validator.chain) + } + assertTrue(result[0].isActive) + assertFalse(result[1].isActive) + assertFalse(result[2].isActive) + assertTrue(result[3].isActive) + assertEquals("osmovaloper1pxphtfhqnx9ny27d53z4052e3r76e7qq495ehm", result[0].id) + assertEquals("osmovaloper1z0sh4s80u99l6y9d3vfy582p8jejeeu6tcucs2", result[1].id) + assertEquals("Redline Validation", result[1].name) + assertEquals("Cypher Core", result[3].name) + assertEquals(0.05, result[0].commision) + assertEquals(0.04, result[2].commision) + assertEquals(5.396, result[0].apr) + assertEquals(5.112, result[3].apr) + } + + @Test + fun testGetValidatorsEmpty() { + val client = CosmosStakeClient( + chain = Chain.Osmosis, + stakeService = TestCosmosStakeService(emptyList()) + ) + val result = runBlocking { client.getValidators(Chain.Osmosis, 5.68) } + assertTrue(result.isEmpty()) + } + + @Test + fun testGetDelegations() { + val client = CosmosStakeClient( + Chain.Osmosis, + TestCosmosStakeService( + delegations = listOf( + CosmosDelegation( + CosmosDelegationData("osmovaloper1pxphtfhqnx9ny27d53z4052e3r76e7qq495ehm"), + CosmosBalance("uosmo", "1000000") + ), + CosmosDelegation( + CosmosDelegationData("osmovaloper1z0sh4s80u99l6y9d3vfy582p8jejeeu6tcucs2"), + CosmosBalance("uosmo", "1000000") + ), + CosmosDelegation( + CosmosDelegationData("osmovaloper1wgmdcxzp49vjgrqusgcagq6qefk4mtjv5c0k7q"), + CosmosBalance("uosmo", "721000000") + ) + ), + unboundings = listOf( + CosmosUnboundingDelegation( + "osmovaloper1pxphtfhqnx9ny27d53z4052e3r76e7qq495ehm", + entries = listOf( + CosmosUnboudingDelegationEntry( + creation_height = "25053096", + completion_time = "2024-12-18T02:11:27.650773866Z", + balance = "2000000", + ) + ) + ), + ), + rewards = listOf( + CosmosReward("osmovaloper1pxphtfhqnx9ny27d53z4052e3r76e7qq495ehm", listOf(CosmosBalance(denom = "uosmo", amount = "808.015913259210000000"))), + CosmosReward("osmovaloper1z0sh4s80u99l6y9d3vfy582p8jejeeu6tcucs2", listOf(CosmosBalance(denom = "uosmo", amount = "808.009961413523000000"))), + CosmosReward("osmovaloper1wgmdcxzp49vjgrqusgcagq6qefk4mtjv5c0k7q", listOf(CosmosBalance(denom = "uosmo", amount = "582579.692713670953000000"))), + ) + ) + ) + val result = runBlocking { client.getStakeDelegations(Chain.Osmosis, "osmo1ac4ns740p4v78n4wr7j3t3s79jjjre6udx7n2v", 5.68) } + assertEquals(4, result.size) + assertEquals(AssetId(Chain.Osmosis), result[0].assetId) + assertEquals(DelegationState.Active, result[0].state) + assertEquals(DelegationState.Pending, result[3].state) + assertEquals("1000000", result[0].balance) + assertEquals("2000000", result[3].balance) + assertEquals("808", result[0].rewards) + assertEquals("808", result[3].rewards) + assertNull(result[0].completionDate) + assertEquals(1735138660866, result[3].completionDate) + assertEquals("", result[0].delegationId) + assertEquals("osmovaloper1pxphtfhqnx9ny27d53z4052e3r76e7qq495ehm", result[0].validatorId) + } +} \ No newline at end of file diff --git a/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/cosmos/TestCosmosTransactions.kt b/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/cosmos/TestCosmosTransactions.kt new file mode 100644 index 000000000..51d3e5c09 --- /dev/null +++ b/blockchain/src/test/kotlin/com/gemwallet/android/blockchain/clients/cosmos/TestCosmosTransactions.kt @@ -0,0 +1,65 @@ +package com.gemwallet.android.blockchain.clients.cosmos + +import com.gemwallet.android.blockchain.clients.cosmos.services.CosmosTransactionsService +import com.wallet.core.blockchain.cosmos.models.CosmosTransactionDataResponse +import com.wallet.core.blockchain.cosmos.models.CosmosTransactionResponse +import com.wallet.core.primitives.Chain +import com.wallet.core.primitives.TransactionState +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import kotlinx.coroutines.runBlocking +import org.junit.Test + +class TestCosmosTransactions { + + @Test + fun testCosmosTransaction() { + var requestId: String = "" + val transactionsService = object : CosmosTransactionsService { + override suspend fun transaction(txId: String): Result { + requestId = txId + return Result.success(CosmosTransactionResponse(CosmosTransactionDataResponse(txId, 0))) + } + + } + val transactionsClient = CosmosTransactionStatusClient(Chain.Osmosis, transactionsService) + val result = runBlocking { + transactionsClient.getStatus(Chain.Cosmos, "some_address", "some_id") + }.getOrNull() + assertNotNull(result) + assertEquals("some_id", requestId) + assertEquals(TransactionState.Confirmed, result!!.state) + } + + @Test + fun testCosmosTransactionPending() { + val transactionsService = object : CosmosTransactionsService { + override suspend fun transaction(txId: String): Result { + return Result.success(CosmosTransactionResponse(CosmosTransactionDataResponse("", 0))) + } + + } + val transactionsClient = CosmosTransactionStatusClient(Chain.Osmosis, transactionsService) + val result = runBlocking { + transactionsClient.getStatus(Chain.Cosmos, "some_address", "some_id") + }.getOrNull() + assertNotNull(result) + assertEquals(TransactionState.Pending, result!!.state) + } + + @Test + fun testCosmosTransactionReverted() { + val transactionsService = object : CosmosTransactionsService { + override suspend fun transaction(txId: String): Result { + return Result.success(CosmosTransactionResponse(CosmosTransactionDataResponse(txId, 1))) + } + + } + val transactionsClient = CosmosTransactionStatusClient(Chain.Osmosis, transactionsService) + val result = runBlocking { + transactionsClient.getStatus(Chain.Cosmos, "some_address", "some_id") + }.getOrNull() + assertNotNull(result) + assertEquals(TransactionState.Reverted, result!!.state) + } +} \ No newline at end of file diff --git a/data/repositories/src/main/java/com/gemwallet/android/data/repositoreis/di/AssetsModule.kt b/data/repositories/src/main/java/com/gemwallet/android/data/repositoreis/di/AssetsModule.kt index e2c1aefb3..420dad001 100644 --- a/data/repositories/src/main/java/com/gemwallet/android/data/repositoreis/di/AssetsModule.kt +++ b/data/repositories/src/main/java/com/gemwallet/android/data/repositoreis/di/AssetsModule.kt @@ -5,6 +5,7 @@ import com.gemwallet.android.blockchain.clients.aptos.AptosBalanceClient import com.gemwallet.android.blockchain.clients.bitcoin.BitcoinBalanceClient import com.gemwallet.android.blockchain.clients.cosmos.CosmosBalanceClient import com.gemwallet.android.blockchain.clients.ethereum.EvmBalanceClient +import com.gemwallet.android.blockchain.clients.ethereum.SmartchainStakeClient import com.gemwallet.android.blockchain.clients.near.NearBalanceClient import com.gemwallet.android.blockchain.clients.solana.SolanaBalanceClient import com.gemwallet.android.blockchain.clients.sui.SuiBalanceClient @@ -69,9 +70,9 @@ object AssetsModule { Chain.available().map { when (it.toChainType()) { ChainType.Bitcoin -> BitcoinBalanceClient(it, rpcClients.getClient(it)) - ChainType.Ethereum -> EvmBalanceClient(it, rpcClients.getClient(it)) - ChainType.Solana -> SolanaBalanceClient(it, rpcClients.getClient(Chain.Solana)) - ChainType.Cosmos -> CosmosBalanceClient(it, rpcClients.getClient(it)) + ChainType.Ethereum -> EvmBalanceClient(it, rpcClients.getClient(it), rpcClients.getClient(it), SmartchainStakeClient(it, rpcClients.getClient(it))) + ChainType.Solana -> SolanaBalanceClient(it, rpcClients.getClient(Chain.Solana), rpcClients.getClient(Chain.Solana), rpcClients.getClient(Chain.Solana)) + ChainType.Cosmos -> CosmosBalanceClient(it, rpcClients.getClient(it), rpcClients.getClient(it)) ChainType.Ton -> TonBalanceClient(it, rpcClients.getClient(Chain.Ton)) ChainType.Tron -> TronBalanceClient(it, rpcClients.getClient(Chain.Tron)) ChainType.Aptos -> AptosBalanceClient(it, rpcClients.getClient(it)) diff --git a/data/services/remote-gem/src/main/java/com/gemwallet/android/data/services/gemapi/di/ClientsModule.kt b/data/services/remote-gem/src/main/java/com/gemwallet/android/data/services/gemapi/di/ClientsModule.kt index 428db4bac..3f0140d9b 100644 --- a/data/services/remote-gem/src/main/java/com/gemwallet/android/data/services/gemapi/di/ClientsModule.kt +++ b/data/services/remote-gem/src/main/java/com/gemwallet/android/data/services/gemapi/di/ClientsModule.kt @@ -2,10 +2,10 @@ package com.gemwallet.android.data.services.gemapi.di import android.content.Context import com.gemwallet.android.blockchain.RpcClientAdapter -import com.gemwallet.android.blockchain.clients.aptos.AptosRpcClient -import com.gemwallet.android.blockchain.clients.bitcoin.BitcoinRpcClient -import com.gemwallet.android.blockchain.clients.cosmos.CosmosRpcClient -import com.gemwallet.android.blockchain.clients.ethereum.EvmRpcClient +import com.gemwallet.android.blockchain.clients.aptos.services.AptosService +import com.gemwallet.android.blockchain.clients.bitcoin.services.BitcoinRpcClient +import com.gemwallet.android.blockchain.clients.cosmos.services.CosmosRpcClient +import com.gemwallet.android.blockchain.clients.ethereum.services.EvmRpcClient import com.gemwallet.android.blockchain.clients.near.NearRpcClient import com.gemwallet.android.blockchain.clients.solana.SolanaRpcClient import com.gemwallet.android.blockchain.clients.sui.SuiRpcClient @@ -183,7 +183,7 @@ object ClientsModule { ChainType.Cosmos -> buildClient(url, CosmosRpcClient::class.java, converter, httpClient) ChainType.Ton -> buildClient(url, TonRpcClient::class.java, tonConverter, httpClient) ChainType.Tron -> buildClient(url, TronRpcClient::class.java, converter, httpClient) - ChainType.Aptos -> buildClient(url, AptosRpcClient::class.java, converter, httpClient) + ChainType.Aptos -> buildClient(url, AptosService::class.java, converter, httpClient) ChainType.Sui -> buildClient(url, SuiRpcClient::class.java, converter, httpClient) ChainType.Xrp -> buildClient(url, XrpRpcClient::class.java, converter, httpClient) ChainType.Near -> buildClient(url, NearRpcClient::class.java, converter, httpClient) diff --git a/features/recipient/viewmodels/src/main/java/com/gemwallet/android/features/recipient/viewmodel/RecipientViewModel.kt b/features/recipient/viewmodels/src/main/java/com/gemwallet/android/features/recipient/viewmodel/RecipientViewModel.kt index 20ab053b2..556ef9d15 100644 --- a/features/recipient/viewmodels/src/main/java/com/gemwallet/android/features/recipient/viewmodel/RecipientViewModel.kt +++ b/features/recipient/viewmodels/src/main/java/com/gemwallet/android/features/recipient/viewmodel/RecipientViewModel.kt @@ -100,7 +100,7 @@ class RecipientViewModel @Inject constructor( && (assetInfo.asset.chain().supportMemo() || !memo.isNullOrEmpty()) ) { val assetId = assetInfo.id() - val params = ConfirmParams.Builder(assetId, amount).transfer(DestinationAddress(address), memo) + val params = ConfirmParams.Builder(assetId, assetInfo.owner, amount).transfer(DestinationAddress(address), memo) confirmAction(params) return } diff --git a/gemcore/src/main/java/com/gemwallet/android/ext/Chain.kt b/gemcore/src/main/java/com/gemwallet/android/ext/Chain.kt index 5353d7fd8..40ee4a6e8 100644 --- a/gemcore/src/main/java/com/gemwallet/android/ext/Chain.kt +++ b/gemcore/src/main/java/com/gemwallet/android/ext/Chain.kt @@ -162,8 +162,13 @@ fun Chain.toChainType(): ChainType { } } + +fun Chain.getNetworkId(): String { + return Config().getChainConfig(string).networkId +} + fun Chain.isSwapSupport(): Boolean { return Config().getChainConfig(string).isSwapSupported } -fun Chain.Companion.swapSupport() = Chain.entries.filter { it.isSwapSupport() } \ No newline at end of file +fun Chain.Companion.swapSupport() = Chain.entries.filter { it.isSwapSupport() } diff --git a/gemcore/src/main/java/com/gemwallet/android/ext/ChainType.kt b/gemcore/src/main/java/com/gemwallet/android/ext/ChainType.kt index fcfc5ec38..a23ab73f6 100644 --- a/gemcore/src/main/java/com/gemwallet/android/ext/ChainType.kt +++ b/gemcore/src/main/java/com/gemwallet/android/ext/ChainType.kt @@ -1 +1,11 @@ package com.gemwallet.android.ext + +import com.wallet.core.primitives.BitcoinChain +import com.wallet.core.primitives.Chain + +fun Chain.toBitcoinChain() = when (this) { + Chain.Bitcoin -> BitcoinChain.Bitcoin + Chain.Doge -> BitcoinChain.Doge + Chain.Litecoin -> BitcoinChain.Litecoin + else -> throw IllegalArgumentException("Not bitcoin chain") +} \ No newline at end of file diff --git a/gemcore/src/main/java/com/gemwallet/android/model/ConfirmParams.kt b/gemcore/src/main/java/com/gemwallet/android/model/ConfirmParams.kt index 9d3aa376b..bf9ed6b07 100644 --- a/gemcore/src/main/java/com/gemwallet/android/model/ConfirmParams.kt +++ b/gemcore/src/main/java/com/gemwallet/android/model/ConfirmParams.kt @@ -1,12 +1,16 @@ package com.gemwallet.android.model import com.gemwallet.android.ext.toIdentifier +import com.gemwallet.android.ext.type import com.gemwallet.android.ext.urlDecode import com.gemwallet.android.ext.urlEncode +import com.gemwallet.android.serializer.AccountSerializer import com.gemwallet.android.serializer.AssetIdSerializer import com.google.gson.Gson import com.google.gson.GsonBuilder +import com.wallet.core.primitives.Account import com.wallet.core.primitives.AssetId +import com.wallet.core.primitives.AssetSubtype import com.wallet.core.primitives.Delegation import com.wallet.core.primitives.TransactionType import java.math.BigInteger @@ -14,41 +18,56 @@ import java.util.Base64 sealed class ConfirmParams( val assetId: AssetId, + val from: Account, val amount: BigInteger = BigInteger.ZERO, ) { class Builder( val assetId: AssetId, + val from: Account, val amount: BigInteger = BigInteger.ZERO, ) { fun transfer(destination: DestinationAddress, memo: String? = null, isMax: Boolean = false): TransferParams { - return TransferParams( - assetId = assetId, - amount = amount, - destination = destination, - memo = memo, - isMaxAmount = isMax - ) + return when (assetId.type()) { + AssetSubtype.NATIVE -> TransferParams.Native( + assetId = assetId, + from = from, + amount = amount, + destination = destination, + memo = memo, + isMaxAmount = isMax + ) + AssetSubtype.TOKEN -> TransferParams.Token( + assetId = assetId, + from = from, + amount = amount, + destination = destination, + memo = memo, + isMaxAmount = isMax + ) + } } fun approval(approvalData: String, provider: String): TokenApprovalParams { - return TokenApprovalParams(assetId, approvalData, provider, contract = "") + return TokenApprovalParams(assetId, from, approvalData, provider, contract = "") } - fun delegate(validatorId: String) = DelegateParams(assetId, amount, validatorId) + fun delegate(validatorId: String) = Stake.DelegateParams(assetId, from, amount, validatorId) - fun rewards(validatorsId: List) = RewardsParams(assetId, validatorsId, amount) + fun rewards(validatorsId: List) = Stake.RewardsParams(assetId, from, validatorsId, amount) - fun withdraw(delegation: Delegation) = WithdrawParams( + fun withdraw(delegation: Delegation) = Stake.WithdrawParams( assetId = assetId, + from = from, amount = amount, validatorId = delegation.validator.id, delegationId = delegation.base.delegationId, ) - fun undelegate(delegation: Delegation): UndelegateParams { - return UndelegateParams( + fun undelegate(delegation: Delegation): Stake.UndelegateParams { + return Stake.UndelegateParams( assetId, + from, amount, delegation.validator.id, delegation.base.delegationId, @@ -57,9 +76,10 @@ sealed class ConfirmParams( ) } - fun redelegate(dstValidatorId: String, delegation: Delegation): RedeleateParams { - return RedeleateParams( + fun redelegate(dstValidatorId: String, delegation: Delegation): Stake.RedelegateParams { + return Stake.RedelegateParams( assetId, + from = from, amount, delegation.validator.id, dstValidatorId, @@ -69,13 +89,14 @@ sealed class ConfirmParams( } } - class TransferParams( + sealed class TransferParams( assetId: AssetId, + from: Account, amount: BigInteger, val destination: DestinationAddress, val memo: String? = null, val isMaxAmount: Boolean = false, - ) : ConfirmParams(assetId, amount) { + ) : ConfirmParams(assetId, from, amount) { override fun isMax(): Boolean { return isMaxAmount @@ -88,16 +109,36 @@ sealed class ConfirmParams( override fun memo(): String? { return memo } + + class Native( + assetId: AssetId, + from: Account, + amount: BigInteger, + destination: DestinationAddress, + memo: String? = null, + isMaxAmount: Boolean = false, + ) : TransferParams(assetId, from, amount, destination, memo, isMaxAmount) + + class Token( + assetId: AssetId, + from: Account, + amount: BigInteger, + destination: DestinationAddress, + memo: String? = null, + isMaxAmount: Boolean = false, + ) : TransferParams(assetId, from, amount, destination, memo, isMaxAmount) } class TokenApprovalParams( assetId: AssetId, + from: Account, val data: String, val provider: String, val contract: String - ) : ConfirmParams(assetId) + ) : ConfirmParams(assetId, from) class SwapParams( + from: Account, val fromAssetId: AssetId, val fromAmount: BigInteger, val toAssetId: AssetId, @@ -106,48 +147,61 @@ sealed class ConfirmParams( val provider: String, val to: String, val value: String, - ) : ConfirmParams(fromAssetId, fromAmount) { + ) : ConfirmParams(fromAssetId, from, fromAmount) { override fun destination(): DestinationAddress = DestinationAddress(to) } - class DelegateParams( + sealed class Stake( assetId: AssetId, + from: Account, amount: BigInteger, val validatorId: String, - ) : ConfirmParams(assetId, amount) + ) : ConfirmParams(assetId, from, amount) { - class WithdrawParams( - assetId: AssetId, - amount: BigInteger, - val validatorId: String, - val delegationId: String, - ) : ConfirmParams(assetId, amount) + class DelegateParams( + assetId: AssetId, + from: Account, + amount: BigInteger, + validatorId: String, + ) : Stake(assetId, from, amount, validatorId) - class UndelegateParams( - assetId: AssetId, - amount: BigInteger, - val validatorId: String, - val delegationId: String, - val share: String?, - val balance: String? - ) : ConfirmParams(assetId, amount) + class WithdrawParams( + assetId: AssetId, + from: Account, + amount: BigInteger, + validatorId: String, + val delegationId: String, + ) : Stake(assetId, from, amount, validatorId) - class RedeleateParams( - assetId: AssetId, - amount: BigInteger, - val srcValidatorId: String, - val dstValidatorId: String, - val share: String?, - val balance: String? - ) : ConfirmParams(assetId, amount) + class UndelegateParams( + assetId: AssetId, + from: Account, + amount: BigInteger, + validatorId: String, + val delegationId: String, + val share: String?, + val balance: String? + ) : Stake(assetId, from, amount, validatorId) - class RewardsParams( - assetId: AssetId, - val validatorsId: List, - amount: BigInteger - ) : ConfirmParams(assetId, amount) + class RedelegateParams( + assetId: AssetId, + from: Account, + amount: BigInteger, + val srcValidatorId: String, + val dstValidatorId: String, + val share: String?, + val balance: String? + ) : Stake(assetId, from, amount, srcValidatorId) + + class RewardsParams( + assetId: AssetId, + from: Account, + val validatorsId: List, + amount: BigInteger + ) : Stake(assetId, from, amount, "") + } fun pack(): String? { val json = getGson().toJson(this) @@ -159,11 +213,12 @@ sealed class ConfirmParams( is TransferParams -> TransactionType.Transfer is TokenApprovalParams -> TransactionType.TokenApproval is SwapParams -> TransactionType.Swap - is DelegateParams -> TransactionType.StakeDelegate - is RewardsParams -> TransactionType.StakeRewards - is RedeleateParams -> TransactionType.StakeRedelegate - is UndelegateParams -> TransactionType.StakeUndelegate - is WithdrawParams -> TransactionType.StakeWithdraw + is Stake.DelegateParams -> TransactionType.StakeDelegate + is Stake.RewardsParams -> TransactionType.StakeRewards + is Stake.RedelegateParams -> TransactionType.StakeRedelegate + is Stake.UndelegateParams -> TransactionType.StakeUndelegate + is Stake.WithdrawParams -> TransactionType.StakeWithdraw + is Stake -> throw IllegalArgumentException("Invalid stake parameter") } } @@ -182,14 +237,39 @@ sealed class ConfirmParams( } companion object { - fun unpack(type: Class, input: String): T? { + fun unpack(txType: TransactionType, input: String): ConfirmParams { val json = String(Base64.getDecoder().decode(input.urlDecode())) - return getGson().fromJson(json, type) + val type = when (txType) { + TransactionType.Transfer -> TransferParams.Native::class.java + TransactionType.Swap -> SwapParams::class.java + TransactionType.TokenApproval -> TokenApprovalParams::class.java + TransactionType.StakeDelegate -> Stake.DelegateParams::class.java + TransactionType.StakeUndelegate -> Stake.UndelegateParams::class.java + TransactionType.StakeRewards -> Stake.RewardsParams::class.java + TransactionType.StakeRedelegate -> Stake.RedelegateParams::class.java + TransactionType.StakeWithdraw -> Stake.WithdrawParams::class.java + } + + val result = getGson().fromJson(json, type) + + return if (result.assetId.type() == AssetSubtype.TOKEN && result is TransferParams.Native) { + TransferParams.Token( + result.assetId, + result.from, + result.amount, + result.destination, + result.memo, + result.isMaxAmount, + ) + } else { + result + } } private fun getGson(): Gson { return GsonBuilder() .registerTypeAdapter(AssetId::class.java, AssetIdSerializer()) + .registerTypeAdapter(Account::class.java, AccountSerializer()) .create() } } diff --git a/gemcore/src/main/java/com/gemwallet/android/model/SignerParams.kt b/gemcore/src/main/java/com/gemwallet/android/model/SignerParams.kt index f564982c0..11bd63ce5 100644 --- a/gemcore/src/main/java/com/gemwallet/android/model/SignerParams.kt +++ b/gemcore/src/main/java/com/gemwallet/android/model/SignerParams.kt @@ -4,14 +4,15 @@ import java.math.BigInteger data class SignerParams( val input: ConfirmParams, + val chainData: ChainSignData, val finalAmount: BigInteger = BigInteger.ZERO, - val owner: String, - val info: SignerInputInfo, ) -interface SignerInputInfo { +interface ChainSignData { fun fee(speed: TxSpeed = TxSpeed.Normal): Fee + fun gasGee(speed: TxSpeed = TxSpeed.Normal): GasFee = (fee(speed) as? GasFee) ?: throw Exception("Fee error: wait gas fee") + fun allFee(): List = emptyList() } diff --git a/gemcore/src/main/java/com/gemwallet/android/serializer/AccountSerializer.kt b/gemcore/src/main/java/com/gemwallet/android/serializer/AccountSerializer.kt new file mode 100644 index 000000000..8d739c27f --- /dev/null +++ b/gemcore/src/main/java/com/gemwallet/android/serializer/AccountSerializer.kt @@ -0,0 +1,25 @@ +package com.gemwallet.android.serializer + +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.wallet.core.primitives.Account +import com.wallet.core.primitives.Chain +import org.json.JSONException +import java.lang.reflect.Type + +class AccountSerializer : JsonDeserializer { + override fun deserialize( + json: JsonElement, + typeOfT: Type?, + context: JsonDeserializationContext? + ): Account { + val jObj = json.asJsonObject + return Account( + chain = Chain.entries.firstOrNull { it.name == jObj["chain"].asString } ?: throw JSONException("Can't read account"), + address = jObj["address"].asString, + derivationPath = jObj["derivationPath"].asString, + extendedPublicKey = jObj["extendedPublicKey"].asString + ) + } +} \ No newline at end of file diff --git a/gemcore/src/main/java/com/wallet/core/blockchain/ethereum/models/EthereumTransaction.kt b/gemcore/src/main/java/com/wallet/core/blockchain/ethereum/models/EthereumTransaction.kt index a73125fdd..58c5f34b1 100644 --- a/gemcore/src/main/java/com/wallet/core/blockchain/ethereum/models/EthereumTransaction.kt +++ b/gemcore/src/main/java/com/wallet/core/blockchain/ethereum/models/EthereumTransaction.kt @@ -10,7 +10,7 @@ import kotlinx.serialization.SerialName @Serializable data class EthereumFeeHistory ( val reward: List>, - val baseFeePerGas: List + val baseFeePerGas: List, ) @Serializable