Skip to content

Commit 53c76fa

Browse files
authored
fix(wallet): support TON transfer URI send flow (#6477)
* fix(wallet): support TON transfer URI send flow * fix(wallet): open balance error trade in swap tab
1 parent 7b482b5 commit 53c76fa

7 files changed

Lines changed: 224 additions & 17 deletions

File tree

app/src/main/java/one/mixin/android/pay/ExternalTransferUriParser.kt

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ suspend fun parseExternalTransferUri(
3030
return parseLightning(url, validateAddress, getFee, balanceCheck, parseLighting)
3131
}
3232

33+
if (url.startsWith("ton:", ignoreCase = true)) {
34+
return parseTon(url, validateAddress, getFee, findAssetIdByAssetKey, getAssetPrecisionById, balanceCheck)
35+
}
36+
3337
val uri = url.addSlashesIfNeeded().toUri()
3438
val scheme = uri.scheme?.lowercase()
3539
val chainId = externalTransferAssetIdMap[scheme] ?: return null
@@ -91,6 +95,70 @@ suspend fun parseExternalTransferUri(
9195
return ExternalTransfer(addressResponse.destination, amount, assetId, fee.amount?.toBigDecimalOrNull(), fee.assetId, memo)
9296
}
9397

98+
private suspend fun parseTon(
99+
url: String,
100+
validateAddress: suspend (String, String, String) -> AddressResponse?,
101+
getFee: suspend (String, String) -> List<WithdrawalResponse>?,
102+
findAssetIdByAssetKey: suspend (String) -> String?,
103+
getAssetPrecisionById: suspend (String) -> AssetPrecision?,
104+
balanceCheck: suspend (String, BigDecimal, String?, BigDecimal?) -> Unit,
105+
): ExternalTransfer? {
106+
val uri = url.addSlashesIfNeeded().toUri()
107+
if (!uri.host.equals("transfer", ignoreCase = true)) return null
108+
109+
val destination = uri.pathSegments.firstOrNull()?.takeIf { it.isNotBlank() } ?: return null
110+
val jetton = uri.getQueryParameter("jetton")
111+
val assetId =
112+
if (jetton.isNullOrBlank()) {
113+
Constants.ChainId.TON_CHAIN_ID
114+
} else {
115+
findAssetIdByAssetKey(jetton) ?: return null
116+
}
117+
val precision =
118+
if (assetId == Constants.ChainId.TON_CHAIN_ID) {
119+
TON_PRECISION
120+
} else {
121+
getAssetPrecisionById(assetId)?.precision ?: return null
122+
}
123+
if (precision < 0) return null
124+
125+
val addressResponse = validateAddress(assetId, Constants.ChainId.TON_CHAIN_ID, destination) ?: return null
126+
if (!addressResponse.destination.equals(destination, true)) {
127+
return null
128+
}
129+
130+
val memo = uri.getQueryParameter("text")?.normalizeTonText()
131+
?: uri.getQueryParameter("memo")?.normalizeTonText()
132+
val amount = uri.getQueryParameter("amount")
133+
if (amount.isNullOrEmpty() || amount == "0") {
134+
return ExternalTransfer(addressResponse.destination, null, assetId, null, null, memo)
135+
}
136+
if (amount.amountWithE()) return null
137+
138+
val baseAmount = amount.toBigDecimalOrNull() ?: return null
139+
if (baseAmount == BigDecimal.ZERO) {
140+
return ExternalTransfer(addressResponse.destination, null, assetId, null, null, memo)
141+
}
142+
if (amount != baseAmount.toPlainString()) {
143+
return null
144+
}
145+
val displayAmount = baseAmount.divide(BigDecimal.TEN.pow(precision)).toPlainString().stripAmountZero()
146+
147+
val feeResponse = getFee(assetId, destination) ?: return null
148+
val fee = feeResponse.firstOrNull() ?: return null
149+
150+
if (fee.assetId == assetId) {
151+
val totalAmount = displayAmount.toBigDecimal() + (fee.amount?.toBigDecimalOrNull() ?: BigDecimal.ZERO)
152+
balanceCheck(assetId, totalAmount, null, null)
153+
} else {
154+
balanceCheck(assetId, displayAmount.toBigDecimal(), fee.assetId, fee.amount?.toBigDecimalOrNull() ?: BigDecimal.ZERO)
155+
}
156+
157+
return ExternalTransfer(addressResponse.destination, displayAmount, assetId, fee.amount?.toBigDecimalOrNull(), fee.assetId, memo)
158+
}
159+
160+
private fun String.normalizeTonText(): String = replace('+', ' ')
161+
94162
// check amount has scientific E
95163
fun String?.amountWithE(): Boolean = this?.contains("e") == true
96164

@@ -110,5 +178,8 @@ val externalTransferAssetIdMap by lazy {
110178
"dogecoin" to Constants.ChainId.Dogecoin,
111179
"monero" to Constants.ChainId.Monero,
112180
"solana" to Constants.ChainId.Solana,
181+
"ton" to Constants.ChainId.TON_CHAIN_ID,
113182
)
114183
}
184+
185+
private const val TON_PRECISION = 9

