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
777 changes: 777 additions & 0 deletions app/schemas/one.mixin.android.db.WalletDatabase/8.json

Large diffs are not rendered by default.

13 changes: 10 additions & 3 deletions app/src/main/java/one/mixin/android/db/WalletDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import kotlin.math.min
SafeWallets::class,
WalletOutput::class,
],
version = 7,
version = 8,
)
@TypeConverters(Web3TypeConverters::class, AssetChangeListConverter::class)
abstract class WalletDatabase : RoomDatabase() {
Expand Down Expand Up @@ -115,6 +115,13 @@ abstract class WalletDatabase : RoomDatabase() {
}
}

val MIGRATION_7_8 = object : Migration(7, 8) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE transactions ADD COLUMN sponsor_fee_asset_id TEXT")
db.execSQL("ALTER TABLE transactions ADD COLUMN sponsor_fee_amount TEXT")
}
}

fun getDatabase(
context: Context,
identityNumber: String,
Expand All @@ -139,7 +146,7 @@ abstract class WalletDatabase : RoomDatabase() {
listOf(
object : MixinCorruptionCallback {
override fun onCorruption(database: SupportSQLiteDatabase) {
val e = IllegalStateException("Wallet database is corrupted, current DB version: 7")
val e = IllegalStateException("Wallet database is corrupted, current DB version: 8")
reportException(e)
}
},
Expand All @@ -153,7 +160,7 @@ abstract class WalletDatabase : RoomDatabase() {
supportSQLiteDatabase = db
}
},
).addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7)
).addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8)
.enableMultiInstanceInvalidation()
.setQueryExecutor(
Executors.newFixedThreadPool(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,19 @@ import one.mixin.android.db.web3.vo.Web3TransactionItem
interface Web3TransactionDao : BaseDao<Web3Transaction> {

@Query("""
SELECT DISTINCT w.transaction_hash, w.transaction_type, w.status, w.block_number, w.chain_id, w.address, w.fee, w.senders, w.receivers, w.approvals, w.send_asset_id, w.receive_asset_id, w.transaction_at, w.updated_at, w.level,
SELECT DISTINCT w.transaction_hash, w.transaction_type, w.status, w.block_number, w.chain_id, w.address, w.fee, w.sponsor_fee_asset_id, w.sponsor_fee_amount, w.senders, w.receivers, w.approvals, w.send_asset_id, w.receive_asset_id, w.transaction_at, w.updated_at, w.level,
c.symbol as chain_symbol,
c.icon_url as chain_icon_url,
s.icon_url as send_asset_icon_url,
s.symbol as send_asset_symbol,
r.icon_url as receive_asset_icon_url,
r.symbol as receive_asset_symbol
r.symbol as receive_asset_symbol,
sf.symbol as sponsor_fee_asset_symbol
FROM transactions w
LEFT JOIN tokens c ON c.asset_id = w.chain_id AND c.wallet_id = :walletId
LEFT JOIN tokens s ON s.asset_id = w.send_asset_id AND s.wallet_id = :walletId
LEFT JOIN tokens r ON r.asset_id = w.receive_asset_id AND r.wallet_id = :walletId
LEFT JOIN tokens sf ON sf.asset_id = w.sponsor_fee_asset_id AND sf.wallet_id = :walletId
WHERE (w.send_asset_id = :assetId OR w.receive_asset_id = :assetId) AND (s.wallet_id = :walletId OR c.wallet_id = :walletId) AND w.level >= (SELECT level FROM tokens WHERE asset_id = :assetId)
AND w.address in (SELECT destination FROM addresses WHERE wallet_id = :walletId)
ORDER BY w.transaction_at DESC
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ data class Web3Transaction(
@SerializedName("fee")
val fee: String,

@ColumnInfo(name = "sponsor_fee_asset_id")
@SerializedName("sponsor_fee_asset_id")
val sponsorFeeAssetId: String? = null,

@ColumnInfo(name = "sponsor_fee_amount")
@SerializedName("sponsor_fee_amount")
val sponsorFeeAmount: String? = null,

@TypeConverters(AssetChangeListConverter::class)
@ColumnInfo(name = "senders")
@SerializedName("senders")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ data class Web3TransactionItem(

@ColumnInfo(name = "fee")
val fee: String,

@ColumnInfo(name = "sponsor_fee_asset_id")
val sponsorFeeAssetId: String? = null,

@ColumnInfo(name = "sponsor_fee_amount")
val sponsorFeeAmount: String? = null,

@TypeConverters(AssetChangeListConverter::class)
@ColumnInfo(name = "senders")
Expand Down Expand Up @@ -75,6 +81,9 @@ data class Web3TransactionItem(
@ColumnInfo(name = "receive_asset_symbol")
val receiveAssetSymbol: String? = null,

@ColumnInfo(name = "sponsor_fee_asset_symbol")
val sponsorFeeAssetSymbol: String? = null,

@ColumnInfo(name = "level")
val level: Int,
) : Parcelable {
Expand All @@ -98,6 +107,12 @@ data class Web3TransactionItem(

fun isNotVerified() = level < Constants.AssetLevel.VERIFIED

fun displayFeeAmount(): String = sponsorFeeAmount?.ifBlank { fee } ?: fee

fun displayFeeSymbol(): String? = sponsorFeeAssetSymbol ?: chainSymbol

fun hasSponsorFee(): Boolean = !sponsorFeeAmount.isNullOrBlank()

fun getMainAmount(): String {
return when (transactionType) {
TransactionType.TRANSFER_IN.value -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1551,6 +1551,8 @@ class TokenRepository
nonce = nonce,
createdAt = createdAt,
updatedAt = updatedAt,
sponsorFeeAssetId = assetId,
sponsorFeeAmount = fee,
)
}

Expand Down Expand Up @@ -1593,9 +1595,12 @@ class TokenRepository
nonce: String,
createdAt: String,
updatedAt: String,
sponsorFeeAssetId: String? = null,
sponsorFeeAmount: String? = null,
) {
val normalizedAmount = amount.removePrefix("-")
val normalizedFee = fee.toBigDecimalOrNull()?.stripTrailingZeros()?.toPlainString() ?: fee
val normalizedSponsorFeeAmount = sponsorFeeAmount?.toBigDecimalOrNull()?.stripTrailingZeros()?.toPlainString() ?: sponsorFeeAmount
appDatabase.withTransaction {
web3RawTransactionDao.insertSuspend(
Web3RawTransaction(
Expand All @@ -1619,6 +1624,8 @@ class TokenRepository
status = TransactionStatus.PENDING.value,
blockNumber = 0,
fee = normalizedFee,
sponsorFeeAssetId = sponsorFeeAssetId,
sponsorFeeAmount = normalizedSponsorFeeAmount,
senders = listOf(
AssetChange(
assetId = assetId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,8 @@ constructor(
}

suspend fun mapWeb3Transaction(transaction: Web3TransactionItem, walletId: String): Web3TransactionItem = withContext(Dispatchers.IO) {
val assetIds = transaction.senders.map { it.assetId } + transaction.receivers.map { it.assetId } + (transaction.approvals?.map { it.assetId } ?: emptyList())
val tokens = web3TokenDao.findWeb3TokenItemsByIdsSync(walletId, assetIds.distinct()).associateBy { it.assetId }
val assetIds = transaction.senders.map { it.assetId } + transaction.receivers.map { it.assetId } + (transaction.approvals?.map { it.assetId } ?: emptyList()) + transaction.sponsorFeeAssetId.orEmpty()
val tokens = web3TokenDao.findWeb3TokenItemsByIdsSync(walletId, assetIds.filter(String::isNotBlank).distinct()).associateBy { it.assetId }
transaction.copy(
senders = transaction.senders.map {
it.copy(symbol = tokens[it.assetId]?.symbol)
Expand All @@ -241,7 +241,8 @@ constructor(
},
approvals = transaction.approvals?.map {
it.copy(symbol = tokens[it.assetId]?.symbol)
}
},
sponsorFeeAssetSymbol = tokens[transaction.sponsorFeeAssetId]?.symbol ?: transaction.sponsorFeeAssetSymbol,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ class LimitTransferBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag

private var web3Transaction: JsSignMessage? by mutableStateOf(null)
private var gaslessPrepareResponse: GaslessTxResponse? by mutableStateOf(null)
private var gaslessFeeAmount: String? by mutableStateOf(null)
private var tipGas: TipGas? by mutableStateOf(null)
private var solanaFee: BigDecimal? by mutableStateOf(null)
private var solanaTx: VersionedTransactionCompat? by mutableStateOf(null)
Expand Down Expand Up @@ -815,6 +816,7 @@ class LimitTransferBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag
gaslessPrepareResponse = previewData.gaslessPrepareResponseJson?.let {
GsonHelper.customGson.fromJson(it, GaslessTxResponse::class.java)
}
gaslessFeeAmount = previewData.feeAmount

val previewFee = previewData.feeAmount.toBigDecimalOrNull()
when (token.chainId) {
Expand Down Expand Up @@ -876,6 +878,7 @@ class LimitTransferBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag
amount = amount,
chainId = preparedResponse.chainId,
payload = preparedResponse.payload,
fee = normalizeGaslessPendingFeeAmount(gaslessFeeAmount),
privateKey = privateKey,
)
else -> throw IllegalArgumentException("Gasless is not supported for ${transferToken.chainId}")
Expand Down Expand Up @@ -936,6 +939,7 @@ class LimitTransferBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag
amount: String,
chainId: String,
payload: JsonElement,
fee: String,
privateKey: ByteArray,
) {
if (!payload.isJsonObject) {
Expand Down Expand Up @@ -968,7 +972,7 @@ class LimitTransferBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag
account = fromAddress,
assetId = token.assetId,
amount = amount.stripAmountZero(),
fee = "",
fee = fee,
to = toAddress,
nonce = ethPayload.userOperation.nonce,
createdAt = now,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ class SwapTransferBottomSheetDialogFragment : MixinComposeBottomSheetDialogFragm

private var web3Transaction: JsSignMessage? by mutableStateOf(null)
private var gaslessPrepareResponse: GaslessTxResponse? by mutableStateOf(null)
private var gaslessFeeAmount: String? by mutableStateOf(null)
private var isGaslessLoading by mutableStateOf(false)
private var tipGas: TipGas? by mutableStateOf(null)
private var solanaFee: BigDecimal? by mutableStateOf(null)
Expand Down Expand Up @@ -1075,6 +1076,7 @@ class SwapTransferBottomSheetDialogFragment : MixinComposeBottomSheetDialogFragm
gaslessPrepareResponse = previewData.gaslessPrepareResponseJson?.let {
GsonHelper.customGson.fromJson(it, GaslessTxResponse::class.java)
}
gaslessFeeAmount = previewData.feeAmount

val previewFee = previewData.feeAmount.toBigDecimalOrNull()
when (token.chainId) {
Expand Down Expand Up @@ -1110,6 +1112,7 @@ class SwapTransferBottomSheetDialogFragment : MixinComposeBottomSheetDialogFragm
amount: String,
): GaslessTxResponse? {
val feeAmount = resolveGaslessFeeAmount(token, fromAddress, toAddress) ?: return null
gaslessFeeAmount = feeAmount
return runCatching {
web3ViewModel.gaslessPrepare(
GaslessTxRequest(
Expand All @@ -1135,6 +1138,7 @@ class SwapTransferBottomSheetDialogFragment : MixinComposeBottomSheetDialogFragm
val feeAmount = requireNotNull(resolveGaslessFeeAmount(token, fromAddress, toAddress)) {
"gasless fee amount is required"
}
gaslessFeeAmount = feeAmount
val response = web3ViewModel.gaslessPrepare(
GaslessTxRequest(
from = fromAddress,
Expand Down Expand Up @@ -1196,6 +1200,7 @@ class SwapTransferBottomSheetDialogFragment : MixinComposeBottomSheetDialogFragm
amount = amount,
chainId = preparedResponse.chainId,
payload = preparedResponse.payload,
fee = normalizeGaslessPendingFeeAmount(gaslessFeeAmount),
privateKey = privateKey,
)
else -> throw IllegalArgumentException("Gasless is not supported for ${transferToken.chainId}")
Expand Down Expand Up @@ -1256,6 +1261,7 @@ class SwapTransferBottomSheetDialogFragment : MixinComposeBottomSheetDialogFragm
amount: String,
chainId: String,
payload: JsonElement,
fee: String,
privateKey: ByteArray,
) {
if (!payload.isJsonObject) {
Expand Down Expand Up @@ -1288,7 +1294,7 @@ class SwapTransferBottomSheetDialogFragment : MixinComposeBottomSheetDialogFragm
account = fromAddress,
assetId = token.assetId,
amount = amount.stripAmountZero(),
fee = "",
fee = fee,
to = toAddress,
nonce = ethPayload.userOperation.nonce,
createdAt = now,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,20 +107,21 @@ class Web3FilterParams(

return SimpleSQLiteQuery(
"SELECT DISTINCT w.transaction_hash, w.transaction_type, w.status, w.block_number, w.chain_id, " +
"w.address, w.fee, w.senders, w.receivers, w.approvals, w.send_asset_id, w.receive_asset_id, " +
"w.address, w.fee, w.sponsor_fee_asset_id, w.sponsor_fee_amount, w.senders, w.receivers, w.approvals, w.send_asset_id, w.receive_asset_id, " +
"w.transaction_at, w.updated_at, w.level, " +
"c.symbol as chain_symbol, " +
"c.icon_url as chain_icon_url, " +
"s.icon_url as send_asset_icon_url, " +
"s.symbol as send_asset_symbol, " +
"r.icon_url as receive_asset_icon_url, " +
"r.symbol as receive_asset_symbol " +
"r.symbol as receive_asset_symbol, " +
"sf.symbol as sponsor_fee_asset_symbol " +
"FROM transactions w " +
"LEFT JOIN tokens c ON c.asset_id = w.chain_id " +
"LEFT JOIN tokens s ON s.asset_id = w.send_asset_id " +
"LEFT JOIN tokens r ON r.asset_id = w.receive_asset_id " +
"LEFT JOIN tokens sf ON sf.asset_id = w.sponsor_fee_asset_id " +
"$whereSql $orderSql"
)
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package one.mixin.android.ui.wallet

internal fun normalizeGaslessPendingFeeAmount(feeAmount: String?): String =
feeAmount?.toBigDecimalOrNull()?.stripTrailingZeros()?.toPlainString() ?: feeAmount.orEmpty()
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ class Web3TransactionFragment : BaseFragment(R.layout.fragment_web3_transaction)

dateTv.text = transaction.transactionAt.fullDate()
feeLl.isVisible = shouldShowFee(transaction.status)
feeTv.text = "${transaction.fee} ${transaction.chainSymbol ?: ""}"
feeTv.text = "${transaction.displayFeeAmount()} ${transaction.displayFeeSymbol() ?: ""}"
statusLl.isVisible = false

networkLl.isVisible = true
Expand Down Expand Up @@ -619,13 +619,13 @@ class Web3TransactionFragment : BaseFragment(R.layout.fragment_web3_transaction)
status: String,
pendingRawTx: Web3RawTransaction? = null,
): Boolean {
if (transaction.transactionType == TransactionType.TRANSFER_IN.value || transaction.fee.isEmpty()) {
if (!transaction.hasSponsorFee() && (transaction.transactionType == TransactionType.TRANSFER_IN.value || transaction.fee.isEmpty())) {
return false
}
if (status != TransactionStatus.PENDING.value) {
return true
}
return pendingRawTx?.isGaslessPending() == false || transaction.fee.isNotEmpty()
return pendingRawTx?.isGaslessPending() == false || transaction.displayFeeAmount().isNotEmpty()
}

private suspend fun updateFeeVisibility(status: String = transaction.status) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package one.mixin.android.ui.wallet

import one.mixin.android.db.web3.vo.Web3TransactionItem
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import org.junit.Test

class Web3PendingFeeTest {
@Test
fun gaslessPendingFeeKeepsDisplayableAmount() {
assertEquals("0.00021", normalizeGaslessPendingFeeAmount("0.0002100"))
}

@Test
fun gaslessPendingFeeFallsBackToEmptyWhenMissing() {
assertEquals("", normalizeGaslessPendingFeeAmount(null))
}

@Test
fun sponsorFeeIsPreferredForDisplay() {
val transaction = transactionItem(
fee = "0.0001",
sponsorFeeAmount = "0.25",
sponsorFeeAssetSymbol = "USDT",
)

assertEquals("0.25", transaction.displayFeeAmount())
assertEquals("USDT", transaction.displayFeeSymbol())
assertTrue(transaction.hasSponsorFee())
}

@Test
fun chainFeeIsUsedWhenSponsorFeeIsMissing() {
val transaction = transactionItem(
fee = "0.0001",
chainSymbol = "ETH",
)

assertEquals("0.0001", transaction.displayFeeAmount())
assertEquals("ETH", transaction.displayFeeSymbol())
}

private fun transactionItem(
fee: String,
sponsorFeeAmount: String? = null,
chainSymbol: String? = null,
sponsorFeeAssetSymbol: String? = null,
) = Web3TransactionItem(
transactionHash = "hash",
transactionType = "transfer_in",
status = "success",
blockNumber = 1,
chainId = "chain",
address = "address",
fee = fee,
sponsorFeeAssetId = if (sponsorFeeAmount.isNullOrBlank()) null else "sponsor",
sponsorFeeAmount = sponsorFeeAmount,
senders = emptyList(),
receivers = emptyList(),
approvals = null,
sendAssetId = null,
receiveAssetId = null,
transactionAt = "2026-04-03T14:52:35.000000Z",
updatedAt = "2026-04-03T14:52:55.822512Z",
chainSymbol = chainSymbol,
sponsorFeeAssetSymbol = sponsorFeeAssetSymbol,
level = 0,
)
}