Kotlin SDK
com.layerhop:layerhop is a coroutine-based Kotlin client for the JVM and Android, with OkHttp, kotlinx.serialization models, a watch Flow and webhook verification.
com.layerhop:layerhop is the Kotlin client for the LayerHop REST API, for the JVM and Android, mirroring the Go and TypeScript SDKs. Every call is a suspend function built on OkHttp and kotlinx.serialization, and it compiles to Java 8 bytecode, so it runs on Android minSdk 21 without the Android Gradle plugin.
The SDK never holds keys. It returns unsigned transactions; your app's wallet (Reown AppKit / WalletConnect, the Solana Mobile Wallet Adapter, a KMS on your server…) signs and broadcasts them.
How do I install it?#
// build.gradle.kts
dependencies {
implementation("com.layerhop:layerhop:0.6.0")
}The dependencies are OkHttp 4, kotlinx.coroutines and kotlinx.serialization. The source lives in layerhop/layerhop-kotlin.
Quickstart#
Amounts are integer strings in base units ("100000000" = 100 USDC).
import com.layerhop.*
val lh = LayerHop(LayerHopOptions(integrator = "myapp")) // apiKey = ..., baseUrl = "http://10.0.2.2:8787"
suspend fun swap() {
// 1. Quote
val q = lh.quote(QuoteParams(
srcChain = "base", srcToken = "USDC", amount = "100000000",
dstChain = "solana", dstToken = "SOL",
sender = "0xYourEvmAddress", recipient = "YourSolanaAddress",
))
val best = q.best
// 2. Build the unsigned transaction
val tx = lh.buildTx(best.quoteId)
// 3. Sign + send with YOUR wallet
val txHash = if (tx.isEvm) {
val evm = tx.asEvm() // send evm.approval first when set, then the tx, then evm.extraTxs in order
signEvm(evm)
} else {
val svm = tx.asSvm() // base64 VersionedTransaction; svm.submitViaApi → sign only, then lh.submit(..)
signSvm(svm)
}
// 4. Report the broadcast
lh.reportSubmitted(best.quoteId, txHash)
// 5. Watch until filled / refunded / failed
lh.watch(best.provider, txHash).collect { println(it.state) }
}Gas on arrival: pass gasOnArrival = true (about $2 of native gas for the recipient on the destination chain) or gasOnArrivalUsd = 5.0, then check best.gasOnArrival. See Gas on arrival.
How do I use it in an Android app?#
Add <uses-permission android:name="android.permission.INTERNET" /> to AndroidManifest.xml, create one client (in your Application or DI graph) and call it from viewModelScope or lifecycleScope. Requests are cancelled with the coroutine.
class SwapViewModel : ViewModel() {
private val lh = LayerHop(LayerHopOptions(integrator = "myapp"))
val state = MutableStateFlow("idle")
fun quote(sender: String, recipient: String) = viewModelScope.launch {
state.value = try {
val q = lh.quote(QuoteParams("base", "USDC", "25000000", "arbitrum", "ETH", sender, recipient))
"Receive ${q.best.dstAmount} wei via ${q.best.provider}"
} catch (e: LayerHopException) {
"${e.code}: ${e.apiMessage}" // e.g. NO_ROUTE, see e.docsUrl
} catch (e: java.io.IOException) {
"Network error"
}
}
}The tx.asEvm() fields map directly onto an eth_sendTransaction request through Reown AppKit / WalletConnect. tx.asSvm().serializedTx is a base64 transaction for the Solana Mobile Wallet Adapter's signAndSendTransactions (or signTransactions when submitViaApi is true).
Do not ship an API key inside an app. Quotes and swaps work without one, and keyed calls (checkout, invoices, subscriptions, deposit addresses) belong on your server. R8 needs no extra rules: the models use kotlinx.serialization's generated serializers, not reflection.
Which methods are available?#
| Method | Endpoint |
|---|---|
health() · chains() · tokens(chain) · prices(ids) |
GET /v1/health · /v1/chains · /v1/tokens · /v1/prices |
quote(QuoteParams) |
GET /v1/quote |
buildTx(quoteId) |
POST /v1/orders → BuiltTx with asEvm() / asSvm() |
simulate(quoteId) |
POST /v1/simulate |
submit(quoteId, signedBase64) |
POST /v1/orders/submit (Solana submitViaApi only) |
reportSubmitted(quoteId, txHash) |
POST /v1/orders/{quoteId}/submitted |
status(provider, id) · listOrders(ListOrdersParams) |
GET /v1/orders/{provider}/{id} · GET /v1/orders |
watch(provider, id, interval, timeout) |
A Flow<OrderStatus> of state changes; completes when final, throws WatchTimeoutException after 30 min by default |
createCheckoutSession · getCheckoutSession · listCheckoutSessions · expireCheckoutSession · quoteCheckoutSession |
/v1/checkout/sessions |
createCheckoutProduct · getCheckoutProduct · listCheckoutProducts · updateCheckoutProduct · startProductCheckout |
/v1/checkout/products |
createRefund(sessionId, CreateRefundParams) · listRefunds(sessionId) |
/v1/checkout/sessions/{id}/refunds |
createInvoice · getInvoice · listInvoices · updateInvoice · sendInvoice · voidInvoice · getPublicInvoice · payInvoice |
/v1/invoices |
createDepositAddress · getDepositAddress · listDepositAddresses · refreshDepositAddress · disableDepositAddress · simulateDeposit |
/v1/deposit-addresses |
createSubscription · getSubscription · listSubscriptions · updateSubscription · cancelSubscription(id, atPeriodEnd) · pauseSubscription · resumeSubscription · listSubscriptionInvoices |
/v1/subscriptions |
createLimitOrder · submitLimitOrder · getLimitOrder · listLimitOrders · cancelLimitOrder · simulateLimitOrder |
/v1/limit-orders (Solana, beta) |
serviceStatus() · triggerWebhookEvent(type) |
GET /v1/status · POST /v1/webhooks/trigger |
The update* params send only the fields you set; "" clears a nullable field by sending null. createDepositAddress and createLimitOrder default integrator and integratorFeeBps (and channel for limit orders) to the client's options.
How do I take payments with checkout?#
Run this on your server with an API key (Checkout guide):
val server = LayerHop(LayerHopOptions(apiKey = System.getenv("LAYERHOP_API_KEY")))
val session = server.createCheckoutSession(CreateCheckoutParams(
amount = "49000000", settleChain = "base", settleToken = "USDC",
recipient = "0xYourPayoutAddress", successUrl = "https://shop.example.com/thanks",
))
// Send the customer to session.url; fulfil on the checkout.session.completed webhook.
val sub = server.createSubscription(CreateSubscriptionParams(
customerEmail = "a@example.com", interval = SubscriptionIntervals.MONTH, productId = "prod_123",
))How do I verify webhooks?#
Verify the LayerHop-Signature header against the raw body before parsing it (Webhooks guide). Ktor:
post("/webhooks/layerhop") {
val raw = call.receiveText()
val event = try {
Webhooks.constructEvent(secret, raw, call.request.header(Webhooks.SIGNATURE_HEADER))
} catch (e: WebhookSignatureException) {
return@post call.respond(HttpStatusCode.BadRequest)
}
if (event.type == WebhookEventTypes.CHECKOUT_SESSION_COMPLETED) fulfil(event.data.session!!)
call.respond(HttpStatusCode.OK)
}Webhooks.verifySignature(secret, body, header) returns a Boolean (5-minute tolerance by default, several v1 values accepted, constant-time comparison). Webhooks.sign(secret, body, timestamp) builds a header for your own tests.
How are errors reported?#
Every non-2xx response throws LayerHopException with a stable code (QUOTE_EXPIRED, NO_ROUTE…, see Errors), the HTTP status, the requestId to quote to support, and docsUrl. Network failures throw java.io.IOException.
How do I use test mode?#
Use an lh_test_… key and every call is served by the sandbox: live-priced quotes, placeholder transactions (tx.sandbox == true, never broadcast them) and orders that fill in about 10 seconds. See Test mode.
Last updated