Skip to content

Type-safe Kotlin client for the NEAR JSON-RPC API, featuring generated models with kotlinx.serialization, a Ktor-based client, and a generator for keeping everything in sync with the OpenAPI spec

License

Notifications You must be signed in to change notification settings

near/near-jsonrpc-client-kotlin

NEAR JSON-RPC Kotlin Client

Build Status License Kotlin Multiplatform Release Badge

A type-safe, Kotlin client for the NEAR JSON-RPC API.


Table of contents


πŸ“– Overview

  • A type-safe, coroutine-powered Kotlin client for interacting with the NEAR Protocol JSON-RPC API.

  • Built using Kotlin Multiplatform and kotlinx.serialization, with models and API bindings automatically generated from NEAR’s official OpenAPI specification.

Module Description
client Coroutine-powered JSON-RPC client with full NEAR RPC method wrappers (auto-generated)
models Kotlin @Serializable RPC request and response types (auto-generated)
generator Tooling for generating Kotlin models and client APIs from NEAR’s OpenAPI spec

✨ Features

🎯 Type-Safe Design: Kotlin data class and sealed class models generated directly from NEAR’s OpenAPI specification for compile-time safety

⚑ Coroutine-First API: All network calls are suspend functions, designed for Kotlin Coroutines (structured concurrency)

πŸ” Thread-Safe Concurrency: Client internals are safe for concurrent use (coroutine-friendly synchronization; optional actor model for high concurrency)

πŸ›‘οΈ Comprehensive Type System: kotlinx.serialization @Serializable models for all RPC requests and responses, including nullable and optional fields

πŸ“¦ Minimal Official Dependencies: Small, explicit dependencies on well-known Kotlin libraries (e.g. kotlinx.serialization, ktor); no heavy frameworks required

πŸ§ͺ Extensive Tests: Unit and integration tests (JUnit + MockK or kotlin.test) with mock responses and fixtures generated from OpenAPI examples

πŸ”„ Auto-Generated Core: Models, request builders, and baseline tests are generated from NEAR’s OpenAPI spec

πŸ“± Kotlin Multiplatform Friendly: Designed so common modules can be compiled for JVM/Android, iOS (via Kotlin/Native), JS and Desktop using Kotlin Multiplatform (KMP)


βš™οΈ Requirements

This library is built on top of Ktor and Kotlinx Serialization.
So you must add the following Ktor dependencies to your project:

implementation("io.ktor:ktor-client-core:<ktor_version>")
implementation("io.ktor:ktor-client-cio:<ktor_version>") // or another engine (OkHttp, Darwin, etc.)
implementation("io.ktor:ktor-client-content-negotiation:<ktor_version>")
implementation("io.ktor:ktor-serialization-kotlinx-json:<ktor_version>")

πŸš€ Quickstart

You can easily add NEAR-RPC-Client-Kotlin to your project using JitPack.

Step 1: Add the JitPack repository

In your project-level build.gradle.kts:

repositories { 
    maven { url = uri("https://jitpack.io") }
}

Step 2: Add the dependency

In your module-level build.gradle.kts:

dependencies {
    implementation("com.github.hosseinkarami-dev:NEAR-RPC-Client-Kotlin:<Version>")
}

πŸ› οΈ Generator - Reproduce models & client

The generator module is a CLI that parses NEAR's OpenAPI spec and writes generated Kotlin types and the NearClient implementation.

Run the generator against NEAR's OpenAPI:

./gradlew :generator:run --args="--openapi-url https://raw.githubusercontent.com/near/nearcore/master/chain/jsonrpc/openapi/openapi.json"
  • Use the --models-out argument to specify a custom output directory for generated model classes (e.g. --models-out build/generated/models)
  • Use the --client-out argument to specify a custom output directory for generated client classes (e.g. --client-out build/generated/client)

πŸ’‘ Basic Usage

Use a single HttpClient instance for the app and call NearClient methods from a coroutine scope (e.g., lifecycleScope).

Android example:

import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import kotlinx.serialization.json.Json
import kotlinx.coroutines.launch
import androidx.lifecycle.lifecycleScope

val httpClient = HttpClient(CIO) {
    install(ContentNegotiation) {
        json(Json { ignoreUnknownKeys = true })
    }
}

val nearClient = NearClient(
    httpClient = httpClient,
    baseUrl = "https://rpc.mainnet.near.org" // or "https://rpc.testnet.near.org"
)

lifecycleScope.launch {
  val response = nearClient.block(
    RpcBlockRequest.BlockId(BlockId.BlockHeight(167440515.toULong()))
  )

  when (response) {
    is RpcResponse.Failure -> {
      println("Error: ${response.error}")
    }

    is RpcResponse.Success -> {
      val result = response.getResultOrNull<RpcBlockResponse>()
      println("Result: $result")

    }
  }
}

Broadcast tx example:

val signedTx = SignedTransaction("BASE64_SIGNED_TX")
val req = RpcSendTransactionRequest(signedTxBase64 = signedTx, waitUntil = null)

when (val r = nearClient.broadcastTxAsync(req)) {
  is RpcResponse.Success -> println("tx hash: ${r.result}")
  is RpcResponse.Failure -> println("send failed: ${r.error}")
}

