mirror of
https://github.com/mollyim/monero-wallet-sdk.git
synced 2025-04-14 04:53:06 -04:00
lib: add in-process wallet provider
This commit is contained in:
parent
5f6f67c7fc
commit
1069160fef
@ -15,7 +15,7 @@ import java.io.OutputStream
|
||||
class MoneroSdkClient(private val context: Context) {
|
||||
|
||||
private val providerDeferred = CoroutineScope(Dispatchers.IO).async {
|
||||
WalletProvider.connect(context)
|
||||
SandboxedWalletProvider.connect(context)
|
||||
}
|
||||
|
||||
suspend fun createWallet(network: MoneroNetwork, filename: String): MoneroWallet {
|
||||
|
@ -5,10 +5,13 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<application android:usesCleartextTraffic="true">
|
||||
<service android:name=".service.InProcessWalletService" />
|
||||
|
||||
<service
|
||||
android:name=".WalletService"
|
||||
android:name=".service.SandboxedWalletService"
|
||||
android:isolatedProcess="true"
|
||||
android:process=":wallet_service" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
@ -1,11 +1,11 @@
|
||||
package im.molly.monero;
|
||||
package im.molly.monero.internal;
|
||||
|
||||
import im.molly.monero.IStorageAdapter;
|
||||
import im.molly.monero.IWalletServiceCallbacks;
|
||||
import im.molly.monero.IWalletServiceListener;
|
||||
import im.molly.monero.SecretKey;
|
||||
import im.molly.monero.WalletConfig;
|
||||
import im.molly.monero.internal.IHttpRpcClient;
|
||||
import im.molly.monero.internal.IWalletServiceCallbacks;
|
||||
import im.molly.monero.internal.IWalletServiceListener;
|
||||
|
||||
interface IWalletService {
|
||||
oneway void createWallet(in WalletConfig config, in IStorageAdapter storage, in IHttpRpcClient rpcClient, in IWalletServiceCallbacks callback);
|
@ -1,4 +1,4 @@
|
||||
package im.molly.monero;
|
||||
package im.molly.monero.internal;
|
||||
|
||||
import im.molly.monero.IWallet;
|
||||
|
@ -1,4 +1,4 @@
|
||||
package im.molly.monero;
|
||||
package im.molly.monero.internal;
|
||||
|
||||
oneway interface IWalletServiceListener {
|
||||
void onLogMessage(int priority, String tag, String msg, String cause);
|
@ -1,68 +1,17 @@
|
||||
package im.molly.monero
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
// TODO: Rename to SandboxedWalletProvider and extract interface, add InProcessWalletProvider
|
||||
class WalletProvider private constructor(
|
||||
private val context: Context,
|
||||
private val service: IWalletService,
|
||||
private val serviceConnection: ServiceConnection,
|
||||
// TODO: Remove DataStore dependencies if unused
|
||||
// private val dataStore: DataStore<WalletProto.State>,
|
||||
) {
|
||||
companion object {
|
||||
suspend fun connect(context: Context): WalletProvider {
|
||||
val (serviceConnection, service) = bindService(context)
|
||||
return WalletProvider(context, service, serviceConnection)
|
||||
}
|
||||
|
||||
private suspend fun bindService(context: Context): Pair<ServiceConnection, IWalletService> {
|
||||
val deferredService = CompletableDeferred<IWalletService>()
|
||||
val serviceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
|
||||
val service = IWalletService.Stub.asInterface(binder)
|
||||
service.setListener(WalletServiceListener)
|
||||
deferredService.complete(service)
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
deferredService.completeExceptionally(ServiceNotBoundException())
|
||||
}
|
||||
}
|
||||
Intent(context, WalletService::class.java).also { intent ->
|
||||
if (!context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)) {
|
||||
throw ServiceNotBoundException()
|
||||
}
|
||||
}
|
||||
return serviceConnection to deferredService.await()
|
||||
}
|
||||
}
|
||||
|
||||
/** Exception thrown by [WalletProvider] if the remote service can't be bound. */
|
||||
class ServiceNotBoundException : Exception()
|
||||
|
||||
private val logger = loggerFor<WalletProvider>()
|
||||
import im.molly.monero.internal.WalletServiceClient
|
||||
import im.molly.monero.service.InProcessWalletService
|
||||
import im.molly.monero.service.SandboxedWalletService
|
||||
import java.io.Closeable
|
||||
|
||||
interface WalletProvider : Closeable {
|
||||
suspend fun createNewWallet(
|
||||
network: MoneroNetwork,
|
||||
dataStore: WalletDataStore? = null,
|
||||
client: MoneroNodeClient? = null,
|
||||
): MoneroWallet {
|
||||
require(client == null || client.network == network)
|
||||
val storageAdapter = StorageAdapter(dataStore)
|
||||
val wallet = suspendCancellableCoroutine { continuation ->
|
||||
service.createWallet(
|
||||
buildConfig(network), storageAdapter, client?.httpRpcClient,
|
||||
WalletResultCallback(continuation),
|
||||
)
|
||||
}
|
||||
return MoneroWallet(wallet, storageAdapter, client)
|
||||
}
|
||||
): MoneroWallet
|
||||
|
||||
suspend fun restoreWallet(
|
||||
network: MoneroNetwork,
|
||||
@ -70,70 +19,44 @@ class WalletProvider private constructor(
|
||||
client: MoneroNodeClient? = null,
|
||||
secretSpendKey: SecretKey,
|
||||
restorePoint: RestorePoint,
|
||||
): MoneroWallet {
|
||||
require(client == null || client.network == network)
|
||||
if (restorePoint is BlockchainTime) {
|
||||
require(restorePoint.network == network)
|
||||
}
|
||||
val storageAdapter = StorageAdapter(dataStore)
|
||||
val wallet = suspendCancellableCoroutine { continuation ->
|
||||
service.restoreWallet(
|
||||
buildConfig(network), storageAdapter, client?.httpRpcClient,
|
||||
WalletResultCallback(continuation),
|
||||
secretSpendKey,
|
||||
restorePoint.toLong(),
|
||||
)
|
||||
}
|
||||
return MoneroWallet(wallet, storageAdapter, client)
|
||||
}
|
||||
): MoneroWallet
|
||||
|
||||
suspend fun openWallet(
|
||||
network: MoneroNetwork,
|
||||
dataStore: WalletDataStore,
|
||||
client: MoneroNodeClient? = null,
|
||||
): MoneroWallet {
|
||||
require(client == null || client.network == network)
|
||||
val storageAdapter = StorageAdapter(dataStore)
|
||||
val wallet = suspendCancellableCoroutine { continuation ->
|
||||
service.openWallet(
|
||||
buildConfig(network), storageAdapter, client?.httpRpcClient,
|
||||
WalletResultCallback(continuation),
|
||||
)
|
||||
}
|
||||
return MoneroWallet(wallet, storageAdapter, client)
|
||||
): MoneroWallet
|
||||
|
||||
fun disconnect()
|
||||
|
||||
override fun close() {
|
||||
disconnect()
|
||||
}
|
||||
|
||||
private fun buildConfig(network: MoneroNetwork): WalletConfig {
|
||||
return WalletConfig(network.id)
|
||||
}
|
||||
/** Exception thrown if the wallet service cannot be bound. */
|
||||
class ServiceNotBoundException : Exception()
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
context.unbindService(serviceConnection)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private class WalletResultCallback(
|
||||
private val continuation: CancellableContinuation<IWallet>
|
||||
) : IWalletServiceCallbacks.Stub() {
|
||||
override fun onWalletResult(wallet: IWallet?) {
|
||||
when {
|
||||
wallet != null -> {
|
||||
continuation.resume(wallet) {
|
||||
wallet.close()
|
||||
}
|
||||
}
|
||||
|
||||
else -> TODO()
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Provides wallet services using a sandboxed process.
|
||||
*/
|
||||
object SandboxedWalletProvider {
|
||||
/**
|
||||
* Connects to the sandboxed wallet service and returns a connected [WalletProvider].
|
||||
*/
|
||||
suspend fun connect(context: Context): WalletProvider {
|
||||
return WalletServiceClient.connect(context, SandboxedWalletService::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
object WalletServiceListener : IWalletServiceListener.Stub() {
|
||||
override fun onLogMessage(priority: Int, tag: String, msg: String, cause: String?) {
|
||||
if (Logger.adapter.isLoggable(priority, tag)) {
|
||||
val tr = if (cause != null) Throwable(cause) else null
|
||||
Logger.adapter.print(priority, tag, msg, tr)
|
||||
}
|
||||
/**
|
||||
* Provides wallet services using an in-process bound service.
|
||||
*/
|
||||
object InProcessWalletProvider {
|
||||
/**
|
||||
* Connects to the in-process wallet service and returns a connected [WalletProvider].
|
||||
*/
|
||||
suspend fun connect(context: Context): WalletProvider {
|
||||
return WalletServiceClient.connect(context, InProcessWalletService::class.java)
|
||||
}
|
||||
}
|
||||
|
@ -1,46 +1,42 @@
|
||||
package im.molly.monero
|
||||
package im.molly.monero.internal
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import im.molly.monero.internal.IHttpRpcClient
|
||||
import kotlinx.coroutines.*
|
||||
import im.molly.monero.IStorageAdapter
|
||||
import im.molly.monero.IWallet
|
||||
import im.molly.monero.LogAdapter
|
||||
import im.molly.monero.NativeLoader
|
||||
import im.molly.monero.SecretKey
|
||||
import im.molly.monero.WalletConfig
|
||||
import im.molly.monero.WalletNative
|
||||
import im.molly.monero.loggerFor
|
||||
import im.molly.monero.randomSecretKey
|
||||
import im.molly.monero.setLoggingAdapter
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class WalletService : LifecycleService() {
|
||||
private lateinit var service: IWalletService
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
service = WalletServiceImpl(application.isIsolatedProcess(), lifecycleScope)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
super.onBind(intent)
|
||||
return service.asBinder()
|
||||
}
|
||||
}
|
||||
|
||||
internal class WalletServiceImpl(
|
||||
isIsolated: Boolean,
|
||||
internal class NativeWalletService(
|
||||
private val serviceScope: CoroutineScope,
|
||||
) : IWalletService.Stub(), LogAdapter {
|
||||
|
||||
private val logger = loggerFor<WalletService>()
|
||||
private val logger = loggerFor<NativeWalletService>()
|
||||
|
||||
init {
|
||||
if (isIsolated) {
|
||||
setLoggingAdapter(this)
|
||||
}
|
||||
NativeLoader.loadWalletLibrary(logger = logger)
|
||||
}
|
||||
|
||||
fun configureLoggingAdapter() {
|
||||
setLoggingAdapter(this)
|
||||
}
|
||||
|
||||
private var listener: IWalletServiceListener? = null
|
||||
|
||||
override fun setListener(l: IWalletServiceListener) {
|
||||
listener = l
|
||||
}
|
||||
|
||||
override fun print(priority: Int, tag: String, msg: String?, tr: Throwable?) {
|
||||
listener?.onLogMessage(priority, tag, msg, tr?.toString())
|
||||
}
|
||||
|
||||
override fun createWallet(
|
||||
config: WalletConfig,
|
||||
storage: IStorageAdapter,
|
||||
@ -105,8 +101,4 @@ internal class WalletServiceImpl(
|
||||
coroutineContext = serviceScope.coroutineContext,
|
||||
)
|
||||
}
|
||||
|
||||
override fun print(priority: Int, tag: String, msg: String?, tr: Throwable?) {
|
||||
listener?.onLogMessage(priority, tag, msg, tr?.toString())
|
||||
}
|
||||
}
|
@ -0,0 +1,160 @@
|
||||
package im.molly.monero.internal
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import im.molly.monero.BlockchainTime
|
||||
import im.molly.monero.IWallet
|
||||
import im.molly.monero.MoneroNetwork
|
||||
import im.molly.monero.MoneroNodeClient
|
||||
import im.molly.monero.MoneroWallet
|
||||
import im.molly.monero.RestorePoint
|
||||
import im.molly.monero.SecretKey
|
||||
import im.molly.monero.StorageAdapter
|
||||
import im.molly.monero.WalletConfig
|
||||
import im.molly.monero.WalletDataStore
|
||||
import im.molly.monero.WalletProvider
|
||||
import im.molly.monero.loggerFor
|
||||
import im.molly.monero.service.BaseWalletService
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
||||
internal class WalletServiceClient(
|
||||
private val context: Context,
|
||||
private val service: IWalletService,
|
||||
private val serviceConnection: ServiceConnection,
|
||||
) : WalletProvider {
|
||||
|
||||
companion object {
|
||||
suspend fun connect(
|
||||
context: Context,
|
||||
serviceClass: Class<out BaseWalletService>
|
||||
): WalletServiceClient {
|
||||
val (serviceConnection, service) = bindService(context, serviceClass)
|
||||
return WalletServiceClient(context, service, serviceConnection)
|
||||
}
|
||||
|
||||
private suspend fun bindService(
|
||||
context: Context,
|
||||
serviceClass: Class<out BaseWalletService>,
|
||||
): Pair<ServiceConnection, IWalletService> {
|
||||
val deferredService = CompletableDeferred<IWalletService>()
|
||||
val serviceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
|
||||
val service = IWalletService.Stub.asInterface(binder).apply {
|
||||
setListener(WalletServiceLogListener)
|
||||
}
|
||||
deferredService.complete(service)
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
deferredService.completeExceptionally(WalletProvider.ServiceNotBoundException())
|
||||
}
|
||||
}
|
||||
|
||||
val intent = Intent(context, serviceClass)
|
||||
if (!context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)) {
|
||||
throw WalletProvider.ServiceNotBoundException()
|
||||
}
|
||||
|
||||
return serviceConnection to deferredService.await()
|
||||
}
|
||||
}
|
||||
|
||||
private val logger = loggerFor<WalletServiceClient>()
|
||||
|
||||
override suspend fun createNewWallet(
|
||||
network: MoneroNetwork,
|
||||
dataStore: WalletDataStore?,
|
||||
client: MoneroNodeClient?,
|
||||
): MoneroWallet {
|
||||
validateClientNetwork(client, network)
|
||||
|
||||
val storageAdapter = StorageAdapter(dataStore)
|
||||
val wallet = suspendCancellableCoroutine { continuation ->
|
||||
service.createWallet(
|
||||
buildConfig(network), storageAdapter, client?.httpRpcClient,
|
||||
WalletResultCallback(continuation),
|
||||
)
|
||||
}
|
||||
return MoneroWallet(wallet, storageAdapter, client)
|
||||
}
|
||||
|
||||
override suspend fun restoreWallet(
|
||||
network: MoneroNetwork,
|
||||
dataStore: WalletDataStore?,
|
||||
client: MoneroNodeClient?,
|
||||
secretSpendKey: SecretKey,
|
||||
restorePoint: RestorePoint,
|
||||
): MoneroWallet {
|
||||
validateClientNetwork(client, network)
|
||||
validateRestorePoint(restorePoint, network)
|
||||
|
||||
val storageAdapter = StorageAdapter(dataStore)
|
||||
val wallet = suspendCancellableCoroutine { continuation ->
|
||||
service.restoreWallet(
|
||||
buildConfig(network), storageAdapter, client?.httpRpcClient,
|
||||
WalletResultCallback(continuation),
|
||||
secretSpendKey,
|
||||
restorePoint.toLong(),
|
||||
)
|
||||
}
|
||||
return MoneroWallet(wallet, storageAdapter, client)
|
||||
}
|
||||
|
||||
override suspend fun openWallet(
|
||||
network: MoneroNetwork,
|
||||
dataStore: WalletDataStore,
|
||||
client: MoneroNodeClient?,
|
||||
): MoneroWallet {
|
||||
validateClientNetwork(client, network)
|
||||
|
||||
val storageAdapter = StorageAdapter(dataStore)
|
||||
val wallet = suspendCancellableCoroutine { continuation ->
|
||||
service.openWallet(
|
||||
buildConfig(network), storageAdapter, client?.httpRpcClient,
|
||||
WalletResultCallback(continuation),
|
||||
)
|
||||
}
|
||||
return MoneroWallet(wallet, storageAdapter, client)
|
||||
}
|
||||
|
||||
private fun buildConfig(network: MoneroNetwork): WalletConfig {
|
||||
return WalletConfig(network.id)
|
||||
}
|
||||
|
||||
private fun validateClientNetwork(client: MoneroNodeClient?, network: MoneroNetwork) {
|
||||
require(client == null || client.network == network) {
|
||||
"Client network mismatch: expected $network, got ${client?.network}"
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateRestorePoint(restorePoint: RestorePoint, network: MoneroNetwork) {
|
||||
if (restorePoint is BlockchainTime) {
|
||||
require(restorePoint.network == network) {
|
||||
"Restore point network mismatch: expected $network, got ${restorePoint.network}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun disconnect() {
|
||||
context.unbindService(serviceConnection)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private class WalletResultCallback(
|
||||
private val continuation: CancellableContinuation<IWallet>
|
||||
) : IWalletServiceCallbacks.Stub() {
|
||||
override fun onWalletResult(wallet: IWallet?) {
|
||||
if (wallet != null) {
|
||||
continuation.resume(wallet) { wallet.close() }
|
||||
} else {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package im.molly.monero.internal
|
||||
|
||||
import im.molly.monero.Logger
|
||||
|
||||
internal object WalletServiceLogListener : IWalletServiceListener.Stub() {
|
||||
override fun onLogMessage(priority: Int, tag: String, msg: String, cause: String?) {
|
||||
if (Logger.adapter.isLoggable(priority, tag)) {
|
||||
val tr = if (cause != null) Throwable(cause) else null
|
||||
Logger.adapter.print(priority, tag, msg, tr)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package im.molly.monero.service
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import im.molly.monero.internal.IWalletService
|
||||
import im.molly.monero.internal.NativeWalletService
|
||||
|
||||
abstract class BaseWalletService(private val isolated: Boolean) : LifecycleService() {
|
||||
private val service: IWalletService by lazy {
|
||||
NativeWalletService(lifecycleScope).apply {
|
||||
if (isolated) {
|
||||
configureLoggingAdapter()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
super.onBind(intent)
|
||||
return service.asBinder()
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
package im.molly.monero.service
|
||||
|
||||
class InProcessWalletService : BaseWalletService(isolated = false)
|
@ -0,0 +1,3 @@
|
||||
package im.molly.monero.service
|
||||
|
||||
class SandboxedWalletService : BaseWalletService(isolated = true)
|
Loading…
x
Reference in New Issue
Block a user