diff --git a/app/src/main/java/one/mixin/android/MixinApplication.kt b/app/src/main/java/one/mixin/android/MixinApplication.kt index f37de7ed14..b884cc2f1c 100644 --- a/app/src/main/java/one/mixin/android/MixinApplication.kt +++ b/app/src/main/java/one/mixin/android/MixinApplication.kt @@ -428,13 +428,14 @@ open class MixinApplication : OkHttpClient.Builder() .addInterceptor { chain -> val original = chain.request() - val requestBuilder = original.newBuilder() - .header("User-Agent", API_UA) - .method(original.method, original.body) + val requestBuilder = + original.newBuilder() + .header("User-Agent", API_UA) + .method(original.method, original.body) val request = requestBuilder.build() chain.proceed(request) } - .build() + .build(), ) .components { if (SDK_INT >= Build.VERSION_CODES.P) { diff --git a/app/src/main/java/one/mixin/android/api/request/web3/PriorityFeeRequest.kt b/app/src/main/java/one/mixin/android/api/request/web3/PriorityFeeRequest.kt new file mode 100644 index 0000000000..6236832014 --- /dev/null +++ b/app/src/main/java/one/mixin/android/api/request/web3/PriorityFeeRequest.kt @@ -0,0 +1,5 @@ +package one.mixin.android.api.request.web3 + +data class PriorityFeeRequest( + val transaction: String, +) diff --git a/app/src/main/java/one/mixin/android/api/response/Web3Token.kt b/app/src/main/java/one/mixin/android/api/response/Web3Token.kt index 7df7f1e83d..16c72e5de7 100644 --- a/app/src/main/java/one/mixin/android/api/response/Web3Token.kt +++ b/app/src/main/java/one/mixin/android/api/response/Web3Token.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize import one.mixin.android.Constants +import one.mixin.android.api.response.web3.PriorityFeeResponse import one.mixin.android.api.response.web3.SwapChain import one.mixin.android.api.response.web3.SwapToken import one.mixin.android.extension.base64Encode @@ -21,6 +22,8 @@ import org.sol4k.Transaction import org.sol4k.VersionedTransaction import org.sol4k.instruction.CreateAssociatedTokenAccountInstruction import org.sol4k.instruction.Instruction +import org.sol4k.instruction.SetComputeUnitLimitInstruction +import org.sol4k.instruction.SetComputeUnitPriceInstruction import org.sol4k.instruction.SplTransferInstruction import org.sol4k.instruction.TransferInstruction import org.sol4k.lamportToSol @@ -84,7 +87,7 @@ fun Web3Token.toSwapToken(): SwapToken { price = null, ), balance = balance, - price = price + price = price, ) } @@ -172,19 +175,20 @@ suspend fun Web3Token.buildTransaction( fromAddress: String, toAddress: String, v: String, + estimatePriorityFee: (suspend (String) -> PriorityFeeResponse?)? = null, ): JsSignMessage { if (chainName.equals("solana", true)) { JsSigner.useSolana() val sender = PublicKey(fromAddress) val receiver = PublicKey(toAddress) val instructions = mutableListOf() + val conn = Connection(RpcUrl.MAINNNET) if (isSolToken()) { val amount = solToLamport(v).toLong() instructions.add(TransferInstruction(sender, receiver, amount)) } else { val tokenMintAddress = PublicKey(assetKey) val (receiveAssociatedAccount) = PublicKey.findProgramDerivedAddress(receiver, tokenMintAddress) - val conn = Connection(RpcUrl.MAINNNET) val receiveAssociatedAccountInfo = withContext(Dispatchers.IO) { conn.getAccountInfo(receiveAssociatedAccount) @@ -228,7 +232,26 @@ suspend fun Web3Token.buildTransaction( instructions, sender, ) - val tx = transaction.serialize().base64Encode() + var tx = transaction.serialize().base64Encode() + + val priorityFeeResponse = estimatePriorityFee?.invoke(tx) + if (priorityFeeResponse != null && priorityFeeResponse.unitPrice > 0) { + val newInstructions = mutableListOf() + newInstructions.add( + SetComputeUnitLimitInstruction( + units = priorityFeeResponse.unitLimit, + ), + ) + newInstructions.add( + SetComputeUnitPriceInstruction( + microLamports = priorityFeeResponse.unitPrice, + ), + ) + newInstructions.addAll(instructions) + val newTransaction = Transaction(toAddress, newInstructions, sender) + tx = newTransaction.serialize().base64Encode() + } + return JsSignMessage(0, JsSignMessage.TYPE_RAW_TRANSACTION, data = tx) } else { JsSigner.useEvm() diff --git a/app/src/main/java/one/mixin/android/api/response/Web3Transaction.kt b/app/src/main/java/one/mixin/android/api/response/Web3Transaction.kt index 81b540ee97..04aff6128e 100644 --- a/app/src/main/java/one/mixin/android/api/response/Web3Transaction.kt +++ b/app/src/main/java/one/mixin/android/api/response/Web3Transaction.kt @@ -5,14 +5,11 @@ import android.os.Parcelable import android.text.SpannedString import com.google.gson.annotations.SerializedName import kotlinx.parcelize.Parcelize -import one.mixin.android.MixinApplication import one.mixin.android.R import one.mixin.android.extension.buildAmountSymbol import one.mixin.android.extension.colorFromAttribute import one.mixin.android.extension.numberFormat import one.mixin.android.extension.numberFormat2 -import one.mixin.android.ui.setting.getLanguagePos -import one.mixin.android.util.getLanguage import one.mixin.android.util.needsSpaceBetweenWords import one.mixin.android.vo.Fiats import one.mixin.android.web3.details.Web3TransactionDirection diff --git a/app/src/main/java/one/mixin/android/api/response/web3/PriorityFeeResponse.kt b/app/src/main/java/one/mixin/android/api/response/web3/PriorityFeeResponse.kt new file mode 100644 index 0000000000..587c5ebd9b --- /dev/null +++ b/app/src/main/java/one/mixin/android/api/response/web3/PriorityFeeResponse.kt @@ -0,0 +1,10 @@ +package one.mixin.android.api.response.web3 + +import com.google.gson.annotations.SerializedName + +data class PriorityFeeResponse( + @SerializedName("unit_price") + val unitPrice: Long, + @SerializedName("unit_limit") + val unitLimit: Int, +) diff --git a/app/src/main/java/one/mixin/android/api/response/web3/SwapToken.kt b/app/src/main/java/one/mixin/android/api/response/web3/SwapToken.kt index 9504c8bd6c..bcaf5792c6 100644 --- a/app/src/main/java/one/mixin/android/api/response/web3/SwapToken.kt +++ b/app/src/main/java/one/mixin/android/api/response/web3/SwapToken.kt @@ -3,8 +3,8 @@ package one.mixin.android.api.response.web3 import android.os.Parcelable import com.google.gson.annotations.SerializedName import kotlinx.parcelize.Parcelize -import one.mixin.android.api.response.wrappedSolTokenAssetKey import one.mixin.android.api.response.solanaNativeTokenAssetKey +import one.mixin.android.api.response.wrappedSolTokenAssetKey import java.math.BigDecimal import java.math.RoundingMode diff --git a/app/src/main/java/one/mixin/android/api/service/Web3Service.kt b/app/src/main/java/one/mixin/android/api/service/Web3Service.kt index 1c2b4503ad..b6b726b1fd 100644 --- a/app/src/main/java/one/mixin/android/api/service/Web3Service.kt +++ b/app/src/main/java/one/mixin/android/api/service/Web3Service.kt @@ -1,11 +1,15 @@ package one.mixin.android.api.service import one.mixin.android.api.MixinResponse +import one.mixin.android.api.request.web3.PriorityFeeRequest import one.mixin.android.api.response.Web3Account import one.mixin.android.api.response.Web3Token import one.mixin.android.api.response.Web3Transaction +import one.mixin.android.api.response.web3.PriorityFeeResponse import one.mixin.android.vo.ChainDapp +import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.POST import retrofit2.http.Path import retrofit2.http.Query @@ -31,4 +35,9 @@ interface Web3Service { suspend fun web3Tokens( @Query("addresses") addresses: String, ): MixinResponse> + + @POST("estimate-priority-fees") + suspend fun getPriorityFee( + @Body priorityFeeRequest: PriorityFeeRequest, + ): MixinResponse } diff --git a/app/src/main/java/one/mixin/android/compose/CoilImage.kt b/app/src/main/java/one/mixin/android/compose/CoilImage.kt index aa585fb243..972a851833 100644 --- a/app/src/main/java/one/mixin/android/compose/CoilImage.kt +++ b/app/src/main/java/one/mixin/android/compose/CoilImage.kt @@ -8,25 +8,35 @@ import coil.compose.AsyncImage import coil.request.ImageRequest @Composable -fun CoilImage(model: String?, placeholder: Int, modifier: Modifier, contentScale: ContentScale = ContentScale.Fit) { +fun CoilImage( + model: String?, + placeholder: Int, + modifier: Modifier, + contentScale: ContentScale = ContentScale.Fit, +) { AsyncImage( modifier = modifier, model = model, contentDescription = null, placeholder = painterResource(id = placeholder), error = painterResource(id = placeholder), - contentScale = contentScale + contentScale = contentScale, ) } @Composable -fun CoilImage(model: ImageRequest, placeholder: Int, modifier: Modifier, contentScale: ContentScale = ContentScale.Fit) { +fun CoilImage( + model: ImageRequest, + placeholder: Int, + modifier: Modifier, + contentScale: ContentScale = ContentScale.Fit, +) { AsyncImage( modifier = modifier, model = model, contentDescription = null, placeholder = painterResource(id = placeholder), error = painterResource(id = placeholder), - contentScale = contentScale + contentScale = contentScale, ) -} \ No newline at end of file +} diff --git a/app/src/main/java/one/mixin/android/extension/ImageViewExtension.kt b/app/src/main/java/one/mixin/android/extension/ImageViewExtension.kt index 1fe9198e6b..d73081e00e 100644 --- a/app/src/main/java/one/mixin/android/extension/ImageViewExtension.kt +++ b/app/src/main/java/one/mixin/android/extension/ImageViewExtension.kt @@ -27,9 +27,12 @@ fun ImageView.loadImage( data: String?, @DrawableRes holder: Int? = null, base64Holder: String? = null, - onSuccess: (( - request: ImageRequest, result: SuccessResult - ) -> Unit)? = null, + onSuccess: ( + ( + request: ImageRequest, + result: SuccessResult, + ) -> Unit + )? = null, onError: ((request: ImageRequest, result: ErrorResult) -> Unit)? = null, transformation: Transformation? = null, ) { @@ -45,7 +48,7 @@ fun ImageView.loadImage( onSuccess?.let { listener( onSuccess = onSuccess, - onError = onError ?: { _, _ -> } + onError = onError ?: { _, _ -> }, ) } } @@ -322,10 +325,10 @@ fun RLottieImageView.loadSticker( loadLottie(it, cacheKey) "GIF" -> { - loadImage(url,null,null) + loadImage(url, null, null) } - else -> loadImage(url,null,null) + else -> loadImage(url, null, null) } } } diff --git a/app/src/main/java/one/mixin/android/job/GenerateAvatarJob.kt b/app/src/main/java/one/mixin/android/job/GenerateAvatarJob.kt index b6cfa2e97c..c51c4b9b77 100644 --- a/app/src/main/java/one/mixin/android/job/GenerateAvatarJob.kt +++ b/app/src/main/java/one/mixin/android/job/GenerateAvatarJob.kt @@ -53,45 +53,46 @@ class GenerateAvatarJob( override fun getRetryLimit() = 0 - override fun onRun() = runBlocking{ - val users = mutableListOf() - texts = ArrayMap() - if (list == null) { - users.addAll(participantDao.getParticipantsAvatar(groupId)) - } else { - val us = runBlocking { userDao.findMultiUsersByIds(list.toSet()) } - users.addAll(us) - } - val name = getIconUrlName(groupId, users) - val f = applicationContext.getGroupAvatarPath(name, false) - val icon = conversationDao.getGroupIconUrl(groupId) - if (f.exists()) { - if (f.absolutePath != name) { - conversationDao.updateGroupIconUrl(groupId, f.absolutePath) + override fun onRun() = + runBlocking { + val users = mutableListOf() + texts = ArrayMap() + if (list == null) { + users.addAll(participantDao.getParticipantsAvatar(groupId)) + } else { + val us = runBlocking { userDao.findMultiUsersByIds(list.toSet()) } + users.addAll(us) + } + val name = getIconUrlName(groupId, users) + val f = applicationContext.getGroupAvatarPath(name, false) + val icon = conversationDao.getGroupIconUrl(groupId) + if (f.exists()) { + if (f.absolutePath != name) { + conversationDao.updateGroupIconUrl(groupId, f.absolutePath) + } + RxBus.publish(AvatarEvent(groupId, f.absolutePath)) + return@runBlocking } - RxBus.publish(AvatarEvent(groupId, f.absolutePath)) - return@runBlocking - } - val result = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888) - val c = Canvas(result) - val bitmaps = mutableListOf() - try { - getBitmaps(bitmaps, users) - } catch (e: Exception) { - return@runBlocking - } - drawInternal(c, bitmaps) - result.saveGroupAvatar(applicationContext, name) - if (icon != null && icon != f.absolutePath) { + val result = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888) + val c = Canvas(result) + val bitmaps = mutableListOf() try { - File(icon).delete() + getBitmaps(bitmaps, users) } catch (e: Exception) { + return@runBlocking + } + drawInternal(c, bitmaps) + result.saveGroupAvatar(applicationContext, name) + if (icon != null && icon != f.absolutePath) { + try { + File(icon).delete() + } catch (e: Exception) { + } } + conversationDao.updateGroupIconUrl(groupId, f.absolutePath) + RxBus.publish(AvatarEvent(groupId, f.absolutePath)) } - conversationDao.updateGroupIconUrl(groupId, f.absolutePath) - RxBus.publish(AvatarEvent(groupId, f.absolutePath)) - } private fun drawInternal( canvas: Canvas, @@ -360,10 +361,11 @@ class GenerateAvatarJob( texts[i] = AvatarView.checkEmoji(user.fullName) bitmaps.add(getBitmapByPlaceHolder(user.userId)) } else { - val request = ImageRequest.Builder(applicationContext) - .data(item) - .allowHardware(false) // Disable hardware bitmaps since we're getting a Bitmap - .build() + val request = + ImageRequest.Builder(applicationContext) + .data(item) + .allowHardware(false) // Disable hardware bitmaps since we're getting a Bitmap + .build() val result = applicationContext.imageLoader.execute(request) val bitmap = (result as? SuccessResult)?.drawable?.toBitmap() diff --git a/app/src/main/java/one/mixin/android/job/NotificationGenerator.kt b/app/src/main/java/one/mixin/android/job/NotificationGenerator.kt index 264c2b39eb..2126c507ef 100644 --- a/app/src/main/java/one/mixin/android/job/NotificationGenerator.kt +++ b/app/src/main/java/one/mixin/android/job/NotificationGenerator.kt @@ -63,7 +63,6 @@ import one.mixin.android.vo.isTranscript import one.mixin.android.vo.isVideo import one.mixin.android.websocket.SystemConversationAction import one.mixin.android.widget.picker.toTimeInterval -import timber.log.Timber const val KEY_REPLY = "key_reply" const val CONVERSATION_ID = "conversation_id" @@ -606,23 +605,29 @@ object NotificationGenerator : Injector() { } } - - private fun loadImageWithCoil(context: Context, url: String?, width: Int, height: Int, onComplete: (Bitmap?) -> Unit) { + private fun loadImageWithCoil( + context: Context, + url: String?, + width: Int, + height: Int, + onComplete: (Bitmap?) -> Unit, + ) { val imageLoader = context.imageLoader - val request = ImageRequest.Builder(context) - .data(url) - .size(width, height) - .transformations(CircleCropTransformation()) - .target( - onSuccess = { drawable -> - val bitmap = (drawable as? BitmapDrawable)?.bitmap - onComplete(bitmap) - }, - onError = { - onComplete(null) - } - ) - .build() + val request = + ImageRequest.Builder(context) + .data(url) + .size(width, height) + .transformations(CircleCropTransformation()) + .target( + onSuccess = { drawable -> + val bitmap = (drawable as? BitmapDrawable)?.bitmap + onComplete(bitmap) + }, + onError = { + onComplete(null) + }, + ) + .build() imageLoader.enqueue(request) } diff --git a/app/src/main/java/one/mixin/android/job/SendGiphyJob.kt b/app/src/main/java/one/mixin/android/job/SendGiphyJob.kt index 88ddb167d3..601853c1bc 100644 --- a/app/src/main/java/one/mixin/android/job/SendGiphyJob.kt +++ b/app/src/main/java/one/mixin/android/job/SendGiphyJob.kt @@ -63,43 +63,48 @@ class SendGiphyJob( } @OptIn(ExperimentalCoilApi::class) - override fun onRun() = runBlocking { - val ctx = MixinApplication.appContext - val loader = ctx.imageLoader - val request = ImageRequest.Builder(ctx).data(url).build() - val result = loader.execute(request) - if (result !is SuccessResult) return@runBlocking - val f = loader.diskCache?.openSnapshot(url)?.data?.toFile()?:return@runBlocking - sendMessage(ctx, f) - return@runBlocking - } + override fun onRun() = + runBlocking { + val ctx = MixinApplication.appContext + val loader = ctx.imageLoader + val request = ImageRequest.Builder(ctx).data(url).build() + val result = loader.execute(request) + if (result !is SuccessResult) return@runBlocking + val f = loader.diskCache?.openSnapshot(url)?.data?.toFile() ?: return@runBlocking + sendMessage(ctx, f) + return@runBlocking + } - private fun sendMessage(ctx: Context, imageFile: File) = runBlocking(Dispatchers.IO) { - val file = ctx.getImagePath().createGifTemp(conversationId, messageId) - imageFile.copy(file) - val thumbnail = file.encodeBlurHash() - val mediaSize = file.length() - val message = - createMediaMessage( - messageId, - conversationId, - senderId, - category, - null, - file.name, - MimeType.GIF.toString(), - mediaSize, - width, - height, - thumbnail, - null, - null, - time, - MediaStatus.PENDING, - MessageStatus.SENDING.name, - ) - messageDao.updateGiphyMessage(messageId, file.name, mediaSize, thumbnail) - MessageFlow.update(message.conversationId, message.messageId) - jobManager.addJobInBackground(SendAttachmentMessageJob(message)) - } + private fun sendMessage( + ctx: Context, + imageFile: File, + ) = + runBlocking(Dispatchers.IO) { + val file = ctx.getImagePath().createGifTemp(conversationId, messageId) + imageFile.copy(file) + val thumbnail = file.encodeBlurHash() + val mediaSize = file.length() + val message = + createMediaMessage( + messageId, + conversationId, + senderId, + category, + null, + file.name, + MimeType.GIF.toString(), + mediaSize, + width, + height, + thumbnail, + null, + null, + time, + MediaStatus.PENDING, + MessageStatus.SENDING.name, + ) + messageDao.updateGiphyMessage(messageId, file.name, mediaSize, thumbnail) + MessageFlow.update(message.conversationId, message.messageId) + jobManager.addJobInBackground(SendAttachmentMessageJob(message)) + } } diff --git a/app/src/main/java/one/mixin/android/tip/wc/WalletConnectV2.kt b/app/src/main/java/one/mixin/android/tip/wc/WalletConnectV2.kt index 3c8073532b..68d9358119 100644 --- a/app/src/main/java/one/mixin/android/tip/wc/WalletConnectV2.kt +++ b/app/src/main/java/one/mixin/android/tip/wc/WalletConnectV2.kt @@ -32,6 +32,7 @@ import org.sol4k.Connection import org.sol4k.Keypair import org.sol4k.RpcUrl import org.sol4k.VersionedTransaction +import org.sol4k.api.Commitment import org.web3j.crypto.Credentials import org.web3j.crypto.ECKeyPair import org.web3j.crypto.Keys @@ -435,7 +436,7 @@ object WalletConnectV2 : WalletConnect() { // use latest blockhash should not break other signatures if (signMessage.signatures.size <= 1) { val conn = Connection(RpcUrl.MAINNNET) - val blockhash = conn.getLatestBlockhash() + val blockhash = conn.getLatestBlockhash(Commitment.CONFIRMED) signMessage.message.recentBlockhash = blockhash } signMessage.sign(holder) diff --git a/app/src/main/java/one/mixin/android/ui/auth/compose/AuthBottomSheetDialogCompose.kt b/app/src/main/java/one/mixin/android/ui/auth/compose/AuthBottomSheetDialogCompose.kt index cf1585c4b9..bcbb62cf9c 100644 --- a/app/src/main/java/one/mixin/android/ui/auth/compose/AuthBottomSheetDialogCompose.kt +++ b/app/src/main/java/one/mixin/android/ui/auth/compose/AuthBottomSheetDialogCompose.kt @@ -127,10 +127,12 @@ fun AuthBottomSheetDialogCompose( ) { if (iconUrl != null) { CoilImage( - model = iconUrl, placeholder = R.drawable.ic_avatar_place_holder, - modifier = Modifier - .size(16.dp) - .clip(CircleShape), + model = iconUrl, + placeholder = R.drawable.ic_avatar_place_holder, + modifier = + Modifier + .size(16.dp) + .clip(CircleShape), ) Spacer(modifier = Modifier.width(3.dp)) } diff --git a/app/src/main/java/one/mixin/android/ui/common/UserBottomSheetDialogFragment.kt b/app/src/main/java/one/mixin/android/ui/common/UserBottomSheetDialogFragment.kt index f57bef8c4e..232b415f27 100644 --- a/app/src/main/java/one/mixin/android/ui/common/UserBottomSheetDialogFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/common/UserBottomSheetDialogFragment.kt @@ -6,7 +6,6 @@ import android.annotation.SuppressLint import android.app.Dialog import android.content.ClipData import android.content.Intent -import android.graphics.Bitmap import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.net.Uri @@ -1030,7 +1029,7 @@ class UserBottomSheetDialogFragment : MixinScrollableBottomSheetDialogFragment() lifecycleScope.launch { val loader = requireContext().imageLoader val request = ImageRequest.Builder(requireContext()).data(user.avatarUrl).build() - val result = loader.execute(request).drawable as BitmapDrawable? ?:return@launch + val result = loader.execute(request).drawable as BitmapDrawable? ?: return@launch user.fullName?.let { val conversationId = conversationId addPinShortcut( diff --git a/app/src/main/java/one/mixin/android/ui/common/share/renderer/ShareImageRenderer.kt b/app/src/main/java/one/mixin/android/ui/common/share/renderer/ShareImageRenderer.kt index 46f4eaaa5e..e08271e5c3 100644 --- a/app/src/main/java/one/mixin/android/ui/common/share/renderer/ShareImageRenderer.kt +++ b/app/src/main/java/one/mixin/android/ui/common/share/renderer/ShareImageRenderer.kt @@ -10,7 +10,6 @@ import one.mixin.android.R import one.mixin.android.databinding.ItemChatImageBinding import one.mixin.android.extension.dp import one.mixin.android.extension.loadImage -import one.mixin.android.extension.loadImageMark import one.mixin.android.extension.nowInUtc import one.mixin.android.extension.realSize import one.mixin.android.vo.MessageStatus diff --git a/app/src/main/java/one/mixin/android/ui/conversation/link/parser/NewSchemeParser.kt b/app/src/main/java/one/mixin/android/ui/conversation/link/parser/NewSchemeParser.kt index 1b5474987e..cae463f74b 100644 --- a/app/src/main/java/one/mixin/android/ui/conversation/link/parser/NewSchemeParser.kt +++ b/app/src/main/java/one/mixin/android/ui/conversation/link/parser/NewSchemeParser.kt @@ -68,11 +68,12 @@ class NewSchemeParser( if (payType == PayType.Uuid) { val user = linkViewModel.refreshUser(urlQueryParser.userId) ?: return Result.failure(ParserError(FAILURE)) - val biometricItem = if (urlQueryParser.inscription != null) { - buildInscriptionTransfer(urlQueryParser, user.userId, traceId) - } else { - TransferBiometricItem(listOf(user), 1, traceId, token, amount, urlQueryParser.memo, status, null, urlQueryParser.returnTo, reference = urlQueryParser.reference) - } + val biometricItem = + if (urlQueryParser.inscription != null) { + buildInscriptionTransfer(urlQueryParser, user.userId, traceId) + } else { + TransferBiometricItem(listOf(user), 1, traceId, token, amount, urlQueryParser.memo, status, null, urlQueryParser.returnTo, reference = urlQueryParser.reference) + } checkRawTransaction(biometricItem) } else if (payType == PayType.MixAddress) { val mixAddress = urlQueryParser.mixAddress @@ -81,17 +82,17 @@ class NewSchemeParser( if (users.isEmpty() || users.size < mixAddress.uuidMembers.size) { return Result.failure(ParserError(FAILURE)) } - val biometricItem = TransferBiometricItem(users, mixAddress.threshold, traceId, token, amount, urlQueryParser.memo, status, null, urlQueryParser.returnTo, reference = urlQueryParser.reference) + val biometricItem = TransferBiometricItem(users, mixAddress.threshold, traceId, token, amount, urlQueryParser.memo, status, null, urlQueryParser.returnTo, reference = urlQueryParser.reference) checkRawTransaction(biometricItem) } else if (mixAddress.xinMembers.isNotEmpty()) { - val addressTransferBiometricItem = AddressTransferBiometricItem(mixAddress.xinMembers.first().string(), traceId, token, amount, urlQueryParser.memo, status, urlQueryParser.returnTo, reference = urlQueryParser.reference) + val addressTransferBiometricItem = AddressTransferBiometricItem(mixAddress.xinMembers.first().string(), traceId, token, amount, urlQueryParser.memo, status, urlQueryParser.returnTo, reference = urlQueryParser.reference) checkRawTransaction(addressTransferBiometricItem) } else { return Result.failure(ParserError(FAILURE)) } } else { // TODO verify address? - val addressTransferBiometricItem = AddressTransferBiometricItem(urlQueryParser.lastPath, traceId, token, amount, urlQueryParser.memo, status, urlQueryParser.returnTo, reference = urlQueryParser.reference) + val addressTransferBiometricItem = AddressTransferBiometricItem(urlQueryParser.lastPath, traceId, token, amount, urlQueryParser.memo, status, urlQueryParser.returnTo, reference = urlQueryParser.reference) checkRawTransaction(addressTransferBiometricItem) } } else { @@ -109,7 +110,7 @@ class NewSchemeParser( buildInscriptionTransfer(urlQueryParser, user.userId, traceId) } else { buildTransferBiometricItem(user, token, amount ?: "", traceId, urlQueryParser.memo, urlQueryParser.returnTo) - } + }, ) } else if (payType == PayType.MixAddress) { val mixAddress = urlQueryParser.mixAddress @@ -121,24 +122,24 @@ class NewSchemeParser( if (urlQueryParser.inscription != null) { buildInscriptionTransfer(urlQueryParser, user.userId, traceId) } else { - buildTransferBiometricItem(user, token, amount ?: "", traceId, urlQueryParser.memo, urlQueryParser.returnTo, reference = urlQueryParser.reference) - } + buildTransferBiometricItem(user, token, amount ?: "", traceId, urlQueryParser.memo, urlQueryParser.returnTo, reference = urlQueryParser.reference) + }, ) } else { val users = linkViewModel.findOrRefreshUsers(members) if (users.isEmpty() || users.size < members.size) { return Result.failure(ParserError(FAILURE)) } - val item = TransferBiometricItem(users, mixAddress.threshold, traceId, token, amount ?: "", urlQueryParser.memo, PaymentStatus.pending.name, null, urlQueryParser.returnTo, reference = urlQueryParser.reference) + val item = TransferBiometricItem(users, mixAddress.threshold, traceId, token, amount ?: "", urlQueryParser.memo, PaymentStatus.pending.name, null, urlQueryParser.returnTo, reference = urlQueryParser.reference) TransferFragment.newInstance(item) } } else if (mixAddress.xinMembers.size == 1) { // TODO Support for multiple address - TransferFragment.newInstance(buildAddressBiometricItem(mixAddress.xinMembers.first().string(), traceId, token, amount ?: "", urlQueryParser.memo, urlQueryParser.returnTo, from, reference = urlQueryParser.reference)) + TransferFragment.newInstance(buildAddressBiometricItem(mixAddress.xinMembers.first().string(), traceId, token, amount ?: "", urlQueryParser.memo, urlQueryParser.returnTo, from, reference = urlQueryParser.reference)) } else { null } } else { - TransferFragment.newInstance(buildAddressBiometricItem(urlQueryParser.lastPath, traceId, token, amount ?: "", urlQueryParser.memo, urlQueryParser.returnTo, from, reference = urlQueryParser.reference)) + TransferFragment.newInstance(buildAddressBiometricItem(urlQueryParser.lastPath, traceId, token, amount ?: "", urlQueryParser.memo, urlQueryParser.returnTo, from, reference = urlQueryParser.reference)) } if (transferFragment == null) return Result.failure(ParserError(FAILURE)) transferFragment.show(bottomSheet.parentFragmentManager, TransferFragment.TAG) @@ -150,7 +151,9 @@ class NewSchemeParser( } private suspend fun buildInscriptionTransfer( - urlQueryParser: UrlQueryParser, userId: String, traceId: String + urlQueryParser: UrlQueryParser, + userId: String, + traceId: String, ): NftBiometricItem { val token = checkToken(urlQueryParser.asset!!) ?: throw ParserError(FAILURE) val inscriptionHash = urlQueryParser.inscription ?: throw ParserError(FAILURE) diff --git a/app/src/main/java/one/mixin/android/ui/conversation/link/parser/UrlQueryParser.kt b/app/src/main/java/one/mixin/android/ui/conversation/link/parser/UrlQueryParser.kt index 59a431b674..db0f3d0175 100644 --- a/app/src/main/java/one/mixin/android/ui/conversation/link/parser/UrlQueryParser.kt +++ b/app/src/main/java/one/mixin/android/ui/conversation/link/parser/UrlQueryParser.kt @@ -1,15 +1,15 @@ package one.mixin.android.ui.conversation.link.parser import android.net.Uri -import java.io.UnsupportedEncodingException -import java.math.BigDecimal -import java.net.URLDecoder -import java.nio.charset.StandardCharsets import one.mixin.android.extension.isUUID import one.mixin.android.ui.conversation.link.LinkBottomSheetDialogFragment import one.mixin.android.ui.conversation.link.parser.NewSchemeParser.Companion.FAILURE import one.mixin.android.vo.MixAddressPrefix import one.mixin.android.vo.toMixAddress +import java.io.UnsupportedEncodingException +import java.math.BigDecimal +import java.net.URLDecoder +import java.nio.charset.StandardCharsets class UrlQueryParser(uri: Uri, from: Int) { val lastPath: String = uri.lastPathSegment ?: throw ParserError(FAILURE) @@ -27,13 +27,19 @@ class UrlQueryParser(uri: Uri, from: Int) { } val userId: String by lazy { - if (payType == PayType.Uuid) lastPath - else throw ParserError(FAILURE) + if (payType == PayType.Uuid) { + lastPath + } else { + throw ParserError(FAILURE) + } } val mixAddress by lazy { - if (payType == PayType.MixAddress) lastPath.toMixAddress() ?: throw ParserError(FAILURE) - else throw ParserError(FAILURE) + if (payType == PayType.MixAddress) { + lastPath.toMixAddress() ?: throw ParserError(FAILURE) + } else { + throw ParserError(FAILURE) + } } val asset: String? by lazy { @@ -50,7 +56,9 @@ class UrlQueryParser(uri: Uri, from: Int) { throw ParserError(FAILURE) } a.stripTrailingZeros().toPlainString() - } else null + } else { + null + } } val memo: String? by lazy { diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/BrowserWalletBottomSheetDialogFragment.kt b/app/src/main/java/one/mixin/android/ui/home/web3/BrowserWalletBottomSheetDialogFragment.kt index 37b1049dcb..17799eca0b 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/BrowserWalletBottomSheetDialogFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/BrowserWalletBottomSheetDialogFragment.kt @@ -31,6 +31,7 @@ import one.mixin.android.R import one.mixin.android.api.response.Web3Token import one.mixin.android.api.response.calcSolBalanceChange import one.mixin.android.api.response.getChainFromName +import one.mixin.android.extension.base64Encode import one.mixin.android.extension.booleanFromAttribute import one.mixin.android.extension.getParcelableCompat import one.mixin.android.extension.isNightMode @@ -298,7 +299,7 @@ class BrowserWalletBottomSheetDialogFragment : BottomSheetDialogFragment() { val tx = JsSigner.signSolanaTransaction(priv, requireNotNull(solanaTx) { "required solana tx can not be null" }) step = Step.Sending val sig = JsSigner.sendSolanaTransaction(tx) - onTxhash?.invoke(sig) + onTxhash?.invoke(sig, tx.serialize().base64Encode()) onDone?.invoke("window.${JsSigner.currentNetwork}.sendResponse(${signMessage.callbackId}, \"$sig\");") } else if (signMessage.type == JsSignMessage.TYPE_TYPED_MESSAGE || signMessage.type == JsSignMessage.TYPE_MESSAGE || signMessage.type == JsSignMessage.TYPE_PERSONAL_MESSAGE) { val priv = viewModel.getWeb3Priv(requireContext(), pin, JsSigner.currentChain.assetId) @@ -403,7 +404,7 @@ class BrowserWalletBottomSheetDialogFragment : BottomSheetDialogFragment() { return this } - fun setOnTxhash(callback: (String) -> Unit): BrowserWalletBottomSheetDialogFragment { + fun setOnTxhash(callback: (String, String) -> Unit): BrowserWalletBottomSheetDialogFragment { onTxhash = callback return this } @@ -411,7 +412,7 @@ class BrowserWalletBottomSheetDialogFragment : BottomSheetDialogFragment() { private var onDone: ((String?) -> Unit)? = null private var onRejectAction: (() -> Unit)? = null private var onDismissAction: (() -> Unit)? = null - private var onTxhash: ((String) -> Unit)? = null + private var onTxhash: ((String, String) -> Unit)? = null fun getBiometricInfo() = BiometricInfo( @@ -448,7 +449,7 @@ fun showBrowserBottomSheetDialogFragment( currentTitle: String? = null, onReject: (() -> Unit)? = null, onDone: ((String?) -> Unit)? = null, - onTxhash: ((String) -> Unit)? = null, + onTxhash: ((String, String) -> Unit)? = null, ) { val wcBottomSheet = BrowserWalletBottomSheetDialogFragment.newInstance(signMessage, currentUrl, currentTitle, amount, token, chainToken, toAddress) onDone?.let { diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/TransactionStateFragment.kt b/app/src/main/java/one/mixin/android/ui/home/web3/TransactionStateFragment.kt new file mode 100644 index 0000000000..2d4e6a6d7b --- /dev/null +++ b/app/src/main/java/one/mixin/android/ui/home/web3/TransactionStateFragment.kt @@ -0,0 +1,160 @@ +package one.mixin.android.ui.home.web3 + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withContext +import one.mixin.android.Constants.RouteConfig.ROUTE_BOT_USER_ID +import one.mixin.android.api.handleMixinResponse +import one.mixin.android.api.response.web3.Tx +import one.mixin.android.api.response.web3.TxState +import one.mixin.android.api.response.web3.isFinalTxState +import one.mixin.android.compose.theme.MixinAppTheme +import one.mixin.android.extension.isNightMode +import one.mixin.android.extension.withArgs +import one.mixin.android.ui.common.BaseFragment +import one.mixin.android.ui.web.WebActivity +import one.mixin.android.util.tickerFlow +import org.sol4k.Connection +import org.sol4k.RpcUrl +import org.sol4k.VersionedTransaction +import org.sol4k.api.Commitment +import timber.log.Timber +import kotlin.time.Duration.Companion.seconds + +@AndroidEntryPoint +class TransactionStateFragment : BaseFragment() { + companion object { + const val TAG = "TransactionStateFragment" + + const val ARGS_TX = "args_tx" + const val ARGS_TOKEN_SYMBOL = "args_token_symbol" + + fun newInstance( + tx: String, + tokenSymbol: String?, + ) = + TransactionStateFragment().withArgs { + putString(ARGS_TX, tx) + tokenSymbol?.let { putString(ARGS_TOKEN_SYMBOL, it) } + } + } + + private val web3ViewModel by viewModels() + + private val tx: VersionedTransaction by lazy { + val serializedTx = requireArguments().getString(ARGS_TX)!! + VersionedTransaction.from(serializedTx) + } + private val symbol: String? by lazy { requireArguments().getString(ARGS_TOKEN_SYMBOL) } + + private var txState: Tx? by mutableStateOf(null) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + return ComposeView(inflater.context).apply { + setContent { + MixinAppTheme( + darkTheme = context.isNightMode(), + ) { + TransactionStatePage( + tx = txState ?: Tx(TxState.NotFound.name), + symbol = symbol, + viewTx = { + WebActivity.show(context, "https://solscan.io/tx/${tx.signatures[0]}", null) + }, + ) { + val action = closeAction + if (action == null) { + activity?.onBackPressedDispatcher?.onBackPressed() + } else { + closeAction?.invoke() + } + } + } + } + refreshTx() + } + } + + private var refreshTxJob: Job? = null + private val conn = Connection(RpcUrl.MAINNNET) + + private fun refreshTx() { + val txhash = tx.signatures[0] + val blockhash = tx.message.recentBlockhash + Timber.e("$TAG txhash: ${txhash}, blockhash: $blockhash") + refreshTxJob?.cancel() + refreshTxJob = + tickerFlow(2.seconds) + .onEach { + try { + withContext(Dispatchers.IO) { + try { + conn.sendTransaction(tx.serialize()) + } catch (ignored: Exception) { + Timber.d("loop sendTransaction ${ignored.stackTraceToString()}") + } + } + handleMixinResponse( + invokeNetwork = { web3ViewModel.getWeb3Tx(txhash) }, + successBlock = { + txState = it.data + }, + failureBlock = { + if (it.errorCode == 401) { + web3ViewModel.getBotPublicKey(ROUTE_BOT_USER_ID) + refreshTx() + } + return@handleMixinResponse true + }, + ) + if (txState?.state?.isFinalTxState() == true) { + refreshTxJob?.cancel() + } else { + val isBlockhashValid = + withContext(Dispatchers.IO) { + conn.isBlockhashValid(blockhash, Commitment.CONFIRMED) + } + if (!isBlockhashValid) { + Timber.e("$TAG blockhash $blockhash valid") + val ts = handleMixinResponse( + invokeNetwork = { web3ViewModel.getWeb3Tx(txhash) }, + successBlock = { it.data }, + ) + refreshTxJob?.cancel() + txState = if (ts?.state?.isFinalTxState() == true) { + ts + } else { + Tx(TxState.Failed.name) + } + } + } + } catch (e: Exception) { + Timber.e(e) + } + }.launchIn(lifecycleScope) + } + + private var closeAction: (() -> Unit)? = null + + fun setCloseAction(action: () -> Unit): TransactionStateFragment { + this.closeAction = action + return this + } +} diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/swap/SwapStatePage.kt b/app/src/main/java/one/mixin/android/ui/home/web3/TransactionStatePage.kt similarity index 87% rename from app/src/main/java/one/mixin/android/ui/home/web3/swap/SwapStatePage.kt rename to app/src/main/java/one/mixin/android/ui/home/web3/TransactionStatePage.kt index a112f89fc9..174d252cd8 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/swap/SwapStatePage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/TransactionStatePage.kt @@ -1,4 +1,4 @@ -package one.mixin.android.ui.home.web3.swap +package one.mixin.android.ui.home.web3 import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -31,7 +31,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import one.mixin.android.R -import one.mixin.android.api.response.web3.SwapToken import one.mixin.android.api.response.web3.Tx import one.mixin.android.api.response.web3.isFinalTxState import one.mixin.android.api.response.web3.isTxFailed @@ -39,9 +38,9 @@ import one.mixin.android.api.response.web3.isTxSuccess import one.mixin.android.compose.theme.MixinAppTheme @Composable -fun SwapStatePage( +fun TransactionStatePage( tx: Tx, - toToken: SwapToken, + symbol: String?, viewTx: () -> Unit, close: () -> Unit, ) { @@ -56,7 +55,7 @@ fun SwapStatePage( verticalScroll(rememberScrollState()) }, ) { - Content(tx, toToken, viewTx, close) + Content(tx, symbol, viewTx, close) } } } @@ -64,7 +63,7 @@ fun SwapStatePage( @Composable private fun Content( tx: Tx, - toToken: SwapToken, + symbol: String?, viewTx: () -> Unit, close: () -> Unit, ) { @@ -73,7 +72,7 @@ private fun Content( ) { Spacer(modifier = Modifier.height(64.dp)) Spacer(modifier = Modifier.height(100.dp)) - StateInfo(tx = tx, toToken) + StateInfo(tx = tx, symbol) Spacer(modifier = Modifier.height(20.dp)) Box( modifier = @@ -125,7 +124,7 @@ private fun Content( @Composable private fun StateInfo( tx: Tx, - toToken: SwapToken, + symbol: String?, ) { Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -172,16 +171,20 @@ private fun StateInfo( color = MixinAppTheme.colors.textPrimary, ), ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - modifier = Modifier.alpha(if (tx.state.isTxFailed()) 0f else 1f), - text = stringResource(id = R.string.swap_desc, toToken.symbol), - textAlign = TextAlign.Center, - style = - TextStyle( - fontSize = 14.sp, - color = MixinAppTheme.colors.textSubtitle, - ), - ) + if (symbol != null) { + Spacer(modifier = Modifier.height(16.dp)) + Text( + modifier = Modifier.alpha(if (tx.state.isTxFailed()) 0f else 1f), + text = stringResource(id = R.string.swap_desc, symbol), + textAlign = TextAlign.Center, + style = + TextStyle( + fontSize = 14.sp, + color = MixinAppTheme.colors.textSubtitle, + ), + ) + } else { + Spacer(modifier = Modifier.height(24.dp)) + } } } diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/Web3ViewModel.kt b/app/src/main/java/one/mixin/android/ui/home/web3/Web3ViewModel.kt index 4e80258da5..5295c60545 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/Web3ViewModel.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/Web3ViewModel.kt @@ -7,11 +7,14 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import one.mixin.android.Constants.DEFAULT_GAS_LIMIT_FOR_NONFUNGIBLE_TOKENS import one.mixin.android.MixinApplication +import one.mixin.android.api.handleMixinResponse +import one.mixin.android.api.request.web3.PriorityFeeRequest import one.mixin.android.api.response.PaymentStatus import one.mixin.android.api.response.Web3Token import one.mixin.android.api.response.getChainFromName import one.mixin.android.api.response.getChainIdFromName import one.mixin.android.api.response.isSolToken +import one.mixin.android.api.response.web3.PriorityFeeResponse import one.mixin.android.api.service.Web3Service import one.mixin.android.extension.defaultSharedPreferences import one.mixin.android.repository.TokenRepository @@ -21,6 +24,7 @@ import one.mixin.android.tip.wc.WalletConnectV2 import one.mixin.android.tip.wc.internal.Chain import one.mixin.android.tip.wc.internal.toTransaction import one.mixin.android.ui.common.biometric.NftBiometricItem +import one.mixin.android.ui.oldwallet.AssetRepository import one.mixin.android.util.GsonHelper import one.mixin.android.util.mlkit.firstUrl import one.mixin.android.vo.ConnectionUI @@ -33,6 +37,7 @@ import org.sol4k.Connection import org.sol4k.PublicKey import org.sol4k.RpcUrl import org.sol4k.VersionedTransaction +import org.sol4k.api.Commitment import org.sol4k.lamportToSol import org.web3j.exceptions.MessageDecodingException import org.web3j.protocol.core.methods.request.Transaction @@ -49,6 +54,7 @@ class Web3ViewModel @Inject internal constructor( private val userRepository: UserRepository, + private val assetRepository: AssetRepository, private val tokenRepository: TokenRepository, private val web3Service: Web3Service, ) : ViewModel() { @@ -263,4 +269,23 @@ class Web3ViewModel return Convert.fromWei(gasPrice.run { BigDecimal(this) }.multiply(gasLimit.run { BigDecimal(this) }), Convert.Unit.ETHER) } } + + suspend fun getWeb3Tx(txhash: String) = assetRepository.getWeb3Tx(txhash) + + suspend fun isBlockhashValid(blockhash: String): Boolean = + withContext(Dispatchers.IO) { + val conn = Connection(RpcUrl.MAINNNET) + return@withContext conn.isBlockhashValid(blockhash, Commitment.PROCESSED) + } + + suspend fun getBotPublicKey(botId: String) = userRepository.getBotPublicKey(botId) + + suspend fun getPriorityFee(tx: String): PriorityFeeResponse? { + return handleMixinResponse( + invokeNetwork = { web3Service.getPriorityFee(PriorityFeeRequest(tx)) }, + successBlock = { + it.data + }, + ) + } } diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/components/Component.kt b/app/src/main/java/one/mixin/android/ui/home/web3/components/Component.kt index 9058e4b10e..15b9150707 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/components/Component.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/components/Component.kt @@ -93,9 +93,9 @@ fun TransactionPreview( CoilImage( model = asset?.iconUrl, modifier = - Modifier - .size(32.dp) - .clip(CircleShape), + Modifier + .size(32.dp) + .clip(CircleShape), placeholder = R.drawable.ic_avatar_place_holder, ) } @@ -153,9 +153,9 @@ fun TokenTransactionPreview( CoilImage( model = token.iconUrl, modifier = - Modifier - .size(32.dp) - .clip(CircleShape), + Modifier + .size(32.dp) + .clip(CircleShape), placeholder = R.drawable.ic_avatar_place_holder, ) } diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/swap/InputTextField.kt b/app/src/main/java/one/mixin/android/ui/home/web3/swap/InputTextField.kt index 5728f64429..b9afeed97a 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/swap/InputTextField.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/swap/InputTextField.kt @@ -109,7 +109,7 @@ fun InputContent( mutableStateOf(v.multiply(BigDecimal(token.price ?: "0")).setScale(2, RoundingMode.CEILING)) } else { mutableStateOf(BigDecimal.ZERO) - } + }, ) Row( modifier = Modifier.fillMaxWidth(), diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/swap/SwapFragment.kt b/app/src/main/java/one/mixin/android/ui/home/web3/swap/SwapFragment.kt index 9a827146d3..ee205d7e3f 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/swap/SwapFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/swap/SwapFragment.kt @@ -26,30 +26,25 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import one.mixin.android.Constants.RouteConfig.ROUTE_BOT_USER_ID import one.mixin.android.api.handleMixinResponse import one.mixin.android.api.request.web3.SwapRequest import one.mixin.android.api.response.Web3Token -import one.mixin.android.api.response.wrappedSolTokenAssetKey import one.mixin.android.api.response.solanaNativeTokenAssetKey import one.mixin.android.api.response.toSwapToken import one.mixin.android.api.response.web3.QuoteResponse import one.mixin.android.api.response.web3.SwapToken -import one.mixin.android.api.response.web3.Tx -import one.mixin.android.api.response.web3.TxState -import one.mixin.android.api.response.web3.isFinalTxState +import one.mixin.android.api.response.wrappedSolTokenAssetKey import one.mixin.android.compose.theme.MixinAppTheme import one.mixin.android.extension.getParcelableArrayListCompat import one.mixin.android.extension.isNightMode +import one.mixin.android.extension.navTo import one.mixin.android.extension.safeNavigateUp import one.mixin.android.extension.withArgs import one.mixin.android.ui.common.BaseFragment +import one.mixin.android.ui.home.web3.TransactionStateFragment import one.mixin.android.ui.home.web3.showBrowserBottomSheetDialogFragment -import one.mixin.android.ui.web.WebActivity -import one.mixin.android.util.tickerFlow import one.mixin.android.web3.js.JsSignMessage import one.mixin.android.web3.js.JsSigner import one.mixin.android.web3.receive.Web3TokenListBottomSheetDialogFragment @@ -78,7 +73,6 @@ class SwapFragment : BaseFragment() { enum class SwapDestination { Swap, - SwapState, } private var swapTokens: List by mutableStateOf(emptyList()) @@ -94,8 +88,6 @@ class SwapFragment : BaseFragment() { requireArguments().getParcelableArrayListCompat("TOKENS", Web3Token::class.java)!! } - private var tx: Tx? by mutableStateOf(null) - private var quoteResp: QuoteResponse? = null private var txhash: String? = null @@ -231,14 +223,17 @@ class SwapFragment : BaseFragment() { requireActivity(), signMessage, amount = qr.inAmount, - onTxhash = { hash -> + onTxhash = { hash, serializedTx -> lifecycleScope.launch { - Timber.d("hash $hash") txhash = hash - navController.navigate("${SwapDestination.SwapState.name}/$hash") { - popUpTo(SwapDestination.Swap.name) - } - refreshTx(hash) + val txStateFragment = + TransactionStateFragment.newInstance(serializedTx, toToken!!.symbol).apply { + setCloseAction { + navigateUp(navController) + parentFragmentManager.popBackStackImmediate() + } + } + navTo(txStateFragment, TransactionStateFragment.TAG) } }, ) @@ -247,20 +242,6 @@ class SwapFragment : BaseFragment() { navigateUp(navController) } } - composable("${SwapDestination.SwapState.name}/{txhash}") { navBackStackEntry -> - navBackStackEntry.arguments?.getString("txhash")?.let { txhash -> - SwapStatePage( - tx = tx ?: Tx(TxState.NotFound.name), - toToken = toToken!!, - viewTx = { - WebActivity.show(context, "https://solscan.io/tx/$txhash", null) - }, - ) { - navigateUp(navController) - parentFragmentManager.popBackStackImmediate() - } - } - } } } } @@ -396,25 +377,6 @@ class SwapFragment : BaseFragment() { } } - private var refreshTxJob: Job? = null - - private fun refreshTx(txhash: String) { - refreshTxJob?.cancel() - refreshTxJob = - tickerFlow(2.seconds) - .onEach { - handleMixinResponse( - invokeNetwork = { swapViewModel.getWeb3Tx(txhash) }, - successBlock = { - tx = it.data - }, - ) - if (tx?.state?.isFinalTxState() == true) { - refreshTxJob?.cancel() - } - }.launchIn(lifecycleScope) - } - private suspend fun quote(input: String) { val inputMint = fromToken?.address ?: return val outputMint = toToken?.address ?: return diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/swap/SwapPage.kt b/app/src/main/java/one/mixin/android/ui/home/web3/swap/SwapPage.kt index d2104b4dcb..777f36e852 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/swap/SwapPage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/swap/SwapPage.kt @@ -4,12 +4,10 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues @@ -46,6 +44,8 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle @@ -98,25 +98,25 @@ fun SwapPage( } else { Column( modifier = - Modifier - .verticalScroll(rememberScrollState()) + Modifier + .verticalScroll(rememberScrollState()), ) { SwapLayout( centerCompose = { Box( modifier = - Modifier - .width(40.dp) - .height(40.dp) - .clip(CircleShape) - .border(width = 6.dp, color = MixinAppTheme.colors.background, shape = CircleShape) - .background(MixinAppTheme.colors.backgroundGray) - .clickable { - isReverse = !isReverse - switch.invoke() - context.clickVibrate() - } - .rotate(rotation), + Modifier + .width(40.dp) + .height(40.dp) + .clip(CircleShape) + .border(width = 6.dp, color = MixinAppTheme.colors.background, shape = CircleShape) + .background(MixinAppTheme.colors.backgroundGray) + .clickable { + isReverse = !isReverse + switch.invoke() + context.clickVibrate() + } + .rotate(rotation), contentAlignment = Alignment.Center, ) { Icon( @@ -131,12 +131,11 @@ fun SwapPage( inputText.value = it onInputChanged.invoke(it) } - }, bottomCompose = { InputArea(token = toToken, text = outputText, title = stringResource(id = R.string.To), readOnly = true, { selectCallback(1) }) }, - margin = 6.dp + margin = 6.dp, ) Column(modifier = Modifier.padding(horizontal = 20.dp)) { Column( @@ -182,11 +181,15 @@ fun SwapPage( } Spacer(modifier = Modifier.height(20.dp)) val checkBalance = checkBalance(inputText.value, fromToken.balance) + val keyboardController = androidx.compose.ui.platform.LocalSoftwareKeyboardController.current + val focusManager = LocalFocusManager.current if (inputText.value.isNotEmpty()) { Button( modifier = Modifier.fillMaxWidth(), enabled = !isLoading && checkBalance == true, onClick = { + keyboardController?.hide() + focusManager.clearFocus() onSwap.invoke() }, colors = @@ -459,38 +462,47 @@ fun SwapLayout( headerCompose: @Composable () -> Unit, bottomCompose: @Composable () -> Unit, centerCompose: @Composable () -> Unit, - margin: Dp + margin: Dp, ) { ConstraintLayout( modifier = - Modifier - .wrapContentHeight() - .wrapContentWidth() - .padding(horizontal = 20.dp, vertical = margin) + Modifier + .wrapContentHeight() + .wrapContentWidth() + .padding(horizontal = 20.dp, vertical = margin), ) { val (headerRef, bottomRef, centerRef) = createRefs() - Box(modifier = Modifier.constrainAs(headerRef) { - top.linkTo(parent.top) - bottom.linkTo(bottomRef.top, margin) - start.linkTo(parent.start) - end.linkTo(parent.end) - }) { + Box( + modifier = + Modifier.constrainAs(headerRef) { + top.linkTo(parent.top) + bottom.linkTo(bottomRef.top, margin) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + ) { headerCompose() } - Box(modifier = Modifier.constrainAs(bottomRef) { - top.linkTo(parent.bottom) - bottom.linkTo(headerRef.bottom, margin) - start.linkTo(parent.start) - end.linkTo(parent.end) - }) { + Box( + modifier = + Modifier.constrainAs(bottomRef) { + top.linkTo(parent.bottom) + bottom.linkTo(headerRef.bottom, margin) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + ) { bottomCompose() } - Box(modifier = Modifier.constrainAs(centerRef) { - top.linkTo(headerRef.bottom) - bottom.linkTo(bottomRef.top) - start.linkTo(parent.start) - end.linkTo(parent.end) - }) { + Box( + modifier = + Modifier.constrainAs(centerRef) { + top.linkTo(headerRef.bottom) + bottom.linkTo(bottomRef.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + ) { centerCompose() } } @@ -501,23 +513,32 @@ fun SwapLayout( fun SwapLayoutPreview() { SwapLayout( headerCompose = { - Box(modifier = Modifier - .height(100.dp) - .fillMaxWidth() - .background(color = Color.Red)) + Box( + modifier = + Modifier + .height(100.dp) + .fillMaxWidth() + .background(color = Color.Red), + ) }, bottomCompose = { - Box(modifier = Modifier - .height(140.dp) - .fillMaxWidth() - .background(color = Color.Green)) + Box( + modifier = + Modifier + .height(140.dp) + .fillMaxWidth() + .background(color = Color.Green), + ) }, centerCompose = { - Box(modifier = Modifier - .size(40.dp) - .background(color = Color.Blue)) + Box( + modifier = + Modifier + .size(40.dp) + .background(color = Color.Blue), + ) }, - margin = 20.dp + margin = 20.dp, ) } diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/swap/SwapViewModel.kt b/app/src/main/java/one/mixin/android/ui/home/web3/swap/SwapViewModel.kt index 42f0bab233..5c2e057e56 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/swap/SwapViewModel.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/swap/SwapViewModel.kt @@ -38,8 +38,6 @@ class SwapViewModel swapRequest: SwapRequest, ): MixinResponse = assetRepository.web3Swap(swapRequest) - suspend fun getWeb3Tx(txhash: String) = assetRepository.getWeb3Tx(txhash) - suspend fun getSwapToken(address: String) = assetRepository.getSwapToken(address) suspend fun searchTokens(query: String) = assetRepository.searchTokens(query) diff --git a/app/src/main/java/one/mixin/android/ui/media/pager/PhotoHolder.kt b/app/src/main/java/one/mixin/android/ui/media/pager/PhotoHolder.kt index 8cc2261ee0..81b859f751 100644 --- a/app/src/main/java/one/mixin/android/ui/media/pager/PhotoHolder.kt +++ b/app/src/main/java/one/mixin/android/ui/media/pager/PhotoHolder.kt @@ -34,7 +34,7 @@ class PhotoHolder(itemView: View) : MediaPagerHolder(itemView) { ViewCompat.setTransitionName(imageView, "transition") mediaPagerAdapterListener.onReadyPostTransition(imageView) } - } + }, ) } else { imageView.loadImage( diff --git a/app/src/main/java/one/mixin/android/ui/media/pager/VideoHolder.kt b/app/src/main/java/one/mixin/android/ui/media/pager/VideoHolder.kt index fe3bb05233..8b14732c4a 100644 --- a/app/src/main/java/one/mixin/android/ui/media/pager/VideoHolder.kt +++ b/app/src/main/java/one/mixin/android/ui/media/pager/VideoHolder.kt @@ -102,7 +102,7 @@ class VideoHolder( itemView.tag = "$PREFIX${messageItem.messageId}" if (messageItem.isLive()) { circleProgress.isVisible = false - binding.previewIv.loadImage(messageItem.thumbUrl, null, base64Holder = messageItem.thumbImage) + binding.previewIv.loadImage(messageItem.thumbUrl, null, base64Holder = messageItem.thumbImage) } else { if (messageItem.absolutePath() != null) { binding.previewIv.loadImage(messageItem.absolutePath(), null, null) diff --git a/app/src/main/java/one/mixin/android/ui/media/pager/transcript/PhotoHolder.kt b/app/src/main/java/one/mixin/android/ui/media/pager/transcript/PhotoHolder.kt index e963d033a0..242404aa6b 100644 --- a/app/src/main/java/one/mixin/android/ui/media/pager/transcript/PhotoHolder.kt +++ b/app/src/main/java/one/mixin/android/ui/media/pager/transcript/PhotoHolder.kt @@ -33,7 +33,6 @@ class PhotoHolder(itemView: View) : MediaPagerHolder(itemView) { ViewCompat.setTransitionName(imageView, "transition") mediaPagerAdapterListener.onReadyPostTransition(imageView) } - }, base64Holder = messageItem.thumbImage, ) diff --git a/app/src/main/java/one/mixin/android/ui/qr/EditFragment.kt b/app/src/main/java/one/mixin/android/ui/qr/EditFragment.kt index 34d9b9d88f..b604be5168 100644 --- a/app/src/main/java/one/mixin/android/ui/qr/EditFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/qr/EditFragment.kt @@ -326,6 +326,4 @@ class EditFragment : VisionFragment() { binding.previewVideoTexture.setTransform(matrix) } } - - } diff --git a/app/src/main/java/one/mixin/android/ui/sticker/StickerAddFragment.kt b/app/src/main/java/one/mixin/android/ui/sticker/StickerAddFragment.kt index 6b0507c752..b425beae17 100644 --- a/app/src/main/java/one/mixin/android/ui/sticker/StickerAddFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/sticker/StickerAddFragment.kt @@ -21,7 +21,6 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import one.mixin.android.MixinApplication import one.mixin.android.R import one.mixin.android.api.MixinResponse import one.mixin.android.api.request.StickerAddRequest @@ -50,7 +49,6 @@ import one.mixin.android.widget.gallery.MimeType import java.io.File import java.lang.Integer.max import java.lang.Integer.min -import java.util.concurrent.TimeUnit @Suppress("BlockingMethodInNonBlockingContext") @AndroidEntryPoint @@ -130,7 +128,7 @@ class StickerAddFragment : BaseFragment() { try { val loader = requireContext().imageLoader val request = ImageRequest.Builder(requireContext()).data(url).build() - val result = loader.execute(request).drawable as BitmapDrawable? ?:return@withContext 0 + val result = loader.execute(request).drawable as BitmapDrawable? ?: return@withContext 0 val byteArray = result.bitmap.toBytes() val bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size, BitmapFactory.Options()) if (bitmap.width < dp100) { @@ -197,10 +195,11 @@ class StickerAddFragment : BaseFragment() { stickerViewModel.addStickerLocal(r.data as Sticker, personalAlbum.albumId) } val loader = requireContext().imageLoader - val request = ImageRequest.Builder(requireContext()).data(r.data?.assetUrl).size(r.data!!.assetWidth, r.data!!.assetHeight) - .listener { _, _ -> - handleBack(R.string.Add_success) - }.build() + val request = + ImageRequest.Builder(requireContext()).data(r.data?.assetUrl).size(r.data!!.assetWidth, r.data!!.assetHeight) + .listener { _, _ -> + handleBack(R.string.Add_success) + }.build() loader.execute(request) } @@ -226,10 +225,10 @@ class StickerAddFragment : BaseFragment() { val byteArray = if (mimeType == MimeType.GIF.toString()) { - val request = ImageRequest.Builder( requireContext()).data(url).build() - val result = loader.execute(request).drawable?:return@withContext null - val w =result.intrinsicWidth - val h =result.intrinsicHeight + val request = ImageRequest.Builder(requireContext()).data(url).build() + val result = loader.execute(request).drawable ?: return@withContext null + val w = result.intrinsicWidth + val h = result.intrinsicHeight if (min(w, h) >= MIN_SIZE && max(w, h) <= MAX_SIZE) { loader.diskCache?.openSnapshot(url)?.data?.toFile()?.toByteArray() ?: return@withContext null } else { @@ -237,14 +236,14 @@ class StickerAddFragment : BaseFragment() { return@withContext null } } else { - val request = ImageRequest.Builder( requireContext()).data(url).build() + val request = ImageRequest.Builder(requireContext()).data(url).build() loader.execute(request) loader.diskCache?.openSnapshot(url)?.data?.toFile()?.toByteArray() ?: return@withContext null } StickerAddRequest(Base64.encodeToString(byteArray, Base64.NO_WRAP)) } else { - val request = ImageRequest.Builder( requireContext()).data(url).build() + val request = ImageRequest.Builder(requireContext()).data(url).build() var bitmap = (loader.execute(request).drawable as BitmapDrawable?)?.bitmap ?: return@withContext null val ratio = bitmap.width / bitmap.height.toFloat() diff --git a/app/src/main/java/one/mixin/android/ui/tip/wc/connections/ConnectionDetailsPage.kt b/app/src/main/java/one/mixin/android/ui/tip/wc/connections/ConnectionDetailsPage.kt index 325e436583..dd9ac63e30 100644 --- a/app/src/main/java/one/mixin/android/ui/tip/wc/connections/ConnectionDetailsPage.kt +++ b/app/src/main/java/one/mixin/android/ui/tip/wc/connections/ConnectionDetailsPage.kt @@ -66,9 +66,9 @@ private fun Content( CoilImage( model = connectionUI.icon, modifier = - Modifier - .size(90.dp) - .clip(CircleShape), + Modifier + .size(90.dp) + .clip(CircleShape), placeholder = R.drawable.ic_avatar_place_holder, ) Box(modifier = Modifier.height(10.dp)) diff --git a/app/src/main/java/one/mixin/android/ui/tip/wc/sessionproposal/SessionProposalPage.kt b/app/src/main/java/one/mixin/android/ui/tip/wc/sessionproposal/SessionProposalPage.kt index 8102f11b8a..4efe65cdd1 100644 --- a/app/src/main/java/one/mixin/android/ui/tip/wc/sessionproposal/SessionProposalPage.kt +++ b/app/src/main/java/one/mixin/android/ui/tip/wc/sessionproposal/SessionProposalPage.kt @@ -23,7 +23,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle diff --git a/app/src/main/java/one/mixin/android/ui/tip/wc/sessionrequest/SessionRequestPage.kt b/app/src/main/java/one/mixin/android/ui/tip/wc/sessionrequest/SessionRequestPage.kt index b62bfb2f68..9f9546bede 100644 --- a/app/src/main/java/one/mixin/android/ui/tip/wc/sessionrequest/SessionRequestPage.kt +++ b/app/src/main/java/one/mixin/android/ui/tip/wc/sessionrequest/SessionRequestPage.kt @@ -27,7 +27,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle @@ -148,9 +147,9 @@ fun SessionRequestPage( CoilImage( sessionRequestUI.peerUI.icon, modifier = - Modifier - .size(70.dp) - .clip(CircleShape), + Modifier + .size(70.dp) + .clip(CircleShape), placeholder = R.drawable.ic_avatar_place_holder, ) } diff --git a/app/src/main/java/one/mixin/android/ui/web/WebFragment.kt b/app/src/main/java/one/mixin/android/ui/web/WebFragment.kt index 16ea59c699..c60c46d8aa 100644 --- a/app/src/main/java/one/mixin/android/ui/web/WebFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/web/WebFragment.kt @@ -167,8 +167,6 @@ import one.mixin.android.widget.WebControlView import org.json.JSONObject import timber.log.Timber import java.io.ByteArrayInputStream -import java.io.FileInputStream -import java.util.concurrent.TimeUnit import javax.inject.Inject @AndroidEntryPoint @@ -1529,9 +1527,9 @@ class WebFragment : BaseFragment() { if (url == null) return@launch val loader = requireContext().imageLoader val request = ImageRequest.Builder(requireContext()).data(url).build() - val result = loader.execute(request) + val result = loader.execute(request) if (result !is SuccessResult) return@launch - val f = loader.diskCache?.openSnapshot(url)?.data?.toFile()?:return@launch + val f = loader.diskCache?.openSnapshot(url)?.data?.toFile() ?: return@launch f.copy(outFile) } MediaScannerConnection.scanFile( diff --git a/app/src/main/java/one/mixin/android/web3/InputFragment.kt b/app/src/main/java/one/mixin/android/web3/InputFragment.kt index 1eea346740..4c3e2df019 100644 --- a/app/src/main/java/one/mixin/android/web3/InputFragment.kt +++ b/app/src/main/java/one/mixin/android/web3/InputFragment.kt @@ -22,6 +22,7 @@ import one.mixin.android.extension.formatPublicKey import one.mixin.android.extension.getParcelableCompat import one.mixin.android.extension.indeterminateProgressDialog import one.mixin.android.extension.loadImage +import one.mixin.android.extension.navTo import one.mixin.android.extension.nowInUtc import one.mixin.android.extension.numberFormat2 import one.mixin.android.extension.numberFormat8 @@ -33,6 +34,7 @@ import one.mixin.android.extension.withArgs import one.mixin.android.ui.common.BaseFragment import one.mixin.android.ui.common.biometric.WithdrawBiometricItem import one.mixin.android.ui.conversation.TransferFragment +import one.mixin.android.ui.home.web3.TransactionStateFragment import one.mixin.android.ui.home.web3.Web3ViewModel import one.mixin.android.ui.home.web3.showBrowserBottomSheetDialogFragment import one.mixin.android.ui.wallet.NetworkFee @@ -281,7 +283,10 @@ class InputFragment : BaseFragment(R.layout.fragment_input) { alertDialog.dismiss() }, ) { - val transaction = token.buildTransaction(fromAddress, toAddress, amount) + val transaction = + token.buildTransaction(fromAddress, toAddress, amount) { tx -> + web3ViewModel.getPriorityFee(tx) + } showBrowserBottomSheetDialogFragment( requireActivity(), transaction, @@ -289,6 +294,10 @@ class InputFragment : BaseFragment(R.layout.fragment_input) { amount = amount, toAddress = toAddress, chainToken = chainToken, + onTxhash = { _, serializedTx -> + val txStateFragment = TransactionStateFragment.newInstance(serializedTx, null) + navTo(txStateFragment, TransactionStateFragment.TAG) + }, ) } } @@ -484,7 +493,7 @@ class InputFragment : BaseFragment(R.layout.fragment_input) { } } - private suspend fun refreshGas(t:Web3Token) { + private suspend fun refreshGas(t: Web3Token) { if (t.fungibleId == chainToken?.fungibleId) { val fromAddress = fromAddress ?: return val transaction = diff --git a/app/src/main/java/one/mixin/android/web3/details/Web3TransactionAdapter.kt b/app/src/main/java/one/mixin/android/web3/details/Web3TransactionAdapter.kt index 28a1a7d0df..73127242bd 100644 --- a/app/src/main/java/one/mixin/android/web3/details/Web3TransactionAdapter.kt +++ b/app/src/main/java/one/mixin/android/web3/details/Web3TransactionAdapter.kt @@ -67,7 +67,10 @@ class Web3TransactionAdapter(val token: Web3Token) : RecyclerView.Adapter { avatar.bg.loadImage( - transaction.fee.iconUrl, holder = R.drawable.ic_avatar_place_holder, transformation = if (transaction.status == Web3TransactionStatus.Failed.value) { - GrayscaleTransformation() - } else { - null - } + transaction.fee.iconUrl, + holder = R.drawable.ic_avatar_place_holder, + transformation = + if (transaction.status == Web3TransactionStatus.Failed.value) { + GrayscaleTransformation() + } else { + null + }, ) avatar.badge.loadImage(transaction.approvals.firstOrNull()?.iconUrl, holder = R.drawable.ic_no_dapp) avatar.badge.isVisible = true @@ -193,11 +199,14 @@ class Web3TransactionHolder(val binding: ItemWeb3TransactionBinding) : RecyclerV Web3TransactionType.Mint.value -> { avatar.bg.loadImage( - transaction.fee.iconUrl, holder = R.drawable.ic_avatar_place_holder, transformation = if (transaction.status == Web3TransactionStatus.Failed.value) { - GrayscaleTransformation() - } else { - null - } + transaction.fee.iconUrl, + holder = R.drawable.ic_avatar_place_holder, + transformation = + if (transaction.status == Web3TransactionStatus.Failed.value) { + GrayscaleTransformation() + } else { + null + }, ) avatar.badge.loadImage(transaction.approvals.firstOrNull()?.iconUrl, holder = R.drawable.ic_no_dapp) avatar.badge.isVisible = true @@ -215,11 +224,14 @@ class Web3TransactionHolder(val binding: ItemWeb3TransactionBinding) : RecyclerV if (transaction.transfers.isNotEmpty()) { transaction.transfers.find { it.direction == Web3TransactionDirection.In.value }?.let { inTransfer -> avatar.bg.loadImage( - inTransfer.iconUrl, holder = R.drawable.ic_avatar_place_holder, transformation = if (transaction.status == Web3TransactionStatus.Failed.value) { - GrayscaleTransformation() - } else { - null - } + inTransfer.iconUrl, + holder = R.drawable.ic_avatar_place_holder, + transformation = + if (transaction.status == Web3TransactionStatus.Failed.value) { + GrayscaleTransformation() + } else { + null + }, ) inTv.textColorResource = R.color.wallet_green inTv.text = "+${inTransfer.amount.numberFormat8()}" @@ -241,11 +253,14 @@ class Web3TransactionHolder(val binding: ItemWeb3TransactionBinding) : RecyclerV if (transaction.transfers.isNotEmpty()) { transaction.transfers.find { it.direction == Web3TransactionDirection.In.value }?.let { inTransfer -> avatar.bg.loadImage( - inTransfer.iconUrl, holder = R.drawable.ic_avatar_place_holder, transformation = if (transaction.status == Web3TransactionStatus.Failed.value) { - GrayscaleTransformation() - } else { - null - } + inTransfer.iconUrl, + holder = R.drawable.ic_avatar_place_holder, + transformation = + if (transaction.status == Web3TransactionStatus.Failed.value) { + GrayscaleTransformation() + } else { + null + }, ) inTv.textColorResource = R.color.wallet_green inTv.text = "+${inTransfer.amount.numberFormat8()}" diff --git a/app/src/main/java/one/mixin/android/web3/js/JsSigner.kt b/app/src/main/java/one/mixin/android/web3/js/JsSigner.kt index be6b59f1f4..56c950f81f 100644 --- a/app/src/main/java/one/mixin/android/web3/js/JsSigner.kt +++ b/app/src/main/java/one/mixin/android/web3/js/JsSigner.kt @@ -25,6 +25,7 @@ import org.sol4k.RpcUrl import org.sol4k.SignInAccount import org.sol4k.SignInInput import org.sol4k.SignInOutput +import org.sol4k.api.Commitment import org.web3j.crypto.Credentials import org.web3j.crypto.ECKeyPair import org.web3j.crypto.RawTransaction @@ -283,7 +284,7 @@ object JsSigner { // use latest blockhash should not break other signatures if (tx.signatures.size <= 1) { val conn = Connection(RpcUrl.MAINNNET) - val blockhash = conn.getLatestBlockhash() + val blockhash = conn.getLatestBlockhash(Commitment.CONFIRMED) tx.message.recentBlockhash = blockhash } tx.sign(holder) diff --git a/app/src/main/java/one/mixin/android/widget/GrayscaleTransformation.kt b/app/src/main/java/one/mixin/android/widget/GrayscaleTransformation.kt index 76b65396e6..ef2d842ae6 100644 --- a/app/src/main/java/one/mixin/android/widget/GrayscaleTransformation.kt +++ b/app/src/main/java/one/mixin/android/widget/GrayscaleTransformation.kt @@ -8,7 +8,10 @@ class GrayscaleTransformation : Transformation { override val cacheKey: String get() = "grayscale_transformation" - override suspend fun transform(input: Bitmap, size: Size): Bitmap { + override suspend fun transform( + input: Bitmap, + size: Size, + ): Bitmap { val width = input.width val height = input.height val grayBitmap = Bitmap.createBitmap(width, height, input.config) diff --git a/app/src/main/java/org/sol4k/ComputeBudget.kt b/app/src/main/java/org/sol4k/ComputeBudget.kt index 8739781397..c66408aaf7 100644 --- a/app/src/main/java/org/sol4k/ComputeBudget.kt +++ b/app/src/main/java/org/sol4k/ComputeBudget.kt @@ -7,8 +7,8 @@ import java.math.BigDecimal private const val InstructionRequestUnits = 0 private const val InstructionRequestHeapFrame = 1 -private const val InstructionSetComputeUnitLimit = 2 -private const val InstructionSetComputeUnitPrice = 3 +const val InstructionSetComputeUnitLimit = 2 +const val InstructionSetComputeUnitPrice = 3 internal fun computeBudget(data: List): BigDecimal { if (data.size != 2) return BigDecimal.ZERO diff --git a/app/src/main/java/org/sol4k/Connection.kt b/app/src/main/java/org/sol4k/Connection.kt index 7460d3b456..98306e4ba7 100644 --- a/app/src/main/java/org/sol4k/Connection.kt +++ b/app/src/main/java/org/sol4k/Connection.kt @@ -187,8 +187,8 @@ class Connection @JvmOverloads constructor( ) } - fun simulateTransaction(transaction: VersionedTransaction): TransactionSimulation { - val encodedTransaction = Base64.getEncoder().encodeToString(transaction.serialize()) + fun simulateTransaction(transaction: ByteArray): TransactionSimulation { + val encodedTransaction = Base64.getEncoder().encodeToString(transaction) val result: SimulateTransactionResponse = rpcCall( "simulateTransaction", listOf( @@ -196,14 +196,14 @@ class Connection @JvmOverloads constructor( Json.encodeToJsonElement(mapOf("encoding" to "base64")), ) ) - val (err, logs) = result.value + val (err, logs, unitsConsumed) = result.value if (err != null) { when (err) { is JsonPrimitive -> return TransactionSimulationError(err.content) else -> throw IllegalArgumentException("Failed to parse the error") } } else if (logs != null) { - return TransactionSimulationSuccess(logs) + return TransactionSimulationSuccess(logs, unitsConsumed ?: 0) } throw IllegalArgumentException("Unable to parse simulation response") } @@ -228,7 +228,7 @@ class Connection @JvmOverloads constructor( try { val (result) = jsonParser.decodeFromString>(responseBody) return result - } catch (_: SerializationException) { + } catch (e: SerializationException) { val (error) = jsonParser.decodeFromString(responseBody) throw RpcException(error.code, error.message, responseBody) } diff --git a/app/src/main/java/org/sol4k/Transaction.kt b/app/src/main/java/org/sol4k/Transaction.kt index ce0604d0e9..afa8464939 100644 --- a/app/src/main/java/org/sol4k/Transaction.kt +++ b/app/src/main/java/org/sol4k/Transaction.kt @@ -4,7 +4,7 @@ import org.sol4k.instruction.Instruction import java.nio.ByteBuffer class Transaction( - private val recentBlockhash: String, + var recentBlockhash: String, private val instructions: List, private val feePayer: PublicKey, ) { @@ -22,6 +22,10 @@ class Transaction( signatures.add(Base58.encode(signature)) } + fun addPlaceholderSignature() { + signatures.add(Base58.encode(ByteArray(SIGNATURE_LENGTH))) + } + private fun transactionMessage(): ByteArray { val accountKeys = buildAccountKeys() val transactionAccountPublicKeys = accountKeys.map { it.publicKey } diff --git a/app/src/main/java/org/sol4k/api/TransactionSimulation.kt b/app/src/main/java/org/sol4k/api/TransactionSimulation.kt index 33e7a13b56..f3e55a409d 100644 --- a/app/src/main/java/org/sol4k/api/TransactionSimulation.kt +++ b/app/src/main/java/org/sol4k/api/TransactionSimulation.kt @@ -4,4 +4,7 @@ sealed class TransactionSimulation class TransactionSimulationError(val error: String) : TransactionSimulation() -class TransactionSimulationSuccess(val logs: List) : TransactionSimulation() +class TransactionSimulationSuccess( + val logs: List, + val unitsConsumed: Long, +) : TransactionSimulation() diff --git a/app/src/main/java/org/sol4k/instruction/SetComputeUnitLimitInstruction.kt b/app/src/main/java/org/sol4k/instruction/SetComputeUnitLimitInstruction.kt new file mode 100644 index 0000000000..9b30829b6c --- /dev/null +++ b/app/src/main/java/org/sol4k/instruction/SetComputeUnitLimitInstruction.kt @@ -0,0 +1,24 @@ +package org.sol4k.instruction + +import okio.Buffer +import org.sol4k.AccountMeta +import org.sol4k.Constants.COMPUTE_BUDGET_PROGRAM_ID +import org.sol4k.InstructionSetComputeUnitLimit +import org.sol4k.PublicKey + +class SetComputeUnitLimitInstruction( + private val units: Int, +) : Instruction { + + override val data: ByteArray + get() { + val buffer = Buffer() + buffer.writeByte(InstructionSetComputeUnitLimit) + .writeIntLe(units) + return buffer.readByteArray() + } + + override val keys: List = emptyList() + + override val programId: PublicKey = COMPUTE_BUDGET_PROGRAM_ID +} \ No newline at end of file diff --git a/app/src/main/java/org/sol4k/instruction/SetComputeUnitPriceInstruction.kt b/app/src/main/java/org/sol4k/instruction/SetComputeUnitPriceInstruction.kt new file mode 100644 index 0000000000..e0687f98f8 --- /dev/null +++ b/app/src/main/java/org/sol4k/instruction/SetComputeUnitPriceInstruction.kt @@ -0,0 +1,24 @@ +package org.sol4k.instruction + +import okio.Buffer +import org.sol4k.AccountMeta +import org.sol4k.Constants.COMPUTE_BUDGET_PROGRAM_ID +import org.sol4k.InstructionSetComputeUnitPrice +import org.sol4k.PublicKey + +class SetComputeUnitPriceInstruction( + private val microLamports: Long, +) : Instruction { + + override val data: ByteArray + get() { + val buffer = Buffer() + buffer.writeByte(InstructionSetComputeUnitPrice) + .writeLongLe(microLamports) + return buffer.readByteArray() + } + + override val keys: List = emptyList() + + override val programId: PublicKey = COMPUTE_BUDGET_PROGRAM_ID +} \ No newline at end of file