diff --git a/.idea/androidTestResultsUserPreferences.xml b/.idea/androidTestResultsUserPreferences.xml index 366b67a..b05cc61 100644 --- a/.idea/androidTestResultsUserPreferences.xml +++ b/.idea/androidTestResultsUserPreferences.xml @@ -68,6 +68,19 @@ + + + + + + + diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index 8d18801..620ba8f 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -11,15 +11,18 @@ - - + + + + \ No newline at end of file diff --git a/app/src/androidTest/java/com/altude/android/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/altude/android/ExampleInstrumentedTest.kt index b5952db..ba0fd5c 100644 --- a/app/src/androidTest/java/com/altude/android/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/altude/android/ExampleInstrumentedTest.kt @@ -53,7 +53,7 @@ class ExampleInstrumentedTest { result .onSuccess { println("✅ Sent: $it") - assert(it.signature.isNotEmpty()) + assert(it.Signature.isNotEmpty()) } .onFailure { println("❌ Failed: ${it.message}") @@ -74,7 +74,7 @@ class ExampleInstrumentedTest { closeresult .onSuccess { println("✅ Sent: $it") - assert(it.signature.isNotEmpty()) + assert(it.Signature.isNotEmpty()) } .onFailure { println("❌ Failed: ${it.message}") diff --git a/core/src/main/java/com/altude/core/Programs/Jupiter.kt b/core/src/main/java/com/altude/core/Programs/Jupiter.kt new file mode 100644 index 0000000..ef392bd --- /dev/null +++ b/core/src/main/java/com/altude/core/Programs/Jupiter.kt @@ -0,0 +1,63 @@ +package com.altude.core.Programs + +import android.os.Build +import androidx.annotation.RequiresApi +import com.altude.core.data.JupiterInstruction +import com.altude.core.data.JupiterSwapResponse +import com.altude.core.model.AltudeTransactionBuilder +import com.altude.core.network.QuickNodeRpc +import foundation.metaplex.base58.decodeBase58 +import foundation.metaplex.solana.transactions.AccountMeta +import foundation.metaplex.solana.transactions.SolanaTransaction +import foundation.metaplex.solana.transactions.Transaction +import foundation.metaplex.solana.transactions.TransactionInstruction +import foundation.metaplex.solanapublickeys.PublicKey +import okio.ByteString.Companion.decodeBase64 +import java.util.Base64 + +object Jupiter { + suspend fun buildJupiterTransaction( + jupiterResponse: JupiterSwapResponse + ): List { + val instructions = mutableListOf() + + // Helper function to convert JSON instruction objects into TransactionInstruction + @RequiresApi(Build.VERSION_CODES.O) + fun parseInstruction(obj: JupiterInstruction): TransactionInstruction { + val programId = PublicKey(obj.ProgramId) + val accounts = obj.Accounts.map { + AccountMeta(PublicKey(it.Pubkey), it.IsSigner, it.IsWritable) + } + val data = Base64.getDecoder().decode(obj.Data) + return TransactionInstruction(programId, accounts, data) + } + + // 1️⃣ Add compute budget instructions + jupiterResponse.ComputeBudgetInstructions?.forEach { + instructions.add(parseInstruction(it)) + } + + // 2️⃣ Add setup instructions + jupiterResponse.SetupInstructions?.forEach { + instructions.add(parseInstruction(it)) + } + + // 3️⃣ Add swap instruction + jupiterResponse.SwapInstruction?.let { + instructions.add(parseInstruction(it)) + } + + // 4️⃣ Add cleanup instruction + jupiterResponse.CleanupInstruction?.let { + instructions.add(parseInstruction(it)) + } + + // 5️⃣ Add any "other" instructions + jupiterResponse.OtherInstructions?.forEach { + instructions.add(parseInstruction(it)) + } + + + return instructions + } +} \ No newline at end of file diff --git a/core/src/main/java/com/altude/core/api/TransactionService.kt b/core/src/main/java/com/altude/core/api/TransactionService.kt index 9e9dd51..0d7d134 100644 --- a/core/src/main/java/com/altude/core/api/TransactionService.kt +++ b/core/src/main/java/com/altude/core/api/TransactionService.kt @@ -3,6 +3,7 @@ package com.altude.core.api import com.altude.core.data.BatchTransactionRequest import com.altude.core.data.MintData import com.altude.core.data.SendTransactionRequest +import com.altude.core.data.SwapTransactionRequest import kotlinx.serialization.Contextual import retrofit2.Call import retrofit2.http.Body @@ -65,6 +66,10 @@ interface TransactionService { fun sendTransaction( @Body body: SendTransactionRequest ): Call + @POST("api/transaction/swap") + fun swapTransaction( + @Body body: SwapTransactionRequest + ): Call @POST("api/transaction/sendbatch") fun sendBatchTransaction( @@ -107,7 +112,10 @@ interface TransactionService { fun postCreateCollectionNft( @Body body: ISendTransactionRequest ): Call - + @POST("api/jupiter/swap") + fun jupiterSwap( + @Body body: JsonElement + ): Call @GET("api/transaction/config") fun getConfig(): Call } diff --git a/core/src/main/java/com/altude/core/data/JupiterSwapRequest.kt b/core/src/main/java/com/altude/core/data/JupiterSwapRequest.kt new file mode 100644 index 0000000..217365d --- /dev/null +++ b/core/src/main/java/com/altude/core/data/JupiterSwapRequest.kt @@ -0,0 +1,68 @@ +package com.altude.core.data +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement + +@Serializable +data class JupiterSwapRequest( + val userPublicKey: String, + val quoteResponse: QuoteResponse, + val prioritizationFeeLamports: PrioritizationFeeLamports, + val dynamicComputeUnitLimit: Boolean = false, + val wrapAndUnwrapSol: Boolean = true, + val asLegacyTransaction: Boolean = false, + val skipUserAccountsRpcCalls: Boolean = false, + val dynamicSlippage: Boolean = false +) + +@Serializable +data class SwapRequest( + val UserPublicKey: String, + val InputMint: String, + val OutputMint: String, + val Amount: Int, + val SlippageBps: Int = 50, +) + +@Serializable +data class QuoteResponse( + val inputMint: String, + val inAmount: String, + val outputMint: String, + val outAmount: String, + val otherAmountThreshold: String, + val swapMode: String, + val slippageBps: Int, + val platformFee: JsonElement? = null, + val priceImpactPct: String, + val routePlan: List +) + +@Serializable +data class RoutePlan( + val swapInfo: SwapInfo, + val percent: Int +) + +@Serializable +data class SwapInfo( + val ammKey: String, + val label: String, + val inputMint: String, + val outputMint: String, + val inAmount: String, + val outAmount: String, + val feeAmount: String, + val feeMint: String +) + +@Serializable +data class PrioritizationFeeLamports( + val priorityLevelWithMaxLamports: PriorityLevelWithMaxLamports +) + +@Serializable +data class PriorityLevelWithMaxLamports( + val maxLamports: Long, + val priorityLevel: String, + val global: Boolean +) diff --git a/core/src/main/java/com/altude/core/data/JupiterSwapResponse.kt b/core/src/main/java/com/altude/core/data/JupiterSwapResponse.kt new file mode 100644 index 0000000..1241886 --- /dev/null +++ b/core/src/main/java/com/altude/core/data/JupiterSwapResponse.kt @@ -0,0 +1,26 @@ +package com.altude.core.data +import kotlinx.serialization.Serializable + +@Serializable +data class JupiterSwapResponse( + val OtherInstructions: List? = null, + val ComputeBudgetInstructions: List? = null, + val SetupInstructions: List? = null, + val SwapInstruction: JupiterInstruction? = null, + val CleanupInstruction: JupiterInstruction? = null, + val AddressLookupTableAddresses: List? = null +) + +@Serializable +data class JupiterInstruction( + val ProgramId: String, + val Accounts: List, + val Data: String +) + +@Serializable +data class JupiterAccountMeta( + val Pubkey: String, + val IsSigner: Boolean, + val IsWritable: Boolean +) diff --git a/core/src/main/java/com/altude/core/data/SwapTransactionRequest.kt b/core/src/main/java/com/altude/core/data/SwapTransactionRequest.kt new file mode 100644 index 0000000..5f7b5dd --- /dev/null +++ b/core/src/main/java/com/altude/core/data/SwapTransactionRequest.kt @@ -0,0 +1,7 @@ +package com.altude.core.data + +import com.altude.core.api.ISendTransactionRequest +import kotlinx.serialization.Serializable + +@Serializable +data class SwapTransactionRequest(override val SignedTransaction: String): ISendTransactionRequest \ No newline at end of file diff --git a/gasstation/src/androidTest/java/com/altude/gasstation/ExampleInstrumentedTest.kt b/gasstation/src/androidTest/java/com/altude/gasstation/ExampleInstrumentedTest.kt index 15cd213..1693e5d 100644 --- a/gasstation/src/androidTest/java/com/altude/gasstation/ExampleInstrumentedTest.kt +++ b/gasstation/src/androidTest/java/com/altude/gasstation/ExampleInstrumentedTest.kt @@ -3,8 +3,6 @@ package com.altude.gasstation import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import com.altude.core.Programs.AssociatedTokenAccountProgram -import com.altude.core.Programs.MPLCore import com.altude.gasstation.data.CloseAccountOption import com.altude.gasstation.data.CreateAccountOption import com.altude.gasstation.data.GetAccountInfoOption @@ -14,10 +12,9 @@ import com.altude.gasstation.data.SendOptions import com.altude.core.helper.Mnemonic import com.altude.gasstation.data.KeyPair import com.altude.gasstation.data.Token -import com.altude.core.network.QuickNodeRpc import com.altude.core.service.StorageService import com.altude.gasstation.data.Commitment -import foundation.metaplex.solanapublickeys.PublicKey +import com.altude.gasstation.data.SwapOption import kotlinx.coroutines.runBlocking import org.junit.Before @@ -43,7 +40,7 @@ class ExampleInstrumentedTest { @Before fun setup()=runBlocking{ context = InstrumentationRegistry.getInstrumentation().targetContext//ApplicationProvider.getApplicationContext() - Altude.setApiKey(context,"ak_acGsSti_GD9jaIisAf1a2_PhtOD5cJ3qq1u-PGYZo7k") + Altude.setApiKey(context,"ak_7KRePt6yFlsv_DYkuNGznpzpKFJTecsagXZwwSB0U2o") } // @Test @@ -115,7 +112,7 @@ class ExampleInstrumentedTest { result .onSuccess { println("✅ Sent: $it") - assert(it.signature.isNotEmpty()) + assert(it.Signature.isNotEmpty()) } .onFailure { println("❌ Failed: ${it.message}") @@ -136,7 +133,7 @@ class ExampleInstrumentedTest { closeresult .onSuccess { println("✅ Sent: $it") - assert(it.signature.isNotEmpty()) + assert(it.Signature.isNotEmpty()) } .onFailure { println("❌ Failed: ${it.message}") @@ -221,7 +218,7 @@ class ExampleInstrumentedTest { val result = Altude.send(options) result - .onSuccess { println("✅ Sent: ${it.signature}") } + .onSuccess { println("✅ Sent: ${it.Signature}") } .onFailure { println("❌ Failed: ${it.message}") } @@ -259,7 +256,7 @@ class ExampleInstrumentedTest { val result = Altude.sendBatch(options) result - .onSuccess { println("✅ Sent: ${it.signature}") } + .onSuccess { println("✅ Sent: ${it.Signature}") } .onFailure { println("❌ Failed: ${it.message}") } @@ -281,6 +278,21 @@ class ExampleInstrumentedTest { println("Balance: $result") } @Test + fun testSwap() = runBlocking { + Altude.savePrivateKey(accountPrivateKey ) + val option = SwapOption( + account = "chenGqdufWByiUyxqg7xEhUVMqF3aS9sxYLSzDNmwqu", + inputMint = Token.SOL.mint(), + outputMint = Token.USDT.mint(), + amount = 100, + commitment = Commitment.finalized + ) + + // Wrap the callback in a suspendable way (like a suspendCoroutine) + val result = Altude.swap(option) + println("Balance: $result") + } + @Test fun testGetAccountInfo() = runBlocking { // val pda1 = MPLCore.findTreeConfigPda(PublicKey("14QSPv5BtZCh8itGrUCu2j7e7A88fwZo3cAjxi4R5Fgj")) Altude.savePrivateKey(accountPrivateKey ) diff --git a/gasstation/src/main/java/com/altude/gasstation/Altude.kt b/gasstation/src/main/java/com/altude/gasstation/Altude.kt index 383a95d..0a5e7be 100644 --- a/gasstation/src/main/java/com/altude/gasstation/Altude.kt +++ b/gasstation/src/main/java/com/altude/gasstation/Altude.kt @@ -22,8 +22,10 @@ import com.altude.gasstation.data.SolanaKeypair import com.altude.core.service.StorageService import com.altude.core.data.BatchTransactionRequest import com.altude.core.data.SendTransactionRequest +import com.altude.core.data.SwapTransactionRequest import com.altude.gasstation.data.GetAccountResponse import com.altude.gasstation.data.GetBalanceResponse +import com.altude.gasstation.data.SwapOption import com.altude.gasstation.data.TransactionResponse import foundation.metaplex.solanapublickeys.PublicKey import kotlinx.coroutines.Dispatchers @@ -133,6 +135,25 @@ object Altude { } } @OptIn(ExperimentalCoroutinesApi::class) + suspend fun swap( + options: SwapOption + ): Result = withContext(Dispatchers.IO) { + try { + val result = GaslessManager.jupiterSwap(options) + + if (result.isFailure) return@withContext Result.failure(result.exceptionOrNull()!!) + + val signedTransaction = result.getOrThrow() + val service = SdkConfig.createService(TransactionService::class.java) + val request = SwapTransactionRequest(signedTransaction) + + val res = service.swapTransaction(request).await() + Result.success(deCodeJson(res)) + } catch (e: Exception) { + return@withContext Result.failure(e) + } + } + @OptIn(ExperimentalCoroutinesApi::class) suspend fun getHistory( options: GetHistoryOption ): Result = withContext(Dispatchers.IO) { diff --git a/gasstation/src/main/java/com/altude/gasstation/GaslessManager.kt b/gasstation/src/main/java/com/altude/gasstation/GaslessManager.kt index 165e749..4194c71 100644 --- a/gasstation/src/main/java/com/altude/gasstation/GaslessManager.kt +++ b/gasstation/src/main/java/com/altude/gasstation/GaslessManager.kt @@ -2,21 +2,26 @@ package com.altude.gasstation import android.util.Base64 import com.altude.core.Programs.AssociatedTokenAccountProgram +import com.altude.core.Programs.Jupiter import com.altude.core.Programs.TokenProgram +import com.altude.core.api.TransactionService import com.altude.gasstation.helper.Utility import com.altude.core.config.SdkConfig +import com.altude.core.data.JupiterSwapResponse import com.altude.gasstation.data.CloseAccountOption import com.altude.gasstation.data.CreateAccountOption import com.altude.gasstation.data.ISendOption import com.altude.gasstation.data.SendOptions import com.altude.core.helper.Mnemonic import com.altude.core.model.AltudeTransactionBuilder -import com.altude.core.model.EmptySignature import com.altude.core.model.HotSigner import com.altude.gasstation.data.KeyPair import com.altude.gasstation.data.SolanaKeypair import com.altude.core.network.QuickNodeRpc import com.altude.core.service.StorageService +import com.altude.core.data.SwapRequest +import com.altude.gasstation.data.SwapOption +import com.altude.gasstation.data.Token import com.metaplex.signer.Signer import foundation.metaplex.solana.transactions.SerializeConfig import foundation.metaplex.solana.transactions.TransactionInstruction @@ -24,6 +29,12 @@ import foundation.metaplex.solanaeddsa.Keypair import foundation.metaplex.solanapublickeys.PublicKey import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonNamingStrategy +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.encodeToJsonElement +import retrofit2.await import java.lang.Error object GaslessManager { @@ -31,7 +42,14 @@ object GaslessManager { private val rpc = QuickNodeRpc(SdkConfig.apiConfig.RpcUrl) val feePayerPubKey = PublicKey(SdkConfig.apiConfig.FeePayer) // PublicKey("Hwdo4thQCFKB3yuohhmmnb1gbUBXySaVJwBnkmRgN8cK") //ALZ8NJcf8JDL7j7iVfoyXM8u3fT3DoBXsnAU6ML7Sb5W BjLvdmqDjnyFsewJkzqPSfpZThE8dGPqCAZzVbJtQFSr - + @OptIn(ExperimentalSerializationApi::class) + val json = Json { + ignoreUnknownKeys = true // don’t crash if backend adds new fields + isLenient = true // accept non-strict JSON (unquoted keys, etc.) + encodeDefaults = true // include default values in request bodies + explicitNulls = false // don’t send nulls unless explicitly set + decodeEnumsCaseInsensitive = true + } suspend fun transferToken(option: ISendOption): Result = withContext(Dispatchers.IO) { return@withContext try { val pubKeyMint = PublicKey(option.token) @@ -295,6 +313,47 @@ object GaslessManager { } } + suspend fun jupiterSwap( + option: SwapOption + ): Result = withContext(Dispatchers.IO) { + return@withContext try { + var defaultWallet = getKeyPair(option.account) + + var service = SdkConfig.createService(TransactionService::class.java) + val swapRequest = SwapRequest( + UserPublicKey = option.account, + InputMint = option.inputMint, + OutputMint =option.outputMint, + Amount = option.amount, + SlippageBps = option.slippageBps + ) + + val response = service.jupiterSwap(json.encodeToJsonElement(swapRequest)).await() + val txInstructions = Jupiter.buildJupiterTransaction( Altude.json.decodeFromJsonElement(response)) + + + val blockhashInfo = rpc.getLatestBlockhash( + commitment = option.commitment.name + ) + + val tx = AltudeTransactionBuilder() + .setFeePayer(feePayerPubKey) + .addRangeInstruction(txInstructions) + .setRecentBlockHash(blockhashInfo.blockhash) + .setSigners(listOf(HotSigner(defaultWallet))) + .build() + + //val sign = Core.SignTransaction(privateKeyBytes,message) + val serialized = Base64.encodeToString( + tx.serialize(SerializeConfig(requireAllSignatures = false)), + Base64.NO_WRAP + ) + Result.success(serialized) + } catch (e: Exception) { + Result.failure(e) + + } + } suspend fun getKeyPair(account: String = ""): Keypair { val seedData = StorageService.getDecryptedSeed(account) if (seedData != null) { diff --git a/gasstation/src/main/java/com/altude/gasstation/data/SwapOption.kt b/gasstation/src/main/java/com/altude/gasstation/data/SwapOption.kt new file mode 100644 index 0000000..2ab16e1 --- /dev/null +++ b/gasstation/src/main/java/com/altude/gasstation/data/SwapOption.kt @@ -0,0 +1,10 @@ +package com.altude.gasstation.data + +data class SwapOption( + val account: String, + val inputMint: String, + val outputMint: String, + val amount: Int, + val slippageBps: Int = 50, + val commitment: Commitment +) diff --git a/gasstation/src/main/java/com/altude/gasstation/data/Token.kt b/gasstation/src/main/java/com/altude/gasstation/data/Token.kt index c6c290a..29af5c8 100644 --- a/gasstation/src/main/java/com/altude/gasstation/data/Token.kt +++ b/gasstation/src/main/java/com/altude/gasstation/data/Token.kt @@ -8,11 +8,11 @@ enum class Token(val mainnet: String, val devnet: String) { devnet = "So11111111111111111111111111111111111111112" ), USDT( - mainnet = "Es9vMFrzaCERGFF3rT97sC5LMiRWqgmT2o7qC1xNNjXG", - devnet = "" // no official devnet USDT + mainnet = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", + devnet = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB" // no official devnet USDT ), USDC( - mainnet = "EPjFWdd5AufqSSqeM2qSX2mVesERs85x3n5wjLWe1RtK", + mainnet = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", devnet = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU" ), LINK( diff --git a/gasstation/src/main/java/com/altude/gasstation/data/TransactionResponse.kt b/gasstation/src/main/java/com/altude/gasstation/data/TransactionResponse.kt index eb280c5..873a1e0 100644 --- a/gasstation/src/main/java/com/altude/gasstation/data/TransactionResponse.kt +++ b/gasstation/src/main/java/com/altude/gasstation/data/TransactionResponse.kt @@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable @Serializable data class TransactionResponse( - override val status: String, - override val message: String, - override val signature: String + override val Status: String, + override val Message: String, + override val Signature: String ) : ITransactionResponse diff --git a/gasstation/src/main/java/com/altude/gasstation/interfaces/ITransactionResponse.kt b/gasstation/src/main/java/com/altude/gasstation/interfaces/ITransactionResponse.kt index 9e92ce7..a8460d4 100644 --- a/gasstation/src/main/java/com/altude/gasstation/interfaces/ITransactionResponse.kt +++ b/gasstation/src/main/java/com/altude/gasstation/interfaces/ITransactionResponse.kt @@ -2,11 +2,11 @@ package com.altude.gasstation.interfaces interface ITransactionResponse{ //@SerializedName("Status") - val status: String // Match C# string type + val Status: String // Match C# string type //@SerializedName("Message") - val message: String + val Message: String //@SerializedName("Signature") - val signature: String + val Signature: String } \ No newline at end of file