metaplex-foundation / solana-kmp

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Kotlin Multiplatform SDK for Solana (KMP)

Overview

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.

Features

  • 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

Getting Started

Prerequisites

Ensure you have the Kotlin Multiplatform development environment set up. Refer to the Kotlin Multiplatform documentation to set up your environment.

Modules

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 example lamports or sol

  • :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.

Installation

KMP

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.

Android

Import the SDK into your Android project by adding the following dependency to your build.gradle.kts:

implementation("foundation.metaplex:solana:$Version")

iOS/MacOS in Xcode

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.

Usage

RPC

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.

Koltin
val rpcUrl: String = "https://api.mainnet-beta.solana.com"
val rpc = RPC(rpcUrl)
Swift
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:

Koltin
val slot = rpc.getSlot(null)
Swift
// await/async
let slot = await rpc.getSlot(configuration: nil)

// closure
rpc.getSlot(configuration: nil) { slot, error in
    debugPrint("\(slot!)")
}
GetAccountInfo Example
val randomPublicKey = PublicKey("9VHphpWFmUxVHxzWyeYJYYbQADWZ7X6PLzyWER8Lc3k2")

val rpc = RPC(rpcUrl)
val metadata = rpc.getAccountInfo(
    randomPublicKey,
    null,
    serializer = BorshAsBase64JsonArraySerializer(
        Metadata.serializer()
    )
)?.data
Interface

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
}

Transactions

Create transactions using the TransactionBuilder class which facilitates the creation of Solana transactions in a Kotlin-friendly way.

Kotlin
val memo = "Other Test memo"
val transaction: Transaction = SolanaTransactionBuilder()
    .addInstruction(
        writeUtf8(
            signer().publicKey,
                memo
            )
        )
    .setRecentBlockHash("Eit7RCyhUixAe2hGBS8oqnw59QK3kgMMjfLME5bm9wRn")
    .setSigners(listOf(signer()))
    .build()
Swift
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()

Signing a Transaction

Use the signer module to securely sign your transactions with the necessary credentials:

Kotlin
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)
}
Swift
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
    }
}

More Examples

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.

Keypair and signers

Generate a PublicKey from a Base58 Private key.

Kotlin
val privateKey = "4Z7cXSyeFR8wNGMVXUE1TwtKn5D5Vu7FzEv69dokLv7KrQk7h6pu4LF8ZRR9yQBhc7uSM6RTTZtU1fmaxiNrxXrs".decodeBase58().copyOfRange(0, 32)

val k = SolanaEddsa.createKeypairFromSecretKey(privateKey)
val signer = HotSigner(SolanaKeypair(k.publicKey, k.secretKey))
Swift
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))

Memo transaction

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

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)

License

This project is licensed under the Metaplex License. See the LICENSE file for details.

About

License:Other


Languages

Language:Kotlin 100.0%