Skip to content
Open
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
2 changes: 2 additions & 0 deletions app/src/main/java/one/mixin/android/extension/UrlExtension.kt
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,8 @@ fun String.isValidStartParam(): Boolean {

fun String.isExternalTransferUrl() = externalTransferAssetIdMap.keys.any { startsWith("$it:", ignoreCase = true) }

fun String.isEthereumOrSolURLString() = startsWith("ethereum:", true) || startsWith("solana:", true)

fun String.isLightningUrl() = startsWith("lnbc", true) || startsWith("lnurl", true) || startsWith("lightning:", true)

private fun String.isUserScheme() =
Expand Down
34 changes: 32 additions & 2 deletions app/src/main/java/one/mixin/android/repository/Web3Repository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import one.mixin.android.db.web3.Web3WalletDao
import one.mixin.android.db.web3.updateWithLocalKeyInfo
import one.mixin.android.db.web3.vo.Web3Address
import one.mixin.android.db.web3.vo.Web3Chain
import one.mixin.android.db.web3.vo.Web3Token
import one.mixin.android.db.web3.vo.Web3TokenItem
import one.mixin.android.db.web3.vo.Web3TokensExtra
import one.mixin.android.db.web3.vo.Web3TransactionItem
Expand Down Expand Up @@ -52,8 +53,37 @@ constructor(
suspend fun web3TokenItemByAddress(address: String) = web3TokenDao.web3TokenItemByAddress(address)

suspend fun web3TokenItemById(walletId: String, assetId: String) = web3TokenDao.web3TokenItemById(walletId, assetId)

suspend fun findWeb3TokenItemsByIds(walletId: String, assetIds: List<String>) = web3TokenDao.findWeb3TokenItemsByIds(walletId, assetIds)

suspend fun findAndRefreshWeb3TokenItem(walletId: String, assetId: String): Web3TokenItem? {
val localToken = web3TokenDao.web3TokenItemById(walletId, assetId)
if (localToken != null) {
return localToken
}

return try {
val token = tokenRepository.findOrSyncAsset(assetId) ?: return null
val w = token.toWeb3TokenItem(walletId)
web3TokenDao.insert(
Web3Token(
walletId = walletId,
assetId = token.assetId,
chainId = token.chainId,
assetKey = token.assetKey ?: "",
name = token.name,
symbol = token.symbol,
iconUrl = token.chainIconUrl ?: "",
priceUsd = token.priceUsd,
precision = token.precision,
balance = "0",
changeUsd = "0"
)
)
w
} catch (e: Exception) {
Timber.e(e)
null
}
}

fun web3TokensExcludeHidden(walletId: String) = web3TokenDao.web3TokenItemsExcludeHidden(walletId)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,22 @@ import one.mixin.android.compose.theme.MixinAppTheme
import one.mixin.android.databinding.FragmentAddressInputBinding
import one.mixin.android.db.web3.vo.Web3TokenItem
import one.mixin.android.db.web3.vo.Web3Wallet
import one.mixin.android.db.web3.vo.buildTransaction
import one.mixin.android.db.web3.vo.isImported
import one.mixin.android.db.web3.vo.isWatch
import one.mixin.android.extension.getParcelableCompat
import one.mixin.android.extension.hideKeyboard
import one.mixin.android.extension.indeterminateProgressDialog
import one.mixin.android.extension.isEthereumOrSolURLString
import one.mixin.android.extension.isExternalTransferUrl
import one.mixin.android.extension.isLightningUrl
import one.mixin.android.extension.openPermissionSetting
import one.mixin.android.extension.toast
import one.mixin.android.job.MixinJobManager
import one.mixin.android.job.RefreshAddressJob
import one.mixin.android.job.SyncOutputJob
import one.mixin.android.pay.ExternalTransfer
import one.mixin.android.pay.parseExternalTransferUri
import one.mixin.android.ui.address.FetchUserAddressFragment.Companion.ARGS_TO_USER
import one.mixin.android.ui.address.page.AddressInputPage
import one.mixin.android.ui.address.page.LabelInputPage
Expand All @@ -55,6 +59,7 @@ import one.mixin.android.ui.common.BaseFragment
import one.mixin.android.ui.common.biometric.AddressManageBiometricItem
import one.mixin.android.ui.conversation.link.LinkBottomSheetDialogFragment
import one.mixin.android.ui.home.web3.Web3ViewModel
import one.mixin.android.ui.home.web3.showGasCheckAndBrowserBottomSheetDialogFragment
import one.mixin.android.ui.qr.CaptureActivity
import one.mixin.android.ui.wallet.InputFragment
import one.mixin.android.ui.wallet.TransactionsFragment.Companion.ARGS_ASSET
Expand All @@ -70,7 +75,10 @@ import one.mixin.android.util.viewBinding
import one.mixin.android.vo.Address
import one.mixin.android.vo.WithdrawalMemoPossibility
import one.mixin.android.vo.safe.TokenItem
import one.mixin.android.web3.Rpc
import one.mixin.android.web3.js.Web3Signer
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject

@AndroidEntryPoint
Expand All @@ -83,6 +91,105 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres
const val ARGS_WALLET = "args_wallet"
}

private suspend fun handleWeb3ExternalTransfer(url: String) {
Timber.d("[$TAG] handleWeb3ExternalTransfer url=%s", url)
val (ext, insufficientSymbol) = parseExternalForWeb3(url)
if (insufficientSymbol != null) {
withContext(Dispatchers.Main) {
val message: String = getString(R.string.insufficient_balance_symbol, insufficientSymbol)
toast(message)
}
return
}
if (ext == null) {
Timber.e("[$TAG] handleWeb3ExternalTransfer parseExternalForWeb3 returned null, url=%s", url)
toast(R.string.Data_error)
return
}
Timber.d("[$TAG] handleWeb3ExternalTransfer parsed assetId=%s destination=%s amount=%s", ext.assetId, ext.destination, ext.amount)
val t = web3ViewModel.findAndRefreshWeb3TokenItem(Web3Signer.currentWalletId, ext.assetId)
if (t == null) {
Timber.e("[$TAG] handleWeb3ExternalTransfer web3 token not found for assetId=%s", ext.assetId)
toast(R.string.Data_error)
return
}
val c = web3ViewModel.findAndRefreshWeb3TokenItem(Web3Signer.currentWalletId, t.chainId)
if (c == null) {
Timber.e("[$TAG] handleWeb3ExternalTransfer web3 chain token not found for chainId=%s", t.chainId)
toast(R.string.Data_error)
return
}
val from = web3ViewModel.getAddressesByChainId(walletId = Web3Signer.currentWalletId, c.chainId)?.destination
if (from == null) {
Timber.e("[$TAG] handleWeb3ExternalTransfer from address not found for chainId=%s", c.chainId)
toast(R.string.Data_error)
return
}
val to = ext.destination
val amount = ext.amount
if (amount.isNullOrBlank() || amount == "0") {
navigateToInputFragmentWithBundle(
Bundle().apply {
putString(InputFragment.ARGS_FROM_ADDRESS, from)
putString(InputFragment.ARGS_TO_ADDRESS, to)
putParcelable(InputFragment.ARGS_WEB3_TOKEN, t)
putParcelable(InputFragment.ARGS_WEB3_CHAIN_TOKEN, c)
putParcelable(ARGS_WALLET, wallet)
}
)
} else {
val transaction = t.buildTransaction(rpc, from, to, amount)
showGasCheckAndBrowserBottomSheetDialogFragment(
requireActivity(),
transaction,
amount = amount,
token = t,
chainToken = c,
toAddress = to,
onTxhash = { _, _ -> },
onDismiss = { _ -> }
)
}
}

private suspend fun parseExternalForWeb3(url: String): Pair<ExternalTransfer?,String?> {
var insufficientSymbol:String? = null
val result = parseExternalTransferUri(
url,
validateAddress = { assetId, chainId, destination ->
web3ViewModel.validateExternalAddress(assetId, chainId, destination, null).data
},
getFee = { assetId, destination ->
web3ViewModel.getFees(assetId, destination).data
},
findAssetIdByAssetKey = { assetKey ->
web3ViewModel.findAssetIdByAssetKey(assetKey)
},
getAssetPrecisionById = { assetId ->
web3ViewModel.getAssetPrecisionById(assetId).data
},
balanceCheck = { assetId, amount, feeAssetId, feeAmount ->
if (feeAssetId != null && feeAmount != null) {
val feeExtra = web3ViewModel.findAndRefreshWeb3TokenItem(Web3Signer.currentWalletId, feeAssetId)
val feeBalance = feeExtra?.balance?.toBigDecimalOrNull() ?: BigDecimal.ZERO
if (feeBalance < feeAmount) {
insufficientSymbol = feeExtra?.symbol
}
}
val extra = web3ViewModel.findAndRefreshWeb3TokenItem(Web3Signer.currentWalletId, assetId)
val balance = extra?.balance?.toBigDecimalOrNull() ?: BigDecimal.ZERO
if (balance < amount) {
insufficientSymbol = extra?.symbol
}
},
parseLighting = { ln ->
val r = web3ViewModel.paySuspend(one.mixin.android.api.request.TransferRequest(assetId = one.mixin.android.Constants.ChainId.LIGHTNING_NETWORK_CHAIN_ID, rawPaymentUrl = ln))
r.data
}
)
return Pair(result, insufficientSymbol)
}

private val token: TokenItem? by lazy {
requireArguments().getParcelableCompat(
ARGS_ASSET,
Expand Down Expand Up @@ -151,6 +258,9 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres
@Inject
lateinit var jobManager: MixinJobManager

@Inject
lateinit var rpc: Rpc

enum class TransferDestination {
Initial,
Address,
Expand Down Expand Up @@ -332,12 +442,19 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres
navController.navigate(TransferDestination.Address.name)
},
onSend = { address ->
Timber.d("[$TAG] onSend address=%s token=%s web3Token=%s", address, token, web3Token)
errorInfo = null
if (token != null && (address.isExternalTransferUrl() || address.isLightningUrl())) {
LinkBottomSheetDialogFragment.newInstance(address).show(
parentFragmentManager,
LinkBottomSheetDialogFragment.TAG
)
} else if (web3Token != null && address.isEthereumOrSolURLString()) {
lifecycleScope.launch {
isLoading = true
handleWeb3ExternalTransfer(address)
isLoading = false
}
} else {
val memoEnabled =
token?.withdrawalMemoPossibility == WithdrawalMemoPossibility.POSITIVE || token?.withdrawalMemoPossibility == WithdrawalMemoPossibility.POSSIBLE
Expand All @@ -355,6 +472,7 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres
}
} else if (web3Token != null) {
lifecycleScope.launch {
Timber.d("[$TAG] onSend web3 normal address=%s", address)
web3Token?.let { token ->
val fromAddress = web3ViewModel.getAddressesByChainId(token.walletId, token.chainId)?.destination
if (fromAddress.isNullOrBlank()) {
Expand All @@ -374,6 +492,7 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres
}
} else {
token?.let { t ->
Timber.d("[$TAG] onSend normal token address=%s", address)
validateAndNavigateToInput(
assetId = t.assetId,
chainId = t.chainId,
Expand Down Expand Up @@ -590,15 +709,23 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres

private fun handleScanResult(data: Intent?, isAddr: Boolean = true) {
if (data == null) return

data.getStringExtra(CaptureActivity.Companion.ARGS_FOR_SCAN_RESULT)?.let { result ->
data.getStringExtra(CaptureActivity.ARGS_FOR_SCAN_RESULT)?.let { result ->
Timber.d("[$TAG] handleScanResult result=%s currentScanType=%s", result, currentScanType)
if (token != null && (result.isLightningUrl() || result.isExternalTransferUrl())) {
LinkBottomSheetDialogFragment.newInstance(result).show(
parentFragmentManager,
LinkBottomSheetDialogFragment.TAG
)
return@let
}
if (web3Token != null && result.isEthereumOrSolURLString()) {
lifecycleScope.launch {
isLoading = true
handleWeb3ExternalTransfer(result)
isLoading = false
}
return@let
}
when (currentScanType) {
ScanType.ADDRESS -> {
scannedAddress = if (isIcapAddress(result)) {
Expand Down
Loading
Loading