Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import one.mixin.android.tip.wc.internal.WCEthereumSignMessage
import one.mixin.android.tip.wc.internal.WCEthereumTransaction
import one.mixin.android.tip.wc.internal.WalletConnectException
import one.mixin.android.tip.wc.internal.WalletConnectAddresses
import one.mixin.android.tip.wc.internal.WalletConnectPairingErrors
import one.mixin.android.tip.wc.internal.WcInstruction
import one.mixin.android.tip.wc.internal.WcInstructionDeserializer
import one.mixin.android.tip.wc.internal.WcBitcoinAccountAddress
Expand Down Expand Up @@ -73,6 +74,8 @@ import java.util.concurrent.TimeUnit
object WalletConnectV2 : WalletConnect() {
const val TAG = "WalletConnectV2"

internal val pairingErrors = WalletConnectPairingErrors()

private const val CHAIN_TYPE_ETH: String = "eth"
private const val CHAIN_TYPE_POLYGON: String = "polygon"
private const val CHAIN_TYPE_BSC: String = "bsc"
Expand Down Expand Up @@ -136,7 +139,7 @@ object WalletConnectV2 : WalletConnect() {

override fun onError(error: Wallet.Model.Error) {
Timber.d("$TAG onError $error")
// RxBus.publish(WCErrorEvent(WCError(error.throwable)))
pairingErrors.report(error.throwable)
}

override fun onProposalExpired(proposal: Wallet.Model.ExpiredProposal) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package one.mixin.android.tip.wc.internal

import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeoutOrNull

internal class WalletConnectPairingErrors {
private val errors = MutableSharedFlow<Throwable>(extraBufferCapacity = 1)

fun report(error: Throwable) {
errors.tryEmit(error)
}

suspend fun await(timeoutMillis: Long = 30_000): Throwable? =
withTimeoutOrNull(timeoutMillis) { errors.first() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,17 @@ import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import one.mixin.android.Constants
import one.mixin.android.R
import one.mixin.android.api.request.web3.EstimateFeeRequest
import one.mixin.android.api.response.web3.ParsedTx
import one.mixin.android.api.response.web3.WalletOutput
import one.mixin.android.db.web3.vo.Web3TokenItem
Expand All @@ -41,7 +43,6 @@ import one.mixin.android.extension.toast
import one.mixin.android.extension.withArgs
import one.mixin.android.tip.wc.internal.Chain
import one.mixin.android.tip.wc.internal.TipGas
import one.mixin.android.tip.wc.internal.buildTipGas
import one.mixin.android.ui.common.MixinComposeBottomSheetDialogFragment
import one.mixin.android.ui.common.PinInputBottomSheetDialogFragment
import one.mixin.android.ui.common.biometric.BiometricInfo
Expand All @@ -66,7 +67,6 @@ import one.mixin.android.web3.js.JsSignMessage
import one.mixin.android.web3.js.SolanaTxSource
import one.mixin.android.web3.js.Web3Signer
import one.mixin.android.web3.js.WalletErrorCode
import one.mixin.android.web3.js.throwIfAnyMaliciousInstruction
import one.mixin.android.web3.send.UtxoTransactionSigner
import org.json.JSONObject
import org.sol4k.Base58
Expand All @@ -75,8 +75,6 @@ import org.sol4k.exception.RpcException
import org.sol4kt.SignInInput
import org.sol4kt.VersionedTransactionCompat
import org.web3j.crypto.Hash
import org.web3j.utils.Convert
import org.web3j.utils.Numeric
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
Expand Down Expand Up @@ -190,6 +188,7 @@ class BrowserWalletBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag
private var parsedTx: ParsedTx? by mutableStateOf(null)
private var solanaSignInInput: SignInInput? by mutableStateOf(null)
private var requestSettled = false
private var transactionRefreshJob: Job? = null

@Inject
lateinit var rpc: Rpc
Expand Down Expand Up @@ -334,39 +333,23 @@ class BrowserWalletBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag
val transaction = signMessage.wcEthereumTransaction ?: return
val cachedTipGas = tipGas
var useCachedTipGas = cachedTipGas != null
tickerFlow(15.seconds)
transactionRefreshJob = tickerFlow(15.seconds)
.onEach {
asset = viewModel.refreshAsset(assetId)
try {
val currentTipGas = if (useCachedTipGas) {
useCachedTipGas = false
cachedTipGas
} else {
withContext(Dispatchers.IO) {
val r = runCatching {
viewModel.estimateFee(
EstimateFeeRequest(
assetId,
null,
transaction.data,
transaction.from,
transaction.to,
transaction.value,
)
)
}.getOrNull()
if (r?.isSuccess != true) {
step = Step.Error
ErrorHandler.handleMixinError(r?.errorCode ?: 0, r?.errorDescription ?: "")
return@withContext null
}
buildTipGas(chain.chainId, r.data!!)
}
} ?: return@onEach
val result = requireNotNull(
viewModel.preflightTransaction(
signMessage, chain, token?.walletId ?: Web3Signer.currentWalletId,
if (useCachedTipGas) cachedTipGas else null,
),
)
useCachedTipGas = false
val currentTipGas = requireNotNull(result.tipGas)
tipGas = currentTipGas
insufficientGas = checkGas(token, chainToken, currentTipGas, transaction.value, transaction.maxFeePerGas)
insufficientGas = result.insufficientBalance
if (insufficientGas) {
handleException(IllegalArgumentException(requireContext().getString(R.string.insufficient_gas, chainToken?.symbol ?: currentChain.symbol)))
handleException(IllegalArgumentException(requireContext().getString(R.string.insufficient_gas, result.balance.token.symbol)))
return@onEach
}
val hex = Web3Signer.ethPreviewTransaction(
Web3Signer.evmAddress,
Expand All @@ -380,15 +363,18 @@ class BrowserWalletBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag
if (parsedTx == null) {
parsedTx = viewModel.simulateWeb3Tx(hex, assetId, from = Web3Signer.evmAddress, toAddress)
}
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Timber.e(e)
tipGas = null
handleException(e)
}
}
.launchIn(lifecycleScope)
.launchIn(viewLifecycleOwner.lifecycleScope)
}

private fun refreshSolana() {
tickerFlow(15.seconds)
transactionRefreshJob = tickerFlow(15.seconds)
.onEach {
try {
if (signMessage.type == JsSignMessage.TYPE_RAW_TRANSACTION) {
Expand All @@ -404,20 +390,31 @@ class BrowserWalletBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag
if (parsedTx == null) {
parsedTx = viewModel.simulateWeb3Tx(tx.serialize().base64Encode(), Constants.ChainId.Solana, Web3Signer.solanaAddress, toAddress)
}
tx.throwIfAnyMaliciousInstruction()
val result = viewModel.preflightTransaction(
JsSignMessage(signMessage.callbackId, JsSignMessage.TYPE_RAW_TRANSACTION, data = tx.serialize().base64Encode()),
Chain.Solana,
token?.walletId ?: Web3Signer.currentWalletId,
)
insufficientGas = result?.insufficientBalance == true
if (result?.insufficientBalance == true) {
handleException(IllegalArgumentException(getString(R.string.insufficient_gas, result.balance.token.symbol)))
}
} else if (signMessage.type == JsSignMessage.TYPE_SIGN_IN) {
solanaSignInInput = SignInInput.from(signMessage.data ?: "", Web3Signer.address)
}
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
handleException(e)
}
asset = viewModel.refreshAsset(Chain.Solana.assetId)
}.launchIn(lifecycleScope)
}.launchIn(viewLifecycleOwner.lifecycleScope)
}

private fun doAfterPinComplete(pin: String) =
lifecycleScope.launch(Dispatchers.IO) {
try {
transactionRefreshJob?.cancelAndJoin()
step = Step.Loading
errorInfo = null
customPinAction?.let { action ->
Expand All @@ -431,6 +428,17 @@ class BrowserWalletBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag
}
return@launch
}
val preflightMessage = if (signMessage.type == JsSignMessage.TYPE_RAW_TRANSACTION && solanaTx != null) {
JsSignMessage(signMessage.callbackId, JsSignMessage.TYPE_RAW_TRANSACTION, data = solanaTx!!.serialize().base64Encode())
} else {
signMessage
}
val preflight = viewModel.preflightTransaction(preflightMessage, currentChain, token?.walletId ?: Web3Signer.currentWalletId, tipGas)
if (preflight?.insufficientBalance == true) {
insufficientGas = true
throw IllegalArgumentException(getString(R.string.insufficient_gas, preflight.balance.token.symbol))
}
preflight?.tipGas?.let { tipGas = it }
if (signMessage.type == JsSignMessage.TYPE_UTXO_TRANSACTION) {
val rawHex = signMessage.data ?: throw IllegalArgumentException("empty UTXO transaction hex")
val chainId = utxoChainId
Expand Down Expand Up @@ -487,13 +495,16 @@ class BrowserWalletBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag
Constants.BIOMETRIC_PIN_CHECK,
System.currentTimeMillis(),
)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
settleError(WalletErrorCode.INTERNAL_ERROR, e.message ?: "Signing failed")
handleException(e)
}
}

override fun onDismiss(dialog: DialogInterface) {
transactionRefreshJob?.cancel()
super.onDismiss(dialog)
onDismissAction?.invoke(step == Step.Done)
}
Expand All @@ -514,31 +525,6 @@ class BrowserWalletBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag
return tx
}

private fun checkGas(
web3Token: Web3TokenItem?,
chainToken: Web3TokenItem?,
tipGas: TipGas?,
value: String?,
maxFeePerGas: String?,
): Boolean {
return if (web3Token != null) {
if (chainToken == null) {
true
} else if (tipGas != null) {
val maxGas = tipGas.displayValue(maxFeePerGas) ?: BigDecimal.ZERO
if (web3Token.assetId == chainToken.assetId && web3Token.chainId == chainToken.chainId) {
Convert.fromWei(Numeric.decodeQuantity(value ?: "0x0").toBigDecimal(), Convert.Unit.ETHER) + maxGas > BigDecimal(chainToken.balance)
} else {
maxGas > BigDecimal(chainToken.balance)
}
} else {
false
}
} else {
false
}
}

private fun handleException(e: Throwable) {
Timber.e(e)
val msg =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ import one.mixin.android.repository.TokenRepository
import one.mixin.android.repository.UserRepository
import one.mixin.android.repository.Web3Repository
import one.mixin.android.tip.Tip
import one.mixin.android.tip.wc.internal.Chain
import one.mixin.android.tip.wc.internal.TipGas
import one.mixin.android.util.ErrorHandler
import one.mixin.android.web3.js.JsSignMessage
import one.mixin.android.web3.js.Web3Signer
import one.mixin.android.web3.preflightDappTransaction
import org.sol4k.exception.RpcException
import java.math.BigDecimal
import javax.inject.Inject
Expand Down Expand Up @@ -105,7 +110,14 @@ class BrowserWalletBottomSheetViewModel
}
}

suspend fun estimateFee(request: EstimateFeeRequest) = web3Repository.estimateFee(request)
suspend fun preflightTransaction(message: JsSignMessage, chain: Chain, walletId: String, cachedTipGas: TipGas? = null) = withContext(Dispatchers.IO) {
preflightDappTransaction(
message, chain, if (chain == Chain.Solana) Web3Signer.solanaAddress else Web3Signer.evmAddress,
findToken = { web3Repository.web3TokenItemById(walletId, it) },
estimateFee = web3Repository::estimateFee,
cachedTipGas = cachedTipGas,
)
}

suspend fun outputsByAddress(address: String, assetId: String): List<WalletOutput> = withContext(Dispatchers.IO) {
web3Repository.outputsByAddress(address, assetId)
Expand Down
Loading
Loading