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