app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,7 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres
397397
},
398398
onSend = { address ->
399399
errorInfo = null
400-
if (token != null && (address.isExternalTransferUrl() || address.isLightningUrl())) {
400+
if (address.isExternalTransferUrl() || address.isLightningUrl()) {
401401
LinkBottomSheetDialogFragment.newInstance(address).show(
402402
parentFragmentManager,
403403
LinkBottomSheetDialogFragment.TAG
@@ -727,7 +727,7 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres
727727
if (data == null) return
728728

729729
data.getStringExtra(CaptureActivity.ARGS_FOR_SCAN_RESULT)?.let { result ->
730-
if (token != null && (result.isLightningUrl() || result.isExternalTransferUrl())) {
730+
if (result.isLightningUrl() || result.isExternalTransferUrl()) {
731731
LinkBottomSheetDialogFragment.newInstance(result).show(
732732
parentFragmentManager,
733733
LinkBottomSheetDialogFragment.TAG
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package one.mixin.android.ui.wallet.transfer
2+
3+
import android.content.Context
4+
import one.mixin.android.ui.home.web3.trade.SwapActivity
5+
import one.mixin.android.ui.home.web3.trade.TradeFragment
6+
7+
object BalanceErrorTradeNavigator {
8+
fun showSwapTrade(
9+
context: Context,
10+
input: String?,
11+
output: String?,
12+
amount: String? = null,
13+
referral: String? = null,
14+
inMixin: Boolean = true,
15+
walletId: String? = null,
16+
) {
17+
SwapActivity.show(
18+
context = context,
19+
input = input,
20+
output = output,
21+
amount = amount,
22+
referral = referral,
23+
inMixin = inMixin,
24+
walletId = walletId,
25+
initialTab = TradeFragment.TAB_SIMPLE,
26+
)
27+
}
28+
}

app/src/main/java/one/mixin/android/ui/wallet/transfer/TransferBalanceErrorBottomSheetDialogFragment.kt

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import one.mixin.android.ui.common.MixinBottomSheetDialogFragment
2020
import one.mixin.android.ui.common.biometric.AssetBiometricItem
2121
import one.mixin.android.ui.common.biometric.BiometricItem
2222
import one.mixin.android.ui.common.biometric.WithdrawBiometricItem
23-
import one.mixin.android.ui.home.web3.trade.SwapActivity
2423
import one.mixin.android.ui.wallet.AddFeeBottomSheetDialogFragment
2524
import one.mixin.android.ui.wallet.WalletActivity
2625
import one.mixin.android.util.analytics.AnalyticsTracker
@@ -84,7 +83,7 @@ class TransferBalanceErrorBottomSheetDialogFragment : MixinBottomSheetDialogFrag
8483
binding.contentTv.text = getString(R.string.swap_usdt_hint, u.symbol)
8584
binding.positive.setOnClickListener {
8685
AnalyticsTracker.trackTradeStart(TradeWallet.MAIN, TradeSource.BALANCE)
87-
SwapActivity.show(requireActivity(), input = u.assetId, output = asset.assetId, null, null)
86+
BalanceErrorTradeNavigator.showSwapTrade(requireActivity(), input = u.assetId, output = asset.assetId)
8887
dismiss()
8988
}
9089
binding.negative.setOnClickListener {
@@ -104,12 +103,10 @@ class TransferBalanceErrorBottomSheetDialogFragment : MixinBottomSheetDialogFrag
104103
onAction = { type, fee ->
105104
if (type == AddFeeBottomSheetDialogFragment.ActionType.SWAP) {
106105
AnalyticsTracker.trackTradeStart(TradeWallet.MAIN, TradeSource.BALANCE)
107-
SwapActivity.show(
106+
BalanceErrorTradeNavigator.showSwapTrade(
108107
requireActivity(),
109108
input = Constants.AssetId.USDT_ASSET_ETH_ID,
110109
output = fee.assetId,
111-
null,
112-
null
113110
)
114111
this@TransferBalanceErrorBottomSheetDialogFragment.dismiss()
115112
} else if (type == AddFeeBottomSheetDialogFragment.ActionType.DEPOSIT) {
@@ -132,12 +129,10 @@ class TransferBalanceErrorBottomSheetDialogFragment : MixinBottomSheetDialogFrag
132129
onAction = { type, asset ->
133130
if (type == AddFeeBottomSheetDialogFragment.ActionType.SWAP) {
134131
AnalyticsTracker.trackTradeStart(TradeWallet.MAIN, TradeSource.BALANCE)
135-
SwapActivity.show(
132+
BalanceErrorTradeNavigator.showSwapTrade(
136133
requireActivity(),
137134
input = Constants.AssetId.USDT_ASSET_ETH_ID,
138135
output = asset.assetId,
139-
null,
140-
null
141136
)
142137
this@TransferBalanceErrorBottomSheetDialogFragment.dismiss()
143138
} else if (type == AddFeeBottomSheetDialogFragment.ActionType.DEPOSIT) {

app/src/main/java/one/mixin/android/ui/wallet/transfer/TransferWeb3BalanceErrorBottomSheetDialogFragment.kt

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import one.mixin.android.extension.visibleDisplayHeight
1919
import one.mixin.android.extension.withArgs
2020
import one.mixin.android.ui.common.MixinBottomSheetDialogFragment
2121
import one.mixin.android.ui.home.web3.Web3ViewModel
22-
import one.mixin.android.ui.home.web3.trade.SwapActivity
2322
import one.mixin.android.ui.wallet.AddFeeBottomSheetDialogFragment
2423
import one.mixin.android.util.analytics.AnalyticsTracker
2524
import one.mixin.android.util.analytics.AnalyticsTracker.TradeSource
@@ -79,12 +78,10 @@ class TransferWeb3BalanceErrorBottomSheetDialogFragment : MixinBottomSheetDialog
7978
binding.contentTv.text = getString(R.string.swap_usdt_hint, u.symbol)
8079
binding.positive.setOnClickListener {
8180
AnalyticsTracker.trackTradeStart(TradeWallet.WEB3, TradeSource.BALANCE)
82-
SwapActivity.show(
81+
BalanceErrorTradeNavigator.showSwapTrade(
8382
requireActivity(),
8483
input = u.assetId,
8584
output = asset.assetId,
86-
null,
87-
null,
8885
inMixin = false,
8986
walletId = wallet?.id
9087
)
@@ -107,12 +104,10 @@ class TransferWeb3BalanceErrorBottomSheetDialogFragment : MixinBottomSheetDialog
107104
onWeb3Action = { type, fee ->
108105
if (type == AddFeeBottomSheetDialogFragment.ActionType.SWAP) {
109106
AnalyticsTracker.trackTradeStart(TradeWallet.WEB3, TradeSource.BALANCE)
110-
SwapActivity.show(
107+
BalanceErrorTradeNavigator.showSwapTrade(
111108
requireActivity(),
112109
input = Constants.AssetId.USDT_ASSET_ETH_ID,
113110
output = asset.assetId,
114-
null,
115-
null,
116111
inMixin = false,
117112
walletId = wallet?.id
118113
)
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package one.mixin.android.pay
2+
3+
import kotlinx.coroutines.runBlocking
4+
import one.mixin.android.Constants
5+
import one.mixin.android.api.response.AddressResponse
6+
import one.mixin.android.api.response.WithdrawalResponse
7+
import one.mixin.android.extension.isExternalTransferUrl
8+
import one.mixin.android.vo.AssetPrecision
9+
import org.junit.Assert.assertEquals
10+
import org.junit.Assert.assertNotNull
11+
import org.junit.Assert.assertTrue
12+
import org.junit.Test
13+
import org.junit.runner.RunWith
14+
import org.robolectric.RobolectricTestRunner
15+
16+
@RunWith(RobolectricTestRunner::class)
17+
class ExternalTransferUriParserTonTest {
18+
@Test
19+
fun tonTransferUriIsExternalTransferUrl() {
20+
assertTrue("ton://transfer/UQAG6wmpZWRwXQFP7HBZVEkHgwsH1wC2zHTV1VHKBDYgoMii".isExternalTransferUrl())
21+
}
22+
23+
@Test
24+
fun parseTonJettonTransferUri() =
25+
runBlocking {
26+
val result = parse(
27+
"ton://transfer/UQAG6wmpZWRwXQFP7HBZVEkHgwsH1wC2zHTV1VHKBDYgoMii" +
28+
"?jetton=EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs" +
29+
"&amount=40000000&text=TON+Console%3A+Refill"
30+
)
31+
32+
assertNotNull(result)
33+
assertEquals(Constants.AssetId.USDT_ASSET_TON_ID, result!!.assetId)
34+
assertEquals("UQAG6wmpZWRwXQFP7HBZVEkHgwsH1wC2zHTV1VHKBDYgoMii", result.destination)
35+
assertEquals("40", result.amount)
36+
assertEquals("TON Console: Refill", result.memo)
37+
}
38+
39+
@Test
40+
fun parseNativeTonTransferUri() =
41+
runBlocking {
42+
val result = parse("ton://transfer/UQAG6wmpZWRwXQFP7HBZVEkHgwsH1wC2zHTV1VHKBDYgoMii?amount=40000000")
43+
44+
assertNotNull(result)
45+
assertEquals(Constants.ChainId.TON_CHAIN_ID, result!!.assetId)
46+
assertEquals("UQAG6wmpZWRwXQFP7HBZVEkHgwsH1wC2zHTV1VHKBDYgoMii", result.destination)
47+
assertEquals("0.04", result.amount)
48+
}
49+
50+
private suspend fun parse(url: String) =
51+
parseExternalTransferUri(
52+
url,
53+
{ assetId, _, destination ->
54+
AddressResponse(destination, null, assetId)
55+
},
56+
{ assetId, _ ->
57+
listOf(WithdrawalResponse(assetId, "0"))
58+
},
59+
{ assetKey ->
60+
mockAssetKeyAssetId[assetKey]
61+
},
62+
{ assetId ->
63+
AssetPrecision(assetId, Constants.ChainId.TON_CHAIN_ID, mockAssetPrecision[assetId] ?: 9)
64+
},
65+
{ _, _, _, _ -> },
66+
parseLighting = { null }
67+
)
68+
69+
private val mockAssetKeyAssetId =
70+
mapOf(
71+
"EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs" to Constants.AssetId.USDT_ASSET_TON_ID,
72+
)
73+
74+
private val mockAssetPrecision =
75+
mapOf(
76+
Constants.AssetId.USDT_ASSET_TON_ID to 6,
77+
Constants.ChainId.TON_CHAIN_ID to 9,
78+
)
79+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package one.mixin.android.ui.wallet.transfer
2+
3+
import android.app.Activity
4+
import one.mixin.android.ui.home.web3.trade.TradeFragment
5+
import org.junit.Assert.assertEquals
6+
import org.junit.Assert.assertFalse
7+
import org.junit.Assert.assertTrue
8+
import org.junit.Test
9+
import org.junit.runner.RunWith
10+
import org.robolectric.Robolectric
11+
import org.robolectric.RobolectricTestRunner
12+
import org.robolectric.Shadows.shadowOf
13+
14+
@RunWith(RobolectricTestRunner::class)
15+
class BalanceErrorTradeNavigatorTest {
16+
@Test
17+
fun showSwapTradeOpensSimpleSwapTab() {
18+
val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
19+
20+
BalanceErrorTradeNavigator.showSwapTrade(
21+
context = activity,
22+
input = "input-asset",
23+
output = "output-asset",
24+
inMixin = false,
25+
walletId = "wallet-id",
26+
)
27+
28+
val intent = shadowOf(activity).nextStartedActivity
29+
assertEquals("input-asset", intent.getStringExtra(TradeFragment.ARGS_INPUT))
30+
assertEquals("output-asset", intent.getStringExtra(TradeFragment.ARGS_OUTPUT))
31+
assertFalse(intent.getBooleanExtra(TradeFragment.ARGS_IN_MIXIN, true))
32+
assertEquals("wallet-id", intent.getStringExtra(TradeFragment.ARGS_WALLET_ID))
33+
assertTrue(intent.hasExtra(TradeFragment.ARGS_INITIAL_TAB))
34+
assertEquals(
35+
TradeFragment.TAB_SIMPLE,
36+
intent.getIntExtra(TradeFragment.ARGS_INITIAL_TAB, TradeFragment.TAB_PERPETUAL),
37+
)
38+
}
39+
}

0 commit comments

Comments
 (0)