diff --git a/feature-bridge-api/build.gradle b/feature-bridge-api/build.gradle new file mode 100644 index 0000000..39b78db --- /dev/null +++ b/feature-bridge-api/build.gradle @@ -0,0 +1,22 @@ +apply plugin: 'kotlin-parcelize' + +android { + namespace 'io.novafoundation.nova.feature_bridge_api' +} + +dependencies { + implementation coroutinesDep + implementation project(':runtime') + implementation project(":feature-account-api") + implementation project(":feature-wallet-api") + implementation project(":common") + + implementation daggerDep + ksp daggerCompiler + + implementation substrateSdkDep + + api project(':core-api') + + testImplementation project(':test-shared') +} diff --git a/feature-bridge-api/src/main/AndroidManifest.xml b/feature-bridge-api/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9a40236 --- /dev/null +++ b/feature-bridge-api/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/feature-bridge-api/src/main/java/io/novafoundation/nova/feature_bridge_api/di/BridgeFeatureApi.kt b/feature-bridge-api/src/main/java/io/novafoundation/nova/feature_bridge_api/di/BridgeFeatureApi.kt new file mode 100644 index 0000000..5d50190 --- /dev/null +++ b/feature-bridge-api/src/main/java/io/novafoundation/nova/feature_bridge_api/di/BridgeFeatureApi.kt @@ -0,0 +1,8 @@ +package io.novafoundation.nova.feature_bridge_api.di + +import io.novafoundation.nova.feature_bridge_api.domain.model.BridgeConfig + +interface BridgeFeatureApi { + + val bridgeConfig: BridgeConfig +} diff --git a/feature-bridge-api/src/main/java/io/novafoundation/nova/feature_bridge_api/domain/model/BridgeConfig.kt b/feature-bridge-api/src/main/java/io/novafoundation/nova/feature_bridge_api/domain/model/BridgeConfig.kt new file mode 100644 index 0000000..5697cfb --- /dev/null +++ b/feature-bridge-api/src/main/java/io/novafoundation/nova/feature_bridge_api/domain/model/BridgeConfig.kt @@ -0,0 +1,58 @@ +package io.novafoundation.nova.feature_bridge_api.domain.model + +import java.math.BigDecimal + +/** + * wUSDT Bridge Configuration + * + * This bridge enables 1:1 backed wUSDT on Pezkuwi Asset Hub, + * backed by real USDT on Polkadot Asset Hub. + */ +data class BridgeConfig( + /** Bridge wallet address on Polkadot Asset Hub (for deposits) */ + val polkadotDepositAddress: String, + + /** Bridge wallet address on Pezkuwi Asset Hub */ + val pezkuwiAddress: String, + + /** USDT Asset ID on Polkadot Asset Hub */ + val polkadotUsdtAssetId: Int, + + /** wUSDT Asset ID on Pezkuwi Asset Hub */ + val pezkuwiWusdtAssetId: Int, + + /** Minimum deposit amount in USDT */ + val minDeposit: BigDecimal, + + /** Minimum withdrawal amount in USDT */ + val minWithdraw: BigDecimal, + + /** Bridge fee in basis points (e.g., 10 = 0.1%) */ + val feeBasisPoints: Int +) { + companion object { + val DEFAULT = BridgeConfig( + polkadotDepositAddress = "16dSTc3BexjQKiPta7yNncF8nio4YgDQiPbudHzkuh7XJi8K", + pezkuwiAddress = "5Hh9KGn7oBTvtBPNcUvNeTQyw6oQrNfGdtsRU11QMc618Rse", + polkadotUsdtAssetId = 1984, + pezkuwiWusdtAssetId = 1000, + minDeposit = BigDecimal("10"), + minWithdraw = BigDecimal("10"), + feeBasisPoints = 10 + ) + } + + /** Fee percentage as a human-readable string */ + val feePercentage: String + get() = "${feeBasisPoints.toDouble() / 100}%" + + /** Calculate fee for a given amount */ + fun calculateFee(amount: BigDecimal): BigDecimal { + return amount.multiply(BigDecimal(feeBasisPoints)).divide(BigDecimal(10000)) + } + + /** Calculate net amount after fee */ + fun calculateNetAmount(amount: BigDecimal): BigDecimal { + return amount.subtract(calculateFee(amount)) + } +} diff --git a/feature-bridge-api/src/main/java/io/novafoundation/nova/feature_bridge_api/domain/model/BridgeStatus.kt b/feature-bridge-api/src/main/java/io/novafoundation/nova/feature_bridge_api/domain/model/BridgeStatus.kt new file mode 100644 index 0000000..f48486c --- /dev/null +++ b/feature-bridge-api/src/main/java/io/novafoundation/nova/feature_bridge_api/domain/model/BridgeStatus.kt @@ -0,0 +1,37 @@ +package io.novafoundation.nova.feature_bridge_api.domain.model + +import java.math.BigDecimal + +/** + * Bridge status showing backing ratio and reserves + */ +data class BridgeStatus( + /** Total USDT held in bridge wallet on Polkadot */ + val totalUsdtBacking: BigDecimal, + + /** Total wUSDT in circulation on Pezkuwi */ + val totalWusdtCirculating: BigDecimal, + + /** Bridge operational status */ + val isOperational: Boolean, + + /** Last sync timestamp */ + val lastSyncTimestamp: Long +) { + /** Backing ratio (should be >= 100%) */ + val backingRatio: BigDecimal + get() = if (totalWusdtCirculating > BigDecimal.ZERO) { + totalUsdtBacking.divide(totalWusdtCirculating, 4, java.math.RoundingMode.HALF_UP) + .multiply(BigDecimal(100)) + } else { + BigDecimal(100) + } + + /** Reserve (excess USDT in bridge, i.e., collected fees) */ + val reserve: BigDecimal + get() = totalUsdtBacking.subtract(totalWusdtCirculating).coerceAtLeast(BigDecimal.ZERO) + + /** Is the bridge fully backed? */ + val isFullyBacked: Boolean + get() = backingRatio >= BigDecimal(100) +} diff --git a/feature-bridge-api/src/main/java/io/novafoundation/nova/feature_bridge_api/domain/model/BridgeTransaction.kt b/feature-bridge-api/src/main/java/io/novafoundation/nova/feature_bridge_api/domain/model/BridgeTransaction.kt new file mode 100644 index 0000000..f9dbfc3 --- /dev/null +++ b/feature-bridge-api/src/main/java/io/novafoundation/nova/feature_bridge_api/domain/model/BridgeTransaction.kt @@ -0,0 +1,33 @@ +package io.novafoundation.nova.feature_bridge_api.domain.model + +import java.math.BigDecimal + +enum class BridgeTransactionType { + DEPOSIT, // Polkadot USDT -> Pezkuwi wUSDT + WITHDRAW // Pezkuwi wUSDT -> Polkadot USDT +} + +enum class BridgeTransactionStatus { + PENDING, + PROCESSING, + COMPLETED, + FAILED +} + +/** + * A bridge transaction (deposit or withdrawal) + */ +data class BridgeTransaction( + val id: String, + val type: BridgeTransactionType, + val status: BridgeTransactionStatus, + val amount: BigDecimal, + val fee: BigDecimal, + val netAmount: BigDecimal, + val sourceAddress: String, + val destinationAddress: String, + val sourceTxHash: String?, + val destinationTxHash: String?, + val createdAt: Long, + val completedAt: Long? +) diff --git a/feature-bridge-api/src/main/java/io/novafoundation/nova/feature_bridge_api/presentation/BridgeRouter.kt b/feature-bridge-api/src/main/java/io/novafoundation/nova/feature_bridge_api/presentation/BridgeRouter.kt new file mode 100644 index 0000000..b87c6f7 --- /dev/null +++ b/feature-bridge-api/src/main/java/io/novafoundation/nova/feature_bridge_api/presentation/BridgeRouter.kt @@ -0,0 +1,12 @@ +package io.novafoundation.nova.feature_bridge_api.presentation + +interface BridgeRouter { + + fun openBridgeDeposit() + + fun openBridgeWithdraw() + + fun openBridgeStatus() + + fun back() +} diff --git a/feature-bridge-impl/build.gradle b/feature-bridge-impl/build.gradle new file mode 100644 index 0000000..3c3f49e --- /dev/null +++ b/feature-bridge-impl/build.gradle @@ -0,0 +1,41 @@ +apply plugin: 'kotlin-parcelize' + +android { + namespace 'io.novafoundation.nova.feature_bridge_impl' + + buildFeatures { + viewBinding = true + } +} + +dependencies { + implementation coroutinesDep + implementation project(':runtime') + implementation project(":feature-account-api") + implementation project(":feature-wallet-api") + implementation project(":feature-bridge-api") + implementation project(":common") + + implementation materialDep + + implementation daggerDep + ksp daggerCompiler + + implementation substrateSdkDep + + implementation androidDep + implementation constraintDep + + implementation lifeCycleKtxDep + implementation viewModelKtxDep + + implementation coroutinesDep + implementation coroutinesAndroidDep + + // QR Code generation + implementation zXingCoreDep + + api project(':core-api') + + testImplementation project(':test-shared') +} diff --git a/feature-bridge-impl/src/main/AndroidManifest.xml b/feature-bridge-impl/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9a40236 --- /dev/null +++ b/feature-bridge-impl/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/feature-bridge-impl/src/main/java/io/novafoundation/nova/feature_bridge_impl/di/BridgeFeatureComponent.kt b/feature-bridge-impl/src/main/java/io/novafoundation/nova/feature_bridge_impl/di/BridgeFeatureComponent.kt new file mode 100644 index 0000000..a663ce4 --- /dev/null +++ b/feature-bridge-impl/src/main/java/io/novafoundation/nova/feature_bridge_impl/di/BridgeFeatureComponent.kt @@ -0,0 +1,31 @@ +package io.novafoundation.nova.feature_bridge_impl.di + +import dagger.Component +import io.novafoundation.nova.common.di.CommonApi +import io.novafoundation.nova.common.di.scope.FeatureScope +import io.novafoundation.nova.feature_bridge_api.di.BridgeFeatureApi +import io.novafoundation.nova.feature_bridge_impl.presentation.deposit.BridgeDepositFragment + +@Component( + dependencies = [ + BridgeFeatureDependencies::class + ], + modules = [ + BridgeFeatureModule::class + ] +) +@FeatureScope +interface BridgeFeatureComponent : BridgeFeatureApi { + + fun inject(fragment: BridgeDepositFragment) + + @Component.Factory + interface Factory { + fun create( + dependencies: BridgeFeatureDependencies + ): BridgeFeatureComponent + } + + @Component(dependencies = [CommonApi::class]) + interface BridgeFeatureDependenciesComponent : BridgeFeatureDependencies +} diff --git a/feature-bridge-impl/src/main/java/io/novafoundation/nova/feature_bridge_impl/di/BridgeFeatureDependencies.kt b/feature-bridge-impl/src/main/java/io/novafoundation/nova/feature_bridge_impl/di/BridgeFeatureDependencies.kt new file mode 100644 index 0000000..3e0c0ca --- /dev/null +++ b/feature-bridge-impl/src/main/java/io/novafoundation/nova/feature_bridge_impl/di/BridgeFeatureDependencies.kt @@ -0,0 +1,8 @@ +package io.novafoundation.nova.feature_bridge_impl.di + +import android.content.Context + +interface BridgeFeatureDependencies { + + fun context(): Context +} diff --git a/feature-bridge-impl/src/main/java/io/novafoundation/nova/feature_bridge_impl/di/BridgeFeatureModule.kt b/feature-bridge-impl/src/main/java/io/novafoundation/nova/feature_bridge_impl/di/BridgeFeatureModule.kt new file mode 100644 index 0000000..1501081 --- /dev/null +++ b/feature-bridge-impl/src/main/java/io/novafoundation/nova/feature_bridge_impl/di/BridgeFeatureModule.kt @@ -0,0 +1,14 @@ +package io.novafoundation.nova.feature_bridge_impl.di + +import dagger.Module +import dagger.Provides +import io.novafoundation.nova.common.di.scope.FeatureScope +import io.novafoundation.nova.feature_bridge_api.domain.model.BridgeConfig + +@Module +class BridgeFeatureModule { + + @Provides + @FeatureScope + fun provideBridgeConfig(): BridgeConfig = BridgeConfig.DEFAULT +} diff --git a/feature-bridge-impl/src/main/java/io/novafoundation/nova/feature_bridge_impl/presentation/deposit/BridgeDepositFragment.kt b/feature-bridge-impl/src/main/java/io/novafoundation/nova/feature_bridge_impl/presentation/deposit/BridgeDepositFragment.kt new file mode 100644 index 0000000..28c7c16 --- /dev/null +++ b/feature-bridge-impl/src/main/java/io/novafoundation/nova/feature_bridge_impl/presentation/deposit/BridgeDepositFragment.kt @@ -0,0 +1,123 @@ +package io.novafoundation.nova.feature_bridge_impl.presentation.deposit + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.graphics.Bitmap +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import com.google.zxing.BarcodeFormat +import com.google.zxing.qrcode.QRCodeWriter +import io.novafoundation.nova.feature_bridge_api.domain.model.BridgeConfig +import io.novafoundation.nova.feature_bridge_impl.databinding.FragmentBridgeDepositBinding +import kotlinx.coroutines.launch + +/** + * Bridge Deposit Screen + * + * Shows the Polkadot Asset Hub address where users should send USDT + * to receive wUSDT on Pezkuwi Asset Hub. + */ +class BridgeDepositFragment : Fragment() { + + private var _binding: FragmentBridgeDepositBinding? = null + private val binding get() = _binding!! + + private val bridgeConfig = BridgeConfig.DEFAULT + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentBridgeDepositBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupUI() + generateQRCode() + } + + private fun setupUI() { + with(binding) { + // Set bridge address + bridgeAddressText.text = bridgeConfig.polkadotDepositAddress + + // Set info text + minDepositText.text = "Minimum: ${bridgeConfig.minDeposit} USDT" + feeText.text = "Fee: ${bridgeConfig.feePercentage}" + + // Copy button + copyAddressButton.setOnClickListener { + copyToClipboard(bridgeConfig.polkadotDepositAddress) + } + + // Address text click also copies + bridgeAddressText.setOnClickListener { + copyToClipboard(bridgeConfig.polkadotDepositAddress) + } + + // Back button + backButton.setOnClickListener { + requireActivity().onBackPressed() + } + } + } + + private fun generateQRCode() { + lifecycleScope.launch { + try { + val qrCodeWriter = QRCodeWriter() + val bitMatrix = qrCodeWriter.encode( + bridgeConfig.polkadotDepositAddress, + BarcodeFormat.QR_CODE, + 512, + 512 + ) + + val width = bitMatrix.width + val height = bitMatrix.height + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565) + + for (x in 0 until width) { + for (y in 0 until height) { + bitmap.setPixel( + x, y, + if (bitMatrix.get(x, y)) android.graphics.Color.BLACK + else android.graphics.Color.WHITE + ) + } + } + + binding.qrCodeImage.setImageBitmap(bitmap) + } catch (e: Exception) { + // QR code generation failed, hide the image + binding.qrCodeImage.visibility = View.GONE + } + } + } + + private fun copyToClipboard(text: String) { + val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("Bridge Address", text) + clipboard.setPrimaryClip(clip) + Toast.makeText(requireContext(), "Address copied!", Toast.LENGTH_SHORT).show() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + companion object { + fun newInstance() = BridgeDepositFragment() + } +} diff --git a/feature-bridge-impl/src/main/java/io/novafoundation/nova/feature_bridge_impl/presentation/status/BridgeStatusFragment.kt b/feature-bridge-impl/src/main/java/io/novafoundation/nova/feature_bridge_impl/presentation/status/BridgeStatusFragment.kt new file mode 100644 index 0000000..c63dd36 --- /dev/null +++ b/feature-bridge-impl/src/main/java/io/novafoundation/nova/feature_bridge_impl/presentation/status/BridgeStatusFragment.kt @@ -0,0 +1,105 @@ +package io.novafoundation.nova.feature_bridge_impl.presentation.status + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import io.novafoundation.nova.feature_bridge_api.domain.model.BridgeConfig +import io.novafoundation.nova.feature_bridge_api.domain.model.BridgeStatus +import io.novafoundation.nova.feature_bridge_impl.databinding.FragmentBridgeStatusBinding +import java.math.BigDecimal + +/** + * Bridge Status Screen + * + * Shows the current bridge backing status, reserves, and transparency info. + */ +class BridgeStatusFragment : Fragment() { + + private var _binding: FragmentBridgeStatusBinding? = null + private val binding get() = _binding!! + + private val bridgeConfig = BridgeConfig.DEFAULT + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentBridgeStatusBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupUI() + loadStatus() + } + + private fun setupUI() { + with(binding) { + backButton.setOnClickListener { + requireActivity().onBackPressed() + } + } + } + + private fun loadStatus() { + // In production, this would fetch real data from the chain + // For now, show placeholder data + val status = BridgeStatus( + totalUsdtBacking = BigDecimal("0"), + totalWusdtCirculating = BigDecimal("0"), + isOperational = true, + lastSyncTimestamp = System.currentTimeMillis() + ) + + displayStatus(status) + } + + private fun displayStatus(status: BridgeStatus) { + with(binding) { + // Backing ratio + backingRatioText.text = "${status.backingRatio.setScale(2)}%" + backingRatioText.setTextColor( + if (status.isFullyBacked) { + resources.getColor(io.novafoundation.nova.feature_bridge_impl.R.color.bridge_success, null) + } else { + resources.getColor(io.novafoundation.nova.feature_bridge_impl.R.color.bridge_error, null) + } + ) + + // Backing details + usdtBackingText.text = "${status.totalUsdtBacking} USDT" + wusdtCirculatingText.text = "${status.totalWusdtCirculating} wUSDT" + reserveText.text = "${status.reserve} USDT" + + // Bridge config + polkadotAddressText.text = bridgeConfig.polkadotDepositAddress + pezkuwiAddressText.text = bridgeConfig.pezkuwiAddress + feeText.text = bridgeConfig.feePercentage + minDepositText.text = "${bridgeConfig.minDeposit} USDT" + + // Status + statusIndicator.setBackgroundColor( + if (status.isOperational) { + resources.getColor(io.novafoundation.nova.feature_bridge_impl.R.color.bridge_success, null) + } else { + resources.getColor(io.novafoundation.nova.feature_bridge_impl.R.color.bridge_error, null) + } + ) + statusText.text = if (status.isOperational) "Operational" else "Offline" + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + companion object { + fun newInstance() = BridgeStatusFragment() + } +} diff --git a/feature-bridge-impl/src/main/res/drawable/bg_address_field.xml b/feature-bridge-impl/src/main/res/drawable/bg_address_field.xml new file mode 100644 index 0000000..237f2c3 --- /dev/null +++ b/feature-bridge-impl/src/main/res/drawable/bg_address_field.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/feature-bridge-impl/src/main/res/drawable/bg_status_indicator.xml b/feature-bridge-impl/src/main/res/drawable/bg_status_indicator.xml new file mode 100644 index 0000000..a66a7e4 --- /dev/null +++ b/feature-bridge-impl/src/main/res/drawable/bg_status_indicator.xml @@ -0,0 +1,5 @@ + + + + diff --git a/feature-bridge-impl/src/main/res/drawable/ic_arrow_back.xml b/feature-bridge-impl/src/main/res/drawable/ic_arrow_back.xml new file mode 100644 index 0000000..4e139d4 --- /dev/null +++ b/feature-bridge-impl/src/main/res/drawable/ic_arrow_back.xml @@ -0,0 +1,10 @@ + + + + diff --git a/feature-bridge-impl/src/main/res/layout/fragment_bridge_deposit.xml b/feature-bridge-impl/src/main/res/layout/fragment_bridge_deposit.xml new file mode 100644 index 0000000..04297a8 --- /dev/null +++ b/feature-bridge-impl/src/main/res/layout/fragment_bridge_deposit.xml @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/feature-bridge-impl/src/main/res/layout/fragment_bridge_status.xml b/feature-bridge-impl/src/main/res/layout/fragment_bridge_status.xml new file mode 100644 index 0000000..64fb689 --- /dev/null +++ b/feature-bridge-impl/src/main/res/layout/fragment_bridge_status.xml @@ -0,0 +1,329 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/feature-bridge-impl/src/main/res/values/colors.xml b/feature-bridge-impl/src/main/res/values/colors.xml new file mode 100644 index 0000000..49f3bd6 --- /dev/null +++ b/feature-bridge-impl/src/main/res/values/colors.xml @@ -0,0 +1,14 @@ + + + + #121212 + #1E1E1E + #FFFFFF + #B3B3B3 + #00D395 + #00A676 + #FFB800 + #FF4444 + #00D395 + #2A2A2A + diff --git a/settings.gradle b/settings.gradle index f955276..86e0c07 100644 --- a/settings.gradle +++ b/settings.gradle @@ -58,3 +58,5 @@ include ':feature-ahm-api' include ':feature-ahm-impl' include ':feature-gift-api' include ':feature-gift-impl' +include ':feature-bridge-api' +include ':feature-bridge-impl'