diff --git a/common/src/main/res/values-tr/strings.xml b/common/src/main/res/values-tr/strings.xml
index d793816b..405cc64b 100644
--- a/common/src/main/res/values-tr/strings.xml
+++ b/common/src/main/res/values-tr/strings.xml
@@ -253,7 +253,7 @@ Sizin için en iyi seçeneği bulmak amacıyla tekliflerini karşılaştırın.<
Tutar girin
HEZ → DOT yönü manuel işlenir, 24 saate kadar sürebilir.
HEZ → DOT yönü şu anda devre dışı.
- USDT(Pez) → USDT(Pol) yönü şu anda devre dışı.
+ Bu tutar köprünün mevcut rezervini (%s USDT) aşıyor. Daha küçük bir tutar deneyin.
Al/Sat
Telefon ayarlarınızda %s
OTG\'yi etkinleştir
diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml
index 6fff5081..a10dbc96 100644
--- a/common/src/main/res/values/strings.xml
+++ b/common/src/main/res/values/strings.xml
@@ -364,7 +364,7 @@
Insufficient balance
Amount below minimum
Enter amount
- USDT(Pez)→USDT(Pol) swaps are temporarily unavailable. Waiting for 1:1 liquidity to be established.
+ This amount exceeds the bridge\'s current available reserve (%s USDT). Try a smaller amount.
Bridge allowance OK (%s)
Sign renewal (%s)
diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigConstants.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigConstants.kt
index 09c827f1..f3647c0d 100644
--- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigConstants.kt
+++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigConstants.kt
@@ -14,8 +14,15 @@ package io.novafoundation.nova.feature_assets.domain.bridge.multisig
object BridgeMultisigConstants {
const val WUSDT_ASSET_ID = 1000
+ const val POLKADOT_USDT_ASSET_ID = 1
const val MULTISIG_ADDRESS = "5GvwxmCDp3PC33KHoeWSgj3S7ocE7nzk1jiCCZMPSDBFeNcj"
+ /** Same multisig account, Polkadot Asset Hub SS58 encoding - where the real USDT backing
+ * wUSDT withdrawals actually sits. Used to check a specific withdrawal amount against the
+ * real reserve (see BridgeMultisigInteractor.getPolkadotUsdtReserve) instead of the old
+ * dead-external-service boolean check this replaced (217.77.6.126:3030/status belonged to
+ * the legacy bridge bot, stopped this same session - see res/validators-tiki.md). */
+ const val MULTISIG_ADDRESS_POLKADOT = "15sF76THfpefUaKomHZSpssayRbsp6Yt6ESgMrLjzJCmpe66"
const val AUTOMATION_KEY_ADDRESS = "5GQu4PFUb1f3MTJ7i7c1CtLgDk3TVvpSW1VbQCRmfkMoC8cM"
const val THRESHOLD = 3
diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigInteractor.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigInteractor.kt
index 9ef7c743..27ecfff3 100644
--- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigInteractor.kt
+++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigInteractor.kt
@@ -38,6 +38,14 @@ interface BridgeMultisigInteractor {
suspend fun getSignerState(): BridgeSignerState?
suspend fun submitRenewalSignature(): Result
+
+ /** Real USDT (base units) the multisig actually holds on Polkadot Asset Hub right now - the
+ * true backing for wUSDT->USDT withdrawals. Replaces the old wusdtToUsdtActive boolean
+ * fetched from the legacy bridge bot's :3030/status endpoint, which this session stopped
+ * (see res/validators-tiki.md) - that made the old check always report "inactive"
+ * regardless of real reserve. A specific withdrawal should be allowed whenever it's covered
+ * by this real balance, not gated on an unrelated dead service or on total supply parity. */
+ suspend fun getPolkadotUsdtReserve(): BigInteger
}
@FeatureScope
@@ -118,6 +126,18 @@ class RealBridgeMultisigInteractor @Inject constructor(
}.getOrThrow().requireOk()
}
+ override suspend fun getPolkadotUsdtReserve(): BigInteger {
+ val polkadotChain = chainRegistry.getChain(ChainGeneses.POLKADOT_ASSET_HUB)
+ val multisigAccountId = BridgeMultisigConstants.MULTISIG_ADDRESS_POLKADOT.toAccountId().intoKey()
+
+ return storageDataSource.query(polkadotChain.id) {
+ runtime.metadata.bridgeAssets().assetBalance.query(
+ BridgeMultisigConstants.POLKADOT_USDT_ASSET_ID.toBigInteger(),
+ multisigAccountId,
+ )
+ } ?: BigInteger.ZERO
+ }
+
private suspend fun queryRemainingAllowance(chain: Chain): BigInteger {
val multisigAccountId = BridgeMultisigConstants.MULTISIG_ADDRESS.toAccountId().intoKey()
val delegateAccountId = BridgeMultisigConstants.AUTOMATION_KEY_ADDRESS.toAccountId().intoKey()
diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigRuntimeApi.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigRuntimeApi.kt
index ebc5456b..f72852b2 100644
--- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigRuntimeApi.kt
+++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/domain/bridge/multisig/BridgeMultisigRuntimeApi.kt
@@ -48,6 +48,18 @@ val BridgeAssetsApi.approvalAmount: QueryableStorageEntry3 AssetAccount { balance, ... }` - only `balance` needed.
+ * Used to check a custody account's real held amount (e.g. the multisig's real USDT reserve on
+ * Polkadot Asset Hub), not the connected wallet's own balance. */
+context(StorageQueryContext)
+val BridgeAssetsApi.assetBalance: QueryableStorageEntry2
+ get() = storage2(
+ name = "Account",
+ binding = { decoded, _, _ -> bindNumber(decoded.castToStruct()["balance"]) },
+ key2ToInternalConverter = AccountIdKey.scaleEncoder,
+ key2FromInternalConverter = AccountIdKey.scaleDecoder,
+ )
+
@JvmInline
value class BridgeMultisigApi(override val module: Module) : QueryableModule
diff --git a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeViewModel.kt b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeViewModel.kt
index f15b02d6..414dcedd 100644
--- a/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeViewModel.kt
+++ b/feature-assets/src/main/java/io/novafoundation/nova/feature_assets/presentation/bridge/BridgeViewModel.kt
@@ -21,14 +21,10 @@ import io.novafoundation.nova.runtime.ext.addressOf
import io.novafoundation.nova.runtime.ext.displayNameWithAssetStandard
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import org.json.JSONObject
import java.math.BigDecimal
import java.math.RoundingMode
-import java.net.URL
/**
* DOT<->HEZ used to be a second pair here, retired 2026-07 in favor of the multisig-custodied
@@ -60,7 +56,8 @@ class BridgeViewModel(
const val FEE_PERCENT = 0.001
const val MIN_USDT = 1.0
- const val BRIDGE_STATUS_API = "http://217.77.6.126:3030/status"
+ // USDT has 6 decimals on both Polkadot and Pezkuwi Asset Hub.
+ val USDT_DECIMALS_DIVISOR: BigDecimal = BigDecimal.TEN.pow(6)
}
private val _pair = MutableLiveData(BridgePair.USDT)
@@ -121,12 +118,15 @@ class BridgeViewModel(
val fillAmountEvent: LiveData> = _fillAmountEvent
private var currentAmount: Double = 0.0
- private var isWusdtToUsdtActive: Boolean = false
+ /** Real USDT the multisig actually holds on Polkadot Asset Hub - see
+ * BridgeMultisigInteractor.getPolkadotUsdtReserve for why this replaced a dead external
+ * status check that always reported "inactive" regardless of the real reserve. */
+ private var polkadotUsdtReserve: BigDecimal = BigDecimal.ZERO
private var availableBalance: BigDecimal = BigDecimal.ZERO
private var balanceJob: Job? = null
init {
- fetchBridgeStatus()
+ fetchReserveStatus()
refreshSignerState()
updateCards()
loadPairOptions()
@@ -171,7 +171,7 @@ class BridgeViewModel(
currentAmount = amount
calculateOutput()
updateInsufficientBalanceState()
- updateButtonState()
+ updateWarningState() // re-check the entered amount against the cached reserve
}
fun maxClicked() {
@@ -208,27 +208,15 @@ class BridgeViewModel(
router.back()
}
- private fun fetchBridgeStatus() {
+ private fun fetchReserveStatus() {
launch {
- try {
- isWusdtToUsdtActive = withContext(Dispatchers.IO) {
- fetchStatusFromApi()
- }
- updateWarningState()
+ polkadotUsdtReserve = try {
+ val raw = bridgeMultisigInteractor.getPolkadotUsdtReserve()
+ BigDecimal(raw).divide(USDT_DECIMALS_DIVISOR)
} catch (e: Exception) {
- isWusdtToUsdtActive = false
- updateWarningState()
+ BigDecimal.ZERO
}
- }
- }
-
- private fun fetchStatusFromApi(): Boolean {
- return try {
- val response = URL(BRIDGE_STATUS_API).readText()
- val json = JSONObject(response)
- json.optBoolean("wusdtToUsdtActive", false)
- } catch (e: Exception) {
- false
+ updateWarningState()
}
}
@@ -237,14 +225,20 @@ class BridgeViewModel(
when (dir) {
BridgeDirection.WUSDT_TO_USDT -> {
- _showWarning.postValue(true)
- if (!isWusdtToUsdtActive) {
+ val requested = BigDecimal.valueOf(currentAmount)
+ val exceedsReserve = currentAmount > 0 && requested > polkadotUsdtReserve
+ _showWarning.postValue(exceedsReserve)
+ if (exceedsReserve) {
_warningBlocked.postValue(true)
- _warningText.postValue(resourceManager.getString(R.string.bridge_wusdt_to_usdt_blocked))
+ _warningText.postValue(
+ resourceManager.getString(
+ R.string.bridge_wusdt_to_usdt_blocked,
+ polkadotUsdtReserve.setScale(2, RoundingMode.DOWN).stripTrailingZeros().toPlainString()
+ )
+ )
} else {
_warningBlocked.postValue(false)
_warningText.postValue("")
- _showWarning.postValue(false)
}
}
else -> {
@@ -274,11 +268,12 @@ class BridgeViewModel(
private fun updateButtonState() {
val dir = _direction.value ?: return
+ val requested = BigDecimal.valueOf(currentAmount)
_buttonState.value = when {
currentAmount <= 0 -> ButtonState.DISABLED
currentAmount < MIN_USDT -> ButtonState.DISABLED
- BigDecimal.valueOf(currentAmount) > availableBalance -> ButtonState.DISABLED
- dir == BridgeDirection.WUSDT_TO_USDT && !isWusdtToUsdtActive -> ButtonState.DISABLED
+ requested > availableBalance -> ButtonState.DISABLED
+ dir == BridgeDirection.WUSDT_TO_USDT && requested > polkadotUsdtReserve -> ButtonState.DISABLED
else -> ButtonState.NORMAL
}
}
@@ -292,7 +287,7 @@ class BridgeViewModel(
}
fun refreshBridgeStatus() {
- fetchBridgeStatus()
+ fetchReserveStatus()
refreshSignerState()
}