Welcome to the Kotlin Multiplatform SDK for Solana! This SDK leverages the power of Kotlin Multiplatform to enable developers to work with Solana blockchain across various platforms including Android, iOS Ecosystem, and the JVM. Utilizing this SDK, developers can create transactions, sign them using various signer modules, and interact with Solana through RPC calls with ease.
- Cross-Platform: Use a single codebase to target Android, iOS Ecosystem, and JVM.
- Create Transactions: Construct and manage Solana transactions with ease.
- Sign with Signer: Safely and securely sign your transactions with a robust signer module.
- Interact with RPC: Facilitate communication with the Solana blockchain through RPC calls. With a powerful serialization capabilities.
- Metaplex Read API: Supports the Metaplex Read API extra rpc calls
- Keypair Generator: Generate an EDDSA keypairs
Ensure you have the Kotlin Multiplatform development environment set up. Refer to the Kotlin Multiplatform documentation to set up your environment.
We do support composability. Each module can be used alone or by referencing the main solana
module. The modules we support:
-
:solana:
This module provides functionality and utilities for the Solana blockchain platform. This is the main module, and might be the only one you need to build on top. It exposes all the modules required to interact with solana. -
:solanapublickeys:
This module handles public keys within the Solana ecosystem. -
:base58:
This module encodes and decodes data in Base58 format. -
:solanaeddsa:
This module deals with Elliptic Curve Digital Signature Algorithm (EdDSA) cryptography specific to Solana. It also provides Keypair generation. Its the want you need if you need to generate a Offline Random Keypair. -
:amount:
This module is used for managing numeric values, potentially within the context of financial transactions or cryptocurrency amounts. For examplelamports
orsol
-
:readapi:
This module is designed to consume Metaplex DAS Read API. Check the documentation. -
:rpc:
This module provides functionality for making remote procedure calls (RPC) to interact with a blockchain Solana network. -
:signer:
This module handles cryptographic signing and verifying operations, often used for secure transactions and authentication. Its just an interface. -
:mplbubblegum:
The purpose of this module is to mint and interact with compressed NFTs. -
:mpltokenmetadata:
The purpose of this module is to interact with Metaplex Token Metadata program.
Import the SDK into your Kotlin Multiplatform project by adding the following dependency to your build.gradle.kts:
implementation("foundation.metaplex:solana:$Version")
Our SDK is architecturally composable, allowing you to install only the dependencies necessary for your specific use case, which keeps your application optimized and resource-efficient. The library modules are available on Maven, facilitating easy integration into your Kotlin project.
For those focusing on RPC calling, you can choose to exclusively install modules pertinent to RPC interactions, promoting a clean and efficient codebase. Refer to our module documentation to understand the diverse range of modules available and how to selectively integrate them based on your requirements. This strategy ensures a lean development process, affording you the convenience of using only the tools you need, without the encumbrance of extraneous features.
implementation("foundation.metaplex:rpc:$Version")
Refer to the modules names to reference them individualy.
Import the SDK into your Android project by adding the following dependency to your build.gradle.kts:
implementation("foundation.metaplex:solana:$Version")
By using KMP we got iOS/MacOs for free. We think that KMP is the future of developing solana sdks since it solves the performance problem and makes a good enough sdk for swift. Its far better than a RN or flutter counterparts since it uses actual Native Code.
Its important to notice that the fat binary is not swift-first. You can still use pure swift sdk. But progress is coming along KMP that I am confident that this version will become the more complete version.
This libraries prebuilds and deploys xcframeworks (fat binaries). To use them you can check the release section and download the one you need.
They are provided in zip format. When unzipped a fat binary is available. Just drag and drop it in your Xcode Project. It can be possible to use it with SPM. Currently we dont provide sha256 hashes.
Using solana.xcframework
maybe sufficient for most projects.
If you don`t trust the prebuilds you are allow to build them locally. By running this gradle command from android studio. This will take some time to build.
./gradlew buildReleaseXCFramework
or for specific module
./gradlew solana:buildReleaseXCFramework
Important: API support callbacks and async/await. For the second only @MainActor scope is supported. Main Actor can be validated Thread.current.isMainThread
.
RPC a provide the implementation for the RpcInterface. Its possible to call the most important rpc calls. First we need to configure the RPC endpoints.
val rpcUrl: String = "https://api.mainnet-beta.solana.com"
val rpc = RPC(rpcUrl)
let rpcURL = "https://api.mainnet-beta.solana.com"
let driver = NetworkDriver(httpClient: NetworkingClientKt.NetworkClient())
let rpc = RPC(
rpcUrl: rpcURL,
httpNetworkDriver: driver
)
This is how to make a getSlot
rpc call:
val slot = rpc.getSlot(null)
// await/async
let slot = await rpc.getSlot(configuration: nil)
// closure
rpc.getSlot(configuration: nil) { slot, error in
debugPrint("\(slot!)")
}
val randomPublicKey = PublicKey("9VHphpWFmUxVHxzWyeYJYYbQADWZ7X6PLzyWER8Lc3k2")
val rpc = RPC(rpcUrl)
val metadata = rpc.getAccountInfo(
randomPublicKey,
null,
serializer = BorshAsBase64JsonArraySerializer(
Metadata.serializer()
)
)?.data
This is the RPC interface. Its possible to implement and alternative Interface.
interface RpcInterface {
suspend fun <T> getAccountInfo(
publicKey: PublicKey,
configuration: RpcGetAccountInfoConfiguration?,
serializer: KSerializer<T>,
): Account<T>?
suspend fun <T> getMultipleAccounts(
publicKeys: List<PublicKey>,
configuration: RpcGetMultipleAccountsConfiguration?,
serializer: KSerializer<T>,
): List<Account<T>?>?
suspend fun <T> getProgramAccounts(
programId: PublicKey,
configuration: RpcGetProgramAccountsConfiguration?,
serializer: KSerializer<T>
): List<Account<T>?>?
suspend fun getLatestBlockhash(
configuration: RpcGetLatestBlockhashConfiguration?
): BlockhashWithExpiryBlockHeight
suspend fun getSlot(
configuration: RpcGetSlotConfiguration?
): ULong
suspend fun getMinimumBalanceForRentExemption(
usize: ULong
): ULong
suspend fun requestAirdrop(
configuration: RpcRequestAirdropConfiguration
): TransactionSignature
suspend fun getBalance(
publicKey: PublicKey,
configuration: RpcGetBalanceConfiguration?
): Long
suspend fun sendTransaction(
transaction: SerializedTransaction,
configuration: RpcSendTransactionConfiguration?
): TransactionSignature
}
Create transactions using the TransactionBuilder class which facilitates the creation of Solana transactions in a Kotlin-friendly way.
val memo = "Other Test memo"
val transaction: Transaction = SolanaTransactionBuilder()
.addInstruction(
writeUtf8(
signer().publicKey,
memo
)
)
.setRecentBlockHash("Eit7RCyhUixAe2hGBS8oqnw59QK3kgMMjfLME5bm9wRn")
.setSigners(listOf(signer()))
.build()
let memo = "Other Test memo"
let memoInstruction = MemoProgram.shared.writeUtf8(
account: signer().publicKey,
memo: memo
)
let transaction: = SolanaTransactionBuilder()
.addInstruction(
transactionInstruction:
memoInstruction
)
.setRecentBlockHash(recentBlockHash: "BlockHash")
.setSigners(signers: [signer()])
.build()
Use the signer module to securely sign your transactions with the necessary credentials:
class SolanaKeypair(
override val publicKey: PublicKey,
override val secretKey: ByteArray
) : Keypair
class HotSigner(private val keyPair: Keypair) : Signer {
override val publicKey: PublicKey = keyPair.publicKey
override suspend fun signMessage(message: ByteArray): ByteArray = SolanaEddsa.sign(message, keyPair)
}
class SolanaKeypair: Keypair {
var publicKey: PublicKey
var secretKey: KotlinByteArray
init(publicKey: PublicKey, secretKey: KotlinByteArray) {
self.publicKey = publicKey
self.secretKey = secretKey
}
}
class HotSigner: Signer {
private let keypair: Keypair
init(keypair: Keypair) {
self.keypair = keypair
}
func signMessage(message: KotlinByteArray) async throws -> KotlinByteArray {
return try await SolanaEddsa.shared.sign(message: message, keypair: self.keypair)
}
var publicKey: PublicKey {
self.keypair.publicKey
}
}
KMP can be hard to inferred directly. Especially extensions, static methods and companion/shared objects. Here is a good example of an implementation for a signer using the HotSigner implementation previously developed.
Generate a PublicKey from a Base58 Private key.
val privateKey = "4Z7cXSyeFR8wNGMVXUE1TwtKn5D5Vu7FzEv69dokLv7KrQk7h6pu4LF8ZRR9yQBhc7uSM6RTTZtU1fmaxiNrxXrs".decodeBase58().copyOfRange(0, 32)
val k = SolanaEddsa.createKeypairFromSecretKey(privateKey)
val signer = HotSigner(SolanaKeypair(k.publicKey, k.secretKey))
let decoded = try Base58Kt.decodeBase58("4Z7cXSyeFR8wNGMVXUE1TwtKn5D5Vu7FzEv69dokLv7KrQk7h6pu4LF8ZRR9yQBhc7uSM6RTTZtU1fmaxiNrxXrs")
let privateKey = decoded.toData().subdata(in: 0..<32)
let k = try await SolanaEddsa.shared.createKeypairFromSecretKey(secretKey: NSDataByteArrayKt.toByteArray(privateKey))
let signer = HotSigner(keypair: SolanaKeypair(publicKey: k.publicKey, secretKey: k.secretKey))
Write to a memo using the Memo program.
// Configure RPC
val rpcUrl = "https://api.devnet.solana.com/"
val rpc = RPC(rpcUrl)
val blockhash = rpc.getLatestBlockhash(null)
val memo = "Other Test memo"
val transaction: Transaction = SolanaTransactionBuilder()
.addInstruction(
writeUtf8(
signer.publicKey,
memo
)
)
.setRecentBlockHash(blockhash.blockhash)
.setSigners(listOf(signer))
.build()
val serializedTransaction = transaction.serialize()
val transactionSignature = rpc.sendTransaction(serializedTransaction, null)
// Configure RPC
let rpcURL = "https://api.devnet.solana.com/"
let rpc = RPC(
rpcUrl: self.rpcURL,
httpNetworkDriver: NetworkDriver(httpClient: NetworkingClientKt.NetworkClient())
)
let blockHash = try await rpc!.getLatestBlockhash(configuration: nil)
let memo = "Other Test memo"
let transaction = try await SolanaTransactionBuilder()
.addInstruction(
transactionInstruction: MemoProgram.shared.writeUtf8(
account: signer.publicKey,
memo: memo
)
)
.setRecentBlockHash(recentBlockHash: blockHash.blockhash)
.setSigners(signers: [signer])
.build()
let serializedTransaction = try await transaction.serialize(
config: SerializeConfig(requireAllSignatures: true, verifySignatures: true)
)
let transactionSignature = try await rpc!.sendTransaction(
transaction: serializedTransaction,
configuration: nil
)
Read Api getAssetsByOwner
example
val randomPublicKey = PublicKey("Geh5Ss5knQGym81toYGXDbH3MFU2JCMK7E4QyeBHor1b")
val assets = readApiDecorator.getAssetsByOwner(GetAssetsByOwnerRpcInput(randomPublicKey))
assertTrue { assets.total > 0 }
let decorator = ReadApiDecorator(rpcUrl: self.rpcURL, httpNetworkDriver: driver!)
let randomPublicKey = PublicKey(base58String: "Geh5Ss5knQGym81toYGXDbH3MFU2JCMK7E4QyeBHor1b")
let assets = try await decorator.getAssetsByOwner(
input: GetAssetsByOwnerRpcInput(
ownerAddress: randomPublicKey,
page: 1,
limit: nil,
before: nil,
after: nil,
sortBy: nil
)
)
assert(assets.total > 0)
This project is licensed under the Metaplex License. See the LICENSE file for details.