Skip to main content

Overview

Build, sign, and send SOL transactions with blockhash management.

Prerequisites

Send SOL Transaction

import com.dynamic.sdk.android.DynamicSDK
import com.dynamic.sdk.android.Models.BaseWallet
import com.solanaweb3.*
import org.sol4k.PublicKey

val sdk = DynamicSDK.getInstance()

suspend fun sendSOL(
    wallet: BaseWallet,
    recipient: String,
    amountInSOL: Double
): String {
    try {
        val connection = Connection(Cluster.DEVNET)
        val fromPubkey = PublicKey(wallet.address)
        val toPubkey = PublicKey(recipient)
        val lamports = (amountInSOL * 1_000_000_000).toLong()
        val blockhash = connection.getLatestBlockhash()

        val instruction = SystemProgram.transfer(fromPubkey, toPubkey, lamports)
        val transaction = Transaction.v0(fromPubkey, listOf(instruction), blockhash.blockhash)
        val base64Transaction = transaction.serializeUnsignedToBase64()
        val signature = sdk.solana.signAndSendTransaction(base64Transaction, wallet)

        println("Transaction sent!")
        println("Signature: $signature")
        return signature
    } catch (e: Exception) {
        println("Transaction failed: ${e.message}")
        throw e
    }
}

Sign Transaction (Without Sending)

suspend fun signTransactionOnly(wallet: BaseWallet, base64Transaction: String): String {
    try {
        val signedTransaction = sdk.solana.signTransaction(base64Transaction, wallet)
        println("Signed transaction: $signedTransaction")
        return signedTransaction
    } catch (e: Exception) {
        println("Failed to sign: ${e.message}")
        throw e
    }
}

Lamports Conversion Helpers

object SolanaUtils {
    const val LAMPORTS_PER_SOL = 1_000_000_000L

    fun solToLamports(sol: Double): Long {
        return (sol * LAMPORTS_PER_SOL).toLong()
    }

    fun lamportsToSol(lamports: Long): Double {
        return lamports.toDouble() / LAMPORTS_PER_SOL
    }

    fun formatSol(sol: Double): String {
        return String.format("%.9f SOL", sol)
    }

    fun formatLamports(lamports: Long): String {
        return String.format("%,d lamports", lamports)
    }
}

// Usage
val amount = 1.5
val lamports = SolanaUtils.solToLamports(amount)
println("${amount} SOL = ${SolanaUtils.formatLamports(lamports)}")
fun getExplorerUrl(signature: String, cluster: Cluster = Cluster.DEVNET): String {
    val clusterParam = when (cluster) {
        Cluster.DEVNET -> "devnet"
        Cluster.TESTNET -> "testnet"
        Cluster.MAINNET_BETA -> "mainnet-beta"
        else -> "devnet"
    }
    return "https://explorer.solana.com/tx/$signature?cluster=$clusterParam"
}

// Usage in Compose
@Composable
fun ExplorerLink(signature: String, cluster: Cluster) {
    val url = getExplorerUrl(signature, cluster)
    Button(onClick = { /* Open URL in browser */ }) {
        Text("View on Solana Explorer")
    }
}

Best Practices

  • Get fresh blockhash immediately before sending (valid ~60 seconds)
  • Validate addresses before creating transactions
  • Check balance includes transaction fees (~5000 lamports)
  • Show transaction confirmations to users
  • Display transaction status with explorer links

Error Handling

Common Transaction Errors

sealed class SolanaTransactionError {
    data class InsufficientFunds(val message: String) : SolanaTransactionError()
    data class InvalidBlockhash(val message: String) : SolanaTransactionError()
    data class NetworkError(val message: String) : SolanaTransactionError()
    data class InvalidAddress(val message: String) : SolanaTransactionError()
    data class UserRejected(val message: String) : SolanaTransactionError()
    data class Unknown(val message: String) : SolanaTransactionError()
}

fun parseTransactionError(e: Exception): SolanaTransactionError {
    val errorMessage = e.message?.lowercase() ?: ""

    return when {
        errorMessage.contains("insufficient") || errorMessage.contains("balance") ->
            SolanaTransactionError.InsufficientFunds("Insufficient balance for transaction")
        errorMessage.contains("blockhash") ->
            SolanaTransactionError.InvalidBlockhash("Blockhash expired, please try again")
        errorMessage.contains("network") || errorMessage.contains("connection") ->
            SolanaTransactionError.NetworkError("Network error, check your connection")
        errorMessage.contains("invalid") && errorMessage.contains("address") ->
            SolanaTransactionError.InvalidAddress("Invalid recipient address")
        errorMessage.contains("rejected") || errorMessage.contains("denied") ->
            SolanaTransactionError.UserRejected("Transaction was rejected")
        else ->
            SolanaTransactionError.Unknown("Transaction failed: ${e.message}")
    }
}

// Usage in ViewModel
try {
    val signature = sdk.solana.signAndSendTransaction(base64Transaction, wallet)
    _transactionSignature.value = signature
} catch (e: Exception) {
    val error = parseTransactionError(e)
    _errorMessage.value = when (error) {
        is SolanaTransactionError.InsufficientFunds -> error.message
        is SolanaTransactionError.InvalidBlockhash -> error.message
        is SolanaTransactionError.NetworkError -> error.message
        is SolanaTransactionError.InvalidAddress -> error.message
        is SolanaTransactionError.UserRejected -> error.message
        is SolanaTransactionError.Unknown -> error.message
    }
}

Troubleshooting

Transaction not confirming: Check transaction status on explorer Blockhash expired: Get fresh blockhash immediately before sending, implement retry logic Network mismatch: Use devnet for testing, mainnet for production Insufficient funds: Get devnet SOL from faucet.solana.com for testing

What’s Next