Notes

  • You can use getResultOrNull<T>() directly on RpcResponse.
    If the response is Success, it returns the typed model (e.g., RpcBlockResponse).
    If it's Failure, it safely returns null.
  • Prefer a single application-wide HttpClient (don't recreate per request).
  • Close the client (httpClient.close()) when your app terminates.

πŸ›  Error Handling

This client leverages Kotlin's sealed class pattern to represent both successful and failed RPC responses in a type-safe and expressive way.

Each RPC call returns an instance of RpcResponse<T>:

sealed class RpcResponse<out T> {
    data class Success<T>(val result: T): RpcResponse<T>()
    data class Failure(val error: ErrorResult): RpcResponse<Nothing>()
}

βœ… Success Response

On success, the response is of type RpcResponse.Success and contains the expected result:

val result = (response as RpcResponse.Success).result

❌ Failure Response

On failure, the response is of type RpcResponse.Failure, which wraps an ErrorResult instance describing the type of error.

The ErrorResult is a sealed class representing different error categories that can occur while performing an RPC call:

sealed class ErrorResult {
    data class Rpc(val error: RpcError): ErrorResult()                  // JSON-RPC level errors
    data class RpcRuntime(val error: String): ErrorResult()             // RPC runtime-related errors
    data class Http(val statusCode: Int, val body: String? = null): ErrorResult() // Non-2xx HTTP responses
    data class Timeout(val cause: Throwable? = null): ErrorResult()     // Request timed out
    data class Network(val cause: Throwable): ErrorResult()             // Network issues (e.g., UnknownHostException)
    data class Deserialization(val cause: Throwable, val rawBody: String? = null): ErrorResult() // JSON parsing issues
    data class Cancellation(val cause: Throwable? = null): ErrorResult() // Request was cancelled
    data class Unknown(val message: String? = null, val cause: Throwable? = null): ErrorResult() // Catch-all for unexpected errors
}

πŸ“Œ Example Usage

Here’s how to handle both success and different types of errors in an RPC response:

val response = rpcClient.callSomeRpcMethod(params)

when (response) {
    is RpcResponse.Success -> {
        val result = response.result
        println("βœ… RPC call succeeded: $result")
    }
    is RpcResponse.Failure -> {
        when (val error = response.error) {
            is ErrorResult.Rpc -> {
                when (error.error) {
                    is RpcError.HandlerError -> {
                        println("❌ RPC Handler Error: ${(error.error as RpcError.HandlerError).message}")
                    }

                    is RpcError.InternalError -> {
                        println("❌ RPC Internal Error: ${(error.error as RpcError.InternalError).message}")
                    }

                    is RpcError.RequestValidationError -> {
                        println("❌ RPC Internal Error: ${(error.error as RpcError.RequestValidationError).message}")
                    }
                }
            }
            is ErrorResult.Http -> {
                println("❌ HTTP Error: Status ${error.statusCode}, body: ${error.body}")
            }
            is ErrorResult.Timeout -> {
                println("⏳ Timeout Error: ${error.cause?.message}")
            }
            is ErrorResult.Network -> {
                println("🌐 Network Error: ${error.cause.message}")
            }
            is ErrorResult.Deserialization -> {
                println("πŸ”„ Deserialization Error: ${error.cause.message}, raw body: ${error.rawBody}")
            }
            is ErrorResult.Cancellation -> {
                println("🚫 Request Cancelled: ${error.cause?.message}")
            }
            is ErrorResult.Unknown -> {
                println("❓ Unknown Error: ${error.message}, cause: ${error.cause?.message}")
            }
            is ErrorResult.RpcRuntime -> {
                println("⚠️ Runtime RPC Error: ${error.error}")
            }
        }
    }
}

This structured approach to error handling makes it easy to differentiate between expected JSON-RPC errors, HTTP/network issues, and unexpected failures β€” providing a clean and maintainable way to handle failures gracefully in your application.


πŸ§ͺ Testing

Running Tests

# Run all tests
./gradlew :generator:test
./gradlew :client:test

Test Structure

  • Unit Tests: Validate the behavior of models and functions in isolation, including helpers, utilities, and serialization logic.
  • Integration Tests: Validate end-to-end behavior of the client using a mock RPC engine, simulating actual RPC calls without requiring a live network connection.
  • Decoding Tests: Ensure all @Serializable models correctly parse and map data from mock JSON responses.

πŸ“¦ Deployment Guide

For detailed instructions on project structure, CI/CD workflow, versioning, and deployment steps, see the DEPLOYMENT.md file.


🀝 Contributing

Contributions welcome!

  1. Fork and create a branch.
  2. If you modify generator, include resulting generated files or open a separate PR to regenerate.
  3. Add tests for new behaviors.
  4. Run:
./gradlew build
./gradlew :generator:test
./gradlew :client:test
  1. Open a PR with clear description.
  2. More details: see the Contributing Overview tab in this repository.

πŸ“œ License

This project is licensed under the Apache-2.0 License. See LICENSE for details.


πŸ“¬ Contact & References

About

Type-safe Kotlin client for the NEAR JSON-RPC API, featuring generated models with kotlinx.serialization, a Ktor-based client, and a generator for keeping everything in sync with the OpenAPI spec

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •  

Languages