https://github.com/0xsequence/kotlin-sdk
Kotlin SDK for Embedded Wallets
https://github.com/0xsequence/kotlin-sdk
Last synced: about 1 month ago
JSON representation
Kotlin SDK for Embedded Wallets
- Host: GitHub
- URL: https://github.com/0xsequence/kotlin-sdk
- Owner: 0xsequence
- License: apache-2.0
- Created: 2026-04-06T09:08:13.000Z (4 months ago)
- Default Branch: master
- Last Pushed: 2026-07-02T14:17:10.000Z (about 1 month ago)
- Last Synced: 2026-07-02T16:08:01.320Z (about 1 month ago)
- Language: Kotlin
- Size: 662 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Agents: AGENTS.md
Awesome Lists containing this project
README
# OMS Client Kotlin SDK
Android and Kotlin SDK for wallet, auth, signing, and API/indexer integrations.
## Installation
Maven Central:
```kotlin
implementation("io.github.0xsequence:oms-client-kotlin-sdk:0.1.0-alpha.4")
```
This is the only artifact consumers add. The generated WaaS client is packaged
inside the AAR as an internal implementation detail; consumers should use the
SDK APIs documented below instead of importing generated classes.
## What It Provides
- email sign-in flow against the wallet API
- OIDC ID-token sign-in flow against the wallet API
- non-extractable Android Keystore request credential for wallet API signing
- persisted wallet session metadata
- wallet selection and wallet creation flows
- message signing
- typed-data signing
- transaction sending and contract calls
- transaction status lookup
- wallet access listing and revocation
- message and typed-data signature verification through WaaS
- native and token balance lookups plus transaction history through the indexer
service, including optional token contract info and token metadata
- unit formatting and parsing helpers for raw token amounts
## Requirements
- Android 10 / API 29 or newer
- Android `compileSdk 34` or newer
- Java 17 Android compile options
- Kotlin/Android app using the Android library module
- a valid `publishableKey`
The SDK does not require consumer apps to enable core library desugaring.
The published artifact declares `minSdk 24` so apps with lower manifest floors,
including Expo/React Native apps, can include the dependency. This is a packaging
compatibility floor; the SDK requires Android 10 / API 29 or newer at runtime
because the service endpoints require TLS 1.3.
The sample app in this repository uses additional Google Sign-In / AndroidX
Credential Manager dependencies and therefore compiles with SDK 35. That sample
app requirement does not raise the published SDK artifact's consumer
`compileSdk` floor.
## Quick Start
Create the SDK with the Android-friendly constructor:
```kotlin
val client = OMSClient(
context = context,
publishableKey = "YOUR_PUBLISHABLE_KEY",
)
```
The SDK derives Wallet API and IndexerGateway routing from the publishable key.
Session restore persists completed wallet-session metadata only; it does not
store private signing material.
Pending email OTP state is kept in memory. OIDC redirect state is stored only to
complete the browser redirect flow and is cleared when the flow completes, fails,
or is replaced.
Expired sessions are made inactive before protected wallet operations and throw
`OmsSessionException` with `code = OmsSdkErrorCode.SessionExpired`. The SDK
clears the active signer/session state, but keeps expired completed-session
metadata in storage until the app starts a new auth flow or calls `signOut()`.
Subscribe with `client.wallet.onSessionExpired { event -> ... }` to route users
back to sign-in while preserving the expired session snapshot for reauth.
Listeners are delivered on the Android main thread.
## Example Flow
`OMSClient` restores a persisted session automatically when it is created. Apps
can hide sign-in controls while a wallet is selected, but starting a new auth
flow intentionally replaces any existing wallet session so users can re-auth or
switch accounts:
By default email OTP and OIDC ID-token auth completion use
`WalletSelectionBehavior.Automatic`. They select a wallet for the requested
wallet type, create one when none exists, and return
`CompleteAuthResult.WalletSelected`. If more than one matching wallet exists,
automatic mode selects the first matching wallet returned by WaaS. Use manual
mode for apps that need to let users choose between multiple wallets.
Completed auth requests ask WaaS for a one-week session lifetime by default
(`WalletClient.DEFAULT_SESSION_LIFETIME_SECONDS`, `604_800` seconds).
Pass `sessionLifetimeSeconds` to `completeEmailAuth`, `signInWithOidcIdToken`,
or `handleOidcRedirectCallback` to request a different positive whole-number
lifetime in seconds. Invalid lifetimes are reported as
`OmsSdkErrorCode.ValidationError`.
```kotlin
if (client.wallet.walletAddress == null) {
client.wallet.startEmailAuth("user@example.com")
// A one-time code is sent to the user's email inbox.
val result = client.wallet.completeEmailAuth("123456")
check(result is CompleteAuthResult.WalletSelected)
showWallet(result.wallet)
}
```
For OIDC ID-token flows such as Google Sign-In with Credential Manager:
```kotlin
val result =
client.wallet.signInWithOidcIdToken(
idToken = googleIdToken,
issuer = "https://accounts.google.com",
audience = "YOUR_WEB_CLIENT_ID",
)
check(result is CompleteAuthResult.WalletSelected)
showWallet(result.wallet)
```
For OIDC authorization-code PKCE redirect flows, start the redirect, open the
returned URL with your browser or Custom Tabs, then safely handle incoming app
links from `onCreate` / `onNewIntent`:
```kotlin
val started = client.wallet.startOidcRedirectAuth(
provider = OidcProviders.google(),
redirectUri = "yourapp://auth/callback",
)
// Open started.authorizationUrl.
when (val result = client.wallet.handleOidcRedirectCallback(intent.data?.toString())) {
is OidcRedirectAuthResult.Completed -> showWallet(result.wallet)
OidcRedirectAuthResult.NotOidcRedirectCallback -> Unit
OidcRedirectAuthResult.NoPendingAuth -> Unit
is OidcRedirectAuthResult.Failed -> showRestartSignIn(result.error)
}
```
Use a redirect URI that matches a deep link registered by your app, such as
`yourapp://auth/callback`. If your Google OAuth setup uses a custom web client
ID, pass it with `OidcProviders.google(clientId = "YOUR_WEB_CLIENT_ID")`.
Pass `loginHint` only when you want to prefill or select a specific Google
account, such as during session-expiry reauth. When omitted, the SDK falls back
to the previous active session email when one exists before redirect auth
starts. Pass an empty string to force no `login_hint` for a call. Non-Google
providers do not receive `login_hint`.
With the default automatic behavior, a successful redirect callback returns
`OidcRedirectAuthResult.Completed`; `WalletSelection` is only a successful branch
when the callback is handled with manual wallet selection.
To use your own wallet-selection UI, pass
`walletSelection = WalletSelectionBehavior.Manual` when completing auth:
```kotlin
val result =
client.wallet.completeEmailAuth(
code = "123456",
walletSelection = WalletSelectionBehavior.Manual,
)
check(result is CompleteAuthResult.WalletSelection)
val selected = selectOrCreateWallet(result.pendingSelection)
showWallet(selected.wallet)
```
Manual mode completes auth but does not select or create a wallet until the app
calls `pendingSelection.selectWallet(...)` or
`pendingSelection.createAndSelectWallet(...)`. `pendingSelection.wallets` is
already filtered to the requested wallet type, so the app picker can show those
wallets plus a "Create New Wallet" action:
```kotlin
private suspend fun selectOrCreateWallet(
pendingSelection: PendingWalletSelection,
): WalletSelectionResult {
val choice =
showWalletPickerAndWaitForChoice(
wallets = pendingSelection.wallets,
includeCreateNewWallet = true,
)
return when (choice) {
WalletPickerChoice.CreateNew ->
pendingSelection.createAndSelectWallet()
is WalletPickerChoice.Existing ->
pendingSelection.selectWallet(choice.wallet.id)
}
}
```
`WalletPickerChoice` is app UI state in this example. Both SDK calls return the
selected wallet and persist it as the active wallet session.
For OIDC redirect auth, pass the same behavior when handling the callback:
```kotlin
when (
val result =
client.wallet.handleOidcRedirectCallback(
callbackUrl = intent.data?.toString(),
walletSelection = WalletSelectionBehavior.Manual,
)
) {
is OidcRedirectAuthResult.WalletSelection -> {
val selected = selectOrCreateWallet(result.pendingSelection)
showWallet(selected.wallet)
}
OidcRedirectAuthResult.NotOidcRedirectCallback -> Unit
OidcRedirectAuthResult.NoPendingAuth -> Unit
is OidcRedirectAuthResult.Failed -> showRestartSignIn(result.error)
is OidcRedirectAuthResult.Completed -> error("Expected manual wallet selection")
}
```
Useful state checks:
```kotlin
val walletAddress = client.session.walletAddress
val expiresAt = client.session.expiresAt
val loginType = client.session.loginType
val sessionEmail = client.session.sessionEmail
```
`expiresAt` is an ISO-8601 timestamp string returned by the wallet API.
`client.session` only reports completed wallet-session state. It does not
include pending auth progress. Show OTP or redirect waiting UI from the method
result that started the flow, not from session state. Always pass incoming app
links to `handleOidcRedirectCallback`;
if it returns `NoPendingAuth`, show sign-in UI and let the user start again. A
fresh SDK instance restores completed wallet sessions, including the session
expiry, login type, and email returned by the wallet API, but not email OTP
pending state. Completed auth requests ask the wallet API for a one-week session
lifetime by default; pass `sessionLifetimeSeconds` to request a different
positive whole-number lifetime in seconds. Auth completion loads all wallet
pages before selecting or creating a wallet. If auth completes but wallet
selection, wallet creation, or session persistence fails, the SDK clears the
in-memory auth session instead of retaining unrecoverable transient state.
Use the selected wallet:
```kotlin
val network = Network.AMOY
val typedDataJson =
buildJsonObject {
putJsonObject("types") {
putJsonArray("EIP712Domain") {
add(buildJsonObject {
put("name", "name")
put("type", "string")
})
add(buildJsonObject {
put("name", "version")
put("type", "string")
})
add(buildJsonObject {
put("name", "chainId")
put("type", "uint256")
})
}
putJsonArray("Message") {
add(buildJsonObject {
put("name", "contents")
put("type", "string")
})
}
}
put("primaryType", "Message")
putJsonObject("domain") {
put("name", "OMS Client")
put("version", "1")
put("chainId", JsonPrimitive(network.id.toLong()))
}
putJsonObject("message") {
put("contents", "hello from android")
}
}
val signResult = client.wallet.signMessage(
network = network,
message = "hello from android",
)
val verifyResult = client.wallet.isValidMessageSignature(
network = network,
message = "hello from android",
signature = signResult,
)
val typedSignature = client.wallet.signTypedData(
network = network,
typedData = typedDataJson,
)
val txResult = client.wallet.sendTransaction(
network = network,
to = "0xE5E8B483FfC05967FcFed58cc98D053265af6D99",
value = parseUnits("0.01", 18),
)
```
`sendTransaction` prepares and executes the transaction, then polls the WaaS
status endpoint briefly for an executed status or transaction hash. If the
transaction is still pending when polling times out, the response keeps the
`txnId` with `status = TransactionStatus.Pending` and `txnHash = null`.
Transaction values are raw base-unit integers. Use `parseUnits` to convert
human-entered decimal values before sending. Import the helpers from
`com.omsclient.kotlin_sdk.utils`.
## Errors
Public SDK APIs throw `OmsSdkException` subclasses with stable fields such as
`code`, `operation`, `status`, nullable `retryable`, and `txnId`. When a failure comes
from a remote OMS service response or transport failure, the error also includes
`upstreamError` with normalized WaaS or indexer details for logging and
service-specific troubleshooting. Application logic should usually branch on the
SDK-level `code`.
For transaction writes, `TransactionExecutionUnconfirmed` means the SDK has a
`txnId` from preparation, but the execute request failed before the SDK could
confirm whether the transaction was submitted; do not blindly resend the same
write. `TransactionStatusLookupFailed` means the transaction was submitted but
status polling failed, so retry status lookup with the returned `txnId`.
`retryable` describes the failed SDK operation, not the whole user intent.
```kotlin
try {
client.wallet.startEmailAuth("user@example.com")
} catch (error: OmsSdkException) {
println("${error.code} ${error.operation?.id} ${error.upstreamError}")
}
```
For raw token amount formatting and parsing:
```kotlin
val rawAmount = parseUnits("1.5", 18)
val displayAmount = formatUnits(rawAmount, 18)
```
For indexer balance lookups:
```kotlin
val walletAddress = requireNotNull(client.wallet.walletAddress)
val tokenBalances = client.indexer.getBalances(
walletAddress = walletAddress,
networks = listOf(network),
contractAddresses = listOf("0xTokenContract"),
includeMetadata = true,
)
tokenBalances.nativeBalances.forEach { balance ->
println("${balance.symbol.orEmpty()} ${balance.balance.orEmpty()}")
}
tokenBalances.balances.forEach { balance ->
println("${balance.contractInfo?.symbol.orEmpty()} ${balance.contractInfo?.decimals ?: 0}")
}
```
Pass `includeMetadata = true` when you need token contract details or NFT/token
metadata from `balance.contractInfo` and `balance.tokenMetadata`.
For transaction history:
```kotlin
val history = client.indexer.getTransactionHistory(
walletAddress = walletAddress,
networks = listOf(network),
)
```
For raw calldata or transaction parameters beyond `to` and `value`, use the request overload:
```kotlin
val network = Network.AMOY
val txResult = client.wallet.sendTransaction(
network = network,
request = SendTransactionRequest(
to = "0xContractAddress",
value = parseUnits("0", 18),
data = "0x1234",
mode = TransactionMode.Native,
),
)
```
For WaaS ABI-style contract calls, use `callContract`:
```kotlin
val txResult = client.wallet.callContract(
network = network,
contract = "0xContractAddress",
method = "transfer(address,uint256)",
args =
listOf(
AbiArg(type = "address", value = JsonPrimitive("0xRecipient")),
AbiArg(type = "uint256", value = JsonPrimitive("1000000000000000000")),
),
)
```
To pick the first fee option the selected wallet can afford, pass the built-in
selector:
```kotlin
val txResult = client.wallet.sendTransaction(
network = network,
request = SendTransactionRequest(
to = "0xContractAddress",
value = parseUnits("0", 18),
data = "0x1234",
mode = TransactionMode.Native,
),
selectFeeOption = FeeOptionSelector.firstAvailable,
)
```
For a custom fee picker, return the selected option's `selection`:
```kotlin
val txResult = client.wallet.sendTransaction(
network = network,
request = SendTransactionRequest(
to = "0xContractAddress",
value = parseUnits("0", 18),
data = "0x1234",
mode = TransactionMode.Native,
),
) { feeOptions ->
val selected = showFeePickerAndWaitForChoice(feeOptions)
selected.selection
}
```
The selector receives `FeeOptionWithBalance` values. `balance` is the selected
wallet's raw indexer balance for that fee token when available. `available` is
formatted with the token decimals, while `availableRaw` keeps the raw integer
value. `decimals` is exposed as a regular `Int`. `selection` preserves the
API-provided `tokenID` when present and falls back to the token symbol. Sponsored
transactions skip fee selection; unsponsored transactions fail before execute
when no fee option can be selected.
To refresh a transaction later or manage active wallet credentials:
```kotlin
val status = client.wallet.getTransactionStatus(txnId = txResult.txnId)
val idToken = client.wallet.getIdToken(ttlSeconds = 300u)
val credentials = client.wallet.listAccess(pageSize = 25u)
client.wallet.listAccessPages(pageSize = 25u).collect { page ->
renderCredentials(page.credentials)
}
credentials
.firstOrNull { !it.isCaller }
?.let { client.wallet.revokeAccess(targetCredentialId = it.credentialId) }
```
## API Reference
The full public API surface is documented in [docs/api.md](docs/api.md).
## Sample App
This repository includes an Android sample app in [`app/`](app/) that demonstrates:
- Google sign-in with Android Credential Manager
- Google OIDC redirect sign-in
- email sign-in
- custom session lifetime input for expiry testing
- expired-session reauth UI
- wallet selection after sign-in
- message signing and verification
- transaction sending
## Build From Source
To enable the local pre-push Kotlin style gate for this checkout:
```sh
tools/install-git-hooks.sh
```
The hook runs `./gradlew ktlintCheck` before push. This is intentionally local
and is not wired into GitHub CI.
```sh
./gradlew :oms-client-kotlin-sdk:testDebugUnitTest
./gradlew ktlintCheck
./gradlew :oms-client-kotlin-sdk:lintDebug
./gradlew :app:lintDebug
./gradlew :app:assembleDebug
```
## Publishing
See [publishing.md](publishing.md) for release PR and Maven Central publishing
steps.