mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-10-20 18:56:25 +00:00
Add pre-submit consent gate for amounts exceeding auto-pay approval
Symmetric across both bridge directions, checked in priority order before any funds move: 1. Real reserve exceeded (withdrawal only) - the multisig doesn't hold enough real USDT on Polkadot Asset Hub. No signature can fix this, so this stays a hard block (existing bridge_wusdt_to_usdt_blocked message), never offered as "proceed anyway". 2. Automation-key approval exceeded (either direction) - funds exist, the automation key just isn't currently approved to auto-pay that much. Resolvable by 3-of-5 signing, so this is now an explicit opt-in: a warning banner names the ~2h typical review window and the signatories' contact channel (t.me/pezkuwidestek), gated behind a WarningCheckBox the user must tick before Swap re-enables. Previously there was no signal at all here - a large amount would just silently queue for manual review after debiting the user, with no way to know that going in. Both getWusdtRemainingAllowance/getPolkadotUsdtRemainingAllowance query the real on-chain Assets.Approvals amount per leg - not a guess. Confirmed on-chain (2026-07-16) the Polkadot leg has never had an approval granted at all, so every wUSDT->USDT withdrawal currently hits the consent gate regardless of amount, symmetric with what happens once a signatory grants one (mirrors the existing wUSDT-side renewal flow exactly, including the same 40,000/200,000 20%-threshold pattern) - added a second sign button so a signatory can actually grant it from this screen. swapClicked()'s confirmation gate now also enforces the consent requirement server-side (well, client-domain-side) rather than only via the button's enabled look, consistent with how it already re-checks balance/reserve.
This commit is contained in:
1 parent
cf8127c8f8
commit
3c06ae00e0
6 files changed
+354
-64
No files matched your search
@@ -365,6 +365,8 @@
|
|||||||
<string name="bridge_below_minimum">Amount below minimum</string>
|
<string name="bridge_below_minimum">Amount below minimum</string>
|
||||||
<string name="bridge_enter_amount">Enter amount</string>
|
<string name="bridge_enter_amount">Enter amount</string>
|
||||||
<string name="bridge_wusdt_to_usdt_blocked">This amount exceeds the bridge\'s current available reserve (%s USDT). Try a smaller amount.</string>
|
<string name="bridge_wusdt_to_usdt_blocked">This amount exceeds the bridge\'s current available reserve (%s USDT). Try a smaller amount.</string>
|
||||||
|
<string name="bridge_consent_required_message">This amount exceeds the automatic payout limit and needs approval from 3 of the bridge\'s 5 signatories - typically resolved within about 2 hours. You can reach them at t.me/pezkuwidestek in the meantime.</string>
|
||||||
|
<string name="bridge_consent_checkbox_label">I understand this may take time to complete and want to proceed</string>
|
||||||
|
|
||||||
<string name="bridge_sign_status_ok">Bridge allowance OK (%s)</string>
|
<string name="bridge_sign_status_ok">Bridge allowance OK (%s)</string>
|
||||||
<string name="bridge_sign_button">Sign renewal (%s)</string>
|
<string name="bridge_sign_button">Sign renewal (%s)</string>
|
||||||
|
|||||||
+17
@@ -34,6 +34,14 @@ object BridgeMultisigConstants {
|
|||||||
const val MULTISIG_ADDRESS_POLKADOT = "15sF76THfpefUaKomHZSpssayRbsp6Yt6ESgMrLjzJCmpe66"
|
const val MULTISIG_ADDRESS_POLKADOT = "15sF76THfpefUaKomHZSpssayRbsp6Yt6ESgMrLjzJCmpe66"
|
||||||
const val AUTOMATION_KEY_ADDRESS = "5GQu4PFUb1f3MTJ7i7c1CtLgDk3TVvpSW1VbQCRmfkMoC8cM"
|
const val AUTOMATION_KEY_ADDRESS = "5GQu4PFUb1f3MTJ7i7c1CtLgDk3TVvpSW1VbQCRmfkMoC8cM"
|
||||||
|
|
||||||
|
/** Same automation key, Polkadot Asset Hub SS58 encoding - needed to query/renew its real
|
||||||
|
* spending approval on that chain (BridgeMultisigInteractor.getPolkadotUsdtRemainingAllowance
|
||||||
|
* / submitPolkadotRenewalSignature). Confirmed on-chain (2026-07-16): unlike the Pezkuwi/
|
||||||
|
* wUSDT side, this approval has never been granted at all - every wUSDT->USDT withdrawal
|
||||||
|
* falls back to manual 3-of-5 review until the signatories grant one via the renewal flow
|
||||||
|
* below, same as they did for the wUSDT side. */
|
||||||
|
const val AUTOMATION_KEY_ADDRESS_POLKADOT = "15MCCiWYSnvWnzJdfkf1M3Aq5N37CENaaWE5ZVR8DqPKNcVj"
|
||||||
|
|
||||||
const val THRESHOLD = 3
|
const val THRESHOLD = 3
|
||||||
|
|
||||||
/** Renewal is offered once the remaining allowance drops below this (6 decimals). Must match
|
/** Renewal is offered once the remaining allowance drops below this (6 decimals). Must match
|
||||||
@@ -47,6 +55,15 @@ object BridgeMultisigConstants {
|
|||||||
/** The standard amount a renewal tops the allowance back up to (6 decimals). */
|
/** The standard amount a renewal tops the allowance back up to (6 decimals). */
|
||||||
const val TOPUP_AMOUNT = 200_000_000_000L // 200,000 wUSDT
|
const val TOPUP_AMOUNT = 200_000_000_000L // 200,000 wUSDT
|
||||||
|
|
||||||
|
/** Same 40,000/200,000 (20%) ratio as the wUSDT side, mirrored for the Polkadot/real-USDT
|
||||||
|
* leg for the exact same reason: catch a draining allowance and renew it before users ever
|
||||||
|
* hit the "needs manual review" consent gate, rather than only reacting after the fact. The
|
||||||
|
* ceiling being far above the multisig's current real Polkadot reserve is intentional and
|
||||||
|
* harmless - transfer_approved is still bounded by the real on-chain balance underneath, an
|
||||||
|
* approval is unused headroom, not a promise of funds (see getPolkadotUsdtReserve). */
|
||||||
|
const val POLKADOT_RENEWAL_THRESHOLD = 40_000_000_000L // 40,000 USDT
|
||||||
|
const val POLKADOT_TOPUP_AMOUNT = 200_000_000_000L // 200,000 USDT
|
||||||
|
|
||||||
data class Signatory(val role: String, val address: String)
|
data class Signatory(val role: String, val address: String)
|
||||||
|
|
||||||
val SIGNATORIES = listOf(
|
val SIGNATORIES = listOf(
|
||||||
|
|||||||
+122
-33
@@ -39,6 +39,14 @@ interface BridgeMultisigInteractor {
|
|||||||
|
|
||||||
suspend fun submitRenewalSignature(): Result<ExtrinsicExecutionResult>
|
suspend fun submitRenewalSignature(): Result<ExtrinsicExecutionResult>
|
||||||
|
|
||||||
|
/** Same as getSignerState/submitRenewalSignature but for the automation key's real-USDT
|
||||||
|
* spending approval on Polkadot Asset Hub - the leg that gates wUSDT->USDT withdrawal
|
||||||
|
* auto-pay. Confirmed on-chain (2026-07-16) this approval has never been granted at all, so
|
||||||
|
* needsRenewal is currently always true for every signatory until the first renewal signs. */
|
||||||
|
suspend fun getPolkadotSignerState(): BridgeSignerState?
|
||||||
|
|
||||||
|
suspend fun submitPolkadotRenewalSignature(): Result<ExtrinsicExecutionResult>
|
||||||
|
|
||||||
/** Real USDT (base units) the multisig actually holds on Polkadot Asset Hub right now - the
|
/** 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
|
* 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
|
* fetched from the legacy bridge bot's :3030/status endpoint, which this session stopped
|
||||||
@@ -46,6 +54,18 @@ interface BridgeMultisigInteractor {
|
|||||||
* regardless of real reserve. A specific withdrawal should be allowed whenever it's covered
|
* 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. */
|
* by this real balance, not gated on an unrelated dead service or on total supply parity. */
|
||||||
suspend fun getPolkadotUsdtReserve(): BigInteger
|
suspend fun getPolkadotUsdtReserve(): BigInteger
|
||||||
|
|
||||||
|
/** Real remaining amount (base units) the automation key is currently approved to auto-pay
|
||||||
|
* out of the multisig's own wUSDT on Pezkuwi Asset Hub - the deterministic on-chain fact
|
||||||
|
* that decides whether a USDT->wUSDT deposit CAN possibly auto-pay (bounded further by the
|
||||||
|
* backend's own daily cap, which isn't visible from the wallet - this is a necessary, not
|
||||||
|
* sufficient, condition for auto-pay). Available to any wallet, not just signatories, since
|
||||||
|
* it drives the Bridge screen's pre-submit consent gate for everyone. */
|
||||||
|
suspend fun getWusdtRemainingAllowance(): BigInteger
|
||||||
|
|
||||||
|
/** Same as getWusdtRemainingAllowance but for the automation key's real USDT approval on
|
||||||
|
* Polkadot Asset Hub - gates wUSDT->USDT withdrawal auto-pay. */
|
||||||
|
suspend fun getPolkadotUsdtRemainingAllowance(): BigInteger
|
||||||
}
|
}
|
||||||
|
|
||||||
@FeatureScope
|
@FeatureScope
|
||||||
@@ -58,6 +78,69 @@ class RealBridgeMultisigInteractor @Inject constructor(
|
|||||||
|
|
||||||
override suspend fun getSignerState(): BridgeSignerState? {
|
override suspend fun getSignerState(): BridgeSignerState? {
|
||||||
val chain = chainRegistry.getChain(ChainGeneses.PEZKUWI_ASSET_HUB)
|
val chain = chainRegistry.getChain(ChainGeneses.PEZKUWI_ASSET_HUB)
|
||||||
|
return getSignerStateFor(
|
||||||
|
chain = chain,
|
||||||
|
assetId = BridgeMultisigConstants.WUSDT_ASSET_ID,
|
||||||
|
automationKeyAddress = BridgeMultisigConstants.AUTOMATION_KEY_ADDRESS,
|
||||||
|
renewalThreshold = BridgeMultisigConstants.RENEWAL_THRESHOLD,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun submitRenewalSignature(): Result<ExtrinsicExecutionResult> = submitRenewalSignatureFor(
|
||||||
|
chain = chainRegistry.getChain(ChainGeneses.PEZKUWI_ASSET_HUB),
|
||||||
|
assetId = BridgeMultisigConstants.WUSDT_ASSET_ID,
|
||||||
|
automationKeyAddress = BridgeMultisigConstants.AUTOMATION_KEY_ADDRESS,
|
||||||
|
topupAmount = BridgeMultisigConstants.TOPUP_AMOUNT,
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getPolkadotSignerState(): BridgeSignerState? {
|
||||||
|
val chain = chainRegistry.getChain(ChainGeneses.POLKADOT_ASSET_HUB)
|
||||||
|
return getSignerStateFor(
|
||||||
|
chain = chain,
|
||||||
|
assetId = BridgeMultisigConstants.POLKADOT_USDT_ASSET_ID,
|
||||||
|
automationKeyAddress = BridgeMultisigConstants.AUTOMATION_KEY_ADDRESS_POLKADOT,
|
||||||
|
renewalThreshold = BridgeMultisigConstants.POLKADOT_RENEWAL_THRESHOLD,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun submitPolkadotRenewalSignature(): Result<ExtrinsicExecutionResult> = submitRenewalSignatureFor(
|
||||||
|
chain = chainRegistry.getChain(ChainGeneses.POLKADOT_ASSET_HUB),
|
||||||
|
assetId = BridgeMultisigConstants.POLKADOT_USDT_ASSET_ID,
|
||||||
|
automationKeyAddress = BridgeMultisigConstants.AUTOMATION_KEY_ADDRESS_POLKADOT,
|
||||||
|
topupAmount = BridgeMultisigConstants.POLKADOT_TOPUP_AMOUNT,
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getWusdtRemainingAllowance(): BigInteger {
|
||||||
|
val chain = chainRegistry.getChain(ChainGeneses.PEZKUWI_ASSET_HUB)
|
||||||
|
return queryRemainingAllowance(chain, BridgeMultisigConstants.WUSDT_ASSET_ID, BridgeMultisigConstants.AUTOMATION_KEY_ADDRESS)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPolkadotUsdtRemainingAllowance(): BigInteger {
|
||||||
|
val chain = chainRegistry.getChain(ChainGeneses.POLKADOT_ASSET_HUB)
|
||||||
|
return queryRemainingAllowance(chain, BridgeMultisigConstants.POLKADOT_USDT_ASSET_ID, BridgeMultisigConstants.AUTOMATION_KEY_ADDRESS_POLKADOT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shared by both legs - the only differences between the wUSDT (Pezkuwi) and USDT (Polkadot)
|
||||||
|
* renewal flows are which chain/asset/automation-key-address/threshold to use, the actual
|
||||||
|
* on-chain call shape (Assets.approve_transfer wrapped in Multisig.as_multi) is identical. */
|
||||||
|
private suspend fun getSignerStateFor(
|
||||||
|
chain: Chain,
|
||||||
|
assetId: Int,
|
||||||
|
automationKeyAddress: String,
|
||||||
|
renewalThreshold: Long,
|
||||||
|
): BridgeSignerState? {
|
||||||
val metaAccount = selectedAccountUseCase.getSelectedMetaAccount()
|
val metaAccount = selectedAccountUseCase.getSelectedMetaAccount()
|
||||||
val myAccountId = metaAccount.accountIdIn(chain)?.intoKey() ?: return null
|
val myAccountId = metaAccount.accountIdIn(chain)?.intoKey() ?: return null
|
||||||
|
|
||||||
@@ -65,13 +148,13 @@ class RealBridgeMultisigInteractor @Inject constructor(
|
|||||||
it.address.toAccountId().intoKey() == myAccountId
|
it.address.toAccountId().intoKey() == myAccountId
|
||||||
} ?: return null
|
} ?: return null
|
||||||
|
|
||||||
val remaining = queryRemainingAllowance(chain)
|
val remaining = queryRemainingAllowance(chain, assetId, automationKeyAddress)
|
||||||
val needsRenewal = remaining < BigInteger.valueOf(BridgeMultisigConstants.RENEWAL_THRESHOLD)
|
val needsRenewal = remaining < BigInteger.valueOf(renewalThreshold)
|
||||||
|
|
||||||
var alreadySigned = false
|
var alreadySigned = false
|
||||||
var approvalsSoFar = 0
|
var approvalsSoFar = 0
|
||||||
if (needsRenewal) {
|
if (needsRenewal) {
|
||||||
val pending = queryPendingRenewal(chain)
|
val pending = queryPendingRenewal(chain, assetId, automationKeyAddress)
|
||||||
if (pending != null) {
|
if (pending != null) {
|
||||||
approvalsSoFar = pending.approvals.size
|
approvalsSoFar = pending.approvals.size
|
||||||
alreadySigned = pending.approvals.contains(myAccountId)
|
alreadySigned = pending.approvals.contains(myAccountId)
|
||||||
@@ -87,11 +170,15 @@ class RealBridgeMultisigInteractor @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun submitRenewalSignature(): Result<ExtrinsicExecutionResult> = runCatching {
|
private suspend fun submitRenewalSignatureFor(
|
||||||
val chain = chainRegistry.getChain(ChainGeneses.PEZKUWI_ASSET_HUB)
|
chain: Chain,
|
||||||
|
assetId: Int,
|
||||||
|
automationKeyAddress: String,
|
||||||
|
topupAmount: Long,
|
||||||
|
): Result<ExtrinsicExecutionResult> = runCatching {
|
||||||
val metaAccount = selectedAccountUseCase.getSelectedMetaAccount()
|
val metaAccount = selectedAccountUseCase.getSelectedMetaAccount()
|
||||||
val myAccountId = requireNotNull(metaAccount.accountIdIn(chain)?.intoKey()) {
|
val myAccountId = requireNotNull(metaAccount.accountIdIn(chain)?.intoKey()) {
|
||||||
"Selected account has no address on Pezkuwi Asset Hub"
|
"Selected account has no address on ${chain.name}"
|
||||||
}
|
}
|
||||||
|
|
||||||
val otherSignatories = BridgeMultisigConstants.SIGNATORIES
|
val otherSignatories = BridgeMultisigConstants.SIGNATORIES
|
||||||
@@ -99,7 +186,7 @@ class RealBridgeMultisigInteractor @Inject constructor(
|
|||||||
.filter { it != myAccountId }
|
.filter { it != myAccountId }
|
||||||
.sortedBy { it.toHexWithPrefix() }
|
.sortedBy { it.toHexWithPrefix() }
|
||||||
|
|
||||||
val pending = queryPendingRenewal(chain)
|
val pending = queryPendingRenewal(chain, assetId, automationKeyAddress)
|
||||||
check(pending == null || !pending.approvals.contains(myAccountId)) {
|
check(pending == null || !pending.approvals.contains(myAccountId)) {
|
||||||
"Already signed this renewal - waiting for other signers"
|
"Already signed this renewal - waiting for other signers"
|
||||||
}
|
}
|
||||||
@@ -109,9 +196,9 @@ class RealBridgeMultisigInteractor @Inject constructor(
|
|||||||
origin = TransactionOrigin.WalletWithId(metaAccount.id)
|
origin = TransactionOrigin.WalletWithId(metaAccount.id)
|
||||||
) {
|
) {
|
||||||
val approveTransferCall = runtime.composeAssetsApproveTransfer(
|
val approveTransferCall = runtime.composeAssetsApproveTransfer(
|
||||||
assetId = BridgeMultisigConstants.WUSDT_ASSET_ID,
|
assetId = assetId,
|
||||||
delegate = BridgeMultisigConstants.AUTOMATION_KEY_ADDRESS.toAccountId().intoKey(),
|
delegate = automationKeyAddress.toAccountId().intoKey(),
|
||||||
amount = BigInteger.valueOf(BridgeMultisigConstants.TOPUP_AMOUNT),
|
amount = BigInteger.valueOf(topupAmount),
|
||||||
)
|
)
|
||||||
|
|
||||||
val multisigCall = runtime.composeBridgeMultisigAsMulti(
|
val multisigCall = runtime.composeBridgeMultisigAsMulti(
|
||||||
@@ -126,47 +213,49 @@ class RealBridgeMultisigInteractor @Inject constructor(
|
|||||||
}.getOrThrow().requireOk()
|
}.getOrThrow().requireOk()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getPolkadotUsdtReserve(): BigInteger {
|
private suspend fun queryRemainingAllowance(chain: Chain, assetId: Int, automationKeyAddress: String): BigInteger {
|
||||||
val polkadotChain = chainRegistry.getChain(ChainGeneses.POLKADOT_ASSET_HUB)
|
val multisigAccountId = multisigAddressFor(chain).toAccountId().intoKey()
|
||||||
val multisigAccountId = BridgeMultisigConstants.MULTISIG_ADDRESS_POLKADOT.toAccountId().intoKey()
|
val delegateAccountId = automationKeyAddress.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()
|
|
||||||
|
|
||||||
return storageDataSource.query(chain.id) {
|
return storageDataSource.query(chain.id) {
|
||||||
runtime.metadata.bridgeAssets().approvalAmount.query(
|
runtime.metadata.bridgeAssets().approvalAmount.query(
|
||||||
BridgeMultisigConstants.WUSDT_ASSET_ID.toBigInteger(),
|
assetId.toBigInteger(),
|
||||||
multisigAccountId,
|
multisigAccountId,
|
||||||
delegateAccountId,
|
delegateAccountId,
|
||||||
)
|
)
|
||||||
} ?: BigInteger.ZERO
|
} ?: BigInteger.ZERO
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun queryPendingRenewal(chain: Chain): BridgeOnChainMultisig? {
|
private suspend fun queryPendingRenewal(chain: Chain, assetId: Int, automationKeyAddress: String): BridgeOnChainMultisig? {
|
||||||
val multisigAccountId = BridgeMultisigConstants.MULTISIG_ADDRESS.toAccountId().intoKey()
|
val multisigAccountId = multisigAddressFor(chain).toAccountId().intoKey()
|
||||||
val callHash = renewalCallHash(chain)
|
val callHash = renewalCallHash(chain, assetId, automationKeyAddress)
|
||||||
|
|
||||||
return storageDataSource.query(chain.id) {
|
return storageDataSource.query(chain.id) {
|
||||||
runtime.metadata.bridgeMultisig().multisigs.query(multisigAccountId, callHash)
|
runtime.metadata.bridgeMultisig().multisigs.query(multisigAccountId, callHash)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun renewalCallHash(chain: Chain): AccountIdKey {
|
private suspend fun renewalCallHash(chain: Chain, assetId: Int, automationKeyAddress: String): AccountIdKey {
|
||||||
|
val topupAmount = if (chain.id == ChainGeneses.POLKADOT_ASSET_HUB) {
|
||||||
|
BridgeMultisigConstants.POLKADOT_TOPUP_AMOUNT
|
||||||
|
} else {
|
||||||
|
BridgeMultisigConstants.TOPUP_AMOUNT
|
||||||
|
}
|
||||||
|
|
||||||
val runtime = chainRegistry.getRuntime(chain.id)
|
val runtime = chainRegistry.getRuntime(chain.id)
|
||||||
val call = runtime.composeAssetsApproveTransfer(
|
val call = runtime.composeAssetsApproveTransfer(
|
||||||
assetId = BridgeMultisigConstants.WUSDT_ASSET_ID,
|
assetId = assetId,
|
||||||
delegate = BridgeMultisigConstants.AUTOMATION_KEY_ADDRESS.toAccountId().intoKey(),
|
delegate = automationKeyAddress.toAccountId().intoKey(),
|
||||||
amount = BigInteger.valueOf(BridgeMultisigConstants.TOPUP_AMOUNT),
|
amount = BigInteger.valueOf(topupAmount),
|
||||||
)
|
)
|
||||||
return call.callHash(runtime).intoCallHash()
|
return call.callHash(runtime).intoCallHash()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun multisigAddressFor(chain: Chain): String {
|
||||||
|
return if (chain.id == ChainGeneses.POLKADOT_ASSET_HUB) {
|
||||||
|
BridgeMultisigConstants.MULTISIG_ADDRESS_POLKADOT
|
||||||
|
} else {
|
||||||
|
BridgeMultisigConstants.MULTISIG_ADDRESS
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+40
-1
@@ -61,10 +61,18 @@ class BridgeFragment : BaseFragment<BridgeViewModel, FragmentBridgeBinding>() {
|
|||||||
viewModel.swapClicked()
|
viewModel.swapClicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multisig signatory-only sign button
|
// Multisig signatory-only sign buttons, one per leg
|
||||||
binder.bridgeSignButton.setOnClickListener {
|
binder.bridgeSignButton.setOnClickListener {
|
||||||
viewModel.signClicked()
|
viewModel.signClicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binder.bridgePolkadotSignButton.setOnClickListener {
|
||||||
|
viewModel.polkadotSignClicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
binder.bridgeConsentCheckbox.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
viewModel.consentCheckboxToggled(isChecked)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -146,6 +154,37 @@ class BridgeFragment : BaseFragment<BridgeViewModel, FragmentBridgeBinding>() {
|
|||||||
binder.bridgeSignButton.text = label
|
binder.bridgeSignButton.text = label
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModel.polkadotSignButtonVisible.observe { visible ->
|
||||||
|
binder.bridgePolkadotSignButton.visibility = if (visible) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.polkadotSignButtonRed.observe { red ->
|
||||||
|
val color = if (red) {
|
||||||
|
resources.getColor(R.color.error_border, null)
|
||||||
|
} else {
|
||||||
|
resources.getColor(R.color.text_positive, null)
|
||||||
|
}
|
||||||
|
binder.bridgePolkadotSignButton.setButtonColor(color)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.polkadotSignButtonEnabled.observe { enabled ->
|
||||||
|
binder.bridgePolkadotSignButton.isEnabled = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.polkadotSignButtonLabel.observe { label ->
|
||||||
|
binder.bridgePolkadotSignButton.text = label
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.consentRequired.observe { required ->
|
||||||
|
binder.bridgeConsentCheckbox.visibility = if (required) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.consentChecked.observe { checked ->
|
||||||
|
if (binder.bridgeConsentCheckbox.isChecked != checked) {
|
||||||
|
binder.bridgeConsentCheckbox.setChecked(checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
viewModel.maxAmountDisplay.observe { display ->
|
viewModel.maxAmountDisplay.observe { display ->
|
||||||
binder.bridgeFromMaxAmount.setMaxAmountDisplay(display)
|
binder.bridgeFromMaxAmount.setMaxAmountDisplay(display)
|
||||||
}
|
}
|
||||||
|
|||||||
+146
-27
@@ -115,6 +115,22 @@ class BridgeViewModel(
|
|||||||
private val _signButtonLabel = MutableLiveData("")
|
private val _signButtonLabel = MutableLiveData("")
|
||||||
val signButtonLabel: LiveData<String> = _signButtonLabel
|
val signButtonLabel: LiveData<String> = _signButtonLabel
|
||||||
|
|
||||||
|
/** Same as the sign* set above but for the automation key's Polkadot-side USDT approval -
|
||||||
|
* see BridgeMultisigInteractor.getPolkadotSignerState for why this is currently always
|
||||||
|
* offered (approval has never been granted at all). A separate row rather than merging with
|
||||||
|
* the wUSDT one since a signatory may need to renew one leg without the other. */
|
||||||
|
private val _polkadotSignButtonVisible = MutableLiveData(false)
|
||||||
|
val polkadotSignButtonVisible: LiveData<Boolean> = _polkadotSignButtonVisible
|
||||||
|
|
||||||
|
private val _polkadotSignButtonRed = MutableLiveData(false)
|
||||||
|
val polkadotSignButtonRed: LiveData<Boolean> = _polkadotSignButtonRed
|
||||||
|
|
||||||
|
private val _polkadotSignButtonEnabled = MutableLiveData(false)
|
||||||
|
val polkadotSignButtonEnabled: LiveData<Boolean> = _polkadotSignButtonEnabled
|
||||||
|
|
||||||
|
private val _polkadotSignButtonLabel = MutableLiveData("")
|
||||||
|
val polkadotSignButtonLabel: LiveData<String> = _polkadotSignButtonLabel
|
||||||
|
|
||||||
private val _fromCard = MutableLiveData<BridgeAssetCardUi>()
|
private val _fromCard = MutableLiveData<BridgeAssetCardUi>()
|
||||||
val fromCard: LiveData<BridgeAssetCardUi> = _fromCard
|
val fromCard: LiveData<BridgeAssetCardUi> = _fromCard
|
||||||
|
|
||||||
@@ -133,12 +149,38 @@ class BridgeViewModel(
|
|||||||
private val _fillAmountEvent = MutableLiveData<Event<String>>()
|
private val _fillAmountEvent = MutableLiveData<Event<String>>()
|
||||||
val fillAmountEvent: LiveData<Event<String>> = _fillAmountEvent
|
val fillAmountEvent: LiveData<Event<String>> = _fillAmountEvent
|
||||||
|
|
||||||
|
/** True when the entered amount exceeds the automation key's current on-chain approval for
|
||||||
|
* this direction - real funds exist and the transfer WILL succeed, it just won't auto-pay
|
||||||
|
* and needs 3-of-5 signatory review. Distinct from showWarning/warningBlocked (which cover
|
||||||
|
* the harder "not enough real reserve at all" case, unresolvable by any signature) - this
|
||||||
|
* one is resolvable, so the UI offers an explicit opt-in instead of a flat block. */
|
||||||
|
private val _consentRequired = MutableLiveData(false)
|
||||||
|
val consentRequired: LiveData<Boolean> = _consentRequired
|
||||||
|
|
||||||
|
private val _consentChecked = MutableLiveData(false)
|
||||||
|
val consentChecked: LiveData<Boolean> = _consentChecked
|
||||||
|
|
||||||
|
fun consentCheckboxToggled(checked: Boolean) {
|
||||||
|
_consentChecked.value = checked
|
||||||
|
updateButtonState()
|
||||||
|
}
|
||||||
|
|
||||||
private var currentAmount: Double = 0.0
|
private var currentAmount: Double = 0.0
|
||||||
|
|
||||||
/** Real USDT the multisig actually holds on Polkadot Asset Hub - see
|
/** Real USDT the multisig actually holds on Polkadot Asset Hub - see
|
||||||
* BridgeMultisigInteractor.getPolkadotUsdtReserve for why this replaced a dead external
|
* BridgeMultisigInteractor.getPolkadotUsdtReserve for why this replaced a dead external
|
||||||
* status check that always reported "inactive" regardless of the real reserve. */
|
* status check that always reported "inactive" regardless of the real reserve. */
|
||||||
private var polkadotUsdtReserve: BigDecimal = BigDecimal.ZERO
|
private var polkadotUsdtReserve: BigDecimal = BigDecimal.ZERO
|
||||||
|
|
||||||
|
/** Remaining on-chain approval (BridgeMultisigInteractor.getWusdtRemainingAllowance /
|
||||||
|
* getPolkadotUsdtRemainingAllowance), one per leg - the deterministic fact that decides
|
||||||
|
* whether a given amount can possibly auto-pay in that direction, independent of the real
|
||||||
|
* reserve check above (a withdrawal can be under-reserved AND under-approved at once; those
|
||||||
|
* are checked in priority order in updateWarningState, since only the reserve one is truly
|
||||||
|
* unresolvable). */
|
||||||
|
private var wusdtRemainingAllowance: BigDecimal = BigDecimal.ZERO
|
||||||
|
private var polkadotUsdtRemainingAllowance: BigDecimal = BigDecimal.ZERO
|
||||||
|
|
||||||
private var availableBalance: BigDecimal = BigDecimal.ZERO
|
private var availableBalance: BigDecimal = BigDecimal.ZERO
|
||||||
private var balanceJob: Job? = null
|
private var balanceJob: Job? = null
|
||||||
|
|
||||||
@@ -155,6 +197,7 @@ class BridgeViewModel(
|
|||||||
_pair.value = newPair
|
_pair.value = newPair
|
||||||
// Reset direction to left (forward) when switching pair
|
// Reset direction to left (forward) when switching pair
|
||||||
_direction.value = BridgeDirection.USDT_TO_WUSDT
|
_direction.value = BridgeDirection.USDT_TO_WUSDT
|
||||||
|
_consentChecked.value = false
|
||||||
updateUI()
|
updateUI()
|
||||||
calculateOutput()
|
calculateOutput()
|
||||||
updateWarningState()
|
updateWarningState()
|
||||||
@@ -166,6 +209,7 @@ class BridgeViewModel(
|
|||||||
val newDir = BridgeDirection.USDT_TO_WUSDT
|
val newDir = BridgeDirection.USDT_TO_WUSDT
|
||||||
if (_direction.value != newDir) {
|
if (_direction.value != newDir) {
|
||||||
_direction.value = newDir
|
_direction.value = newDir
|
||||||
|
_consentChecked.value = false
|
||||||
updateUI()
|
updateUI()
|
||||||
calculateOutput()
|
calculateOutput()
|
||||||
updateWarningState()
|
updateWarningState()
|
||||||
@@ -177,6 +221,7 @@ class BridgeViewModel(
|
|||||||
val newDir = BridgeDirection.WUSDT_TO_USDT
|
val newDir = BridgeDirection.WUSDT_TO_USDT
|
||||||
if (_direction.value != newDir) {
|
if (_direction.value != newDir) {
|
||||||
_direction.value = newDir
|
_direction.value = newDir
|
||||||
|
_consentChecked.value = false
|
||||||
updateUI()
|
updateUI()
|
||||||
calculateOutput()
|
calculateOutput()
|
||||||
updateWarningState()
|
updateWarningState()
|
||||||
@@ -186,9 +231,12 @@ class BridgeViewModel(
|
|||||||
|
|
||||||
fun setAmount(amount: Double) {
|
fun setAmount(amount: Double) {
|
||||||
currentAmount = amount
|
currentAmount = amount
|
||||||
|
// A consent already given was for whatever amount was entered at the time - changing the
|
||||||
|
// amount means re-confirming, not silently carrying an old opt-in over to a new one.
|
||||||
|
_consentChecked.value = false
|
||||||
calculateOutput()
|
calculateOutput()
|
||||||
updateInsufficientBalanceState()
|
updateInsufficientBalanceState()
|
||||||
updateWarningState() // re-check the entered amount against the cached reserve
|
updateWarningState() // re-check the entered amount against the cached reserve/allowance
|
||||||
}
|
}
|
||||||
|
|
||||||
fun maxClicked() {
|
fun maxClicked() {
|
||||||
@@ -202,6 +250,7 @@ class BridgeViewModel(
|
|||||||
* different screens: there is no "just submitted" moment on this screen anymore). */
|
* different screens: there is no "just submitted" moment on this screen anymore). */
|
||||||
fun resetAmount() {
|
fun resetAmount() {
|
||||||
currentAmount = 0.0
|
currentAmount = 0.0
|
||||||
|
_consentChecked.value = false
|
||||||
_fillAmountEvent.value = Event("")
|
_fillAmountEvent.value = Event("")
|
||||||
calculateOutput()
|
calculateOutput()
|
||||||
updateInsufficientBalanceState()
|
updateInsufficientBalanceState()
|
||||||
@@ -227,6 +276,15 @@ class BridgeViewModel(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val relevantAllowance = when (dir) {
|
||||||
|
BridgeDirection.USDT_TO_WUSDT -> wusdtRemainingAllowance
|
||||||
|
BridgeDirection.WUSDT_TO_USDT -> polkadotUsdtRemainingAllowance
|
||||||
|
}
|
||||||
|
if (requested > relevantAllowance && _consentChecked.value != true) {
|
||||||
|
updateWarningState()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val chainId = when (dir) {
|
val chainId = when (dir) {
|
||||||
BridgeDirection.USDT_TO_WUSDT -> POLKADOT_ASSET_HUB_ID
|
BridgeDirection.USDT_TO_WUSDT -> POLKADOT_ASSET_HUB_ID
|
||||||
BridgeDirection.WUSDT_TO_USDT -> PEZKUWI_ASSET_HUB_ID
|
BridgeDirection.WUSDT_TO_USDT -> PEZKUWI_ASSET_HUB_ID
|
||||||
@@ -279,19 +337,36 @@ class BridgeViewModel(
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
BigDecimal.ZERO
|
BigDecimal.ZERO
|
||||||
}
|
}
|
||||||
|
wusdtRemainingAllowance = try {
|
||||||
|
BigDecimal(bridgeMultisigInteractor.getWusdtRemainingAllowance()).divide(USDT_DECIMALS_DIVISOR)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
BigDecimal.ZERO
|
||||||
|
}
|
||||||
|
polkadotUsdtRemainingAllowance = try {
|
||||||
|
BigDecimal(bridgeMultisigInteractor.getPolkadotUsdtRemainingAllowance()).divide(USDT_DECIMALS_DIVISOR)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
BigDecimal.ZERO
|
||||||
|
}
|
||||||
updateWarningState()
|
updateWarningState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Two independent, differently-resolvable "no" conditions, checked in priority order - never
|
||||||
|
* collapsed into one generic warning:
|
||||||
|
* 1. Real reserve exceeded (withdrawal direction only) - the bridge doesn't hold enough real
|
||||||
|
* USDT on Polkadot Asset Hub. No signature can fix this; a hard block.
|
||||||
|
* 2. Automation-key approval exceeded (either direction) - funds exist, the automation key
|
||||||
|
* just isn't currently approved to move that much without 3-of-5 review. Resolvable, so
|
||||||
|
* this is an opt-in consent gate (see consentRequired/consentChecked), not a hard block. */
|
||||||
private fun updateWarningState() {
|
private fun updateWarningState() {
|
||||||
val dir = _direction.value ?: return
|
val dir = _direction.value ?: return
|
||||||
|
|
||||||
when (dir) {
|
|
||||||
BridgeDirection.WUSDT_TO_USDT -> {
|
|
||||||
val requested = BigDecimal.valueOf(currentAmount)
|
val requested = BigDecimal.valueOf(currentAmount)
|
||||||
val exceedsReserve = currentAmount > 0 && requested > polkadotUsdtReserve
|
|
||||||
_showWarning.postValue(exceedsReserve)
|
val reserveExceeded = dir == BridgeDirection.WUSDT_TO_USDT && currentAmount > 0 && requested > polkadotUsdtReserve
|
||||||
if (exceedsReserve) {
|
|
||||||
|
if (reserveExceeded) {
|
||||||
|
_consentRequired.postValue(false)
|
||||||
|
_showWarning.postValue(true)
|
||||||
_warningBlocked.postValue(true)
|
_warningBlocked.postValue(true)
|
||||||
_warningText.postValue(
|
_warningText.postValue(
|
||||||
resourceManager.getString(
|
resourceManager.getString(
|
||||||
@@ -299,15 +374,26 @@ class BridgeViewModel(
|
|||||||
polkadotUsdtReserve.setScale(2, RoundingMode.DOWN).stripTrailingZeros().toPlainString()
|
polkadotUsdtReserve.setScale(2, RoundingMode.DOWN).stripTrailingZeros().toPlainString()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
updateButtonState()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val relevantAllowance = when (dir) {
|
||||||
|
BridgeDirection.USDT_TO_WUSDT -> wusdtRemainingAllowance
|
||||||
|
BridgeDirection.WUSDT_TO_USDT -> polkadotUsdtRemainingAllowance
|
||||||
|
}
|
||||||
|
val needsConsent = currentAmount > 0 && requested > relevantAllowance
|
||||||
|
|
||||||
|
_consentRequired.postValue(needsConsent)
|
||||||
|
if (needsConsent) {
|
||||||
|
_showWarning.postValue(true)
|
||||||
_warningBlocked.postValue(false)
|
_warningBlocked.postValue(false)
|
||||||
|
_warningText.postValue(resourceManager.getString(R.string.bridge_consent_required_message))
|
||||||
|
} else {
|
||||||
|
_showWarning.postValue(false)
|
||||||
_warningText.postValue("")
|
_warningText.postValue("")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
_showWarning.postValue(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateButtonState()
|
updateButtonState()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,11 +418,19 @@ class BridgeViewModel(
|
|||||||
val dir = _direction.value ?: return
|
val dir = _direction.value ?: return
|
||||||
|
|
||||||
val requested = BigDecimal.valueOf(currentAmount)
|
val requested = BigDecimal.valueOf(currentAmount)
|
||||||
|
val reserveExceeded = dir == BridgeDirection.WUSDT_TO_USDT && requested > polkadotUsdtReserve
|
||||||
|
val relevantAllowance = when (dir) {
|
||||||
|
BridgeDirection.USDT_TO_WUSDT -> wusdtRemainingAllowance
|
||||||
|
BridgeDirection.WUSDT_TO_USDT -> polkadotUsdtRemainingAllowance
|
||||||
|
}
|
||||||
|
val consentSatisfied = requested <= relevantAllowance || _consentChecked.value == true
|
||||||
|
|
||||||
_buttonState.value = when {
|
_buttonState.value = when {
|
||||||
currentAmount <= 0 -> ButtonState.DISABLED
|
currentAmount <= 0 -> ButtonState.DISABLED
|
||||||
currentAmount < MIN_USDT -> ButtonState.DISABLED
|
currentAmount < MIN_USDT -> ButtonState.DISABLED
|
||||||
requested > availableBalance -> ButtonState.DISABLED
|
requested > availableBalance -> ButtonState.DISABLED
|
||||||
dir == BridgeDirection.WUSDT_TO_USDT && requested > polkadotUsdtReserve -> ButtonState.DISABLED
|
reserveExceeded -> ButtonState.DISABLED
|
||||||
|
!consentSatisfied -> ButtonState.DISABLED
|
||||||
else -> ButtonState.NORMAL
|
else -> ButtonState.NORMAL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -357,7 +451,11 @@ class BridgeViewModel(
|
|||||||
fun refreshSignerState() {
|
fun refreshSignerState() {
|
||||||
launch {
|
launch {
|
||||||
val state = bridgeMultisigInteractor.getSignerState()
|
val state = bridgeMultisigInteractor.getSignerState()
|
||||||
applySignerState(state)
|
applySignerState(state, _signButtonVisible, _signButtonRed, _signButtonEnabled, _signButtonLabel)
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
val state = bridgeMultisigInteractor.getPolkadotSignerState()
|
||||||
|
applySignerState(state, _polkadotSignButtonVisible, _polkadotSignButtonRed, _polkadotSignButtonEnabled, _polkadotSignButtonLabel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,29 +474,50 @@ class BridgeViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applySignerState(state: BridgeSignerState?) {
|
fun polkadotSignClicked() {
|
||||||
|
if (_polkadotSignButtonEnabled.value != true) return
|
||||||
|
|
||||||
|
_polkadotSignButtonEnabled.postValue(false)
|
||||||
|
_polkadotSignButtonLabel.postValue(resourceManager.getString(R.string.bridge_sign_in_progress))
|
||||||
|
|
||||||
|
launch {
|
||||||
|
bridgeMultisigInteractor.submitPolkadotRenewalSignature()
|
||||||
|
.onFailure {
|
||||||
|
_polkadotSignButtonLabel.postValue(resourceManager.getString(R.string.bridge_sign_error))
|
||||||
|
}
|
||||||
|
refreshSignerState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applySignerState(
|
||||||
|
state: BridgeSignerState?,
|
||||||
|
visible: MutableLiveData<Boolean>,
|
||||||
|
red: MutableLiveData<Boolean>,
|
||||||
|
enabled: MutableLiveData<Boolean>,
|
||||||
|
label: MutableLiveData<String>,
|
||||||
|
) {
|
||||||
if (state == null) {
|
if (state == null) {
|
||||||
_signButtonVisible.postValue(false)
|
visible.postValue(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_signButtonVisible.postValue(true)
|
visible.postValue(true)
|
||||||
|
|
||||||
when {
|
when {
|
||||||
!state.needsRenewal -> {
|
!state.needsRenewal -> {
|
||||||
_signButtonRed.postValue(false)
|
red.postValue(false)
|
||||||
_signButtonEnabled.postValue(false)
|
enabled.postValue(false)
|
||||||
_signButtonLabel.postValue(resourceManager.getString(R.string.bridge_sign_status_ok, state.signatoryRole))
|
label.postValue(resourceManager.getString(R.string.bridge_sign_status_ok, state.signatoryRole))
|
||||||
}
|
}
|
||||||
state.alreadySignedPendingRenewal -> {
|
state.alreadySignedPendingRenewal -> {
|
||||||
_signButtonRed.postValue(true)
|
red.postValue(true)
|
||||||
_signButtonEnabled.postValue(false)
|
enabled.postValue(false)
|
||||||
_signButtonLabel.postValue(resourceManager.getString(R.string.bridge_sign_waiting_others))
|
label.postValue(resourceManager.getString(R.string.bridge_sign_waiting_others))
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
_signButtonRed.postValue(true)
|
red.postValue(true)
|
||||||
_signButtonEnabled.postValue(true)
|
enabled.postValue(true)
|
||||||
_signButtonLabel.postValue(resourceManager.getString(R.string.bridge_sign_button, state.signatoryRole))
|
label.postValue(resourceManager.getString(R.string.bridge_sign_button, state.signatoryRole))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,7 +170,9 @@
|
|||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
<!-- Warning for USDT liquidity -->
|
<!-- Warning for USDT liquidity, or the consent message when the amount exceeds the
|
||||||
|
automation key's current approval (resolvable by 3-of-5 review, unlike the
|
||||||
|
reserve case above which isn't - see BridgeViewModel.updateWarningState). -->
|
||||||
<io.novafoundation.nova.common.view.AlertView
|
<io.novafoundation.nova.common.view.AlertView
|
||||||
android:id="@+id/bridgeWarningAlert"
|
android:id="@+id/bridgeWarningAlert"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -179,8 +181,21 @@
|
|||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:alertMode="warning" />
|
app:alertMode="warning" />
|
||||||
|
|
||||||
<!-- Multisig signatory-only bridge allowance sign button. Only visible to a wallet
|
<!-- Only shown alongside the consent message above (consentRequired) - explicit
|
||||||
that is one of the bridge multisig's 5 known signatories. -->
|
opt-in required before the Swap button re-enables, never enabled by default. -->
|
||||||
|
<io.novafoundation.nova.common.view.WarningCheckBox
|
||||||
|
android:id="@+id/bridgeConsentCheckbox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:icon="@drawable/ic_warning_filled"
|
||||||
|
android:text="@string/bridge_consent_checkbox_label"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:iconTint="@color/icon_warning" />
|
||||||
|
|
||||||
|
<!-- Multisig signatory-only bridge allowance sign buttons. Only visible to a wallet
|
||||||
|
that is one of the bridge multisig's 5 known signatories - one row per leg, since
|
||||||
|
a signatory may need to renew one chain's approval without the other. -->
|
||||||
<io.novafoundation.nova.common.view.PrimaryButton
|
<io.novafoundation.nova.common.view.PrimaryButton
|
||||||
android:id="@+id/bridgeSignButton"
|
android:id="@+id/bridgeSignButton"
|
||||||
style="@style/Widget.Nova.Button.Primary"
|
style="@style/Widget.Nova.Button.Primary"
|
||||||
@@ -190,6 +205,15 @@
|
|||||||
android:text="@string/bridge_sign_button"
|
android:text="@string/bridge_sign_button"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<io.novafoundation.nova.common.view.PrimaryButton
|
||||||
|
android:id="@+id/bridgePolkadotSignButton"
|
||||||
|
style="@style/Widget.Nova.Button.Primary"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/bridge_sign_button"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
Reference in new issue
Block a user