Android and Kotlin SDK for wallet, auth, signing, and API/indexer integrations.
Maven Central:
implementation("io.github.0xsequence:oms-client-kotlin-sdk:0.1.0-alpha.1")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.
- 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 through the indexer service
- unit formatting and parsing helpers for raw token amounts
- Android
minSdk 26 - Android
compileSdk 34or newer - Java 17 Android compile options
- Kotlin/Android app using the Android library module
- a valid
publicApiKey - a valid
projectId
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.
Create the SDK with the Android-friendly constructor:
val client = OMSClient(
context = context,
publicApiKey = "YOUR_PUBLIC_API_KEY",
projectId = "YOUR_PROJECT_ID",
)That constructor wires two separate Android-backed pieces. Wallet API requests
are authorized by a non-extractable Android Keystore P-256 credential
(ecdsa-p256-sha256) owned by the credential signer, so the private credential
key is never written to SDK session storage.
Session restore uses a separate metadata store. The SDK persists only
completed-session metadata in app-private no-backup storage: wallet id/address,
signer address/algorithm, expiry, login type, and optional session email. This
metadata is not wallet authorization material: by itself it cannot sign requests
or access a wallet. Restore succeeds only while the matching Keystore credential
still exists, and wallet operations must sign fresh requests with that
credential. publicApiKey is sent as the API access key, while projectId is
used as the WaaS signing scope.
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.
If you need a custom environment:
val client = OMSClient(
context = context,
publicApiKey = "YOUR_PUBLIC_API_KEY",
projectId = "YOUR_PROJECT_ID",
environment = OMSClientEnvironment(
walletApiUrl = "https://...",
apiRpcUrl = "https://...",
indexerUrlTemplate = "https://{value}-indexer.example.com/rpc/Indexer/",
),
)For demo or staging-style defaults:
val client = OMSClient(
context = context,
publicApiKey = "YOUR_PUBLIC_API_KEY",
projectId = "YOUR_PROJECT_ID",
environment = OMSClientEnvironment.demoDefaults(),
)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.
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:
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:
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").
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:
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:
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:
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:
val walletAddress = client.session.walletAddress
val expiresAt = client.session.expiresAt
val loginType = client.session.loginType
val sessionEmail = client.session.sessionEmailclient.session only reports completed wallet-session state. Pending auth
state, OIDC redirect verifier/state, and signer details are SDK internals. 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. 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:
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.
For raw token amount formatting and parsing:
val rawAmount = parseUnits("1.5", 18)
val displayAmount = formatUnits(rawAmount, 18)For indexer balance lookups:
val walletAddress = requireNotNull(client.wallet.walletAddress)
val nativeBalance = client.indexer.getNativeTokenBalance(
network = network,
walletAddress = walletAddress,
)
val tokenBalances = client.indexer.getTokenBalances(
network = network,
contractAddress = "0xTokenContract",
walletAddress = walletAddress,
includeMetadata = true,
)For raw calldata or transaction parameters beyond to and value, use the request overload:
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:
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")),
),
)If the prepared transaction returns fee options, pass a selector callback:
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)
FeeOptionSelection(token = selected.feeOption.token.symbol)
}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.
To refresh a transaction later or manage active wallet credentials:
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) }The full public API surface is documented in docs/api.md.
This repository includes an Android sample app in app/ that demonstrates:
- Google sign-in with Android Credential Manager
- email sign-in
- wallet selection after sign-in
- message signing and verification
- transaction sending
- a lower-level testbed for manual endpoint/config testing
To enable the local pre-push Kotlin style gate for this checkout:
tools/install-git-hooks.shThe hook runs ./gradlew ktlintCheck before push. This is intentionally local
and is not wired into GitHub CI.
./gradlew :oms-client-kotlin-sdk:testDebugUnitTest
./gradlew ktlintCheck
./gradlew :oms-client-kotlin-sdk:lintDebug
./gradlew :app:lintDebug
./gradlew :app:assembleDebug