diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/android/app/.gitignore b/android/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/android/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..f6b823d --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,77 @@ +plugins { + alias(libs.plugins.androidApplication) + alias(libs.plugins.jetbrainsKotlinAndroid) + alias(libs.plugins.daggerHilt) + alias(libs.plugins.kotlinKapt) +} + +android { + namespace = "com.zk.android.app" + compileSdk = 34 + + defaultConfig { + applicationId = "com.zk.android.app" + minSdk = 24 + targetSdk = 33 + versionCode = 1 + versionName = "1.0" + //TODO Insert your key here + //buildConfigField("String", "PRIVATE_KEY", "\"\"") + } + flavorDimensions.add("default") + + productFlavors { + create("mainnet") { + applicationIdSuffix = ".debug" + buildConfigField("String", "RPC_ETH_URL", "\"https://rpc.ankr.com/eth\"") + buildConfigField("String", "ZK_ERA_URL", "\"https://mainnet.era.zksync.io\"") + buildConfigField("String", "ZK_ERA_SOCKET", "\"wss://mainnet.era.zksync.io/ws\"") + } + create("sepolia") { + buildConfigField("String", "RPC_ETH_URL", "\"https://rpc.ankr.com/eth_sepolia\"") + buildConfigField("String", "ZK_ERA_URL", "\"https://sepolia.era.zksync.dev\"") + buildConfigField("String", "ZK_ERA_SOCKET", "\"wss://sepolia.era.zksync.dev/ws\"") + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + viewBinding = true + buildConfig = true + } +} + +dependencies { + + //Zk + implementation(libs.zksync) + implementation(libs.web3j.core) + implementation(libs.web3j.crypto) + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.core.splashscreen) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.navigation.fragment.ktx) + implementation(libs.androidx.navigation.ui.ktx) + implementation(libs.lifecycle.viewmodel.ktx) + implementation(libs.lifecycle.runtime.ktx) + + implementation(libs.material) + implementation(libs.orbit.viewmodel) + implementation(libs.lottie) + + // Dependency injection + implementation(libs.hilt.android) + kapt(libs.hilt.compiler) + kapt(libs.androidx.hilt.compiler) + implementation(libs.hilt.navigation.fragment) +} \ No newline at end of file diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..6ca1e84 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,37 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +# JSR 305 annotations are for embedding nullability information. +-dontwarn javax.annotation.** + +# A resource is loaded with a relative path so the package of this class must be preserved. +-keeppackagenames okhttp3.internal.publicsuffix.* +-adaptresourcefilenames okhttp3/internal/publicsuffix/PublicSuffixDatabase.gz + +# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. +-dontwarn org.codehaus.mojo.animal_sniffer.* + +# OkHttp platform used only on JVM and when Conscrypt and other security providers are available. +-dontwarn okhttp3.internal.platform.** +-dontwarn org.conscrypt.** +-dontwarn org.bouncycastle.** +-dontwarn org.openjsse.** \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..002840e --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/ic_launcher-playstore.png b/android/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..7c8170e Binary files /dev/null and b/android/app/src/main/ic_launcher-playstore.png differ diff --git a/android/app/src/main/java/com/zk/android/app/ZkSynApp.kt b/android/app/src/main/java/com/zk/android/app/ZkSynApp.kt new file mode 100644 index 0000000..00df95b --- /dev/null +++ b/android/app/src/main/java/com/zk/android/app/ZkSynApp.kt @@ -0,0 +1,7 @@ +package com.zk.android.app + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class ZkSynApp : Application() \ No newline at end of file diff --git a/android/app/src/main/java/com/zk/android/app/di/ZkModule.kt b/android/app/src/main/java/com/zk/android/app/di/ZkModule.kt new file mode 100644 index 0000000..4531a35 --- /dev/null +++ b/android/app/src/main/java/com/zk/android/app/di/ZkModule.kt @@ -0,0 +1,48 @@ +package com.zk.android.app.di + +import com.zk.android.app.BuildConfig +import com.zk.android.app.network.ZkConfig +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import io.zksync.protocol.ZkSync +import org.web3j.crypto.Credentials +import org.web3j.protocol.Web3j +import org.web3j.protocol.http.HttpService +import javax.inject.Singleton + + +@Module +@InstallIn(SingletonComponent::class) +class ZkModule { + + @Provides + @Singleton + fun provideConfig(): ZkConfig { + return ZkConfig( + BuildConfig.RPC_ETH_URL, + BuildConfig.ZK_ERA_URL, + BuildConfig.ZK_ERA_SOCKET, + BuildConfig.PRIVATE_KEY + ) + } + + @Provides + @Singleton + fun provideWeb3j(config: ZkConfig): Web3j { + return Web3j.build(HttpService(config.rpc)) + } + + @Provides + @Singleton + fun provideZkSync(config: ZkConfig): ZkSync { + return ZkSync.build(HttpService(config.url)) + } + + @Provides + @Singleton + fun provideCredentials(config: ZkConfig): Credentials { + return Credentials.create(config.privateKey) + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/zk/android/app/domain/balance/BalanceModelDomain.kt b/android/app/src/main/java/com/zk/android/app/domain/balance/BalanceModelDomain.kt new file mode 100644 index 0000000..0c9519e --- /dev/null +++ b/android/app/src/main/java/com/zk/android/app/domain/balance/BalanceModelDomain.kt @@ -0,0 +1,9 @@ +package com.zk.android.app.domain.balance + +import java.math.BigInteger + + +data class BalanceModelDomain ( + val l1Balance: BigInteger, + val l2Balance: BigInteger, +) \ No newline at end of file diff --git a/android/app/src/main/java/com/zk/android/app/domain/balance/GetBalanceUseCase.kt b/android/app/src/main/java/com/zk/android/app/domain/balance/GetBalanceUseCase.kt new file mode 100644 index 0000000..11b8634 --- /dev/null +++ b/android/app/src/main/java/com/zk/android/app/domain/balance/GetBalanceUseCase.kt @@ -0,0 +1,35 @@ +package com.zk.android.app.domain.balance + +import com.zk.android.app.network.WalletProvider +import com.zk.android.app.utils.sendSafe +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext +import java.math.BigInteger +import javax.inject.Inject + + +class GetBalanceUseCase @Inject constructor( + private val walletProvider: WalletProvider +) { + + suspend fun getBalance(): Result = withContext(Dispatchers.IO) { + val wallet = walletProvider.create().getOrElse { + return@withContext Result.failure(it) + } + + val l1Balance: BigInteger = wallet.balanceL1.sendSafe().getOrElse { + return@withContext Result.failure(it) + } + val l2Balance: BigInteger = wallet.balance.sendSafe().getOrElse { + return@withContext Result.failure(it) + } + + return@withContext Result.success( + BalanceModelDomain( + l1Balance, + l2Balance + ) + ) + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/zk/android/app/domain/deposit/DepositModelDomain.kt b/android/app/src/main/java/com/zk/android/app/domain/deposit/DepositModelDomain.kt new file mode 100644 index 0000000..2e71914 --- /dev/null +++ b/android/app/src/main/java/com/zk/android/app/domain/deposit/DepositModelDomain.kt @@ -0,0 +1,8 @@ +package com.zk.android.app.domain.deposit + +import com.zk.android.app.domain.balance.BalanceModelDomain + + +data class DepositModelDomain ( + val newBalance: BalanceModelDomain +) \ No newline at end of file diff --git a/android/app/src/main/java/com/zk/android/app/domain/deposit/DepositUseCase.kt b/android/app/src/main/java/com/zk/android/app/domain/deposit/DepositUseCase.kt new file mode 100644 index 0000000..ee5f596 --- /dev/null +++ b/android/app/src/main/java/com/zk/android/app/domain/deposit/DepositUseCase.kt @@ -0,0 +1,37 @@ +package com.zk.android.app.domain.deposit + +import com.zk.android.app.domain.balance.GetBalanceUseCase +import com.zk.android.app.network.WalletProvider +import com.zk.android.app.utils.toWei +import com.zk.android.app.utils.waitForTransactionReceiptSafe +import io.zksync.transaction.type.DepositTransaction +import io.zksync.utils.ZkSyncAddresses +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.math.BigInteger +import javax.inject.Inject + + +class DepositUseCase @Inject constructor( + private val walletProvider: WalletProvider, + private val balanceUseCase: GetBalanceUseCase +) { + + suspend fun deposit(amount: Float): Result = withContext(Dispatchers.IO) { + val wallet = walletProvider.create().getOrElse { + return@withContext Result.failure(it) + } + + val sendAmount: BigInteger = amount.toWei() + + val transaction = DepositTransaction(ZkSyncAddresses.ETH_ADDRESS, sendAmount) + val hash = wallet.deposit(transaction).sendAsync().join().result + wallet.transactionReceiptProcessorL1.waitForTransactionReceiptSafe(hash) + + val newBalance = balanceUseCase.getBalance().getOrElse { + return@withContext Result.failure(it) + } + + return@withContext Result.success(DepositModelDomain(newBalance)) + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/zk/android/app/network/WalletProvider.kt b/android/app/src/main/java/com/zk/android/app/network/WalletProvider.kt new file mode 100644 index 0000000..9e3d511 --- /dev/null +++ b/android/app/src/main/java/com/zk/android/app/network/WalletProvider.kt @@ -0,0 +1,22 @@ +package com.zk.android.app.network + +import androidx.annotation.WorkerThread +import io.zksync.protocol.ZkSync +import io.zksync.protocol.account.Wallet +import org.web3j.crypto.Credentials +import org.web3j.protocol.Web3j +import javax.inject.Inject + + +class WalletProvider @Inject constructor( + private val web3: Web3j, + private val zkSync: ZkSync, + private val credentials: Credentials +) { + + @WorkerThread + //TODO can we use single Wallet instance? + fun create(): Result { + return runCatching { Wallet(web3, zkSync, credentials) } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/zk/android/app/network/ZkConfig.kt b/android/app/src/main/java/com/zk/android/app/network/ZkConfig.kt new file mode 100644 index 0000000..c210778 --- /dev/null +++ b/android/app/src/main/java/com/zk/android/app/network/ZkConfig.kt @@ -0,0 +1,9 @@ +package com.zk.android.app.network + + +data class ZkConfig( + val rpc: String, + val url: String, + val socket: String, + val privateKey: String +) \ No newline at end of file diff --git a/android/app/src/main/java/com/zk/android/app/presentation/MainActivity.kt b/android/app/src/main/java/com/zk/android/app/presentation/MainActivity.kt new file mode 100644 index 0000000..2cea8a1 --- /dev/null +++ b/android/app/src/main/java/com/zk/android/app/presentation/MainActivity.kt @@ -0,0 +1,27 @@ +package com.zk.android.app.presentation + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.findNavController +import androidx.navigation.ui.AppBarConfiguration +import com.zk.android.app.R +import com.zk.android.app.databinding.ActivityMainBinding +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MainActivity : AppCompatActivity() { + + private lateinit var appBarConfiguration: AppBarConfiguration + private lateinit var binding: ActivityMainBinding + + override fun onCreate(savedInstanceState: Bundle?) { + setTheme(R.style.Theme_ZkSyncApp) + super.onCreate(savedInstanceState) + + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + val navController = findNavController(R.id.nav_host_fragment_content_main) + appBarConfiguration = AppBarConfiguration(navController.graph) + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/zk/android/app/presentation/transfer/TransferFragment.kt b/android/app/src/main/java/com/zk/android/app/presentation/transfer/TransferFragment.kt new file mode 100644 index 0000000..5a456b3 --- /dev/null +++ b/android/app/src/main/java/com/zk/android/app/presentation/transfer/TransferFragment.kt @@ -0,0 +1,95 @@ +package com.zk.android.app.presentation.transfer + + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import com.zk.android.app.databinding.FragmentTransferBinding +import com.zk.android.app.utils.viewLifecycle +import dagger.hilt.android.AndroidEntryPoint +import org.orbitmvi.orbit.viewmodel.observe + +@AndroidEntryPoint +internal class TransferFragment : Fragment() { + + private val viewModel: TransferViewModel by viewModels() + private var viewBinding: FragmentTransferBinding by viewLifecycle() + + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + viewBinding = FragmentTransferBinding.inflate(inflater, container, false) + return viewBinding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupListeners() + viewModel.observe( + lifecycleOwner = viewLifecycleOwner, + state = ::handleState, + sideEffect = ::handleSideEffect + ) + } + + private fun setupListeners() { + viewBinding.btnTransfer.setOnClickListener { + val amount = viewBinding.balanceFrom.getAmount() + viewModel.obtainAction(Action.ClickSend(amount)) + } + viewBinding.btnConnection.setOnClickListener { viewModel.obtainAction(Action.ClickConnect) } + } + + private fun handleState(state: ViewState) { + applyStateVisibility(state) + when (state) { + is ViewState.Connection -> showConnection(state) + is ViewState.Data -> showData(state) + } + } + + private fun applyStateVisibility(state: ViewState) { + viewBinding.groupConnection.isVisible = state !is ViewState.Data + viewBinding.btnConnection.isVisible = state is ViewState.Connection.Error + viewBinding.groupData.isVisible = state is ViewState.Data + } + + private fun showConnection(state: ViewState.Connection) { + with(viewBinding) { + connectionImg.setAnimation(state.animationRes) + connectionImg.playAnimation() + connectionText.setText(state.descRes) + } + } + + private fun showData(state: ViewState.Data) { + viewBinding.balanceFrom.setupModel(state.balance.fromWalletModel) + viewBinding.balanceTo.setupModel(state.balance.toWalletModel) + viewBinding.balanceFrom.isEnabled = !state.inProgress + viewBinding.balanceTo.isEnabled = !state.inProgress + viewBinding.btnTransfer.isEnabled = !state.inProgress + viewBinding.balanceSending.isVisible = state.inProgress + } + + private fun handleSideEffect(effect: Effect) { + when (effect) { + is Navigation -> handleNavigation(effect) + is ViewEffect -> handleEffect(effect) + } + } + + private fun handleNavigation(navigation: Navigation) {} + private fun handleEffect(effect: ViewEffect) { + when (effect) { + is ViewEffect.ShowToast -> showToast(effect) + } + } + + private fun showToast(effect: ViewEffect.ShowToast) { + Toast.makeText(requireContext(), effect.textResId, Toast.LENGTH_LONG).show() + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/zk/android/app/presentation/transfer/TransferState.kt b/android/app/src/main/java/com/zk/android/app/presentation/transfer/TransferState.kt new file mode 100644 index 0000000..1925ff2 --- /dev/null +++ b/android/app/src/main/java/com/zk/android/app/presentation/transfer/TransferState.kt @@ -0,0 +1,40 @@ +package com.zk.android.app.presentation.transfer + +import androidx.annotation.RawRes +import androidx.annotation.StringRes +import com.zk.android.app.R +import com.zk.android.app.presentation.transfer.model.BalanceModel + +internal sealed class ViewState { + sealed class Connection( + @RawRes val animationRes: Int, + @StringRes val descRes: Int + ) : ViewState() { + data object Loading : Connection( + R.raw.lottie_loading, + R.string.connection + ) + + data object Error : Connection( + R.raw.lottie_connection_error, + R.string.connection_error + ) + } + + data class Data(val balance: BalanceModel, val inProgress: Boolean = false) : ViewState() +} + +internal sealed interface Effect + +internal sealed class Navigation : Effect { + +} + +internal sealed class ViewEffect : Effect { + class ShowToast(@StringRes val textResId: Int) : ViewEffect() +} + +internal sealed class Action { + data object ClickConnect : Action() + class ClickSend(val amount: Float) : Action() +} \ No newline at end of file diff --git a/android/app/src/main/java/com/zk/android/app/presentation/transfer/TransferViewModel.kt b/android/app/src/main/java/com/zk/android/app/presentation/transfer/TransferViewModel.kt new file mode 100644 index 0000000..a96ebaa --- /dev/null +++ b/android/app/src/main/java/com/zk/android/app/presentation/transfer/TransferViewModel.kt @@ -0,0 +1,77 @@ +package com.zk.android.app.presentation.transfer + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import com.zk.android.app.R +import com.zk.android.app.domain.balance.GetBalanceUseCase +import com.zk.android.app.domain.deposit.DepositUseCase +import com.zk.android.app.presentation.transfer.model.ModelMapper +import com.zk.android.app.presentation.transfer.widget.FromWalletViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.container +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.postSideEffect +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.syntax.simple.runOn +import javax.inject.Inject + +@HiltViewModel +internal class TransferViewModel @Inject constructor( + private val application: Application, + private val mapper: ModelMapper, + private val balanceUseCase: GetBalanceUseCase, + private val depositUseCase: DepositUseCase +) : AndroidViewModel(application), ContainerHost { + + override val container = viewModelScope.container(ViewState.Connection.Loading) + + init { + connect() + } + + private fun connect() = intent { + reduce { ViewState.Connection.Loading } + val result = balanceUseCase.getBalance().getOrElse { + reduce { ViewState.Connection.Error } + return@intent + } + val state = ViewState.Data( + mapper.map(result) + ) + reduce { state } + } + + fun obtainAction(action: Action) { + when (action) { + is Action.ClickConnect -> processClickConnect() + is Action.ClickSend -> processClickSend(action) + } + } + + private fun processClickConnect() = intent { + connect() + } + + private fun processClickSend(action: Action.ClickSend) = intent { + runOn(ViewState.Data::class) { + val amount = action.amount + reduce { + state.copy( + balance = state.balance.copy(fromWalletModel = FromWalletViewModel(amount.toString())), + inProgress = true + ) + } + val result = depositUseCase.deposit(action.amount).getOrElse { + reduce { ViewState.Connection.Error } + return@runOn + } + val state = ViewState.Data( + mapper.map(result.newBalance) + ) + reduce { state } + postSideEffect(ViewEffect.ShowToast(R.string.deposit_completed)) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/zk/android/app/presentation/transfer/model/BalanceModel.kt b/android/app/src/main/java/com/zk/android/app/presentation/transfer/model/BalanceModel.kt new file mode 100644 index 0000000..4f89b96 --- /dev/null +++ b/android/app/src/main/java/com/zk/android/app/presentation/transfer/model/BalanceModel.kt @@ -0,0 +1,10 @@ +package com.zk.android.app.presentation.transfer.model + +import com.zk.android.app.presentation.transfer.widget.FromWalletViewModel +import com.zk.android.app.presentation.transfer.widget.ToWalletViewModel + + +data class BalanceModel( + val fromWalletModel: FromWalletViewModel, + val toWalletModel: ToWalletViewModel, +) \ No newline at end of file diff --git a/android/app/src/main/java/com/zk/android/app/presentation/transfer/model/ModelMapper.kt b/android/app/src/main/java/com/zk/android/app/presentation/transfer/model/ModelMapper.kt new file mode 100644 index 0000000..cbf5426 --- /dev/null +++ b/android/app/src/main/java/com/zk/android/app/presentation/transfer/model/ModelMapper.kt @@ -0,0 +1,19 @@ +package com.zk.android.app.presentation.transfer.model + +import com.zk.android.app.domain.balance.BalanceModelDomain +import com.zk.android.app.presentation.transfer.widget.FromWalletViewModel +import com.zk.android.app.presentation.transfer.widget.ToWalletViewModel +import com.zk.android.app.utils.weiToEther +import javax.inject.Inject + + +class ModelMapper @Inject constructor() { + fun map(balanceModelDomain: BalanceModelDomain): BalanceModel { + return with(balanceModelDomain) { + BalanceModel( + FromWalletViewModel(l1Balance.weiToEther().toString()), + ToWalletViewModel(l2Balance.weiToEther().toString()) + ) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/zk/android/app/presentation/transfer/widget/FromWalletView.kt b/android/app/src/main/java/com/zk/android/app/presentation/transfer/widget/FromWalletView.kt new file mode 100644 index 0000000..9d840a7 --- /dev/null +++ b/android/app/src/main/java/com/zk/android/app/presentation/transfer/widget/FromWalletView.kt @@ -0,0 +1,35 @@ +package com.zk.android.app.presentation.transfer.widget + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.setPadding +import com.zk.android.app.R +import com.zk.android.app.databinding.ViewFromBalanceBinding +import com.zk.android.app.utils.toPixelI + + +class FromWalletView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + private val binding = ViewFromBalanceBinding.inflate(LayoutInflater.from(context), this) + + init { + setBackgroundResource(R.drawable.bg_gray_content) + setPadding(context.toPixelI(16)) + } + + fun setupModel(model: FromWalletViewModel) { + binding.amount.setText(model.balance) + binding.amount.clearFocus() + } + + fun getAmount(): Float { + val text = binding.amount.text?.toString() ?: return 0f + return text.toFloat() + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/zk/android/app/presentation/transfer/widget/FromWalletViewModel.kt b/android/app/src/main/java/com/zk/android/app/presentation/transfer/widget/FromWalletViewModel.kt new file mode 100644 index 0000000..6347dea --- /dev/null +++ b/android/app/src/main/java/com/zk/android/app/presentation/transfer/widget/FromWalletViewModel.kt @@ -0,0 +1,6 @@ +package com.zk.android.app.presentation.transfer.widget + + +data class FromWalletViewModel ( + val balance: String +) \ No newline at end of file diff --git a/android/app/src/main/java/com/zk/android/app/presentation/transfer/widget/ToWalletView.kt b/android/app/src/main/java/com/zk/android/app/presentation/transfer/widget/ToWalletView.kt new file mode 100644 index 0000000..0c4ccec --- /dev/null +++ b/android/app/src/main/java/com/zk/android/app/presentation/transfer/widget/ToWalletView.kt @@ -0,0 +1,29 @@ +package com.zk.android.app.presentation.transfer.widget + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.setPadding +import com.zk.android.app.R +import com.zk.android.app.databinding.ViewToBalanceBinding +import com.zk.android.app.utils.toPixelI + + +class ToWalletView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + private val binding = ViewToBalanceBinding.inflate(LayoutInflater.from(context), this) + + init { + setBackgroundResource(R.drawable.bg_gray_content) + setPadding(context.toPixelI(16)) + } + + fun setupModel(model: ToWalletViewModel) { + binding.balanceValue.text = model.balance + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/zk/android/app/presentation/transfer/widget/ToWalletViewModel.kt b/android/app/src/main/java/com/zk/android/app/presentation/transfer/widget/ToWalletViewModel.kt new file mode 100644 index 0000000..024c582 --- /dev/null +++ b/android/app/src/main/java/com/zk/android/app/presentation/transfer/widget/ToWalletViewModel.kt @@ -0,0 +1,6 @@ +package com.zk.android.app.presentation.transfer.widget + + +data class ToWalletViewModel ( + val balance: String +) \ No newline at end of file diff --git a/android/app/src/main/java/com/zk/android/app/utils/EthExt.kt b/android/app/src/main/java/com/zk/android/app/utils/EthExt.kt new file mode 100644 index 0000000..56ad3e3 --- /dev/null +++ b/android/app/src/main/java/com/zk/android/app/utils/EthExt.kt @@ -0,0 +1,27 @@ +package com.zk.android.app.utils + +import org.web3j.protocol.core.RemoteCall +import org.web3j.protocol.core.methods.response.TransactionReceipt +import org.web3j.tx.response.TransactionReceiptProcessor +import java.math.BigInteger + +fun RemoteCall.sendSafe(): Result { + return runCatching { send() } +} + +fun TransactionReceiptProcessor.waitForTransactionReceiptSafe(transactionHash: String): Result { + return runCatching { waitForTransactionReceipt(transactionHash) } +} + +fun BigInteger.weiToEther(): Double { + val etherInWei = BigInteger.TEN.pow(18) // 1 ether = 10^18 wei + return toDouble() / etherInWei.toDouble() +} + +fun Float.toWei(): BigInteger { + // Ethereum wei is the smallest denomination of Ether (1 Ether = 10^18 wei) + val weiPerEther = BigInteger.TEN.pow(18) + + // Convert the Float value to BigInteger by multiplying it by the wei per Ether + return (this * weiPerEther.toFloat()).toBigDecimal().toBigInteger() +} \ No newline at end of file diff --git a/android/app/src/main/java/com/zk/android/app/utils/FragmentExt.kt b/android/app/src/main/java/com/zk/android/app/utils/FragmentExt.kt new file mode 100644 index 0000000..847c105 --- /dev/null +++ b/android/app/src/main/java/com/zk/android/app/utils/FragmentExt.kt @@ -0,0 +1,78 @@ +package com.zk.android.app.utils + +import androidx.fragment.app.Fragment +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +fun Fragment.viewLifecycle(onDestroyFinalize: ((T?) -> Unit)? = null): ReadWriteProperty = + object : ReadWriteProperty, DefaultLifecycleObserver { + + private var field: T? = null + + private var lifecycleOwner: LifecycleOwner? = null + + init { + viewLifecycleOwnerLiveData + .observe(this@viewLifecycle) { newViewLifecycleOwner -> + lifecycleOwner + ?.lifecycle + ?.removeObserver(this) + + lifecycleOwner = newViewLifecycleOwner.also { + it.lifecycle.addObserver(this) + } + } + } + + override fun onDestroy(owner: LifecycleOwner) { + super.onDestroy(owner) + onDestroyFinalize?.invoke(field) + field = null + lifecycleOwner = null + } + + override fun getValue(thisRef: Fragment, property: KProperty<*>): T { + return this.field!! + } + + override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) { + this.field = value + } + } + +fun Fragment.viewLifecycleNullable(): ReadWriteProperty = + object : ReadWriteProperty, DefaultLifecycleObserver { + + private var field: T? = null + + private var lifecycleOwner: LifecycleOwner? = null + + init { + viewLifecycleOwnerLiveData + .observe(this@viewLifecycleNullable) { newViewLifecycleOwner -> + lifecycleOwner + ?.lifecycle + ?.removeObserver(this) + + lifecycleOwner = newViewLifecycleOwner.also { + it.lifecycle.addObserver(this) + } + } + } + + override fun onDestroy(owner: LifecycleOwner) { + super.onDestroy(owner) + field = null + lifecycleOwner = null + } + + override fun getValue(thisRef: Fragment, property: KProperty<*>): T? { + return this.field + } + + override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T?) { + this.field = value + } + } \ No newline at end of file diff --git a/android/app/src/main/java/com/zk/android/app/utils/ViewExt.kt b/android/app/src/main/java/com/zk/android/app/utils/ViewExt.kt new file mode 100644 index 0000000..b4959ae --- /dev/null +++ b/android/app/src/main/java/com/zk/android/app/utils/ViewExt.kt @@ -0,0 +1,12 @@ +package com.zk.android.app.utils + +import android.content.Context +import android.util.TypedValue + +fun Context.toPixelF(dpSize: Int) = toPixelF(dpSize.toFloat()) + +fun Context.toPixelF(dpSizeF: Float) = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, dpSizeF, resources.displayMetrics +) + +fun Context.toPixelI(dpSize: Int) = toPixelF(dpSize).toInt() \ No newline at end of file diff --git a/android/app/src/main/res/animator/views_animation_selector.xml b/android/app/src/main/res/animator/views_animation_selector.xml new file mode 100644 index 0000000..06dbb13 --- /dev/null +++ b/android/app/src/main/res/animator/views_animation_selector.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable-nodpi/img_splash_brand_white.png b/android/app/src/main/res/drawable-nodpi/img_splash_brand_white.png new file mode 100644 index 0000000..338f232 Binary files /dev/null and b/android/app/src/main/res/drawable-nodpi/img_splash_brand_white.png differ diff --git a/android/app/src/main/res/drawable/bg_gray_content.xml b/android/app/src/main/res/drawable/bg_gray_content.xml new file mode 100644 index 0000000..a8ddd26 --- /dev/null +++ b/android/app/src/main/res/drawable/bg_gray_content.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/bg_gray_overlay.xml b/android/app/src/main/res/drawable/bg_gray_overlay.xml new file mode 100644 index 0000000..c8f1222 --- /dev/null +++ b/android/app/src/main/res/drawable/bg_gray_overlay.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/ic_ethereum.xml b/android/app/src/main/res/drawable/ic_ethereum.xml new file mode 100644 index 0000000..3de1fc9 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_ethereum.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/img_zk_logo.png b/android/app/src/main/res/drawable/img_zk_logo.png new file mode 100644 index 0000000..3c86085 Binary files /dev/null and b/android/app/src/main/res/drawable/img_zk_logo.png differ diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..9cc8825 --- /dev/null +++ b/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_transfer.xml b/android/app/src/main/res/layout/fragment_transfer.xml new file mode 100644 index 0000000..ceb20ca --- /dev/null +++ b/android/app/src/main/res/layout/fragment_transfer.xml @@ -0,0 +1,132 @@ + + + + + + + + +