diff --git a/app/src/main/java/com/vultisig/wallet/ui/models/keysign/KeysignFlowViewModel.kt b/app/src/main/java/com/vultisig/wallet/ui/models/keysign/KeysignFlowViewModel.kt index 368f4a829..33da1102a 100644 --- a/app/src/main/java/com/vultisig/wallet/ui/models/keysign/KeysignFlowViewModel.kt +++ b/app/src/main/java/com/vultisig/wallet/ui/models/keysign/KeysignFlowViewModel.kt @@ -302,7 +302,6 @@ internal class KeysignFlowViewModel @Inject constructor( priorityFee = specific.priorityFee.toString(), toTokenAssociatedAddress = specific.toAddressPubKey, fromTokenAssociatedAddress = specific.fromAddressPubKey, - tokenAccountExists = specific.tokenAccountExists, ) } else null, polkadotSpecific = if (specific is BlockChainSpecific.Polkadot) { diff --git a/commondata b/commondata index 0cf90ae77..22c6cb4f1 160000 --- a/commondata +++ b/commondata @@ -1 +1 @@ -Subproject commit 0cf90ae776aedf95ec3f30fdbf58e7748b280e18 +Subproject commit 22c6cb4f1ae46205ad98d114d6c9519b9f4c71cd diff --git a/data/src/main/kotlin/com/vultisig/wallet/data/api/SolanaApi.kt b/data/src/main/kotlin/com/vultisig/wallet/data/api/SolanaApi.kt index 053a0882a..f94584ebc 100644 --- a/data/src/main/kotlin/com/vultisig/wallet/data/api/SolanaApi.kt +++ b/data/src/main/kotlin/com/vultisig/wallet/data/api/SolanaApi.kt @@ -4,7 +4,6 @@ import com.vultisig.wallet.data.api.models.BroadcastTransactionRespJson import com.vultisig.wallet.data.api.models.RecentBlockHashResponseJson import com.vultisig.wallet.data.api.models.RpcPayload import com.vultisig.wallet.data.api.models.SPLTokenRequestJson -import com.vultisig.wallet.data.api.models.SolanaAccountExistResponseJson import com.vultisig.wallet.data.api.models.SolanaBalanceJson import com.vultisig.wallet.data.api.models.SolanaFeeObjectJson import com.vultisig.wallet.data.api.models.SolanaFeeObjectRespJson @@ -46,7 +45,7 @@ interface SolanaApi { suspend fun getSPLTokensInfo(tokens: List): List suspend fun getSPLTokensInfo2(tokens: List): List suspend fun getSPLTokenBalance(walletAddress: String, coinAddress: String): String? - suspend fun doesTokenAccountExist(address: String, mint: String): Boolean + suspend fun getTokenAssociatedAccountByOwner(walletAddress: String, mintAddress: String): String? } internal class SolanaApiImp @Inject constructor( @@ -59,6 +58,7 @@ internal class SolanaApiImp @Inject constructor( private val rpcEndpoint2 = "https://solana-rpc.publicnode.com" private val splTokensInfoEndpoint = "https://api.solana.fm/v1/tokens" private val splTokensInfoEndpoint2 = "https://tokens.jup.ag/token" + private val solanaRentExemptionEndpoint = "https://api.devnet.solana.com" override suspend fun getBalance(address: String): BigInteger { val payload = RpcPayload( @@ -85,7 +85,7 @@ internal class SolanaApiImp @Inject constructor( override suspend fun getMinimumBalanceForRentExemption(): BigInteger = try { httpClient.postRpc( - rpcEndpoint, + solanaRentExemptionEndpoint, "getMinimumBalanceForRentExemption", params = buildJsonArray { add(DATA_LENGTH_MINIMUM_BALANCE_FOR_RENT_EXEMPTION) @@ -153,7 +153,7 @@ internal class SolanaApiImp @Inject constructor( return "0" } - override suspend fun broadcastTransaction(tx: String): String { + override suspend fun broadcastTransaction(tx: String): String? { try { val requestBody = RpcPayload( jsonrpc = "2.0", @@ -287,21 +287,35 @@ internal class SolanaApiImp @Inject constructor( } } - override suspend fun doesTokenAccountExist(address: String, mint: String): Boolean = - httpClient.postRpc( - rpcEndpoint, - "getTokenAccountsByOwner", - params = buildJsonArray { - add(address) - addJsonObject { - put("mint", mint) - } - addJsonObject { - put("encoding", "jsonParsed") + override suspend fun getTokenAssociatedAccountByOwner( + walletAddress: String, + mintAddress: String, + ): String? { + try { + val response = httpClient.postRpc( + url = rpcEndpoint2, + method = "getTokenAccountsByOwner", + params = buildJsonArray { + add(walletAddress) + addJsonObject { + put("mint", mintAddress) + } + addJsonObject { + put("encoding", ENCODING_SPL_REQUEST_PARAM) + } } - }, - ).result.values.isNotEmpty() - + ) + if (response.error != null) { + Timber.d("getTokenAssociatedAccountByOwner error: ${response.error}") + return null + } + val value = response.value ?: error("getTokenAssociatedAccountByOwner error") + return value.value[0].pubKey + } catch (e: Exception) { + Timber.e(e) + return null + } + } companion object { diff --git a/data/src/main/kotlin/com/vultisig/wallet/data/api/models/SplTokenJson.kt b/data/src/main/kotlin/com/vultisig/wallet/data/api/models/SplTokenJson.kt index ed6211df4..f25b17b0b 100644 --- a/data/src/main/kotlin/com/vultisig/wallet/data/api/models/SplTokenJson.kt +++ b/data/src/main/kotlin/com/vultisig/wallet/data/api/models/SplTokenJson.kt @@ -2,7 +2,6 @@ package com.vultisig.wallet.data.api.models import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonArray @Serializable data class SplTokenAmountJson( @@ -148,6 +147,8 @@ data class SplAmountAccountJson( data class SplAmountValueJson( @SerialName("account") val account: SplAmountAccountJson, + @SerialName("pubkey") + val pubKey: String, ) @Serializable @@ -168,16 +169,4 @@ data class SplAmountRpcResponseResultJson( data class SPLTokenRequestJson( @SerialName("tokens") val tokens: List, -) - -@Serializable -data class SolanaAccountExistResponseJson( - @SerialName("result") - val result: SolanaAccountExistResultJson, -) - -@Serializable -data class SolanaAccountExistResultJson( - @SerialName("value") - val values: JsonArray ) \ No newline at end of file diff --git a/data/src/main/kotlin/com/vultisig/wallet/data/chains/helpers/SolanaHelper.kt b/data/src/main/kotlin/com/vultisig/wallet/data/chains/helpers/SolanaHelper.kt index 2e4d2a1c4..67a572596 100644 --- a/data/src/main/kotlin/com/vultisig/wallet/data/chains/helpers/SolanaHelper.kt +++ b/data/src/main/kotlin/com/vultisig/wallet/data/chains/helpers/SolanaHelper.kt @@ -60,39 +60,37 @@ class SolanaHelper( .build() .toByteArray() } else { - val fromAddress = AnyAddress(keysignPayload.coin.address, coinType) - val senderAddress = SolanaAddress(fromAddress.description()) - val receiverAddress = SolanaAddress(toAddress.description()) - val generatedSenderAssociatedAddress = senderAddress.defaultTokenAddress( - keysignPayload.coin.contractAddress - ) - val generatedRecipientAssociatedAddress = receiverAddress.defaultTokenAddress( - keysignPayload.coin.contractAddress - ) - val tokenAccountExists = keysignPayload.blockChainSpecific.tokenAccountExists - return if (tokenAccountExists == true) { - val transferTokenMessage = Solana.TokenTransfer.newBuilder() + if (solanaSpecific.fromAddressPubKey != null && solanaSpecific.toAddressPubKey!= null) { + val transfer = Solana.TokenTransfer.newBuilder() .setTokenMintAddress(keysignPayload.coin.contractAddress) - .setRecipientTokenAddress(generatedRecipientAssociatedAddress) - .setSenderTokenAddress(generatedSenderAssociatedAddress) + .setSenderTokenAddress(solanaSpecific.fromAddressPubKey) + .setRecipientTokenAddress(solanaSpecific.toAddressPubKey) .setAmount(keysignPayload.toAmount.toLong()) .setDecimals(keysignPayload.coin.decimal) - input.setTokenTransferTransaction(transferTokenMessage.build()) + return input + .setTokenTransferTransaction(transfer.build()) + .build() + .toByteArray() } else { - val createAndTransferTokenMessage = Solana.CreateAndTransferToken.newBuilder() - .setTokenMintAddress(keysignPayload.coin.contractAddress) - .setRecipientTokenAddress(generatedRecipientAssociatedAddress) - .setSenderTokenAddress(generatedSenderAssociatedAddress) - .setAmount(keysignPayload.toAmount.toLong()) - .setDecimals(keysignPayload.coin.decimal) - .setTokenProgramId(Solana.TokenProgramId.TokenProgram) - .setRecipientMainAddress(toAddress.description()) + val receiverAddress = SolanaAddress(toAddress.description()) + val generatedRecipientAssociatedAddress = receiverAddress.defaultTokenAddress( + keysignPayload.coin.contractAddress + ) + val transferTokenMessage = + Solana.CreateAndTransferToken.newBuilder() + .setRecipientMainAddress(toAddress.description()) + .setTokenMintAddress(keysignPayload.coin.contractAddress) + .setRecipientTokenAddress(generatedRecipientAssociatedAddress) + .setSenderTokenAddress(solanaSpecific.fromAddressPubKey) + .setAmount(keysignPayload.toAmount.toLong()) + .setDecimals(keysignPayload.coin.decimal) - input.setCreateAndTransferTokenTransaction(createAndTransferTokenMessage.build()) + return input + .setCreateAndTransferTokenTransaction(transferTokenMessage.build()) + .build() + .toByteArray() } - .build() - .toByteArray() } } diff --git a/data/src/main/kotlin/com/vultisig/wallet/data/mappers/KeysignPayloadProtoMapper.kt b/data/src/main/kotlin/com/vultisig/wallet/data/mappers/KeysignPayloadProtoMapper.kt index 9dd556c9f..e24660bb6 100644 --- a/data/src/main/kotlin/com/vultisig/wallet/data/mappers/KeysignPayloadProtoMapper.kt +++ b/data/src/main/kotlin/com/vultisig/wallet/data/mappers/KeysignPayloadProtoMapper.kt @@ -138,7 +138,6 @@ internal class KeysignPayloadProtoMapperImpl @Inject constructor() : KeysignPayl priorityFee = BigInteger(it.priorityFee), fromAddressPubKey = it.fromTokenAssociatedAddress, toAddressPubKey = it.toTokenAssociatedAddress, - tokenAccountExists = it.tokenAccountExists, ) } diff --git a/data/src/main/kotlin/com/vultisig/wallet/data/models/payload/BlockChainSpecfic.kt b/data/src/main/kotlin/com/vultisig/wallet/data/models/payload/BlockChainSpecfic.kt index 2d824dfa0..d3a2600c5 100644 --- a/data/src/main/kotlin/com/vultisig/wallet/data/models/payload/BlockChainSpecfic.kt +++ b/data/src/main/kotlin/com/vultisig/wallet/data/models/payload/BlockChainSpecfic.kt @@ -40,7 +40,6 @@ sealed class BlockChainSpecific { val priorityFee: BigInteger, val fromAddressPubKey: String? = null, val toAddressPubKey: String? = null, - val tokenAccountExists: Boolean? = null, ) : BlockChainSpecific() data class Sui( diff --git a/data/src/main/kotlin/com/vultisig/wallet/data/repositories/BlockChainSpecificRepository.kt b/data/src/main/kotlin/com/vultisig/wallet/data/repositories/BlockChainSpecificRepository.kt index 5a65244ee..60eb6e686 100644 --- a/data/src/main/kotlin/com/vultisig/wallet/data/repositories/BlockChainSpecificRepository.kt +++ b/data/src/main/kotlin/com/vultisig/wallet/data/repositories/BlockChainSpecificRepository.kt @@ -8,6 +8,7 @@ import com.vultisig.wallet.data.api.PolkadotApi import com.vultisig.wallet.data.api.SolanaApi import com.vultisig.wallet.data.api.ThorChainApi import com.vultisig.wallet.data.api.chains.SuiApi +import com.vultisig.wallet.data.api.chains.TonApi import com.vultisig.wallet.data.models.Chain import com.vultisig.wallet.data.models.Coin import com.vultisig.wallet.data.models.TokenStandard @@ -16,6 +17,8 @@ import com.vultisig.wallet.data.models.payload.BlockChainSpecific import com.vultisig.wallet.data.models.payload.UtxoInfo import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope +import kotlinx.datetime.Clock +import timber.log.Timber import java.math.BigInteger import javax.inject.Inject @@ -49,6 +52,7 @@ internal class BlockChainSpecificRepositoryImpl @Inject constructor( private val blockChairApi: BlockChairApi, private val polkadotApi: PolkadotApi, private val suiApi: SuiApi, + private val tonApi: TonApi, ) : BlockChainSpecificRepository { override suspend fun getSpecific( @@ -169,18 +173,30 @@ internal class BlockChainSpecificRepositoryImpl @Inject constructor( val blockHash = async { solanaApi.getRecentBlockHash() } - - val tokenAccountExists = async { + val fromAddressPubKey = async { + solanaApi.getTokenAssociatedAccountByOwner( + token.address, + token.contractAddress + ).takeIf { !token.isNativeToken } + } + val toAddressPubKey = async { dstAddress?.let { - solanaApi.doesTokenAccountExist(dstAddress, token.contractAddress) + solanaApi.getTokenAssociatedAccountByOwner( + dstAddress, + token.contractAddress + ).takeIf { !token.isNativeToken } } } - + val recentBlockHashResult = blockHash.await() + val fromAddressPubKeyResult = fromAddressPubKey.await() + val toAddressPubKeyResult = toAddressPubKey.await() + Timber.d("solana blockhash: $recentBlockHashResult") BlockChainSpecificAndUtxo( BlockChainSpecific.Solana( - recentBlockHash = blockHash.await(), - tokenAccountExists = tokenAccountExists.await(), + recentBlockHash = recentBlockHashResult, priorityFee = gasFee.value, + fromAddressPubKey = fromAddressPubKeyResult, + toAddressPubKey = toAddressPubKeyResult, ) ) } @@ -220,16 +236,26 @@ internal class BlockChainSpecificRepositoryImpl @Inject constructor( } TokenStandard.SUI -> { - suiApi.getAllCoins(address) - BlockChainSpecificAndUtxo( BlockChainSpecific.Sui( - referenceGasPrice = gasFee.value, + referenceGasPrice = suiApi.getReferenceGasPrice(), coins = suiApi.getAllCoins(address), ), utxos = emptyList(), ) } + + TokenStandard.TON -> { + BlockChainSpecificAndUtxo( + blockChainSpecific = BlockChainSpecific.Ton( + sequenceNumber = tonApi.getSpecificTransactionInfo(address) + .toString().toULong(), + expireAt = (Clock.System.now() + .epochSeconds + 600L).toULong(), + bounceable = false, + ), + ) + } } private fun ensureOneGweiPriorityFee(priorityFee: BigInteger): BigInteger {