mirror of
https://github.com/mollyim/monero-wallet-sdk.git
synced 2025-04-24 01:19:25 -04:00
Compare commits
17 Commits
snapshot-2
...
main
Author | SHA1 | Date | |
---|---|---|---|
![]() |
cb79e3421d | ||
![]() |
16ff7b06db | ||
![]() |
d05a056698 | ||
![]() |
301f1efc1c | ||
![]() |
a664ce1652 | ||
![]() |
181b3dc442 | ||
![]() |
9b71aae6c5 | ||
![]() |
9f54eadc61 | ||
![]() |
8dbf0ffb6b | ||
![]() |
874f777deb | ||
![]() |
87d70c1e4f | ||
![]() |
194d6d184e | ||
![]() |
37b68d6062 | ||
![]() |
6bf3174d52 | ||
![]() |
b3fb65b858 | ||
![]() |
30ef8b481b | ||
![]() |
308031250b |
.github/workflows
build-logic
build.gradlebuild.gradle.ktsdemo/android
gradle.propertiesgradle
gradlewgradlew.batlib/android
build.gradlebuild.gradle.kts
settings.gradlesettings.gradle.ktssrc
androidTest/kotlin/im/molly/monero
main
aidl/im/molly/monero
HttpResponse.aidlIHttpRequestCallback.aidlIRemoteNodeClient.aidlIWalletClient.aidlIWalletService.aidl
internal
cpp
boost
monero/wallet2
wallet
kotlin/im/molly/monero
test/kotlin/im/molly/monero
vendor
44
.github/workflows/test.yml
vendored
Normal file
44
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
- '.github/FUNDING.yml'
|
||||
- '.github/ISSUE_TEMPLATE/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
wrapper_validation:
|
||||
name: Validate Gradle wrapper
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Run wrapper validation
|
||||
uses: gradle/actions/wrapper-validation@v3
|
||||
|
||||
test:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
cache: gradle
|
||||
|
||||
- name: Run tests
|
||||
run: ./gradlew build --no-daemon
|
28
build-logic/plugins/build.gradle.kts
Normal file
28
build-logic/plugins/build.gradle.kts
Normal file
@ -0,0 +1,28 @@
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.android.gradle.plugin)
|
||||
compileOnly(libs.kotlin.gradle.plugin)
|
||||
}
|
||||
|
||||
tasks {
|
||||
validatePlugins {
|
||||
enableStricterValidation = true
|
||||
failOnWarning = true
|
||||
}
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
register("androidLibraryJacoco") {
|
||||
id = libs.plugins.sdk.android.library.jacoco.get().pluginId
|
||||
implementationClass = "AndroidLibraryJacocoPlugin"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import com.android.build.api.variant.LibraryAndroidComponentsExtension
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
|
||||
class AndroidLibraryJacocoPlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
apply(plugin = "jacoco")
|
||||
configureJacoco(extensions.getByType<LibraryAndroidComponentsExtension>())
|
||||
}
|
||||
}
|
||||
}
|
92
build-logic/plugins/src/main/kotlin/Jacoco.kt
Normal file
92
build-logic/plugins/src/main/kotlin/Jacoco.kt
Normal file
@ -0,0 +1,92 @@
|
||||
import com.android.build.api.artifact.ScopedArtifact
|
||||
import com.android.build.api.variant.AndroidComponentsExtension
|
||||
import com.android.build.api.variant.ScopedArtifacts
|
||||
import com.android.build.api.variant.SourceDirectories
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.file.Directory
|
||||
import org.gradle.api.file.RegularFile
|
||||
import org.gradle.api.provider.ListProperty
|
||||
import org.gradle.api.provider.Provider
|
||||
import org.gradle.kotlin.dsl.assign
|
||||
import org.gradle.kotlin.dsl.register
|
||||
import org.gradle.testing.jacoco.tasks.JacocoReport
|
||||
import java.util.Locale
|
||||
|
||||
private val coverageExclusions = listOf(
|
||||
// Android
|
||||
"**/R.class",
|
||||
"**/R\$*.class",
|
||||
"**/BuildConfig.*",
|
||||
"**/Manifest*.*",
|
||||
)
|
||||
|
||||
private fun String.capitalize() = replaceFirstChar {
|
||||
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new task that generates a combined coverage report with data from local and
|
||||
* instrumented tests.
|
||||
*
|
||||
* `create{variant}CombinedCoverageReport`
|
||||
*
|
||||
* Note that coverage data must exist before running the task. This allows us to run device
|
||||
* tests on CI using a different Github Action or an external device farm.
|
||||
*/
|
||||
internal fun Project.configureJacoco(
|
||||
androidComponentsExtension: AndroidComponentsExtension<*, *, *>,
|
||||
) {
|
||||
androidComponentsExtension.onVariants { variant ->
|
||||
val myObjFactory = project.objects
|
||||
val buildDir = layout.buildDirectory.get().asFile
|
||||
val allJars: ListProperty<RegularFile> = myObjFactory.listProperty(RegularFile::class.java)
|
||||
val allDirectories: ListProperty<Directory> =
|
||||
myObjFactory.listProperty(Directory::class.java)
|
||||
val reportTask =
|
||||
tasks.register(
|
||||
"create${variant.name.capitalize()}CombinedCoverageReport",
|
||||
JacocoReport::class,
|
||||
) {
|
||||
|
||||
classDirectories.setFrom(
|
||||
allJars,
|
||||
allDirectories.map { dirs ->
|
||||
dirs.map { dir ->
|
||||
myObjFactory.fileTree().setDir(dir).exclude(coverageExclusions)
|
||||
}
|
||||
},
|
||||
)
|
||||
reports {
|
||||
html.required = true
|
||||
xml.required = true
|
||||
}
|
||||
|
||||
fun SourceDirectories.Flat?.toFilePaths(): Provider<List<String>> = this
|
||||
?.all
|
||||
?.map { directories -> directories.map { it.asFile.path } }
|
||||
?: provider { emptyList() }
|
||||
sourceDirectories.setFrom(
|
||||
files(
|
||||
variant.sources.java.toFilePaths(),
|
||||
variant.sources.kotlin.toFilePaths()
|
||||
),
|
||||
)
|
||||
|
||||
executionData.setFrom(
|
||||
project.fileTree("$buildDir/outputs/unit_test_code_coverage/${variant.name}UnitTest")
|
||||
.matching { include("**/*.exec") },
|
||||
|
||||
project.fileTree("$buildDir/outputs/code_coverage/${variant.name}AndroidTest")
|
||||
.matching { include("**/*.ec") },
|
||||
)
|
||||
}
|
||||
|
||||
variant.artifacts.forScope(ScopedArtifacts.Scope.PROJECT)
|
||||
.use(reportTask)
|
||||
.toGet(
|
||||
ScopedArtifact.CLASSES,
|
||||
{ _ -> allJars },
|
||||
{ _ -> allDirectories },
|
||||
)
|
||||
}
|
||||
}
|
28
build-logic/settings.gradle.kts
Normal file
28
build-logic/settings.gradle.kts
Normal file
@ -0,0 +1,28 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
google()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositories {
|
||||
google {
|
||||
content {
|
||||
includeGroupByRegex("com\\.android(\\..*)?")
|
||||
includeGroupByRegex("com\\.google(\\..*)?")
|
||||
includeGroupByRegex("androidx?(\\..*)?")
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
}
|
||||
versionCatalogs {
|
||||
create("libs") {
|
||||
from(files("../gradle/libs.versions.toml"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
include(":plugins")
|
||||
|
||||
rootProject.name = "build-logic"
|
28
build.gradle
28
build.gradle
@ -1,28 +0,0 @@
|
||||
plugins {
|
||||
id('com.android.application') version '8.1.2' apply false
|
||||
id('com.android.library') version '8.1.2' apply false
|
||||
id('org.jetbrains.kotlin.android') version '1.9.10' apply false
|
||||
id('org.jetbrains.kotlin.plugin.parcelize') version '1.9.10' apply false
|
||||
id('com.google.devtools.ksp') version '1.9.10-1.0.13' apply false
|
||||
id('com.google.protobuf') version '0.9.4' apply false
|
||||
}
|
||||
|
||||
wrapper {
|
||||
distributionType = Wrapper.DistributionType.ALL
|
||||
}
|
||||
|
||||
allprojects {
|
||||
group = 'im.molly'
|
||||
|
||||
ext {
|
||||
gitVersion = gitVersion()
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
||||
static def gitVersion() {
|
||||
return 'git describe --tags --always --first-parent'.execute().text.trim()
|
||||
}
|
19
build.gradle.kts
Normal file
19
build.gradle.kts
Normal file
@ -0,0 +1,19 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.android.library) apply false
|
||||
alias(libs.plugins.androidx.room) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
alias(libs.plugins.kotlin.compose) apply false
|
||||
alias(libs.plugins.kotlin.parcelize) apply false
|
||||
alias(libs.plugins.ksp) apply false
|
||||
}
|
||||
|
||||
allprojects {
|
||||
group = "im.molly.monero.sdk"
|
||||
}
|
||||
|
||||
tasks {
|
||||
wrapper {
|
||||
distributionType = Wrapper.DistributionType.ALL
|
||||
}
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'org.jetbrains.kotlin.android'
|
||||
apply plugin: 'com.google.devtools.ksp'
|
||||
|
||||
ext {
|
||||
composeVersion = '1.6.2'
|
||||
composeCompilerVersion = '1.5.3'
|
||||
lifecycleVersion = '2.7.0'
|
||||
navigationVersion = '2.7.7'
|
||||
roomVersion = '2.6.1'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "im.molly.monero.demo"
|
||||
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId "im.molly.monero.demo"
|
||||
minSdk 26
|
||||
targetSdk 34
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary true
|
||||
}
|
||||
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion composeCompilerVersion
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += '/META-INF/{AL2.0,LGPL2.1}'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.core:core-ktx:1.9.0"
|
||||
implementation "androidx.compose.ui:ui:$composeVersion"
|
||||
implementation "androidx.compose.material3:material3:1.2.0"
|
||||
implementation "androidx.compose.ui:ui-tooling-preview:$composeVersion"
|
||||
implementation "com.google.accompanist:accompanist-systemuicontroller:0.32.0"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion"
|
||||
implementation "androidx.activity:activity-compose:1.8.2"
|
||||
implementation "androidx.datastore:datastore-preferences:1.0.0"
|
||||
implementation "androidx.room:room-runtime:$roomVersion"
|
||||
implementation "androidx.room:room-ktx:$roomVersion"
|
||||
implementation "androidx.navigation:navigation-runtime-ktx:$navigationVersion"
|
||||
implementation "androidx.navigation:navigation-compose:$navigationVersion"
|
||||
ksp "androidx.room:room-compiler:$roomVersion"
|
||||
|
||||
implementation project(':lib')
|
||||
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
|
||||
androidTestImplementation "androidx.test.ext:junit:1.1.5"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1"
|
||||
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$composeVersion"
|
||||
|
||||
debugImplementation "androidx.compose.ui:ui-tooling:$composeVersion"
|
||||
debugImplementation "androidx.compose.ui:ui-test-manifest:$composeVersion"
|
||||
}
|
84
demo/android/build.gradle.kts
Normal file
84
demo/android/build.gradle.kts
Normal file
@ -0,0 +1,84 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.androidx.room)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
alias(libs.plugins.ksp)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "im.molly.monero.demo"
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "im.molly.monero.demo"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
merges += "META-INF/LICENSE.md"
|
||||
merges += "META-INF/LICENSE-notice.md"
|
||||
}
|
||||
}
|
||||
|
||||
room {
|
||||
schemaDirectory("$projectDir/schemas")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib"))
|
||||
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.lifecycle.service)
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.ui)
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
|
||||
implementation(libs.androidx.room.ktx)
|
||||
runtimeOnly(libs.androidx.room.runtime)
|
||||
ksp(libs.androidx.room.compiler)
|
||||
|
||||
testImplementation(testLibs.junit)
|
||||
|
||||
androidTestImplementation(testLibs.androidx.test.core)
|
||||
androidTestImplementation(testLibs.androidx.test.junit)
|
||||
androidTestImplementation(testLibs.androidx.test.runner)
|
||||
}
|
4
demo/android/lint.xml
Normal file
4
demo/android/lint.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lint>
|
||||
<issue id="UnusedMaterial3ScaffoldPaddingParameter" severity="ignore" />
|
||||
</lint>
|
@ -7,12 +7,12 @@ import android.content.ServiceConnection
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import im.molly.monero.demo.service.SyncService
|
||||
import im.molly.monero.demo.ui.DemoApp
|
||||
import im.molly.monero.demo.ui.theme.AppTheme
|
||||
@ -27,15 +27,19 @@ class MainActivity : ComponentActivity() {
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
setContent {
|
||||
val systemUiController = rememberSystemUiController()
|
||||
val darkTheme = isSystemInDarkTheme()
|
||||
|
||||
DisposableEffect(systemUiController, darkTheme) {
|
||||
systemUiController.setSystemBarsColor(
|
||||
color = Color.Transparent,
|
||||
darkIcons = !darkTheme,
|
||||
DisposableEffect(darkTheme) {
|
||||
enableEdgeToEdge(
|
||||
statusBarStyle = SystemBarStyle.auto(
|
||||
android.graphics.Color.TRANSPARENT,
|
||||
android.graphics.Color.TRANSPARENT,
|
||||
) { darkTheme },
|
||||
navigationBarStyle = SystemBarStyle.auto(
|
||||
lightScrim,
|
||||
darkScrim,
|
||||
) { darkTheme },
|
||||
)
|
||||
|
||||
onDispose {}
|
||||
}
|
||||
|
||||
@ -67,3 +71,7 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val lightScrim = android.graphics.Color.argb(0xe6, 0xFF, 0xFF, 0xFF)
|
||||
|
||||
private val darkScrim = android.graphics.Color.argb(0x80, 0x1b, 0x1b, 0x1b)
|
||||
|
@ -52,7 +52,7 @@ class MoneroSdkClient(private val context: Context) {
|
||||
httpClient: OkHttpClient,
|
||||
): MoneroWallet {
|
||||
val dataStore = WalletDataStoreFile(filename)
|
||||
val client = RemoteNodeClient.forNetwork(
|
||||
val client = MoneroNodeClient.forNetwork(
|
||||
network = network,
|
||||
remoteNodes = remoteNodes,
|
||||
loadBalancerRule = RoundRobinRule(),
|
||||
|
@ -51,8 +51,8 @@ class WalletRepository(
|
||||
|
||||
fun getWalletIdList() = walletDataSource.readWalletIdList()
|
||||
|
||||
fun getRemoteClients(): Flow<List<RemoteNodeClient>> =
|
||||
getWalletIdList().map { it.mapNotNull { walletId -> getWallet(walletId).remoteNodeClient } }
|
||||
fun getMoneroNodeClients(): Flow<List<MoneroNodeClient>> =
|
||||
getWalletIdList().map { it.mapNotNull { walletId -> getWallet(walletId).moneroNodeClient } }
|
||||
|
||||
fun getWalletConfig(walletId: Long) = walletDataSource.readWalletConfig(walletId)
|
||||
|
||||
|
@ -12,14 +12,12 @@ import im.molly.monero.demo.data.WalletRepository
|
||||
import im.molly.monero.demo.data.model.DefaultMoneroNetwork
|
||||
import im.molly.monero.demo.data.model.RemoteNode
|
||||
import im.molly.monero.mnemonics.MoneroMnemonic
|
||||
import im.molly.monero.util.parseHex
|
||||
import im.molly.monero.util.toHex
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.LocalDate
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalStdlibApi::class)
|
||||
class AddWalletViewModel(
|
||||
private val remoteNodeRepository: RemoteNodeRepository = AppModule.remoteNodeRepository,
|
||||
private val walletRepository: WalletRepository = AppModule.walletRepository,
|
||||
@ -66,7 +64,7 @@ class AddWalletViewModel(
|
||||
MoneroMnemonic.recoverEntropy(words)?.use { mnemonicCode ->
|
||||
val secretKey = SecretKey(mnemonicCode.entropy)
|
||||
viewModelState.update {
|
||||
it.copy(secretSpendKeyHex = secretKey.bytes.toHex())
|
||||
it.copy(secretSpendKeyHex = secretKey.bytes.toHexString())
|
||||
}
|
||||
secretKey.destroy()
|
||||
return true
|
||||
@ -85,7 +83,7 @@ class AddWalletViewModel(
|
||||
fun validateSecretSpendKeyHex(): Boolean =
|
||||
with(viewModelState.value) {
|
||||
return secretSpendKeyHex.length == 64 && runCatching {
|
||||
secretSpendKeyHex.parseHex()
|
||||
secretSpendKeyHex.hexToByteArray()
|
||||
}.isSuccess
|
||||
}
|
||||
|
||||
@ -125,7 +123,7 @@ class AddWalletViewModel(
|
||||
|
||||
else -> RestorePoint.Genesis
|
||||
}
|
||||
SecretKey(state.secretSpendKeyHex.parseHex()).use { secretSpendKey ->
|
||||
SecretKey(state.secretSpendKeyHex.hexToByteArray()).use { secretSpendKey ->
|
||||
walletRepository.restoreWallet(
|
||||
state.network,
|
||||
state.walletName,
|
||||
|
@ -82,7 +82,7 @@ class SettingsViewModel(
|
||||
}
|
||||
|
||||
private suspend fun onProxyChanged(newProxy: Proxy) {
|
||||
walletRepository.getRemoteClients().first().forEach { client ->
|
||||
walletRepository.getMoneroNodeClients().first().forEach { client ->
|
||||
val current = client.httpClient.proxy
|
||||
if (current != newProxy) {
|
||||
val builder = client.httpClient.newBuilder()
|
||||
|
@ -1,4 +1,3 @@
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
android.native.buildOutput=verbose
|
||||
|
48
gradle/libs.versions.toml
Normal file
48
gradle/libs.versions.toml
Normal file
@ -0,0 +1,48 @@
|
||||
[versions]
|
||||
agp = "8.8.0"
|
||||
androidx-activity = "1.10.0"
|
||||
androidx-core-ktx = "1.15.0"
|
||||
androidx-datastore = "1.1.2"
|
||||
androidx-lifecycle = "2.8.7"
|
||||
androidx-navigation = "2.8.5"
|
||||
compose-bom = "2025.01.00"
|
||||
kotlin = "2.1.0"
|
||||
kotlinx-coroutines = '1.9.0'
|
||||
ksp = "2.1.0-1.0.29"
|
||||
okhttp = "4.10.0"
|
||||
room = "2.6.1"
|
||||
|
||||
[libraries]
|
||||
android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "agp" }
|
||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
|
||||
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" }
|
||||
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core-ktx" }
|
||||
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" }
|
||||
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" }
|
||||
androidx-lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "androidx-lifecycle" }
|
||||
androidx-material3 = { module = "androidx.compose.material3:material3" }
|
||||
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
|
||||
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
||||
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
|
||||
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
|
||||
androidx-ui = { module = "androidx.compose.ui:ui" }
|
||||
androidx-ui-graphics = { module = "androidx.compose.ui:ui-graphics" }
|
||||
androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
||||
kotlin-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
|
||||
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines"}
|
||||
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
|
||||
okhttp = { module = "com.squareup.okhttp3:okhttp" }
|
||||
okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttp" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
android-library = { id = "com.android.library", version.ref = "agp" }
|
||||
androidx-room = { id = "androidx.room", version.ref = "room" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
|
||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||
|
||||
# Plugins defined by this project
|
||||
sdk-android-library-jacoco = { id ="sdk.android.library.jacoco" }
|
18
gradle/test-libs.versions.toml
Normal file
18
gradle/test-libs.versions.toml
Normal file
@ -0,0 +1,18 @@
|
||||
[versions]
|
||||
androidx-test-core = "1.6.1"
|
||||
androidx-test-junit = "1.2.1"
|
||||
androidx-test-runner = "1.6.2"
|
||||
androidx-test-truth = "1.6.0"
|
||||
junit = "4.13.2"
|
||||
mockk = "1.13.2"
|
||||
truth = "1.1.3"
|
||||
|
||||
[libraries]
|
||||
androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test-core" }
|
||||
androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-junit" }
|
||||
androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test-core" }
|
||||
androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" }
|
||||
androidx-test-truth = { module = "androidx.test.ext:truth", version.ref = "androidx-test-truth" }
|
||||
junit = { module = "junit:junit", version.ref = "junit" }
|
||||
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
|
||||
truth = { module = "com.google.truth:truth", version.ref = "truth" }
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=47a5bfed9ef814f90f8debcbbb315e8e7c654109acd224595ea39fca95c5d4da
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip
|
||||
distributionSha256Sum=2ab88d6de2c23e6adae7363ae6e29cbdd2a709e992929b48b6530fd0c7133bd6
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
37
gradlew
vendored
37
gradlew
vendored
@ -15,6 +15,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@ -55,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@ -80,13 +82,11 @@ do
|
||||
esac
|
||||
done
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@ -133,22 +133,29 @@ location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@ -193,11 +200,15 @@ if "$cygwin" || "$msys" ; then
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
|
23
gradlew.bat
vendored
23
gradlew.bat
vendored
@ -13,6 +13,8 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@ -26,6 +28,7 @@ if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@ -42,11 +45,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@ -56,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
|
@ -1,142 +0,0 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'org.jetbrains.kotlin.android'
|
||||
apply plugin: 'org.jetbrains.kotlin.plugin.parcelize'
|
||||
apply plugin: 'com.google.protobuf'
|
||||
//apply from: 'deploy.gradle' // FIXME
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = 'com.google.protobuf:protoc:3.20.1'
|
||||
}
|
||||
generateProtoTasks {
|
||||
all().each { task ->
|
||||
task.builtins {
|
||||
java {
|
||||
option "lite"
|
||||
}
|
||||
kotlin {
|
||||
option "lite"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "im.molly.monero"
|
||||
|
||||
buildToolsVersion '33.0.1'
|
||||
compileSdk 33
|
||||
|
||||
ndkVersion '23.1.7779620'
|
||||
|
||||
defaultConfig {
|
||||
minSdk 26
|
||||
targetSdk 33
|
||||
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
consumerProguardFiles 'consumer-rules.pro'
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments "-DVENDOR_DIR=${rootDir}/vendor"
|
||||
arguments "-DDOWNLOAD_CACHE=${buildDir}/downloads"
|
||||
cppFlags ''
|
||||
}
|
||||
}
|
||||
|
||||
ndk {
|
||||
// abiFilters /*'armeabi-v7a', 'arm64-v8a',*/ 'x86_64' // FIXME
|
||||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86_64'
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path file('src/main/cpp/CMakeLists.txt')
|
||||
version '3.22.1'
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
aidl true
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
// testOptions {
|
||||
// managedDevices {
|
||||
// devices {
|
||||
// // run with ../gradlew nexusOneApi30DebugAndroidTest
|
||||
// nexusOneApi30(ManagedVirtualDevice) {
|
||||
// // A lower resolution device is used here for better emulator performance
|
||||
// device = "Nexus One"
|
||||
// apiLevel = 30
|
||||
// // Also use the AOSP ATD image for better emulator performance
|
||||
// systemImageSource = "aosp-atd"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api 'com.squareup.okhttp3:okhttp:4.10.0'
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.9.0'
|
||||
// implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||
// implementation 'com.google.android.material:material:1.4.0'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
|
||||
implementation 'com.google.protobuf:protobuf-kotlin-lite:3.20.1'
|
||||
implementation 'androidx.datastore:datastore-core:1.0.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-service:2.5.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation "com.google.truth:truth:1.1.3"
|
||||
testImplementation "io.mockk:mockk:1.12.5"
|
||||
|
||||
// androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
// To use the androidx.test.core APIs
|
||||
androidTestImplementation "androidx.test:core:1.4.0"
|
||||
// Kotlin extensions for androidx.test.core
|
||||
androidTestImplementation "androidx.test:core-ktx:1.4.0"
|
||||
|
||||
// To use the androidx.test.espresso
|
||||
// androidTestImplementation "androidx.test:espresso:espresso-core:3.4.0"
|
||||
|
||||
// To use the JUnit Extension APIs
|
||||
androidTestImplementation "androidx.test.ext:junit:1.1.3"
|
||||
// Kotlin extensions for androidx.test.ext.junit
|
||||
androidTestImplementation "androidx.test.ext:junit-ktx:1.1.3"
|
||||
|
||||
// To use the Truth Extension APIs
|
||||
androidTestImplementation "androidx.test.ext:truth:1.4.0"
|
||||
|
||||
// To use the androidx.test.runner APIs
|
||||
androidTestImplementation "androidx.test:runner:1.4.0"
|
||||
|
||||
// To use android test orchestrator
|
||||
// androidTestUtil "androidx.test:orchestrator:1.4.1"
|
||||
|
||||
androidTestImplementation "androidx.test:rules:1.4.0"
|
||||
|
||||
androidTestImplementation "io.mockk:mockk-android:1.12.5"
|
||||
}
|
120
lib/android/build.gradle.kts
Normal file
120
lib/android/build.gradle.kts
Normal file
@ -0,0 +1,120 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.parcelize)
|
||||
alias(libs.plugins.sdk.android.library.jacoco)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
}
|
||||
|
||||
val vendorDir = File(rootDir, "vendor")
|
||||
val downloadCacheDir = layout.buildDirectory.dir("downloads").get().asFile
|
||||
|
||||
android {
|
||||
namespace = "im.molly.monero"
|
||||
compileSdk = 35
|
||||
ndkVersion = "23.1.7779620"
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
testHandleProfiling = true
|
||||
testFunctionalTest = true
|
||||
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments += "-DVENDOR_DIR=${vendorDir.path}"
|
||||
arguments += "-DDOWNLOAD_CACHE=${downloadCacheDir.path}"
|
||||
}
|
||||
}
|
||||
|
||||
ndk {
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86_64")
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
getByName("debug") {
|
||||
enableUnitTestCoverage = true
|
||||
enableAndroidTestCoverage = true
|
||||
}
|
||||
|
||||
getByName("release") {
|
||||
isMinifyEnabled = false
|
||||
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android.txt"),
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path = file("src/main/cpp/CMakeLists.txt")
|
||||
version = "3.22.1"
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
aidl = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
merges += "META-INF/LICENSE.md"
|
||||
merges += "META-INF/LICENSE-notice.md"
|
||||
}
|
||||
}
|
||||
|
||||
testOptions {
|
||||
managedDevices {
|
||||
localDevices {
|
||||
create("pixel2api30") {
|
||||
device = "Pixel 2"
|
||||
apiLevel = 30
|
||||
systemImageSource = "aosp-atd"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(platform(libs.okhttp.bom))
|
||||
api(libs.okhttp)
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.lifecycle.service)
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
|
||||
testImplementation(libs.kotlin.junit)
|
||||
testImplementation(testLibs.junit)
|
||||
testImplementation(testLibs.mockk)
|
||||
testImplementation(testLibs.truth)
|
||||
|
||||
androidTestImplementation(libs.kotlinx.coroutines.test)
|
||||
androidTestImplementation(testLibs.androidx.test.core)
|
||||
androidTestImplementation(testLibs.androidx.test.junit)
|
||||
androidTestImplementation(testLibs.androidx.test.truth)
|
||||
androidTestImplementation(testLibs.androidx.test.rules)
|
||||
androidTestImplementation(testLibs.androidx.test.runner)
|
||||
androidTestImplementation(testLibs.mockk)
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package im.molly.monero
|
||||
|
||||
import android.os.Parcel
|
||||
import com.google.common.truth.Truth
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
import kotlin.random.Random
|
||||
|
||||
@ -12,13 +12,13 @@ class SecretKeyParcelableTest {
|
||||
val secret = Random.nextBytes(32)
|
||||
val originalKey = SecretKey(secret)
|
||||
|
||||
val parcel = Parcel.obtain()
|
||||
val parcel = Parcel.obtain().apply {
|
||||
originalKey.writeToParcel(this, 0)
|
||||
setDataPosition(0)
|
||||
}
|
||||
|
||||
originalKey.writeToParcel(parcel, 0)
|
||||
val recreatedKey = SecretKey.CREATOR.createFromParcel(parcel)
|
||||
|
||||
parcel.setDataPosition(0)
|
||||
|
||||
val key = SecretKey.create(parcel)
|
||||
Truth.assertThat(key == originalKey).isTrue()
|
||||
assertThat(recreatedKey).isEqualTo(originalKey)
|
||||
}
|
||||
}
|
||||
|
@ -2,63 +2,64 @@ package im.molly.monero
|
||||
|
||||
import androidx.test.filters.LargeTest
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class WalletNativeTest {
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
fun keyGenerationIsDeterministic() {
|
||||
fun keyGenerationIsDeterministic() = runTest {
|
||||
assertThat(
|
||||
WalletNative.fullNode(
|
||||
WalletNative.localSyncWallet(
|
||||
networkId = MoneroNetwork.Mainnet.id,
|
||||
secretSpendKey = SecretKey("d2ca26e22489bd9871c910c58dee3ab08e66b9d566825a064c8c0af061cd8706".parseHex()),
|
||||
).primaryAccountAddress
|
||||
).publicAddress
|
||||
).isEqualTo("4AYjQM9HoAFNUeC3cvSfgeAN89oMMpMqiByvunzSzhn97cj726rJj3x8hCbH58UnMqQJShczCxbpWRiCJQ3HCUDHLiKuo4T")
|
||||
|
||||
assertThat(
|
||||
WalletNative.fullNode(
|
||||
WalletNative.localSyncWallet(
|
||||
networkId = MoneroNetwork.Testnet.id,
|
||||
secretSpendKey = SecretKey("48a35268bc33227eea43ac1ecfd144d51efc023c115c26ca68a01cc6201e9900".parseHex()),
|
||||
).primaryAccountAddress
|
||||
).publicAddress
|
||||
).isEqualTo("A1v6gVUcGgGE87c1uFRWB1KfPVik2qLLDJiZT3rhZ8qjF3BGA6oHzeDboD23dH8rFaFFcysyqwF6DBj8WUTBWwEhESB7nZz")
|
||||
|
||||
assertThat(
|
||||
WalletNative.fullNode(
|
||||
WalletNative.localSyncWallet(
|
||||
networkId = MoneroNetwork.Stagenet.id,
|
||||
secretSpendKey = SecretKey("561a8d4e121ffca7321a7dc6af79679ceb4cdc8c0dcb0ef588b574586c5fac04".parseHex()),
|
||||
).primaryAccountAddress
|
||||
).publicAddress
|
||||
).isEqualTo("54kPaUhYgGNBT72N8Bv2DFMqstLGJCEcWg1EAjwpxABkKL3uBtBLAh4VAPKvhWBdaD4ZpiftA8YWFLAxnWL4aQ9TD4vhY4W")
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
fun publicAddressesAreDistinct() {
|
||||
fun publicAddressesAreDistinct() = runTest {
|
||||
val publicAddress =
|
||||
WalletNative.fullNode(
|
||||
WalletNative.localSyncWallet(
|
||||
networkId = MoneroNetwork.Mainnet.id,
|
||||
secretSpendKey = randomSecretKey(),
|
||||
).primaryAccountAddress
|
||||
).publicAddress
|
||||
|
||||
val anotherPublicAddress =
|
||||
WalletNative.fullNode(
|
||||
WalletNative.localSyncWallet(
|
||||
networkId = MoneroNetwork.Mainnet.id,
|
||||
secretSpendKey = randomSecretKey(),
|
||||
).primaryAccountAddress
|
||||
).publicAddress
|
||||
|
||||
assertThat(publicAddress).isNotEqualTo(anotherPublicAddress)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun atGenesisBalanceIsZero() {
|
||||
fun atGenesisBalanceIsZero() = runTest {
|
||||
with(
|
||||
WalletNative.fullNode(
|
||||
WalletNative.localSyncWallet(
|
||||
networkId = MoneroNetwork.Mainnet.id,
|
||||
secretSpendKey = randomSecretKey(),
|
||||
).currentBalance
|
||||
).getLedger()
|
||||
) {
|
||||
assertThat(totalAmount).isEqualTo(0.toAtomicUnits())
|
||||
assertThat(totalAmountUnlockedAt(1)).isEqualTo(0.toAtomicUnits())
|
||||
assertThat(transactions).isEmpty()
|
||||
assertThat(isBalanceZero).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,41 +3,65 @@ package im.molly.monero.mnemonics
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.molly.monero.parseHex
|
||||
import org.junit.Test
|
||||
import java.util.Locale
|
||||
|
||||
class MoneroMnemonicTest {
|
||||
|
||||
data class TestCase(val entropy: String, val words: String, val language: String)
|
||||
data class TestCase(val key: String, val words: String, val language: String) {
|
||||
val entropy = key.parseHex()
|
||||
}
|
||||
|
||||
private val testVector = listOf(
|
||||
private val testCases = listOf(
|
||||
TestCase(
|
||||
entropy = "3b094ca7218f175e91fa2402b4ae239a2fe8262792a3e718533a1a357a1e4109",
|
||||
key = "3b094ca7218f175e91fa2402b4ae239a2fe8262792a3e718533a1a357a1e4109",
|
||||
words = "tavern judge beyond bifocals deepest mural onward dummy eagle diode gained vacation rally cause firm idled jerseys moat vigilant upload bobsled jobs cunning doing jobs",
|
||||
language = "en",
|
||||
),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun validateKnownMnemonics() {
|
||||
testVector.forEach {
|
||||
fun testKnownMnemonics() {
|
||||
testCases.forEach {
|
||||
validateMnemonicGeneration(it)
|
||||
validateEntropyRecovery(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun testEmptyEntropy() {
|
||||
MoneroMnemonic.generateMnemonic(ByteArray(0))
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun testInvalidEntropy() {
|
||||
MoneroMnemonic.generateMnemonic(ByteArray(2))
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun testEmptyWords() {
|
||||
MoneroMnemonic.recoverEntropy("")
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun testInvalidLanguage() {
|
||||
MoneroMnemonic.generateMnemonic(ByteArray(32), Locale("ZZ"))
|
||||
}
|
||||
|
||||
private fun validateMnemonicGeneration(testCase: TestCase) {
|
||||
val mnemonicCode = MoneroMnemonic.generateMnemonic(testCase.entropy.parseHex())
|
||||
val mnemonicCode =
|
||||
MoneroMnemonic.generateMnemonic(testCase.entropy, Locale(testCase.language))
|
||||
assertMnemonicCode(mnemonicCode, testCase)
|
||||
}
|
||||
|
||||
private fun validateEntropyRecovery(testCase: TestCase) {
|
||||
val mnemonicCode = MoneroMnemonic.recoverEntropy(testCase.words)
|
||||
assertThat(mnemonicCode).isNotNull()
|
||||
assertMnemonicCode(mnemonicCode!!, testCase)
|
||||
assertMnemonicCode(mnemonicCode, testCase)
|
||||
}
|
||||
|
||||
private fun assertMnemonicCode(mnemonicCode: MnemonicCode, testCase: TestCase) {
|
||||
with(mnemonicCode) {
|
||||
assertThat(entropy).isEqualTo(testCase.entropy.parseHex())
|
||||
private fun assertMnemonicCode(mnemonicCode: MnemonicCode?, testCase: TestCase) {
|
||||
assertThat(mnemonicCode).isNotNull()
|
||||
with(mnemonicCode!!) {
|
||||
assertThat(entropy).isEqualTo(testCase.entropy)
|
||||
assertThat(String(words)).isEqualTo(testCase.words)
|
||||
assertThat(locale.language).isEqualTo(testCase.language)
|
||||
}
|
||||
|
@ -1,3 +0,0 @@
|
||||
package im.molly.monero;
|
||||
|
||||
parcelable HttpResponse;
|
@ -1,6 +0,0 @@
|
||||
package im.molly.monero;
|
||||
|
||||
oneway interface IHttpRequestCallback {
|
||||
void onResponse(int code, String contentType, in ParcelFileDescriptor body);
|
||||
void onFailure();
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
package im.molly.monero;
|
||||
|
||||
import im.molly.monero.IHttpRequestCallback;
|
||||
|
||||
interface IRemoteNodeClient {
|
||||
oneway void requestAsync(int requestId, String method, String path, String header, in byte[] bodyBytes, in IHttpRequestCallback callback);
|
||||
oneway void cancelAsync(int requestId);
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
package im.molly.monero;
|
||||
|
||||
import im.molly.monero.IRemoteNodeClient;
|
||||
|
||||
interface IWalletClient {
|
||||
int getNetworkId();
|
||||
IRemoteNodeClient getRemoteNodeClient();
|
||||
}
|
@ -1,15 +1,15 @@
|
||||
package im.molly.monero;
|
||||
|
||||
import im.molly.monero.IRemoteNodeClient;
|
||||
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;
|
||||
|
||||
interface IWalletService {
|
||||
oneway void createWallet(in WalletConfig config, in IStorageAdapter storage, in IRemoteNodeClient client, in IWalletServiceCallbacks callback);
|
||||
oneway void restoreWallet(in WalletConfig config, in IStorageAdapter storage, in IRemoteNodeClient client, in IWalletServiceCallbacks callback, in SecretKey spendSecretKey, long restorePoint);
|
||||
oneway void openWallet(in WalletConfig config, in IStorageAdapter storage, in IRemoteNodeClient client, in IWalletServiceCallbacks callback);
|
||||
oneway void createWallet(in WalletConfig config, in IStorageAdapter storage, in IHttpRpcClient rpcClient, in IWalletServiceCallbacks callback);
|
||||
oneway void restoreWallet(in WalletConfig config, in IStorageAdapter storage, in IHttpRpcClient rpcClient, in IWalletServiceCallbacks callback, in SecretKey spendSecretKey, long restorePoint);
|
||||
oneway void openWallet(in WalletConfig config, in IStorageAdapter storage, in IHttpRpcClient rpcClient, in IWalletServiceCallbacks callback);
|
||||
void setListener(in IWalletServiceListener listener);
|
||||
}
|
||||
|
@ -0,0 +1,3 @@
|
||||
package im.molly.monero.internal;
|
||||
|
||||
parcelable HttpRequest;
|
@ -0,0 +1,3 @@
|
||||
package im.molly.monero.internal;
|
||||
|
||||
parcelable HttpResponse;
|
@ -0,0 +1,9 @@
|
||||
package im.molly.monero.internal;
|
||||
|
||||
import im.molly.monero.internal.HttpResponse;
|
||||
|
||||
oneway interface IHttpRequestCallback {
|
||||
void onResponse(in HttpResponse response);
|
||||
void onError();
|
||||
void onRequestCanceled();
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package im.molly.monero.internal;
|
||||
|
||||
import im.molly.monero.internal.HttpRequest;
|
||||
import im.molly.monero.internal.IHttpRequestCallback;
|
||||
|
||||
interface IHttpRpcClient {
|
||||
oneway void callAsync(in HttpRequest request, in IHttpRequestCallback callback, int callId);
|
||||
oneway void cancelAsync(int callId);
|
||||
}
|
@ -3,7 +3,7 @@ include(ExternalProject)
|
||||
# The source code of Boost
|
||||
# From https://www.boost.org/users/download/
|
||||
set(BOOST_SOURCE
|
||||
"https://boostorg.jfrog.io/artifactory/main/release/1.78.0/source/boost_1_78_0.tar.bz2"
|
||||
"https://archives.boost.io/release/1.78.0/source/boost_1_78_0.tar.bz2"
|
||||
CACHE STRING "Boost source file to use.")
|
||||
set(BOOST_SHA256
|
||||
"8681f175d4bdb26c52222665793eef08490d7758529330f98d3b29dd0735bccc"
|
||||
|
@ -104,10 +104,6 @@ set(WALLET2_OVERRIDES
|
||||
perf_timer_override.cc
|
||||
)
|
||||
|
||||
set(WALLET2_PRECOMPILED_HEADERS
|
||||
boringssl_compat.h
|
||||
)
|
||||
|
||||
set(WALLET2_INCLUDES
|
||||
contrib/epee/include
|
||||
external
|
||||
@ -135,11 +131,7 @@ add_library(
|
||||
wallet2 STATIC ${WALLET2_SOURCES} ${WALLET2_OVERRIDES}
|
||||
)
|
||||
|
||||
target_precompile_headers(
|
||||
wallet2
|
||||
PRIVATE
|
||||
"${WALLET2_PRECOMPILED_HEADERS}"
|
||||
)
|
||||
target_compile_options(wallet2 PRIVATE -include "${CMAKE_CURRENT_LIST_DIR}/include/boringssl_compat.h")
|
||||
|
||||
target_include_directories(
|
||||
wallet2
|
||||
|
@ -24,7 +24,7 @@ jmethodID ParcelFd_detachFd;
|
||||
ScopedJavaGlobalRef<jclass> StringClass;
|
||||
|
||||
void InitializeJniCache(JNIEnv* env) {
|
||||
jclass httpResponse = GetClass(env, "im/molly/monero/HttpResponse");
|
||||
jclass httpResponse = GetClass(env, "im/molly/monero/internal/HttpResponse");
|
||||
jclass iTransferCallback = GetClass(env, "im/molly/monero/ITransferCallback");
|
||||
jclass logger = GetClass(env, "im/molly/monero/Logger");
|
||||
jclass txInfo = GetClass(env, "im/molly/monero/internal/TxInfo");
|
||||
@ -55,7 +55,7 @@ void InitializeJniCache(JNIEnv* env) {
|
||||
TxInfo_ctor = GetMethodId(
|
||||
env, txInfo,
|
||||
"<init>",
|
||||
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IILjava/lang/String;JIIJJJJZZ)V");
|
||||
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IILjava/lang/String;JIJJJJBZZ)V");
|
||||
WalletNative_createPendingTransfer = GetMethodId(
|
||||
env, walletNative,
|
||||
"createPendingTransfer",
|
||||
@ -63,7 +63,7 @@ void InitializeJniCache(JNIEnv* env) {
|
||||
WalletNative_callRemoteNode = GetMethodId(
|
||||
env, walletNative,
|
||||
"callRemoteNode",
|
||||
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[B)Lim/molly/monero/HttpResponse;");
|
||||
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[B)Lim/molly/monero/internal/HttpResponse;");
|
||||
WalletNative_onRefresh = GetMethodId(
|
||||
env, walletNative,
|
||||
"onRefresh", "(IJZ)V");
|
||||
|
@ -72,6 +72,7 @@ void Wallet::restoreAccount(const std::vector<char>& secret_scalar, uint64_t res
|
||||
std::lock_guard<std::mutex> lock(m_wallet_mutex);
|
||||
auto& account = m_wallet.get_account();
|
||||
GenerateAccountKeys(account, secret_scalar);
|
||||
m_subaddresses[{0, 0}] = m_wallet.get_subaddress_as_str({0, 0});
|
||||
if (restore_point < CRYPTONOTE_MAX_BLOCK_NUMBER) {
|
||||
m_restore_height = restore_point;
|
||||
m_last_block_timestamp = 0;
|
||||
@ -168,7 +169,6 @@ std::string Wallet::addSubaddressInternal(const cryptonote::subaddress_index& in
|
||||
std::unique_ptr<PendingTransfer> Wallet::createPayment(
|
||||
const std::vector<std::string>& addresses,
|
||||
const std::vector<uint64_t>& amounts,
|
||||
uint64_t time_lock,
|
||||
int priority,
|
||||
uint32_t account_index,
|
||||
const std::set<uint32_t>& subaddr_indexes) {
|
||||
@ -196,7 +196,6 @@ std::unique_ptr<PendingTransfer> Wallet::createPayment(
|
||||
auto ptxs = m_wallet.create_transactions_2(
|
||||
dsts,
|
||||
m_wallet.get_min_ring_size() - 1,
|
||||
time_lock,
|
||||
priority,
|
||||
{}, /* extra */
|
||||
account_index,
|
||||
@ -812,11 +811,11 @@ ScopedJavaLocalRef<jobject> NativeToJavaTxInfo(JNIEnv* env,
|
||||
: nullptr,
|
||||
tx.m_amount,
|
||||
static_cast<jint>(tx.m_height),
|
||||
tx.m_state,
|
||||
tx.m_unlock_time,
|
||||
tx.m_timestamp,
|
||||
tx.m_fee,
|
||||
tx.m_change,
|
||||
static_cast<jbyte>(tx.m_state),
|
||||
tx.m_coinbase,
|
||||
tx.m_type == TxInfo::INCOMING)
|
||||
};
|
||||
@ -847,7 +846,6 @@ Java_im_molly_monero_WalletNative_nativeCreatePayment(
|
||||
jlong handle,
|
||||
jobjectArray j_addresses,
|
||||
jlongArray j_amounts,
|
||||
jlong time_lock,
|
||||
jint priority,
|
||||
jint account_index,
|
||||
jintArray j_subaddr_indexes,
|
||||
@ -865,7 +863,7 @@ Java_im_molly_monero_WalletNative_nativeCreatePayment(
|
||||
pending_transfer = wallet->createPayment(
|
||||
addresses,
|
||||
{amounts.begin(), amounts.end()},
|
||||
time_lock, priority,
|
||||
priority,
|
||||
account_index,
|
||||
{subaddr_indexes.begin(), subaddr_indexes.end()});
|
||||
// } catch (error::daemon_busy& e) {
|
||||
|
@ -41,10 +41,10 @@ struct TxInfo {
|
||||
} m_type;
|
||||
|
||||
enum TxState {
|
||||
OFF_CHAIN = 0,
|
||||
PENDING = 1,
|
||||
FAILED = 2,
|
||||
ON_CHAIN = 3,
|
||||
OFF_CHAIN = 1,
|
||||
PENDING = 2,
|
||||
FAILED = 3,
|
||||
ON_CHAIN = 4,
|
||||
} m_state;
|
||||
|
||||
TxInfo(crypto::hash tx_hash, TxType type) :
|
||||
@ -101,7 +101,6 @@ class Wallet : i_wallet2_callback {
|
||||
std::unique_ptr<PendingTransfer> createPayment(
|
||||
const std::vector<std::string>& addresses,
|
||||
const std::vector<uint64_t>& amounts,
|
||||
uint64_t time_lock,
|
||||
int priority,
|
||||
uint32_t account_index,
|
||||
const std::set<uint32_t>& subaddr_indexes);
|
||||
@ -175,7 +174,7 @@ class Wallet : i_wallet2_callback {
|
||||
handleNewBlock(height, block.timestamp);
|
||||
}
|
||||
|
||||
void on_reorg(uint64_t height) override {
|
||||
void on_reorg(uint64_t height, uint64_t blocks_detached, size_t transfers_detached) override {
|
||||
handleReorgEvent(height);
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,9 @@ data class Ledger(
|
||||
val transactions: Collection<Transaction>
|
||||
get() = transactionById.values
|
||||
|
||||
val isBalanceZero: Boolean
|
||||
get() = getBalance().totalAmount.isZero
|
||||
|
||||
fun getBalance(): Balance = enoteSet.calculateBalance()
|
||||
|
||||
fun getBalanceForAccount(accountIndex: Int): Balance =
|
||||
|
@ -0,0 +1,58 @@
|
||||
package im.molly.monero
|
||||
|
||||
import im.molly.monero.internal.IHttpRpcClient
|
||||
import im.molly.monero.internal.RpcClient
|
||||
import im.molly.monero.loadbalancer.LoadBalancer
|
||||
import im.molly.monero.loadbalancer.Rule
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class MoneroNodeClient private constructor(
|
||||
val network: MoneroNetwork,
|
||||
private val rpcClient: RpcClient,
|
||||
private val scope: CoroutineScope,
|
||||
) : AutoCloseable {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Constructs a [MoneroNodeClient] to connect to the Monero [network].
|
||||
*/
|
||||
fun forNetwork(
|
||||
network: MoneroNetwork,
|
||||
remoteNodes: Flow<List<RemoteNode>>,
|
||||
loadBalancerRule: Rule,
|
||||
httpClient: OkHttpClient,
|
||||
retryBackoff: BackoffPolicy = ExponentialBackoff.Default,
|
||||
ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
): MoneroNodeClient {
|
||||
val scope = CoroutineScope(ioDispatcher + SupervisorJob())
|
||||
val loadBalancer = LoadBalancer(remoteNodes, scope)
|
||||
val rpcClient = RpcClient(
|
||||
loadBalancer = loadBalancer,
|
||||
loadBalancerRule = loadBalancerRule,
|
||||
retryBackoff = retryBackoff,
|
||||
requestsScope = scope,
|
||||
httpClient = httpClient,
|
||||
)
|
||||
return MoneroNodeClient(network, rpcClient, scope)
|
||||
}
|
||||
}
|
||||
|
||||
var httpClient: OkHttpClient
|
||||
get() = rpcClient.httpClient
|
||||
set(value) {
|
||||
rpcClient.httpClient = value
|
||||
}
|
||||
|
||||
internal val httpRpcClient: IHttpRpcClient
|
||||
get() = rpcClient
|
||||
|
||||
override fun close() {
|
||||
scope.cancel("MoneroNodeClient is closing: Cancelling all ongoing requests")
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package im.molly.monero
|
||||
|
||||
import im.molly.monero.internal.LedgerFactory
|
||||
import im.molly.monero.internal.TxInfo
|
||||
import im.molly.monero.internal.consolidateTransactions
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
@ -19,7 +19,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||
class MoneroWallet internal constructor(
|
||||
private val wallet: IWallet,
|
||||
private val storageAdapter: StorageAdapter,
|
||||
val remoteNodeClient: RemoteNodeClient?,
|
||||
val moneroNodeClient: MoneroNodeClient?,
|
||||
) : AutoCloseable {
|
||||
|
||||
private val logger = loggerFor<MoneroWallet>()
|
||||
@ -86,7 +86,7 @@ class MoneroWallet internal constructor(
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
wallet.getAddressesForAccount(accountIndex, object : BaseWalletCallbacks() {
|
||||
override fun onSubAddressListReceived(subAddresses: Array<String>) {
|
||||
val accounts = parseAndAggregateAddresses(subAddresses)
|
||||
val accounts = parseAndAggregateAddresses(subAddresses.asIterable())
|
||||
continuation.resume(accounts.single()) {}
|
||||
}
|
||||
})
|
||||
@ -96,23 +96,12 @@ class MoneroWallet internal constructor(
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
wallet.getAllAddresses(object : BaseWalletCallbacks() {
|
||||
override fun onSubAddressListReceived(subAddresses: Array<String>) {
|
||||
val accounts = parseAndAggregateAddresses(subAddresses)
|
||||
val accounts = parseAndAggregateAddresses(subAddresses.asIterable())
|
||||
continuation.resume(accounts) {}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun parseAndAggregateAddresses(subAddresses: Array<String>): List<WalletAccount> =
|
||||
subAddresses.map { AccountAddress.parseWithIndexes(it) }
|
||||
.groupBy { it.accountIndex }
|
||||
.map { (index, addresses) ->
|
||||
WalletAccount(
|
||||
addresses = addresses,
|
||||
accountIndex = index,
|
||||
)
|
||||
}
|
||||
.sortedBy { it.accountIndex }
|
||||
|
||||
/**
|
||||
* A [Flow] of ledger changes.
|
||||
*/
|
||||
@ -125,17 +114,11 @@ class MoneroWallet internal constructor(
|
||||
subAddresses: Array<String>,
|
||||
blockchainTime: BlockchainTime,
|
||||
) {
|
||||
val indexedAccounts = parseAndAggregateAddresses(subAddresses)
|
||||
val (txById, enotes) = txHistory.consolidateTransactions(
|
||||
accounts = indexedAccounts,
|
||||
blockchainContext = blockchainTime,
|
||||
)
|
||||
val ledger = Ledger(
|
||||
publicAddress = publicAddress,
|
||||
indexedAccounts = indexedAccounts,
|
||||
transactionById = txById,
|
||||
enoteSet = enotes,
|
||||
checkedAt = blockchainTime,
|
||||
val accounts = parseAndAggregateAddresses(subAddresses.asIterable())
|
||||
val ledger = LedgerFactory.createFromTxHistory(
|
||||
txHistory = txHistory,
|
||||
accounts = accounts,
|
||||
blockchainTime = blockchainTime,
|
||||
)
|
||||
sendLedger(ledger)
|
||||
}
|
||||
@ -145,7 +128,7 @@ class MoneroWallet internal constructor(
|
||||
}
|
||||
|
||||
override fun onSubAddressListUpdated(subAddresses: Array<String>) {
|
||||
val accountsUpdated = parseAndAggregateAddresses(subAddresses)
|
||||
val accountsUpdated = parseAndAggregateAddresses(subAddresses.asIterable())
|
||||
if (lastKnownLedger.indexedAccounts != accountsUpdated) {
|
||||
sendLedger(lastKnownLedger.copy(indexedAccounts = accountsUpdated))
|
||||
}
|
||||
|
@ -1,224 +0,0 @@
|
||||
package im.molly.monero
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import im.molly.monero.loadbalancer.LoadBalancer
|
||||
import im.molly.monero.loadbalancer.Rule
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import okhttp3.*
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
// TODO: Hide IRemoteNodeClient methods and rename to HttpRpcClient or MoneroNodeClient
|
||||
class RemoteNodeClient private constructor(
|
||||
val network: MoneroNetwork,
|
||||
private val loadBalancer: LoadBalancer,
|
||||
private val loadBalancerRule: Rule,
|
||||
var httpClient: OkHttpClient,
|
||||
private val retryBackoff: BackoffPolicy,
|
||||
private val requestsScope: CoroutineScope,
|
||||
) : IRemoteNodeClient.Stub(), AutoCloseable {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Constructs a [RemoteNodeClient] to connect to the Monero [network].
|
||||
*/
|
||||
fun forNetwork(
|
||||
network: MoneroNetwork,
|
||||
remoteNodes: Flow<List<RemoteNode>>,
|
||||
loadBalancerRule: Rule,
|
||||
httpClient: OkHttpClient,
|
||||
retryBackoff: BackoffPolicy = ExponentialBackoff.Default,
|
||||
ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
): RemoteNodeClient {
|
||||
val scope = CoroutineScope(ioDispatcher + SupervisorJob())
|
||||
return RemoteNodeClient(
|
||||
network,
|
||||
LoadBalancer(remoteNodes, scope),
|
||||
loadBalancerRule,
|
||||
httpClient,
|
||||
retryBackoff,
|
||||
scope
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val logger = loggerFor<RemoteNodeClient>()
|
||||
|
||||
private val requestList = ConcurrentHashMap<Int, Job>()
|
||||
|
||||
override fun requestAsync(
|
||||
requestId: Int,
|
||||
method: String,
|
||||
path: String,
|
||||
header: String?,
|
||||
body: ByteArray?,
|
||||
callback: IHttpRequestCallback?,
|
||||
) {
|
||||
logger.d("HTTP: $method $path, header_len=${header?.length}, body_size=${body?.size}")
|
||||
|
||||
val requestJob = requestsScope.launch {
|
||||
runCatching {
|
||||
requestWithRetry(method, path, header, body)
|
||||
}.onSuccess { response ->
|
||||
val status = response.code
|
||||
val responseBody = response.body
|
||||
if (responseBody == null) {
|
||||
callback?.onResponse(status, null, null)
|
||||
} else {
|
||||
responseBody.use { body ->
|
||||
val contentType = body.contentType()?.toString()
|
||||
val pipe = ParcelFileDescriptor.createPipe()
|
||||
pipe[1].use { writeSide ->
|
||||
callback?.onResponse(status, contentType, pipe[0])
|
||||
FileOutputStream(writeSide.fileDescriptor).use { out ->
|
||||
runCatching { body.byteStream().copyTo(out) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: Log response times
|
||||
}.onFailure { throwable ->
|
||||
logger.e("HTTP: Request failed", throwable)
|
||||
callback?.onFailure()
|
||||
}
|
||||
}.also {
|
||||
requestList[requestId] = it
|
||||
}
|
||||
|
||||
requestJob.invokeOnCompletion {
|
||||
requestList.remove(requestId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancelAsync(requestId: Int) {
|
||||
requestList[requestId]?.cancel()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
requestsScope.cancel()
|
||||
}
|
||||
|
||||
private suspend fun requestWithRetry(
|
||||
method: String,
|
||||
path: String,
|
||||
header: String?,
|
||||
body: ByteArray?,
|
||||
): Response {
|
||||
val headers = parseHttpHeader(header)
|
||||
val contentType = headers["Content-Type"]?.toMediaType()
|
||||
// TODO: Log unsupported headers
|
||||
val requestBuilder = with(Request.Builder()) {
|
||||
when {
|
||||
method.equals("GET", ignoreCase = true) -> {}
|
||||
method.equals("POST", ignoreCase = true) -> {
|
||||
val content = body ?: ByteArray(0)
|
||||
post(content.toRequestBody(contentType))
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException("Unsupported method")
|
||||
}
|
||||
url("http:$path")
|
||||
// TODO: Add authentication
|
||||
}
|
||||
|
||||
val attempts = mutableMapOf<Uri, Int>()
|
||||
|
||||
while (true) {
|
||||
val selected = loadBalancerRule.chooseNode(loadBalancer)
|
||||
if (selected == null) {
|
||||
logger.i("No remote node available")
|
||||
|
||||
return Response.Builder()
|
||||
.request(requestBuilder.build())
|
||||
.protocol(Protocol.HTTP_1_1)
|
||||
.code(499)
|
||||
.message("No remote node available")
|
||||
.build()
|
||||
}
|
||||
|
||||
val uri = selected.uriForPath(path)
|
||||
val retryCount = attempts[uri] ?: 0
|
||||
|
||||
delay(retryBackoff.waitTime(retryCount))
|
||||
|
||||
logger.d("HTTP: $method $uri")
|
||||
|
||||
val response = try {
|
||||
|
||||
val request = requestBuilder.url(uri.toString()).build()
|
||||
|
||||
httpClient.newCall(request).await()
|
||||
} catch (e: IOException) {
|
||||
logger.e("HTTP: Request failed", e)
|
||||
// TODO: Notify loadBalancer
|
||||
continue
|
||||
} finally {
|
||||
attempts[uri] = retryCount + 1
|
||||
}
|
||||
|
||||
if (response.isSuccessful) {
|
||||
// TODO: Notify loadBalancer
|
||||
return response
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseHttpHeader(header: String?): Headers =
|
||||
with(Headers.Builder()) {
|
||||
header?.splitToSequence("\r\n")
|
||||
?.filter { line -> line.isNotEmpty() }
|
||||
?.forEach { line -> add(line) }
|
||||
build()
|
||||
}
|
||||
|
||||
private suspend fun Call.await() = suspendCoroutine { continuation ->
|
||||
enqueue(object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
continuation.resume(response)
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// private val Response.roundTripMillis: Long
|
||||
// get() = sentRequestAtMillis() - receivedResponseAtMillis()
|
||||
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal suspend fun IRemoteNodeClient.request(request: HttpRequest): HttpResponse? =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
val requestId = request.hashCode()
|
||||
val callback = object : IHttpRequestCallback.Stub() {
|
||||
override fun onResponse(
|
||||
code: Int,
|
||||
contentType: String?,
|
||||
body: ParcelFileDescriptor?,
|
||||
) {
|
||||
continuation.resume(HttpResponse(code, contentType, body)) {
|
||||
body?.close()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure() {
|
||||
continuation.resume(null) {}
|
||||
}
|
||||
}
|
||||
with(request) {
|
||||
requestAsync(requestId, method, path, header, bodyBytes, callback)
|
||||
}
|
||||
continuation.invokeOnCancellation {
|
||||
cancelAsync(requestId)
|
||||
}
|
||||
}
|
@ -10,7 +10,6 @@ data class PaymentRequest(
|
||||
val paymentDetails: List<PaymentDetail>,
|
||||
val spendingAccountIndex: Int,
|
||||
val feePriority: FeePriority? = null,
|
||||
val timeLock: UnlockTime? = null,
|
||||
) : TransferRequest
|
||||
|
||||
@Parcelize
|
||||
@ -19,5 +18,4 @@ data class SweepRequest(
|
||||
val splitCount: Int = 1,
|
||||
val keyImageHashes: List<HashDigest>,
|
||||
val feePriority: FeePriority? = null,
|
||||
val timeLock: UnlockTime? = null,
|
||||
) : TransferRequest
|
||||
|
@ -13,3 +13,11 @@ fun Iterable<WalletAccount>.findAddressByIndex(
|
||||
it.accountIndex == accountIndex && it.subAddressIndex == subAddressIndex
|
||||
}
|
||||
}
|
||||
|
||||
fun parseAndAggregateAddresses(addresses: Iterable<String>): List<WalletAccount> {
|
||||
return addresses
|
||||
.map { AccountAddress.parseWithIndexes(it) }
|
||||
.groupBy { it.accountIndex }
|
||||
.toSortedMap()
|
||||
.map { (index, addresses) -> WalletAccount(addresses, index) }
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package im.molly.monero
|
||||
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
@ -7,3 +9,16 @@ interface WalletDataStore {
|
||||
suspend fun write(writer: (OutputStream) -> Unit)
|
||||
suspend fun read(): InputStream
|
||||
}
|
||||
|
||||
class InMemoryWalletDataStore : WalletDataStore {
|
||||
private val data = ByteArrayOutputStream()
|
||||
|
||||
override suspend fun write(writer: (OutputStream) -> Unit) {
|
||||
data.reset()
|
||||
writer(data)
|
||||
}
|
||||
|
||||
override suspend fun read(): InputStream {
|
||||
return ByteArrayInputStream(data.toByteArray())
|
||||
}
|
||||
}
|
||||
|
@ -2,12 +2,17 @@ package im.molly.monero
|
||||
|
||||
import android.os.ParcelFileDescriptor
|
||||
import androidx.annotation.GuardedBy
|
||||
import im.molly.monero.internal.HttpRequest
|
||||
import im.molly.monero.internal.HttpResponse
|
||||
import im.molly.monero.internal.IHttpRequestCallback
|
||||
import im.molly.monero.internal.IHttpRpcClient
|
||||
import im.molly.monero.internal.LedgerFactory
|
||||
import im.molly.monero.internal.TxInfo
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.Closeable
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
@ -15,17 +20,16 @@ import kotlin.coroutines.CoroutineContext
|
||||
internal class WalletNative private constructor(
|
||||
private val network: MoneroNetwork,
|
||||
private val storageAdapter: IStorageAdapter,
|
||||
private val remoteNodeClient: IRemoteNodeClient?,
|
||||
private val rpcClient: IHttpRpcClient?,
|
||||
private val scope: CoroutineScope,
|
||||
private val ioDispatcher: CoroutineDispatcher,
|
||||
) : IWallet.Stub(), Closeable {
|
||||
|
||||
companion object {
|
||||
// TODO: Find better name because this is a local synchronization wallet, not a full node wallet
|
||||
suspend fun fullNode(
|
||||
suspend fun localSyncWallet(
|
||||
networkId: Int,
|
||||
storageAdapter: IStorageAdapter,
|
||||
remoteNodeClient: IRemoteNodeClient? = null,
|
||||
storageAdapter: IStorageAdapter = StorageAdapter(InMemoryWalletDataStore()),
|
||||
rpcClient: IHttpRpcClient? = null,
|
||||
secretSpendKey: SecretKey? = null,
|
||||
restorePoint: Long? = null,
|
||||
coroutineContext: CoroutineContext = Dispatchers.Default + SupervisorJob(),
|
||||
@ -33,7 +37,7 @@ internal class WalletNative private constructor(
|
||||
) = WalletNative(
|
||||
network = MoneroNetwork.fromId(networkId),
|
||||
storageAdapter = storageAdapter,
|
||||
remoteNodeClient = remoteNodeClient,
|
||||
rpcClient = rpcClient,
|
||||
scope = CoroutineScope(coroutineContext),
|
||||
ioDispatcher = ioDispatcher,
|
||||
).apply {
|
||||
@ -102,7 +106,26 @@ internal class WalletNative private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPublicAddress() = nativeGetPublicAddress(handle)
|
||||
override fun getPublicAddress(): String = nativeGetPublicAddress(handle)
|
||||
|
||||
fun getCurrentBlockchainTime(): BlockchainTime {
|
||||
return network.blockchainTime(
|
||||
nativeGetCurrentBlockchainHeight(handle),
|
||||
nativeGetCurrentBlockchainTimestamp(handle),
|
||||
)
|
||||
}
|
||||
|
||||
fun getAllAccounts(): List<WalletAccount> {
|
||||
return parseAndAggregateAddresses(getSubAddresses().asIterable())
|
||||
}
|
||||
|
||||
fun getLedger(): Ledger {
|
||||
return LedgerFactory.createFromTxHistory(
|
||||
txHistory = getTxHistorySnapshot(),
|
||||
accounts = getAllAccounts(),
|
||||
blockchainTime = getCurrentBlockchainTime(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun MoneroNetwork.blockchainTime(height: Int, epochSecond: Long): BlockchainTime {
|
||||
// Block timestamp could be zero during a fast refresh.
|
||||
@ -113,15 +136,6 @@ internal class WalletNative private constructor(
|
||||
return BlockchainTime(height = height, timestamp = timestamp, network = this)
|
||||
}
|
||||
|
||||
val currentBlockchainTime: BlockchainTime
|
||||
get() = network.blockchainTime(
|
||||
nativeGetCurrentBlockchainHeight(handle),
|
||||
nativeGetCurrentBlockchainTimestamp(handle),
|
||||
)
|
||||
|
||||
val currentBalance: Balance
|
||||
get() = TODO() // txHistorySnapshot().consolidateTransactions().second.balance()
|
||||
|
||||
private fun getSubAddresses(accountIndex: Int? = null): Array<String> {
|
||||
return nativeGetSubAddresses(accountIndex ?: -1, handle)
|
||||
}
|
||||
@ -152,7 +166,7 @@ internal class WalletNative private constructor(
|
||||
nativeCancelRefresh(handle)
|
||||
}
|
||||
}
|
||||
callback?.onRefreshResult(currentBlockchainTime, status)
|
||||
callback?.onRefreshResult(getCurrentBlockchainTime(), status)
|
||||
}
|
||||
}
|
||||
|
||||
@ -189,7 +203,6 @@ internal class WalletNative private constructor(
|
||||
handle = handle,
|
||||
addresses = addresses.toTypedArray(),
|
||||
amounts = amounts.toLongArray(),
|
||||
timeLock = request.timeLock?.blockchainTime?.toLong() ?: 0,
|
||||
priority = request.feePriority?.priority ?: 0,
|
||||
accountIndex = request.spendingAccountIndex,
|
||||
subAddressIndexes = IntArray(0),
|
||||
@ -266,7 +279,7 @@ internal class WalletNative private constructor(
|
||||
|
||||
balanceListenersLock.withLock {
|
||||
balanceListeners.add(listener)
|
||||
listener.onBalanceChanged(txHistory, subAddresses, currentBlockchainTime)
|
||||
listener.onBalanceChanged(txHistory, subAddresses, getCurrentBlockchainTime())
|
||||
}
|
||||
}
|
||||
|
||||
@ -381,8 +394,8 @@ internal class WalletNative private constructor(
|
||||
*/
|
||||
@CalledByNative
|
||||
private fun callRemoteNode(
|
||||
method: String?,
|
||||
path: String?,
|
||||
method: String,
|
||||
path: String,
|
||||
header: String?,
|
||||
body: ByteArray?,
|
||||
): HttpResponse? = runBlocking {
|
||||
@ -390,8 +403,9 @@ internal class WalletNative private constructor(
|
||||
if (!requestsAllowed) {
|
||||
return@runBlocking null
|
||||
}
|
||||
val httpRequest = HttpRequest(method, path, header, body)
|
||||
pendingRequest = async {
|
||||
remoteNodeClient?.request(HttpRequest(method, path, header, body))
|
||||
rpcClient?.newCall(httpRequest)
|
||||
}
|
||||
}
|
||||
try {
|
||||
@ -409,6 +423,33 @@ internal class WalletNative private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private val callCounter = AtomicInteger()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private suspend fun IHttpRpcClient.newCall(request: HttpRequest): HttpResponse? =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
val callback = object : IHttpRequestCallback.Stub() {
|
||||
override fun onResponse(response: HttpResponse) {
|
||||
continuation.resume(response) {
|
||||
response.close()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError() {
|
||||
continuation.resume(null) {}
|
||||
}
|
||||
|
||||
override fun onRequestCanceled() {
|
||||
continuation.resume(null) {}
|
||||
}
|
||||
}
|
||||
val callId = callCounter.incrementAndGet()
|
||||
callAsync(request, callback, callId)
|
||||
continuation.invokeOnCancellation {
|
||||
cancelAsync(callId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
scope.cancel()
|
||||
}
|
||||
@ -437,7 +478,6 @@ internal class WalletNative private constructor(
|
||||
handle: Long,
|
||||
addresses: Array<String>,
|
||||
amounts: LongArray,
|
||||
timeLock: Long,
|
||||
priority: Int,
|
||||
accountIndex: Int,
|
||||
subAddressIndexes: IntArray,
|
||||
|
@ -51,13 +51,13 @@ class WalletProvider private constructor(
|
||||
suspend fun createNewWallet(
|
||||
network: MoneroNetwork,
|
||||
dataStore: WalletDataStore? = null,
|
||||
client: RemoteNodeClient? = 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,
|
||||
buildConfig(network), storageAdapter, client?.httpRpcClient,
|
||||
WalletResultCallback(continuation),
|
||||
)
|
||||
}
|
||||
@ -67,7 +67,7 @@ class WalletProvider private constructor(
|
||||
suspend fun restoreWallet(
|
||||
network: MoneroNetwork,
|
||||
dataStore: WalletDataStore? = null,
|
||||
client: RemoteNodeClient? = null,
|
||||
client: MoneroNodeClient? = null,
|
||||
secretSpendKey: SecretKey,
|
||||
restorePoint: RestorePoint,
|
||||
): MoneroWallet {
|
||||
@ -78,7 +78,7 @@ class WalletProvider private constructor(
|
||||
val storageAdapter = StorageAdapter(dataStore)
|
||||
val wallet = suspendCancellableCoroutine { continuation ->
|
||||
service.restoreWallet(
|
||||
buildConfig(network), storageAdapter, client,
|
||||
buildConfig(network), storageAdapter, client?.httpRpcClient,
|
||||
WalletResultCallback(continuation),
|
||||
secretSpendKey,
|
||||
restorePoint.toLong(),
|
||||
@ -90,13 +90,13 @@ class WalletProvider private constructor(
|
||||
suspend fun openWallet(
|
||||
network: MoneroNetwork,
|
||||
dataStore: WalletDataStore,
|
||||
client: RemoteNodeClient? = null,
|
||||
client: MoneroNodeClient? = null,
|
||||
): MoneroWallet {
|
||||
require(client == null || client.network == network)
|
||||
val storageAdapter = StorageAdapter(dataStore)
|
||||
val wallet = suspendCancellableCoroutine { continuation ->
|
||||
service.openWallet(
|
||||
buildConfig(network), storageAdapter, client,
|
||||
buildConfig(network), storageAdapter, client?.httpRpcClient,
|
||||
WalletResultCallback(continuation),
|
||||
)
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ 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.*
|
||||
|
||||
class WalletService : LifecycleService() {
|
||||
@ -43,13 +44,13 @@ internal class WalletServiceImpl(
|
||||
override fun createWallet(
|
||||
config: WalletConfig,
|
||||
storage: IStorageAdapter,
|
||||
client: IRemoteNodeClient?,
|
||||
rpcClient: IHttpRpcClient?,
|
||||
callback: IWalletServiceCallbacks?,
|
||||
) {
|
||||
serviceScope.launch {
|
||||
val secretSpendKey = randomSecretKey()
|
||||
val wallet = secretSpendKey.use { secret ->
|
||||
createOrRestoreWallet(config, storage, client, secret)
|
||||
createOrRestoreWallet(config, storage, rpcClient, secret)
|
||||
}
|
||||
callback?.onWalletResult(wallet)
|
||||
}
|
||||
@ -58,14 +59,14 @@ internal class WalletServiceImpl(
|
||||
override fun restoreWallet(
|
||||
config: WalletConfig,
|
||||
storage: IStorageAdapter,
|
||||
client: IRemoteNodeClient?,
|
||||
rpcClient: IHttpRpcClient?,
|
||||
callback: IWalletServiceCallbacks?,
|
||||
secretSpendKey: SecretKey,
|
||||
restorePoint: Long,
|
||||
) {
|
||||
serviceScope.launch {
|
||||
val wallet = secretSpendKey.use { secret ->
|
||||
createOrRestoreWallet(config, storage, client, secret, restorePoint)
|
||||
createOrRestoreWallet(config, storage, rpcClient, secret, restorePoint)
|
||||
}
|
||||
callback?.onWalletResult(wallet)
|
||||
}
|
||||
@ -74,14 +75,14 @@ internal class WalletServiceImpl(
|
||||
override fun openWallet(
|
||||
config: WalletConfig,
|
||||
storage: IStorageAdapter,
|
||||
client: IRemoteNodeClient?,
|
||||
rpcClient: IHttpRpcClient?,
|
||||
callback: IWalletServiceCallbacks?,
|
||||
) {
|
||||
serviceScope.launch {
|
||||
val wallet = WalletNative.fullNode(
|
||||
val wallet = WalletNative.localSyncWallet(
|
||||
networkId = config.networkId,
|
||||
storageAdapter = storage,
|
||||
remoteNodeClient = client,
|
||||
rpcClient = rpcClient,
|
||||
coroutineContext = serviceScope.coroutineContext,
|
||||
)
|
||||
callback?.onWalletResult(wallet)
|
||||
@ -91,14 +92,14 @@ internal class WalletServiceImpl(
|
||||
private suspend fun createOrRestoreWallet(
|
||||
config: WalletConfig,
|
||||
storage: IStorageAdapter,
|
||||
client: IRemoteNodeClient?,
|
||||
rpcClient: IHttpRpcClient?,
|
||||
secretSpendKey: SecretKey,
|
||||
restorePoint: Long? = null,
|
||||
): IWallet {
|
||||
return WalletNative.fullNode(
|
||||
return WalletNative.localSyncWallet(
|
||||
networkId = config.networkId,
|
||||
storageAdapter = storage,
|
||||
remoteNodeClient = client,
|
||||
rpcClient = rpcClient,
|
||||
secretSpendKey = secretSpendKey,
|
||||
restorePoint = restorePoint,
|
||||
coroutineContext = serviceScope.coroutineContext,
|
||||
|
@ -1,11 +1,19 @@
|
||||
package im.molly.monero
|
||||
package im.molly.monero.internal
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class HttpRequest(
|
||||
val method: String?,
|
||||
val path: String?,
|
||||
val method: String,
|
||||
val path: String,
|
||||
val header: String?,
|
||||
val bodyBytes: ByteArray?,
|
||||
) {
|
||||
) : Parcelable {
|
||||
|
||||
override fun toString(): String =
|
||||
"HttpRequest(method=$method, path=$path, headers=${header?.length}, body=${bodyBytes?.size})"
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
@ -24,8 +32,8 @@ data class HttpRequest(
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = method?.hashCode() ?: 0
|
||||
result = 31 * result + (path?.hashCode() ?: 0)
|
||||
var result = method.hashCode()
|
||||
result = 31 * result + path.hashCode()
|
||||
result = 31 * result + (header?.hashCode() ?: 0)
|
||||
result = 31 * result + (bodyBytes?.contentHashCode() ?: 0)
|
||||
return result
|
@ -1,12 +1,15 @@
|
||||
package im.molly.monero
|
||||
package im.molly.monero.internal
|
||||
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class HttpResponse(
|
||||
val code: Int,
|
||||
val contentType: String? = null,
|
||||
val body: ParcelFileDescriptor? = null,
|
||||
) : AutoCloseable {
|
||||
) : AutoCloseable, Parcelable {
|
||||
override fun close() {
|
||||
body?.close()
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package im.molly.monero.internal
|
||||
|
||||
import im.molly.monero.BlockchainTime
|
||||
import im.molly.monero.Ledger
|
||||
import im.molly.monero.WalletAccount
|
||||
import im.molly.monero.findAddressByIndex
|
||||
|
||||
internal object LedgerFactory {
|
||||
fun createFromTxHistory(
|
||||
txHistory: List<TxInfo>,
|
||||
accounts: List<WalletAccount>,
|
||||
blockchainTime: BlockchainTime,
|
||||
): Ledger {
|
||||
val (txById, enotes) = txHistory.consolidateTransactions(
|
||||
accounts = accounts,
|
||||
blockchainContext = blockchainTime,
|
||||
)
|
||||
val publicAddress = accounts.findAddressByIndex(accountIndex = 0)
|
||||
checkNotNull(publicAddress)
|
||||
return Ledger(
|
||||
publicAddress = publicAddress,
|
||||
indexedAccounts = accounts,
|
||||
transactionById = txById,
|
||||
enoteSet = enotes,
|
||||
checkedAt = blockchainTime,
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,186 @@
|
||||
package im.molly.monero.internal
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import im.molly.monero.BackoffPolicy
|
||||
import im.molly.monero.loadbalancer.LoadBalancer
|
||||
import im.molly.monero.loadbalancer.Rule
|
||||
import im.molly.monero.loggerFor
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Headers
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Protocol
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class RpcClient internal constructor(
|
||||
private val loadBalancer: LoadBalancer,
|
||||
private val loadBalancerRule: Rule,
|
||||
private val retryBackoff: BackoffPolicy,
|
||||
private val requestsScope: CoroutineScope,
|
||||
var httpClient: OkHttpClient,
|
||||
) : IHttpRpcClient.Stub() {
|
||||
|
||||
private val logger = loggerFor<RpcClient>()
|
||||
|
||||
private val activeRequests = ConcurrentHashMap<Int, Job>()
|
||||
|
||||
override fun callAsync(request: HttpRequest, callback: IHttpRequestCallback, callId: Int) {
|
||||
logger.d("[$callId] Dispatching $request")
|
||||
|
||||
val requestJob = requestsScope.launch {
|
||||
runCatching {
|
||||
requestWithRetry(request, callId)
|
||||
}.onSuccess { response ->
|
||||
val status = response.code
|
||||
val responseBody = response.body
|
||||
if (responseBody == null) {
|
||||
callback.onResponse(
|
||||
HttpResponse(code = status, contentType = null, body = null)
|
||||
)
|
||||
} else {
|
||||
responseBody.use { body ->
|
||||
val contentType = body.contentType()?.toString()
|
||||
val pipe = ParcelFileDescriptor.createPipe()
|
||||
pipe[0].use { readSize ->
|
||||
pipe[1].use { writeSide ->
|
||||
val httpResponse = HttpResponse(
|
||||
code = status,
|
||||
contentType = contentType,
|
||||
body = readSize,
|
||||
)
|
||||
callback.onResponse(httpResponse)
|
||||
FileOutputStream(writeSide.fileDescriptor).use { out ->
|
||||
runCatching { body.byteStream().copyTo(out) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: Log response times
|
||||
}.onFailure { throwable ->
|
||||
when (throwable) {
|
||||
is CancellationException -> callback.onRequestCanceled()
|
||||
else -> {
|
||||
logger.e("[$callId] Failed to dispatch $request", throwable)
|
||||
callback.onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
}.also { job ->
|
||||
val oldJob = activeRequests.put(callId, job)
|
||||
check(oldJob == null)
|
||||
}
|
||||
|
||||
requestJob.invokeOnCompletion {
|
||||
activeRequests.remove(callId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancelAsync(requestId: Int) {
|
||||
activeRequests[requestId]?.cancel()
|
||||
}
|
||||
|
||||
private suspend fun requestWithRetry(request: HttpRequest, callId: Int): Response {
|
||||
val headers = parseHttpHeader(request.header)
|
||||
val contentType = headers["Content-Type"]?.toMediaType()
|
||||
// TODO: Log unsupported headers
|
||||
val requestBuilder = createRequestBuilder(request, contentType)
|
||||
|
||||
val attempts = mutableMapOf<Uri, Int>()
|
||||
|
||||
while (true) {
|
||||
val selected = loadBalancerRule.chooseNode(loadBalancer)
|
||||
if (selected == null) {
|
||||
val errorMsg = "No remote node available"
|
||||
logger.i("[$callId] $errorMsg")
|
||||
return Response.Builder()
|
||||
.request(requestBuilder.build())
|
||||
.protocol(Protocol.HTTP_1_1)
|
||||
.code(499)
|
||||
.message(errorMsg)
|
||||
.build()
|
||||
}
|
||||
|
||||
val uri = selected.uriForPath(request.path)
|
||||
val retryCount = attempts[uri] ?: 0
|
||||
|
||||
delay(retryBackoff.waitTime(retryCount))
|
||||
|
||||
logger.d("[$callId] HTTP: ${request.method} $uri")
|
||||
|
||||
try {
|
||||
val response =
|
||||
httpClient.newCall(requestBuilder.url(uri.toString()).build()).await()
|
||||
// TODO: Notify loadBalancer
|
||||
if (response.isSuccessful) {
|
||||
return response
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
logger.w("[$callId] HTTP: Request failed with ${e::class.simpleName}: ${e.message}")
|
||||
// TODO: Notify loadBalancer
|
||||
} finally {
|
||||
attempts[uri] = retryCount + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseHttpHeader(header: String?): Headers {
|
||||
return with(Headers.Builder()) {
|
||||
header?.splitToSequence("\r\n")
|
||||
?.filter { line -> line.isNotEmpty() }
|
||||
?.forEach { line -> add(line) }
|
||||
build()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createRequestBuilder(
|
||||
request: HttpRequest,
|
||||
contentType: MediaType?,
|
||||
): Request.Builder {
|
||||
return with(Request.Builder()) {
|
||||
when {
|
||||
request.method.equals("GET", ignoreCase = true) -> {}
|
||||
request.method.equals("POST", ignoreCase = true) -> {
|
||||
val content = request.bodyBytes ?: ByteArray(0)
|
||||
post(content.toRequestBody(contentType))
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException("Unsupported method")
|
||||
}
|
||||
url("http:${request.path}")
|
||||
// TODO: Add authentication
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun Call.await() = suspendCoroutine { continuation ->
|
||||
enqueue(object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
continuation.resume(response)
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// private val Response.roundTripMillis: Long
|
||||
// get() = sentRequestAtMillis() - receivedResponseAtMillis()
|
||||
|
||||
}
|
@ -37,20 +37,20 @@ internal data class TxInfo @CalledByNative constructor(
|
||||
val recipient: String?,
|
||||
val amount: Long,
|
||||
val height: Int,
|
||||
val state: Int,
|
||||
val unlockTime: Long,
|
||||
val timestamp: Long,
|
||||
val fee: Long,
|
||||
val change: Long,
|
||||
val state: Byte,
|
||||
val coinbase: Boolean,
|
||||
val incoming: Boolean,
|
||||
) : Parcelable {
|
||||
|
||||
companion object State {
|
||||
const val OFF_CHAIN: Int = 0
|
||||
const val PENDING: Int = 1
|
||||
const val FAILED: Int = 2
|
||||
const val ON_CHAIN: Int = 3
|
||||
const val OFF_CHAIN: Byte = 1
|
||||
const val PENDING: Byte = 2
|
||||
const val FAILED: Byte = 3
|
||||
const val ON_CHAIN: Byte = 4
|
||||
}
|
||||
|
||||
init {
|
||||
|
@ -12,12 +12,15 @@ class MnemonicCode private constructor(
|
||||
val locale: Locale,
|
||||
) : Destroyable, Closeable, Iterable<CharArray> {
|
||||
|
||||
constructor(entropy: ByteArray, words: CharBuffer, locale: Locale) : this(
|
||||
constructor(entropy: ByteArray, words: CharBuffer, locale: Locale = Locale.ENGLISH) : this(
|
||||
entropy.clone(),
|
||||
words.array().copyOfRange(words.position(), words.remaining()),
|
||||
locale,
|
||||
)
|
||||
|
||||
internal val isNonZero
|
||||
get() = !MessageDigest.isEqual(_entropy, ByteArray(_entropy.size))
|
||||
|
||||
val entropy: ByteArray
|
||||
get() = checkNotDestroyed { _entropy.clone() }
|
||||
|
||||
@ -66,9 +69,9 @@ class MnemonicCode private constructor(
|
||||
protected fun finalize() = destroy()
|
||||
|
||||
override fun equals(other: Any?): Boolean =
|
||||
this === other || (other is MnemonicCode && MessageDigest.isEqual(entropy, other.entropy))
|
||||
this === other || (other is MnemonicCode && MessageDigest.isEqual(_entropy, other._entropy))
|
||||
|
||||
override fun hashCode(): Int = entropy.contentHashCode()
|
||||
override fun hashCode(): Int = _entropy.contentHashCode()
|
||||
|
||||
private inline fun <T> checkNotDestroyed(block: () -> T): T {
|
||||
check(!destroyed) { "MnemonicCode has already been destroyed" }
|
||||
|
@ -7,7 +7,6 @@ import java.nio.CharBuffer
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
object MoneroMnemonic {
|
||||
init {
|
||||
NativeLoader.loadMnemonicsLibrary()
|
||||
|
@ -1,11 +0,0 @@
|
||||
package im.molly.monero.util
|
||||
|
||||
fun CharSequence.parseHex(): ByteArray {
|
||||
check(length % 2 == 0) { "Must have an even length" }
|
||||
|
||||
return ByteArray(length / 2) {
|
||||
Integer.parseInt(substring(it * 2, (it + 1) * 2), 16).toByte()
|
||||
}
|
||||
}
|
||||
|
||||
fun ByteArray.toHex(): String = joinToString(separator = "") { "%02x".format(it) }
|
@ -1,9 +1,9 @@
|
||||
package im.molly.monero
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Test
|
||||
import kotlin.random.Random
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class SecretKeyTest {
|
||||
|
||||
@ -14,20 +14,29 @@ class SecretKeyTest {
|
||||
if (size == 32) {
|
||||
assertThat(SecretKey(secret).bytes).hasLength(size)
|
||||
} else {
|
||||
assertThrows(RuntimeException::class.java) { SecretKey(secret) }
|
||||
assertFailsWith<IllegalArgumentException> { SecretKey(secret) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `secret key copies buffer`() {
|
||||
val secretBytes = Random.nextBytes(32)
|
||||
val key = SecretKey(secretBytes)
|
||||
|
||||
assertThat(key.bytes).isEqualTo(secretBytes)
|
||||
secretBytes.fill(0)
|
||||
assertThat(key.bytes).isNotEqualTo(secretBytes)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `secret keys cannot be zero`() {
|
||||
assertThrows(RuntimeException::class.java) { SecretKey(ByteArray(32)).bytes }
|
||||
assertFailsWith<IllegalStateException> { SecretKey(ByteArray(32)).bytes }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when key is destroyed secret is zeroed`() {
|
||||
val secretBytes = Random.nextBytes(32)
|
||||
|
||||
val key = SecretKey(secretBytes)
|
||||
|
||||
assertThat(key.destroyed).isFalse()
|
||||
@ -37,20 +46,20 @@ class SecretKeyTest {
|
||||
|
||||
assertThat(key.destroyed).isTrue()
|
||||
assertThat(key.isNonZero).isFalse()
|
||||
assertThrows(RuntimeException::class.java) { key.bytes }
|
||||
assertFailsWith<IllegalStateException> { key.bytes }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `two keys with same secret are the same`() {
|
||||
fun `two keys with same secret are equal`() {
|
||||
val secret = Random.nextBytes(32)
|
||||
|
||||
val key = SecretKey(secret)
|
||||
val sameKey = SecretKey(secret)
|
||||
val anotherKey = randomSecretKey()
|
||||
val differentKey = randomSecretKey()
|
||||
|
||||
assertThat(key).isEqualTo(sameKey)
|
||||
assertThat(sameKey).isNotEqualTo(anotherKey)
|
||||
assertThat(anotherKey).isNotEqualTo(key)
|
||||
assertThat(sameKey).isNotEqualTo(differentKey)
|
||||
assertThat(differentKey).isNotEqualTo(key)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -64,7 +73,6 @@ class SecretKeyTest {
|
||||
@Test
|
||||
fun `keys are not equal to their destroyed versions`() {
|
||||
val secret = Random.nextBytes(32)
|
||||
|
||||
val key = SecretKey(secret)
|
||||
val destroyed = SecretKey(secret).also { it.destroy() }
|
||||
|
||||
@ -73,9 +81,9 @@ class SecretKeyTest {
|
||||
|
||||
@Test
|
||||
fun `destroyed keys are equal`() {
|
||||
val destroyed = randomSecretKey().also { it.destroy() }
|
||||
val anotherDestroyed = randomSecretKey().also { it.destroy() }
|
||||
val destroyed1 = randomSecretKey().also { it.destroy() }
|
||||
val destroyed2 = randomSecretKey().also { it.destroy() }
|
||||
|
||||
assertThat(destroyed).isEqualTo(anotherDestroyed)
|
||||
assertThat(destroyed1).isEqualTo(destroyed2)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,105 @@
|
||||
package im.molly.monero.mnemonic
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.molly.monero.mnemonics.MnemonicCode
|
||||
import org.junit.Test
|
||||
import java.nio.CharBuffer
|
||||
import java.util.Locale
|
||||
import kotlin.random.Random
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class MnemonicCodeTest {
|
||||
|
||||
private fun randomEntropy(size: Int = 32): ByteArray = Random.nextBytes(size)
|
||||
|
||||
private fun charBufferOf(str: String): CharBuffer = CharBuffer.wrap(str.toCharArray())
|
||||
|
||||
@Test
|
||||
fun `mnemonic copies entropy and words`() {
|
||||
val entropy = randomEntropy()
|
||||
val words = charBufferOf("arbre soleil maison")
|
||||
val locale = Locale.FRANCE
|
||||
|
||||
val mnemonic = MnemonicCode(entropy, words, locale)
|
||||
|
||||
assertThat(mnemonic.entropy).isEqualTo(entropy)
|
||||
assertThat(mnemonic.words).isEqualTo(words.array())
|
||||
assertThat(mnemonic.locale).isEqualTo(locale)
|
||||
|
||||
entropy.fill(0)
|
||||
words.put("modified".toCharArray())
|
||||
|
||||
assertThat(mnemonic.entropy).isNotEqualTo(entropy)
|
||||
assertThat(mnemonic.words).isNotEqualTo(words.array())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `destroyed mnemonic code zeroes entropy and words`() {
|
||||
val entropy = randomEntropy()
|
||||
val words = charBufferOf("test mnemonic")
|
||||
|
||||
val mnemonic = MnemonicCode(entropy, words)
|
||||
|
||||
mnemonic.destroy()
|
||||
|
||||
assertThat(mnemonic.destroyed).isTrue()
|
||||
assertThat(mnemonic.isNonZero).isFalse()
|
||||
assertFailsWith<IllegalStateException> { mnemonic.words }
|
||||
assertFailsWith<IllegalStateException> { mnemonic.entropy }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `two mnemonics with same entropy are equal`() {
|
||||
val entropy = randomEntropy()
|
||||
val words = charBufferOf("test mnemonic")
|
||||
val locale = Locale.ENGLISH
|
||||
|
||||
val mnemonic = MnemonicCode(entropy, words, locale)
|
||||
val sameMnemonic = MnemonicCode(entropy, words, locale)
|
||||
val differentMnemonic = MnemonicCode(randomEntropy(), words, locale)
|
||||
|
||||
assertThat(mnemonic).isEqualTo(sameMnemonic)
|
||||
assertThat(differentMnemonic).isNotEqualTo(mnemonic)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `iterator correctly iterates words`() {
|
||||
val words = charBufferOf("word1 word2 word3")
|
||||
val mnemonic = MnemonicCode(randomEntropy(), words)
|
||||
|
||||
val iteratedWords = mnemonic.map { String(it) }
|
||||
|
||||
assertThat(iteratedWords).containsExactly("word1", "word2", "word3").inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calling next on iterator without checking hasNext throws exception`() {
|
||||
val words = charBufferOf("test mnemonic")
|
||||
val mnemonic = MnemonicCode(randomEntropy(), words)
|
||||
val iterator = mnemonic.iterator()
|
||||
|
||||
iterator.next()
|
||||
iterator.next()
|
||||
|
||||
assertFailsWith<NoSuchElementException> { iterator.next() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mnemonics are not equal to their destroyed versions`() {
|
||||
val entropy = randomEntropy()
|
||||
val words = charBufferOf("test mnemonic")
|
||||
|
||||
val mnemonic = MnemonicCode(entropy, words)
|
||||
val destroyed = MnemonicCode(entropy, words).also { it.destroy() }
|
||||
|
||||
assertThat(mnemonic).isNotEqualTo(destroyed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `destroyed mnemonics are equal`() {
|
||||
val destroyed1 = MnemonicCode(randomEntropy(), charBufferOf("word1")).also { it.destroy() }
|
||||
val destroyed2 = MnemonicCode(randomEntropy(), charBufferOf("word2")).also { it.destroy() }
|
||||
|
||||
assertThat(destroyed1).isEqualTo(destroyed2)
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
class Base58Test {
|
||||
|
||||
// Test cases from monero unit_tests/base58.cpp
|
||||
@ -134,7 +135,7 @@ class Base58Test {
|
||||
@Test
|
||||
fun `decode valid base58 strings`() {
|
||||
base58ToHex.forEach { (input, expected) ->
|
||||
assertThat(input.decodeBase58()).isEqualTo(expected.parseHex())
|
||||
assertThat(input.decodeBase58()).isEqualTo(expected.hexToByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,23 +0,0 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
includeProject(':lib', 'lib/android')
|
||||
includeProject(':demo', 'demo/android')
|
||||
|
||||
def includeProject(String name, String filePath) {
|
||||
include(name)
|
||||
project(name).projectDir = file(filePath)
|
||||
}
|
44
settings.gradle.kts
Normal file
44
settings.gradle.kts
Normal file
@ -0,0 +1,44 @@
|
||||
pluginManagement {
|
||||
includeBuild("build-logic")
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
google()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
|
||||
repositories {
|
||||
google {
|
||||
content {
|
||||
includeGroupByRegex("com\\.android(\\..*)?")
|
||||
includeGroupByRegex("com\\.google(\\..*)?")
|
||||
includeGroupByRegex("androidx?(\\..*)?")
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
}
|
||||
versionCatalogs {
|
||||
// "libs" is predefined by Gradle
|
||||
create("testLibs") {
|
||||
from(files("gradle/test-libs.versions.toml"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
check(JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_21)) {
|
||||
"""
|
||||
This project requires JDK 21+ but it is currently using JDK ${JavaVersion.current()}.
|
||||
Java Home: [${System.getProperty("java.home")}]
|
||||
https://developer.android.com/build/jdks#jdk-config-in-studio
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
includeProject("lib", "lib/android")
|
||||
includeProject("demo", "demo/android")
|
||||
|
||||
fun includeProject(projectName: String, projectRoot: String) {
|
||||
val projectId = ":$projectName"
|
||||
include(projectId)
|
||||
project(projectId).projectDir = file(projectRoot)
|
||||
}
|
2
vendor/monero
vendored
2
vendor/monero
vendored
@ -1 +1 @@
|
||||
Subproject commit 36d6a9c4c68a9c787a14d419edc05384fe1c506d
|
||||
Subproject commit 64eebdb3ace488435a95acd6eb4e48f908a47c2c
|
Loading…
x
Reference in New Issue
Block a user