mirror of
https://github.com/mollyim/monero-wallet-sdk.git
synced 2025-09-21 13:04:37 -04:00
Compare commits
51 commits
snapshot-2
...
main
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4f99e95dee | ||
![]() |
0088b1b562 | ||
![]() |
48fc04a490 | ||
![]() |
328a23edea | ||
![]() |
88c689b58d | ||
![]() |
dc3f362ec7 | ||
![]() |
f38b35e792 | ||
![]() |
dd9c2ca7f3 | ||
![]() |
cbe714ea35 | ||
![]() |
607dd440b8 | ||
![]() |
a25334a18c | ||
![]() |
ac021c2567 | ||
![]() |
8c2a27964b | ||
![]() |
b895d2c01c | ||
![]() |
0ad0ade8e3 | ||
![]() |
0d7f98b622 | ||
![]() |
40ca4b4237 | ||
![]() |
59d2c7e402 | ||
![]() |
c030b6565c | ||
![]() |
0152cadc8f | ||
![]() |
a02241128a | ||
![]() |
e9cae0b359 | ||
![]() |
aeebd6ff32 | ||
![]() |
adfe3c8d7a | ||
![]() |
44824c4991 | ||
![]() |
c8331ea58b | ||
![]() |
524314f391 | ||
![]() |
d2dd6171fe | ||
![]() |
e9d08f95d1 | ||
![]() |
21e99c74c0 | ||
![]() |
dcc3552daa | ||
![]() |
c15ba3fb22 | ||
![]() |
5f6f67c7fc | ||
![]() |
f97854e424 | ||
![]() |
cb79e3421d | ||
![]() |
16ff7b06db | ||
![]() |
d05a056698 | ||
![]() |
301f1efc1c | ||
![]() |
a664ce1652 | ||
![]() |
181b3dc442 | ||
![]() |
9b71aae6c5 | ||
![]() |
9f54eadc61 | ||
![]() |
8dbf0ffb6b | ||
![]() |
874f777deb | ||
![]() |
87d70c1e4f | ||
![]() |
194d6d184e | ||
![]() |
37b68d6062 | ||
![]() |
6bf3174d52 | ||
![]() |
b3fb65b858 | ||
![]() |
30ef8b481b | ||
![]() |
308031250b |
197 changed files with 3834 additions and 1749 deletions
37
.github/actions/disk-cleanup/action.yml
vendored
Normal file
37
.github/actions/disk-cleanup/action.yml
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
name: Disk cleanup
|
||||
description: "Free up disk space by removing unused pre-installed software."
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Free up disk space
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Disk space before cleanup:"
|
||||
df -h /
|
||||
|
||||
echo "Removing unused software..."
|
||||
sudo rm -rf \
|
||||
/opt/az \
|
||||
/opt/google/chrome \
|
||||
/opt/hostedtoolcache/CodeQL \
|
||||
/opt/microsoft \
|
||||
/opt/pipx \
|
||||
/usr/lib/dotnet \
|
||||
/usr/lib/firefox \
|
||||
/usr/lib/google-cloud-sdk \
|
||||
/usr/lib/mono \
|
||||
/usr/local/.ghcup \
|
||||
/usr/local/aws-cli \
|
||||
/usr/local/julia* \
|
||||
/usr/local/share/chromium \
|
||||
/usr/local/share/powershell \
|
||||
/usr/local/share/vcpkg \
|
||||
/usr/local/aws-sam-cli \
|
||||
/usr/share/az_* \
|
||||
/usr/share/dotnet \
|
||||
/usr/share/man \
|
||||
/usr/share/miniconda \
|
||||
/usr/share/swift \
|
||||
|
||||
echo "Disk space after cleanup:"
|
||||
df -h /
|
70
.github/workflows/build.yml
vendored
Normal file
70
.github/workflows/build.yml
vendored
Normal file
|
@ -0,0 +1,70 @@
|
|||
name: Build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
- '.github/FUNDING.yml'
|
||||
- '.github/ISSUE_TEMPLATE/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build & Run tests
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Free up disk space in runner
|
||||
uses: ./.github/actions/disk-cleanup
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
build-scan-publish: true
|
||||
build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
|
||||
build-scan-terms-of-use-agree: 'yes'
|
||||
|
||||
- name: Enable KVM
|
||||
run: |
|
||||
sudo tee /etc/udev/rules.d/99-kvm4all.rules >/dev/null <<EOF
|
||||
KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"
|
||||
EOF
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
- name: Extract library version info
|
||||
id: meta
|
||||
run: |
|
||||
version=$(awk -F'"' '/^version\s*=/{print $2}' lib/android/build.gradle.kts)
|
||||
echo "version=v$version" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Compile modules
|
||||
run: ./gradlew assemble --scan
|
||||
|
||||
- name: Run unit tests
|
||||
run: ./gradlew check
|
||||
|
||||
- name: Run instrumented tests on emulator
|
||||
run: ./gradlew ciGroupDebugAndroidTest
|
||||
|
||||
- name: Publish Snapshot
|
||||
if: "github.ref_name == 'main' && endsWith(steps.meta.outputs.version, 'SNAPSHOT')"
|
||||
run: ./gradlew publishToMavenCentral
|
||||
env:
|
||||
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }}
|
||||
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }}
|
96
README.md
Normal file
96
README.md
Normal file
|
@ -0,0 +1,96 @@
|
|||
# Monero Wallet SDK for Android
|
||||
|
||||
[](https://github.com/mollyim/monero-wallet-sdk/actions/workflows/build.yml)
|
||||

|
||||

|
||||
|
||||
A modern Kotlin library that embeds Monero's wallet2 inside a sandboxed Android Service and
|
||||
exposes an idiomatic, asynchronous API for mobile apps.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Kotlin-native API**: Asynchronous by design, using `suspend` functions and `Flow`.
|
||||
- **Sandboxed native code**: All C++ runs in a zero-privilege, isolated process.
|
||||
- **Pluggable storage**: Bring your own persistence layer (files, DB, cloud) via the
|
||||
`StorageProvider` interface.
|
||||
- **Custom HTTP stack**: Inject any networking code (plain, Tor, I2P, QUIC, …) to talk to Monero
|
||||
remote nodes.
|
||||
- **Client-side load-balancing**: Automatic node selection for faster sync & fail-over.
|
||||
- **Tiny library (~6.5 MB AAR)**: LTO, dead-code elimination, and static vendored deps keep the
|
||||
footprint small.
|
||||
- **Jetpack-Compose demo wallet**: Full sample app following Google's official architecture
|
||||
guidelines.
|
||||
|
||||
## Setup
|
||||
|
||||
The SDK is available on Maven Central:
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
implementation("im.molly:monero-wallet-sdk:<latest-version>")
|
||||
}
|
||||
```
|
||||
|
||||
Make sure `mavenCentral()` is in your repositories block.
|
||||
|
||||
To use snapshot versions:
|
||||
|
||||
```kotlin
|
||||
repositories {
|
||||
maven {
|
||||
name = "Central Portal Snapshots"
|
||||
url = uri("https://central.sonatype.com/repository/maven-snapshots/")
|
||||
content {
|
||||
includeModule("im.molly", "monero-wallet-sdk")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
implementation("im.molly:monero-wallet-sdk:<snapshot-version>")
|
||||
}
|
||||
```
|
||||
|
||||
Replace `<latest-version>` or `<snapshot-version>` with the version you want to use.
|
||||
|
||||
## Demo App
|
||||
|
||||
A fully functional demo wallet is included in `demo/android`, implemented using Jetpack Compose
|
||||
and following Android's modern app architecture best practices.
|
||||
|
||||
To try it out:
|
||||
|
||||
1. Clone the repository with submodules:
|
||||
```sh
|
||||
git clone --recursive https://github.com/mollyim/monero-wallet-sdk
|
||||
```
|
||||
2. Open the root project directory in Android Studio (Meerkat or later).
|
||||
3. Select the demo run configuration and press Run.
|
||||
|
||||
The demo app showcases wallet creation, sync, transaction sending, and more.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Android 8.0 (API 26+)
|
||||
- Kotlin **2.1.0**
|
||||
- Android Gradle Plugin **8.1.0+**
|
||||
|
||||
## Roadmap
|
||||
|
||||
| Feature | Status |
|
||||
|---------------------------------|-----------------|
|
||||
| Wallet management (create/open) | ✅ Done |
|
||||
| Balance, history, sync | ✅ Done |
|
||||
| Send XMR | ✅ Done |
|
||||
| Seraphis migration support | 🔜 Planned |
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
- Funded by the Monero Community Crowdfunding System (CCS).
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the
|
||||
[GNU General Public License v3.0](https://www.gnu.org/licenses/gpl-3.0.txt).
|
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(17)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.android.gradle.plugin)
|
||||
compileOnly(libs.kotlin.gradle.plugin)
|
||||
}
|
||||
|
||||
tasks {
|
||||
validatePlugins {
|
||||
enableStricterValidation = true
|
||||
failOnWarning = true
|
||||
}
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
register("MoneroSdkModulePlugin") {
|
||||
id = libs.plugins.monero.sdk.module.get().pluginId
|
||||
implementationClass = "MoneroSdkModulePlugin"
|
||||
}
|
||||
}
|
||||
}
|
92
build-logic/plugins/src/main/kotlin/JacocoCoverageConfig.kt
Normal file
92
build-logic/plugins/src/main/kotlin/JacocoCoverageConfig.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 },
|
||||
)
|
||||
}
|
||||
}
|
22
build-logic/plugins/src/main/kotlin/MoneroSdkModulePlugin.kt
Normal file
22
build-logic/plugins/src/main/kotlin/MoneroSdkModulePlugin.kt
Normal file
|
@ -0,0 +1,22 @@
|
|||
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
|
||||
import java.io.File
|
||||
|
||||
class MoneroSdkModulePlugin : Plugin<Project> {
|
||||
override fun apply(project: Project): Unit = with(project) {
|
||||
apply(plugin = "jacoco")
|
||||
configureJacoco(extensions.getByType<LibraryAndroidComponentsExtension>())
|
||||
}
|
||||
}
|
||||
|
||||
val Project.vendorDir: File
|
||||
get() = rootProject.layout.projectDirectory.dir("vendor").asFile
|
||||
|
||||
val Project.downloadCacheDir: File
|
||||
get() = layout.buildDirectory.dir("downloads").get().asFile
|
||||
|
||||
val Project.isSnapshot: Boolean
|
||||
get() = version.toString().endsWith("SNAPSHOT")
|
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()
|
||||
}
|
16
build.gradle.kts
Normal file
16
build.gradle.kts
Normal file
|
@ -0,0 +1,16 @@
|
|||
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
|
||||
alias(libs.plugins.publish) apply false
|
||||
}
|
||||
|
||||
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>
|
|
@ -1,10 +1,10 @@
|
|||
package im.molly.monero.demo
|
||||
|
||||
import android.net.Uri
|
||||
import im.molly.monero.MoneroNetwork
|
||||
import im.molly.monero.demo.data.AppDatabase
|
||||
import im.molly.monero.demo.data.entity.asEntity
|
||||
import im.molly.monero.demo.data.model.RemoteNode
|
||||
import im.molly.monero.sdk.MoneroNetwork
|
||||
import androidx.core.net.toUri
|
||||
|
||||
val DefaultNodeList = listOf(
|
||||
MoneroNetwork.Mainnet to listOf(
|
||||
|
@ -21,7 +21,7 @@ suspend fun addDefaultRemoteNodes(appDatabase: AppDatabase) {
|
|||
val dao = appDatabase.remoteNodeDao()
|
||||
val nodes = DefaultNodeList.flatMap { (network, urls) ->
|
||||
urls.map { url ->
|
||||
RemoteNode(network = network, uri = Uri.parse(url)).asEntity()
|
||||
RemoteNode(network = network, uri = url.toUri()).asEntity()
|
||||
}
|
||||
}.toTypedArray()
|
||||
dao.upsert(*nodes)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -7,7 +7,7 @@ import androidx.datastore.core.DataStore
|
|||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import im.molly.monero.demo.service.SyncService
|
||||
import im.molly.monero.isIsolatedProcess
|
||||
import im.molly.monero.sdk.isIsolatedProcess
|
||||
|
||||
val Context.preferencesDataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
|
||||
|
||||
|
|
|
@ -2,8 +2,9 @@ package im.molly.monero.demo.data
|
|||
|
||||
import android.content.Context
|
||||
import android.util.AtomicFile
|
||||
import im.molly.monero.*
|
||||
import im.molly.monero.loadbalancer.RoundRobinRule
|
||||
import im.molly.monero.sdk.*
|
||||
import im.molly.monero.sdk.loadbalancer.RoundRobinRule
|
||||
import im.molly.monero.sdk.service.SandboxedWalletService
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import okhttp3.OkHttpClient
|
||||
|
@ -15,7 +16,7 @@ import java.io.OutputStream
|
|||
class MoneroSdkClient(private val context: Context) {
|
||||
|
||||
private val providerDeferred = CoroutineScope(Dispatchers.IO).async {
|
||||
WalletProvider.connect(context)
|
||||
SandboxedWalletService.connect(context)
|
||||
}
|
||||
|
||||
suspend fun createWallet(network: MoneroNetwork, filename: String): MoneroWallet {
|
||||
|
@ -24,7 +25,7 @@ class MoneroSdkClient(private val context: Context) {
|
|||
network = network,
|
||||
dataStore = WalletDataStoreFile(filename, newFile = true),
|
||||
).also { wallet ->
|
||||
wallet.commit()
|
||||
wallet.save()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,7 +42,7 @@ class MoneroSdkClient(private val context: Context) {
|
|||
secretSpendKey = secretSpendKey,
|
||||
restorePoint = restorePoint,
|
||||
).also { wallet ->
|
||||
wallet.commit()
|
||||
wallet.save()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -52,7 +53,7 @@ class MoneroSdkClient(private val context: Context) {
|
|||
httpClient: OkHttpClient,
|
||||
): MoneroWallet {
|
||||
val dataStore = WalletDataStoreFile(filename)
|
||||
val client = RemoteNodeClient.forNetwork(
|
||||
val client = MoneroNodeClient.create(
|
||||
network = network,
|
||||
remoteNodes = remoteNodes,
|
||||
loadBalancerRule = RoundRobinRule(),
|
||||
|
@ -86,7 +87,7 @@ class MoneroSdkClient(private val context: Context) {
|
|||
throw IOException("Cannot create wallet data directory: ${walletDataDir.path}")
|
||||
}
|
||||
|
||||
override suspend fun write(writer: (OutputStream) -> Unit) {
|
||||
override suspend fun save(writer: (OutputStream) -> Unit, overwrite: Boolean) {
|
||||
val output = file.startWrite()
|
||||
try {
|
||||
writer(output)
|
||||
|
@ -97,7 +98,7 @@ class MoneroSdkClient(private val context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun read(): InputStream {
|
||||
override suspend fun load(): InputStream {
|
||||
return file.openRead()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package im.molly.monero.demo.data
|
||||
|
||||
import im.molly.monero.*
|
||||
import im.molly.monero.demo.data.model.WalletConfig
|
||||
import im.molly.monero.sdk.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import okhttp3.OkHttpClient
|
||||
|
@ -33,7 +33,8 @@ class WalletRepository(
|
|||
remoteNodes = configFlow.map {
|
||||
it.remoteNodes.map { node ->
|
||||
RemoteNode(
|
||||
uri = node.uri,
|
||||
url = node.uri.toString(),
|
||||
network = node.network,
|
||||
username = node.username,
|
||||
password = node.password,
|
||||
)
|
||||
|
@ -51,8 +52,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)
|
||||
|
||||
|
|
|
@ -5,8 +5,8 @@ import androidx.room.ColumnInfo
|
|||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import im.molly.monero.MoneroNetwork
|
||||
import im.molly.monero.demo.data.model.RemoteNode
|
||||
import im.molly.monero.sdk.MoneroNetwork
|
||||
|
||||
@Entity(
|
||||
tableName = "remote_nodes",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package im.molly.monero.demo.data.model
|
||||
|
||||
import android.net.Uri
|
||||
import im.molly.monero.MoneroNetwork
|
||||
import im.molly.monero.sdk.MoneroNetwork
|
||||
|
||||
data class RemoteNode(
|
||||
val id: Long? = null,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package im.molly.monero.demo.data.model
|
||||
|
||||
import im.molly.monero.AccountAddress
|
||||
import im.molly.monero.Enote
|
||||
import im.molly.monero.TimeLocked
|
||||
import im.molly.monero.sdk.AccountAddress
|
||||
import im.molly.monero.sdk.Enote
|
||||
import im.molly.monero.sdk.TimeLocked
|
||||
|
||||
data class WalletAddress(
|
||||
val address: AccountAddress,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package im.molly.monero.demo.data.model
|
||||
|
||||
import im.molly.monero.Transaction
|
||||
import im.molly.monero.sdk.Transaction
|
||||
|
||||
data class WalletTransaction(
|
||||
val walletId: Long,
|
||||
|
|
|
@ -38,7 +38,7 @@ class SyncService(
|
|||
syncedWalletIds.map {
|
||||
walletRepository.getWallet(it)
|
||||
}.forEach { wallet ->
|
||||
wallet.commit()
|
||||
wallet.save()
|
||||
}
|
||||
}
|
||||
delay(60.seconds)
|
||||
|
@ -56,7 +56,7 @@ class SyncService(
|
|||
if (result.isError()) {
|
||||
// TODO: Handle non-recoverable errors
|
||||
}
|
||||
wallet.commit()
|
||||
wallet.save()
|
||||
delay(10.seconds)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,23 +3,21 @@ package im.molly.monero.demo.ui
|
|||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import im.molly.monero.MoneroNetwork
|
||||
import im.molly.monero.RestorePoint
|
||||
import im.molly.monero.SecretKey
|
||||
import im.molly.monero.demo.AppModule
|
||||
import im.molly.monero.demo.data.RemoteNodeRepository
|
||||
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 im.molly.monero.sdk.MoneroNetwork
|
||||
import im.molly.monero.sdk.RestorePoint
|
||||
import im.molly.monero.sdk.SecretKey
|
||||
import im.molly.monero.sdk.mnemonics.MoneroMnemonic
|
||||
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,
|
||||
|
|
|
@ -16,13 +16,13 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import im.molly.monero.MoneroNetwork
|
||||
import im.molly.monero.demo.data.model.DefaultMoneroNetwork
|
||||
import im.molly.monero.demo.data.model.RemoteNode
|
||||
import im.molly.monero.demo.ui.component.SelectListBox
|
||||
import im.molly.monero.demo.ui.component.Toolbar
|
||||
import im.molly.monero.demo.ui.theme.AppIcons
|
||||
import im.molly.monero.demo.ui.theme.AppTheme
|
||||
import im.molly.monero.sdk.MoneroNetwork
|
||||
|
||||
@Composable
|
||||
fun AddWalletFirstStepRoute(
|
||||
|
|
|
@ -10,10 +10,10 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.unit.dp
|
||||
import im.molly.monero.calculateBalance
|
||||
import im.molly.monero.demo.data.model.WalletAddress
|
||||
import im.molly.monero.demo.ui.component.CopyableText
|
||||
import im.molly.monero.toFormattedString
|
||||
import im.molly.monero.sdk.calculateBalance
|
||||
import im.molly.monero.sdk.toFormattedString
|
||||
|
||||
@Composable
|
||||
fun AddressCardExpanded(
|
||||
|
|
|
@ -15,10 +15,10 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import im.molly.monero.MoneroNetwork
|
||||
import im.molly.monero.demo.data.model.RemoteNode
|
||||
import im.molly.monero.demo.ui.component.SelectListBox
|
||||
import im.molly.monero.demo.ui.theme.AppTheme
|
||||
import im.molly.monero.sdk.MoneroNetwork
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
@Composable
|
||||
|
|
|
@ -7,8 +7,8 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import im.molly.monero.PendingTransfer
|
||||
import im.molly.monero.toFormattedString
|
||||
import im.molly.monero.sdk.PendingTransfer
|
||||
import im.molly.monero.sdk.toFormattedString
|
||||
|
||||
@Composable
|
||||
fun PendingTransferView(
|
||||
|
|
|
@ -17,7 +17,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import im.molly.monero.MoneroCurrency
|
||||
import im.molly.monero.sdk.MoneroCurrency
|
||||
|
||||
@Composable
|
||||
fun EditableRecipientList(
|
||||
|
|
|
@ -4,15 +4,15 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import im.molly.monero.FeePriority
|
||||
import im.molly.monero.MoneroCurrency
|
||||
import im.molly.monero.PaymentDetail
|
||||
import im.molly.monero.PaymentRequest
|
||||
import im.molly.monero.PendingTransfer
|
||||
import im.molly.monero.PublicAddress
|
||||
import im.molly.monero.TransferRequest
|
||||
import im.molly.monero.demo.AppModule
|
||||
import im.molly.monero.demo.data.WalletRepository
|
||||
import im.molly.monero.sdk.FeePriority
|
||||
import im.molly.monero.sdk.MoneroCurrency
|
||||
import im.molly.monero.sdk.PaymentDetail
|
||||
import im.molly.monero.sdk.PaymentRequest
|
||||
import im.molly.monero.sdk.PendingTransfer
|
||||
import im.molly.monero.sdk.PublicAddress
|
||||
import im.molly.monero.sdk.TransferRequest
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.getAndUpdate
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -15,12 +15,12 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import im.molly.monero.MoneroCurrency
|
||||
import im.molly.monero.Transaction
|
||||
import im.molly.monero.demo.ui.preview.PreviewParameterData
|
||||
import im.molly.monero.demo.ui.theme.AppTheme
|
||||
import im.molly.monero.demo.ui.theme.Blue40
|
||||
import im.molly.monero.demo.ui.theme.Red40
|
||||
import im.molly.monero.sdk.MoneroCurrency
|
||||
import im.molly.monero.sdk.Transaction
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
|
|
|
@ -22,12 +22,12 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import im.molly.monero.MoneroCurrency
|
||||
import im.molly.monero.Transaction
|
||||
import im.molly.monero.demo.ui.component.Toolbar
|
||||
import im.molly.monero.demo.ui.preview.PreviewParameterData
|
||||
import im.molly.monero.demo.ui.theme.AppIcons
|
||||
import im.molly.monero.demo.ui.theme.AppTheme
|
||||
import im.molly.monero.sdk.MoneroCurrency
|
||||
import im.molly.monero.sdk.Transaction
|
||||
|
||||
|
||||
@Composable
|
||||
|
|
|
@ -4,11 +4,11 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import im.molly.monero.Transaction
|
||||
import im.molly.monero.demo.AppModule
|
||||
import im.molly.monero.demo.common.Result
|
||||
import im.molly.monero.demo.common.asResult
|
||||
import im.molly.monero.demo.data.WalletRepository
|
||||
import im.molly.monero.sdk.Transaction
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
|
|
@ -19,15 +19,15 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import im.molly.monero.MoneroAmount
|
||||
import im.molly.monero.Balance
|
||||
import im.molly.monero.BlockchainTime
|
||||
import im.molly.monero.MoneroCurrency
|
||||
import im.molly.monero.MoneroNetwork
|
||||
import im.molly.monero.TimeLocked
|
||||
import im.molly.monero.sdk.MoneroAmount
|
||||
import im.molly.monero.sdk.Balance
|
||||
import im.molly.monero.sdk.BlockchainTime
|
||||
import im.molly.monero.sdk.Mainnet
|
||||
import im.molly.monero.sdk.MoneroCurrency
|
||||
import im.molly.monero.sdk.TimeLocked
|
||||
import im.molly.monero.demo.ui.theme.AppTheme
|
||||
import im.molly.monero.genesisTime
|
||||
import im.molly.monero.xmr
|
||||
import im.molly.monero.sdk.genesisTime
|
||||
import im.molly.monero.sdk.xmr
|
||||
import kotlinx.coroutines.delay
|
||||
import java.math.BigDecimal
|
||||
import java.time.Duration
|
||||
|
@ -120,13 +120,13 @@ fun WalletBalanceDetailsPreview() {
|
|||
WalletBalanceView(
|
||||
balance = Balance(
|
||||
pendingAmount = 5.xmr,
|
||||
timeLockedAmounts = listOf(
|
||||
lockableAmounts = listOf(
|
||||
TimeLocked(10.xmr, null),
|
||||
TimeLocked(BigDecimal("0.000000000001").xmr, null),
|
||||
TimeLocked(30.xmr, null)
|
||||
),
|
||||
),
|
||||
blockchainTime = MoneroNetwork.Mainnet.genesisTime,
|
||||
blockchainTime = Mainnet.genesisTime,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,17 +25,17 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import im.molly.monero.FeePriority
|
||||
import im.molly.monero.Ledger
|
||||
import im.molly.monero.MoneroCurrency
|
||||
import im.molly.monero.PendingTransfer
|
||||
import im.molly.monero.demo.data.model.WalletConfig
|
||||
import im.molly.monero.demo.ui.component.SelectListBox
|
||||
import im.molly.monero.demo.ui.component.Toolbar
|
||||
import im.molly.monero.demo.ui.preview.PreviewParameterData
|
||||
import im.molly.monero.demo.ui.theme.AppIcons
|
||||
import im.molly.monero.demo.ui.theme.AppTheme
|
||||
import im.molly.monero.toFormattedString
|
||||
import im.molly.monero.sdk.FeePriority
|
||||
import im.molly.monero.sdk.Ledger
|
||||
import im.molly.monero.sdk.MoneroCurrency
|
||||
import im.molly.monero.sdk.PendingTransfer
|
||||
import im.molly.monero.sdk.toFormattedString
|
||||
import kotlinx.coroutines.delay
|
||||
import java.time.Instant
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
|
|
@ -4,9 +4,6 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import im.molly.monero.Balance
|
||||
import im.molly.monero.BlockchainTime
|
||||
import im.molly.monero.MoneroNetwork
|
||||
import im.molly.monero.demo.AppModule
|
||||
import im.molly.monero.demo.common.Result
|
||||
import im.molly.monero.demo.common.asResult
|
||||
|
@ -14,6 +11,9 @@ import im.molly.monero.demo.data.WalletRepository
|
|||
import im.molly.monero.demo.data.model.WalletAddress
|
||||
import im.molly.monero.demo.data.model.WalletConfig
|
||||
import im.molly.monero.demo.data.model.WalletTransaction
|
||||
import im.molly.monero.sdk.Balance
|
||||
import im.molly.monero.sdk.BlockchainTime
|
||||
import im.molly.monero.sdk.MoneroNetwork
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.Instant
|
||||
|
|
|
@ -1,21 +1,10 @@
|
|||
package im.molly.monero.demo.ui.preview
|
||||
|
||||
import im.molly.monero.BlockHeader
|
||||
import im.molly.monero.BlockchainTime
|
||||
import im.molly.monero.HashDigest
|
||||
import im.molly.monero.Ledger
|
||||
import im.molly.monero.MoneroAmount
|
||||
import im.molly.monero.MoneroNetwork
|
||||
import im.molly.monero.PaymentDetail
|
||||
import im.molly.monero.PublicAddress
|
||||
import im.molly.monero.Transaction
|
||||
import im.molly.monero.TxState
|
||||
import im.molly.monero.UnlockTime
|
||||
import im.molly.monero.xmr
|
||||
import im.molly.monero.sdk.*
|
||||
import java.time.Instant
|
||||
|
||||
object PreviewParameterData {
|
||||
val network = MoneroNetwork.Mainnet
|
||||
val network = Mainnet
|
||||
|
||||
val blockHeader = BlockHeader(height = 2999840, epochSecond = 1697792826)
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
android.native.buildOutput=verbose
|
||||
|
|
50
gradle/libs.versions.toml
Normal file
50
gradle/libs.versions.toml
Normal file
|
@ -0,0 +1,50 @@
|
|||
[versions]
|
||||
agp = "8.10.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"
|
||||
publish = "0.32.0"
|
||||
|
||||
[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" }
|
||||
publish = { id = "com.vanniktech.maven.publish", version.ref = "publish" }
|
||||
|
||||
# Plugins defined by this project
|
||||
monero-sdk-module = { id ="im.molly.monero.sdk.module" }
|
19
gradle/test-libs.versions.toml
Normal file
19
gradle/test-libs.versions.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[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" }
|
||||
mockk-android = { module = "io.mockk:mockk-android", 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=d7042b3c11565c192041fc8c4703f541b888286404b4f267138c1d094d8ecdca
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-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, 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"
|
||||
}
|
178
lib/android/build.gradle.kts
Normal file
178
lib/android/build.gradle.kts
Normal file
|
@ -0,0 +1,178 @@
|
|||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.parcelize)
|
||||
alias(libs.plugins.monero.sdk.module)
|
||||
alias(libs.plugins.publish)
|
||||
signing
|
||||
}
|
||||
|
||||
version = "1.0.1-SNAPSHOT"
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "im.molly.monero.sdk"
|
||||
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") {
|
||||
isMinifyEnabled = false
|
||||
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("atdApi35") {
|
||||
device = "Small Phone"
|
||||
sdkVersion = 35
|
||||
systemImageSource = "aosp-atd"
|
||||
require64Bit = true
|
||||
}
|
||||
}
|
||||
groups {
|
||||
create("ciGroup") {
|
||||
targetDevices.add(allDevices["atdApi35"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
animationsDisabled = true
|
||||
}
|
||||
}
|
||||
|
||||
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.android)
|
||||
}
|
||||
|
||||
mavenPublishing {
|
||||
coordinates("im.molly", "monero-wallet-sdk", version.toString())
|
||||
|
||||
pom {
|
||||
name = "Monero Wallet SDK for Android"
|
||||
description =
|
||||
"Kotlin-based Android library for interacting with the Monero blockchain and managing Monero wallets."
|
||||
url = "https://github.com/mollyim/monero-wallet-sdk"
|
||||
|
||||
licenses {
|
||||
license {
|
||||
name = "GNU General Public License v3.0"
|
||||
url = "https://www.gnu.org/licenses/gpl-3.0.txt"
|
||||
distribution = "repo"
|
||||
}
|
||||
}
|
||||
|
||||
issueManagement {
|
||||
system = "GitHub Issues"
|
||||
url = "https://github.com/mollyim/monero-wallet-sdk/issues"
|
||||
}
|
||||
|
||||
scm {
|
||||
connection = "scm:git:git://github.com/mollyim/monero-wallet-sdk.git"
|
||||
developerConnection = "scm:git:ssh://git@github.com/mollyim/monero-wallet-sdk.git"
|
||||
url = "https://github.com/mollyim/monero-wallet-sdk"
|
||||
}
|
||||
|
||||
developers {
|
||||
developer {
|
||||
name = "Oscar Mira"
|
||||
url = "https://github.com/valldrac/"
|
||||
organization = "MollyIM"
|
||||
organizationUrl = "https://molly.im"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
publishToMavenCentral(com.vanniktech.maven.publish.SonatypeHost.CENTRAL_PORTAL)
|
||||
}
|
||||
|
||||
signing {
|
||||
useGpgCmd()
|
||||
if (!isSnapshot) {
|
||||
sign(publishing.publications)
|
||||
}
|
||||
}
|
2
lib/android/proguard-rules.pro
vendored
2
lib/android/proguard-rules.pro
vendored
|
@ -22,5 +22,5 @@
|
|||
|
||||
# Keeps methods that are invoked by JNI.
|
||||
-keepclassmembers class * {
|
||||
@im.molly.monero.UsedByNative *;
|
||||
@im.molly.monero.sdk.internal.CalledByNative *;
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
package im.molly.monero
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
package im.molly.monero
|
||||
|
||||
import android.os.Parcel
|
||||
import com.google.common.truth.Truth
|
||||
import org.junit.Test
|
||||
import kotlin.random.Random
|
||||
|
||||
class SecretKeyParcelableTest {
|
||||
|
||||
@Test
|
||||
fun testParcel() {
|
||||
val secret = Random.nextBytes(32)
|
||||
val originalKey = SecretKey(secret)
|
||||
|
||||
val parcel = Parcel.obtain()
|
||||
|
||||
originalKey.writeToParcel(parcel, 0)
|
||||
|
||||
parcel.setDataPosition(0)
|
||||
|
||||
val key = SecretKey.create(parcel)
|
||||
Truth.assertThat(key == originalKey).isTrue()
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package im.molly.monero
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.ServiceTestRule
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class WalletNativeServiceTest {
|
||||
|
||||
@get:Rule
|
||||
val serviceRule = ServiceTestRule()
|
||||
|
||||
private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
|
||||
// private val resources by lazy { context.resources }
|
||||
|
||||
@Test
|
||||
fun testBind() {
|
||||
assertThat(bindService()).isNotNull()
|
||||
}
|
||||
|
||||
private fun bindService(): IWalletService {
|
||||
val binder = serviceRule.bindService(Intent(context, WalletService::class.java))
|
||||
return IWalletService.Stub.asInterface(binder)
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
package im.molly.monero
|
||||
|
||||
import androidx.test.filters.LargeTest
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
|
||||
class WalletNativeTest {
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
fun keyGenerationIsDeterministic() {
|
||||
assertThat(
|
||||
WalletNative.fullNode(
|
||||
networkId = MoneroNetwork.Mainnet.id,
|
||||
secretSpendKey = SecretKey("d2ca26e22489bd9871c910c58dee3ab08e66b9d566825a064c8c0af061cd8706".parseHex()),
|
||||
).primaryAccountAddress
|
||||
).isEqualTo("4AYjQM9HoAFNUeC3cvSfgeAN89oMMpMqiByvunzSzhn97cj726rJj3x8hCbH58UnMqQJShczCxbpWRiCJQ3HCUDHLiKuo4T")
|
||||
|
||||
assertThat(
|
||||
WalletNative.fullNode(
|
||||
networkId = MoneroNetwork.Testnet.id,
|
||||
secretSpendKey = SecretKey("48a35268bc33227eea43ac1ecfd144d51efc023c115c26ca68a01cc6201e9900".parseHex()),
|
||||
).primaryAccountAddress
|
||||
).isEqualTo("A1v6gVUcGgGE87c1uFRWB1KfPVik2qLLDJiZT3rhZ8qjF3BGA6oHzeDboD23dH8rFaFFcysyqwF6DBj8WUTBWwEhESB7nZz")
|
||||
|
||||
assertThat(
|
||||
WalletNative.fullNode(
|
||||
networkId = MoneroNetwork.Stagenet.id,
|
||||
secretSpendKey = SecretKey("561a8d4e121ffca7321a7dc6af79679ceb4cdc8c0dcb0ef588b574586c5fac04".parseHex()),
|
||||
).primaryAccountAddress
|
||||
).isEqualTo("54kPaUhYgGNBT72N8Bv2DFMqstLGJCEcWg1EAjwpxABkKL3uBtBLAh4VAPKvhWBdaD4ZpiftA8YWFLAxnWL4aQ9TD4vhY4W")
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
fun publicAddressesAreDistinct() {
|
||||
val publicAddress =
|
||||
WalletNative.fullNode(
|
||||
networkId = MoneroNetwork.Mainnet.id,
|
||||
secretSpendKey = randomSecretKey(),
|
||||
).primaryAccountAddress
|
||||
|
||||
val anotherPublicAddress =
|
||||
WalletNative.fullNode(
|
||||
networkId = MoneroNetwork.Mainnet.id,
|
||||
secretSpendKey = randomSecretKey(),
|
||||
).primaryAccountAddress
|
||||
|
||||
assertThat(publicAddress).isNotEqualTo(anotherPublicAddress)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun atGenesisBalanceIsZero() {
|
||||
with(
|
||||
WalletNative.fullNode(
|
||||
networkId = MoneroNetwork.Mainnet.id,
|
||||
secretSpendKey = randomSecretKey(),
|
||||
).currentBalance
|
||||
) {
|
||||
assertThat(totalAmount).isEqualTo(0.toAtomicUnits())
|
||||
assertThat(totalAmountUnlockedAt(1)).isEqualTo(0.toAtomicUnits())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
package im.molly.monero.mnemonics
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.molly.monero.parseHex
|
||||
import org.junit.Test
|
||||
|
||||
class MoneroMnemonicTest {
|
||||
|
||||
data class TestCase(val entropy: String, val words: String, val language: String)
|
||||
|
||||
private val testVector = listOf(
|
||||
TestCase(
|
||||
entropy = "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 {
|
||||
validateMnemonicGeneration(it)
|
||||
validateEntropyRecovery(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateMnemonicGeneration(testCase: TestCase) {
|
||||
val mnemonicCode = MoneroMnemonic.generateMnemonic(testCase.entropy.parseHex())
|
||||
assertMnemonicCode(mnemonicCode, testCase)
|
||||
}
|
||||
|
||||
private fun validateEntropyRecovery(testCase: TestCase) {
|
||||
val mnemonicCode = MoneroMnemonic.recoverEntropy(testCase.words)
|
||||
assertThat(mnemonicCode).isNotNull()
|
||||
assertMnemonicCode(mnemonicCode!!, testCase)
|
||||
}
|
||||
|
||||
private fun assertMnemonicCode(mnemonicCode: MnemonicCode, testCase: TestCase) {
|
||||
with(mnemonicCode) {
|
||||
assertThat(entropy).isEqualTo(testCase.entropy.parseHex())
|
||||
assertThat(String(words)).isEqualTo(testCase.words)
|
||||
assertThat(locale.language).isEqualTo(testCase.language)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package im.molly.monero.sdk
|
||||
|
||||
import com.google.common.truth.FailureMetadata
|
||||
import com.google.common.truth.Subject
|
||||
import com.google.common.truth.Truth.assertAbout
|
||||
|
||||
class LedgerChainSubject private constructor(
|
||||
metadata: FailureMetadata,
|
||||
private val actual: List<Ledger>,
|
||||
) : Subject(metadata, actual) {
|
||||
|
||||
companion object {
|
||||
fun assertThat(ledgerChain: List<Ledger>): LedgerChainSubject {
|
||||
return assertAbout(factory).that(ledgerChain)
|
||||
}
|
||||
|
||||
private val factory = Factory { metadata, actual: List<Ledger> ->
|
||||
LedgerChainSubject(metadata, actual)
|
||||
}
|
||||
}
|
||||
|
||||
fun hasValidWalletHistory() {
|
||||
isNotEmpty()
|
||||
hasStableOrGrowingAccountSets()
|
||||
hasStableOrGrowingTransactionSets()
|
||||
hasStableOrGrowingKeyImageSets()
|
||||
allPublicAddressesMatch()
|
||||
}
|
||||
|
||||
fun isNotEmpty() {
|
||||
check("ledgers").that(actual).isNotEmpty()
|
||||
}
|
||||
|
||||
fun hasStableOrGrowingAccountSets() {
|
||||
actual.ledgerTransitionsByHeight { step, prev, next ->
|
||||
val prevAccByIndex = prev.indexedAccounts.associateBy { it.accountIndex }
|
||||
val nextAccByIndex = next.indexedAccounts.associateBy { it.accountIndex }
|
||||
|
||||
prevAccByIndex.forEach { (accIndex, prevAccount) ->
|
||||
val nextAccount = nextAccByIndex[accIndex]
|
||||
val subjectPath = "Ledger[$step → ${step + 1}].indexedAccounts[$accIndex]"
|
||||
|
||||
check(subjectPath).that(nextAccount).isNotNull()
|
||||
|
||||
if (nextAccount != null) {
|
||||
val missingAddresses =
|
||||
prevAccount.addresses.toSet() - nextAccount.addresses.toSet()
|
||||
check("missing addresses in $subjectPath").that(missingAddresses).isEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun hasStableOrGrowingTransactionSets() {
|
||||
actual.ledgerTransitionsByHeight { step, prev, next ->
|
||||
val missingTxIds = prev.transactionById.keys - next.transactionById.keys
|
||||
val subjectPath = "Ledger[$step → ${step + 1}].transactionById"
|
||||
|
||||
check("$subjectPath: missing transaction set").that(missingTxIds).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
fun hasStableOrGrowingKeyImageSets() {
|
||||
actual.ledgerTransitionsByHeight { step, prev, next ->
|
||||
val missingKI = prev.keyImages - next.keyImages
|
||||
val subjectPath = "Ledger[$step → ${step + 1}].keyImages"
|
||||
|
||||
check("$subjectPath: missing key images: $missingKI").that(missingKI).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
fun allPublicAddressesMatch() {
|
||||
val addresses = actual.map { it.publicAddress }.distinct()
|
||||
check("publicAddresses").that(addresses).hasSize(1)
|
||||
}
|
||||
|
||||
private fun List<Ledger>.ledgerTransitionsByHeight(
|
||||
action: (step: Int, prev: Ledger, next: Ledger) -> Unit
|
||||
) {
|
||||
this.sortedBy { it.checkedAt.height }
|
||||
.zipWithNext()
|
||||
.forEachIndexed { step, (prev, next) -> action(step, prev, next) }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package im.molly.monero.sdk
|
||||
|
||||
import com.google.common.truth.FailureMetadata
|
||||
import com.google.common.truth.Subject
|
||||
import com.google.common.truth.Truth.assertAbout
|
||||
import java.math.BigDecimal
|
||||
|
||||
class LedgerSubject private constructor(
|
||||
metadata: FailureMetadata,
|
||||
private val actual: Ledger,
|
||||
) : Subject(metadata, actual) {
|
||||
|
||||
companion object {
|
||||
fun assertThat(ledgerChain: Ledger): LedgerSubject {
|
||||
return assertAbout(factory).that(ledgerChain)
|
||||
}
|
||||
|
||||
private val factory = Factory { metadata, actual: Ledger ->
|
||||
LedgerSubject(metadata, actual)
|
||||
}
|
||||
}
|
||||
|
||||
fun isConsistent() {
|
||||
balanceIsNonNegative()
|
||||
}
|
||||
|
||||
fun balanceIsNonNegative() {
|
||||
actual.indexedAccounts.forEach { account ->
|
||||
val accountIndex = account.accountIndex
|
||||
val balance = actual.getBalanceForAccount(accountIndex)
|
||||
|
||||
val pending = balance.pendingAmount.xmr
|
||||
val confirmed = balance.confirmedAmount.xmr
|
||||
|
||||
check("indexedAccounts[$accountIndex].pendingAmount.xmr").that(pending)
|
||||
.isAtLeast(BigDecimal.ZERO)
|
||||
check("indexedAccounts[$accountIndex].confirmedAmount.xmr").that(confirmed)
|
||||
.isAtLeast(BigDecimal.ZERO)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package im.molly.monero.sdk
|
||||
|
||||
import com.google.common.truth.FailureMetadata
|
||||
import com.google.common.truth.Subject
|
||||
import com.google.common.truth.Truth.assertAbout
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class MoneroWalletSubject private constructor(
|
||||
metadata: FailureMetadata,
|
||||
private val actual: MoneroWallet,
|
||||
) : Subject(metadata, actual) {
|
||||
|
||||
companion object {
|
||||
fun assertThat(wallet: MoneroWallet): MoneroWalletSubject {
|
||||
return assertAbout(factory).that(wallet)
|
||||
}
|
||||
|
||||
private val factory = Factory { metadata, actual: MoneroWallet ->
|
||||
MoneroWalletSubject(metadata, actual)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun matchesStateOf(expected: MoneroWallet) {
|
||||
with(actual) {
|
||||
check("publicAddress").that(publicAddress).isEqualTo(expected.publicAddress)
|
||||
check("ledger").that(ledger().first()).isEqualTo(expected.ledger().first())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package im.molly.monero.sdk
|
||||
|
||||
import android.os.Parcel
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
import kotlin.random.Random
|
||||
|
||||
class SecretKeyParcelableTest {
|
||||
|
||||
@Test
|
||||
fun secretKeyIsParcelable() {
|
||||
val secret = Random.nextBytes(32)
|
||||
val originalKey = SecretKey(secret)
|
||||
|
||||
val parcel = Parcel.obtain().apply {
|
||||
originalKey.writeToParcel(this, 0)
|
||||
setDataPosition(0)
|
||||
}
|
||||
|
||||
val recreatedKey = SecretKey.CREATOR.createFromParcel(parcel)
|
||||
|
||||
assertThat(recreatedKey).isEqualTo(originalKey)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
package im.molly.monero.sdk.e2etest
|
||||
|
||||
import androidx.test.filters.LargeTest
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.molly.monero.sdk.InMemoryWalletDataStore
|
||||
import im.molly.monero.sdk.Mainnet
|
||||
import im.molly.monero.sdk.MoneroWalletSubject
|
||||
import im.molly.monero.sdk.RestorePoint
|
||||
import im.molly.monero.sdk.SecretKey
|
||||
import im.molly.monero.sdk.exceptions.NoSuchAccountException
|
||||
import im.molly.monero.sdk.service.BaseWalletService
|
||||
import im.molly.monero.sdk.service.InProcessWalletService
|
||||
import im.molly.monero.sdk.service.SandboxedWalletService
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
|
||||
abstract class WalletPersistenceTest(
|
||||
serviceClass: Class<out BaseWalletService>,
|
||||
) : WalletTestBase(serviceClass) {
|
||||
|
||||
@Test
|
||||
fun restoredWalletHasExpectedAccountKeys() = runTest {
|
||||
val sk = "148d78d2aba7dbca5cd8f6abcfb0b3c009ffbdbea1ff373d50ed94d78286640e".toSecretKey()
|
||||
val vk = "49774391fa5e8d249fc2c5b45dadef13534bf2483dede880dac88f061e809100".toSecretKey()
|
||||
|
||||
val wallet = walletProvider.restoreWallet(
|
||||
network = Mainnet,
|
||||
secretSpendKey = sk,
|
||||
restorePoint = RestorePoint.creationTime(Instant.now()),
|
||||
)
|
||||
|
||||
wallet.withViewKey { assertThat(it).isEqualTo(vk) }
|
||||
wallet.withSpendKey { assertThat(it).isEqualTo(sk) }
|
||||
wallet.withViewAndSpendKeys { viewKey, spendKey ->
|
||||
assertThat(viewKey).isEqualTo(vk)
|
||||
assertThat(spendKey).isEqualTo(sk)
|
||||
}
|
||||
|
||||
with(wallet.publicAddress) {
|
||||
assertThat(address).isEqualTo("42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm")
|
||||
assertThat(spendPublicKey.toString()).isEqualTo("1b3bd040020d3712ab84992b773d0a965134eb2df0392fb84af95de8a17be2ab")
|
||||
assertThat(viewPublicKey.toString()).isEqualTo("231c9bf8341c6a870d92e3fb98063a90a355fb8dbf74a8561b9d7f9273247e99")
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
fun String.toSecretKey() = SecretKey(this.hexToByteArray())
|
||||
|
||||
@Test
|
||||
fun saveToMultipleStores() = runTest {
|
||||
val defaultStore = InMemoryWalletDataStore()
|
||||
val wallet = wallet(defaultStore)
|
||||
wallet.save()
|
||||
|
||||
val newStore = InMemoryWalletDataStore()
|
||||
wallet.save(newStore)
|
||||
|
||||
assertThat(defaultStore.toByteArray()).isEqualTo(newStore.toByteArray())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getAllAccountsReturnsAll() = runTest {
|
||||
val wallet = wallet().apply {
|
||||
repeat(2) { createAccount() }
|
||||
createSubAddressForAccount(1)
|
||||
}
|
||||
|
||||
val allAccounts = wallet.getAllAccounts()
|
||||
assertThat(allAccounts).hasSize(3)
|
||||
assertThat(allAccounts).containsExactlyElementsIn(
|
||||
(0..2).map { wallet.getAccount(it) }
|
||||
)
|
||||
|
||||
val addresses = allAccounts.map { it.addresses }
|
||||
assertThat(addresses[0]).hasSize(1)
|
||||
assertThat(addresses[1]).hasSize(2)
|
||||
assertThat(addresses[2]).hasSize(1)
|
||||
|
||||
allAccounts.forEach { acc ->
|
||||
val primary = acc.addresses[0]
|
||||
assertThat(primary.isPrimaryAddress).isTrue()
|
||||
assertThat(primary.subAddressIndex).isEqualTo(0)
|
||||
if (acc.accountIndex == 1) {
|
||||
val sub = acc.addresses[1]
|
||||
assertThat(sub.isPrimaryAddress).isFalse()
|
||||
assertThat(sub.subAddressIndex).isEqualTo(1)
|
||||
}
|
||||
}
|
||||
|
||||
withReopenedWallet(wallet) { original, reopened ->
|
||||
MoneroWalletSubject.assertThat(reopened).matchesStateOf(original)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = NoSuchAccountException::class)
|
||||
fun getAccountThrowsForMissingAccount() = runTest {
|
||||
wallet().getAccount(accountIndex = 42)
|
||||
}
|
||||
|
||||
@Test(expected = NoSuchAccountException::class)
|
||||
fun createSubAddressForAccountThrowsForMissingAccount() = runTest {
|
||||
wallet().createSubAddressForAccount(accountIndex = 42)
|
||||
}
|
||||
|
||||
@Test(expected = NoSuchAccountException::class)
|
||||
fun findUnusedSubAddressThrowsForMissingAccount() = runTest {
|
||||
wallet().findUnusedSubAddress(accountIndex = 42)
|
||||
}
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
class WalletPersistenceInProcessTest : WalletPersistenceTest(InProcessWalletService::class.java)
|
||||
|
||||
@LargeTest
|
||||
class WalletPersistenceSandboxedTest : WalletPersistenceTest(SandboxedWalletService::class.java)
|
|
@ -0,0 +1,66 @@
|
|||
package im.molly.monero.sdk.e2etest
|
||||
|
||||
import androidx.test.filters.LargeTest
|
||||
import im.molly.monero.sdk.LedgerChainSubject
|
||||
import im.molly.monero.sdk.Mainnet
|
||||
import im.molly.monero.sdk.MoneroWalletSubject
|
||||
import im.molly.monero.sdk.RemoteNode
|
||||
import im.molly.monero.sdk.RestorePoint
|
||||
import im.molly.monero.sdk.SecretKey
|
||||
import im.molly.monero.sdk.service.BaseWalletService
|
||||
import im.molly.monero.sdk.service.InProcessWalletService
|
||||
import im.molly.monero.sdk.service.SandboxedWalletService
|
||||
import im.molly.monero.sdk.singleNodeClient
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.flow.takeWhile
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.junit.Test
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
abstract class WalletRefreshTest(
|
||||
serviceClass: Class<out BaseWalletService>,
|
||||
) : WalletTestBase(serviceClass) {
|
||||
|
||||
@Test
|
||||
fun restoredWalletEmitsExpectedLedgerOnRefresh(): Unit = runBlocking {
|
||||
val key =
|
||||
SecretKey("148d78d2aba7dbca5cd8f6abcfb0b3c009ffbdbea1ff373d50ed94d78286640e".hexToByteArray())
|
||||
val node = RemoteNode("http://node.monerodevs.org:18089", Mainnet)
|
||||
val restorePoint = RestorePoint.blockHeight(2861767)
|
||||
|
||||
val wallet = walletProvider.restoreWallet(
|
||||
network = Mainnet,
|
||||
client = node.singleNodeClient(),
|
||||
secretSpendKey = key,
|
||||
restorePoint = restorePoint,
|
||||
)
|
||||
|
||||
val refreshJob = launch {
|
||||
wallet.awaitRefresh()
|
||||
}
|
||||
|
||||
val ledgers = withTimeout(5.minutes) {
|
||||
wallet.ledger()
|
||||
.takeWhile { it.checkedAt.height < 2862121 }
|
||||
.toList()
|
||||
}
|
||||
|
||||
refreshJob.cancelAndJoin()
|
||||
|
||||
LedgerChainSubject.assertThat(ledgers).hasValidWalletHistory()
|
||||
|
||||
withReopenedWallet(wallet) { original, reopened ->
|
||||
MoneroWalletSubject.assertThat(reopened).matchesStateOf(original)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
class WalletRefreshInProcessTest : WalletRefreshTest(InProcessWalletService::class.java)
|
||||
|
||||
@LargeTest
|
||||
class WalletRefreshSandboxedTest : WalletRefreshTest(SandboxedWalletService::class.java)
|
|
@ -0,0 +1,41 @@
|
|||
package im.molly.monero.sdk.e2etest
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.ServiceTestRule
|
||||
import im.molly.monero.sdk.WalletProvider
|
||||
import im.molly.monero.sdk.internal.IWalletService
|
||||
import im.molly.monero.sdk.internal.WalletServiceClient
|
||||
import im.molly.monero.sdk.service.BaseWalletService
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
|
||||
class WalletServiceRule(private val serviceClass: Class<out BaseWalletService>) : TestRule {
|
||||
|
||||
val walletProvider: WalletProvider
|
||||
get() = _walletProvider ?: error("WalletService not bound yet")
|
||||
|
||||
private var _walletProvider: WalletProvider? = null
|
||||
|
||||
private val context: Context by lazy { InstrumentationRegistry.getInstrumentation().context }
|
||||
|
||||
private val delegate = ServiceTestRule()
|
||||
|
||||
override fun apply(base: Statement, description: Description): Statement {
|
||||
return delegate.apply(object : Statement() {
|
||||
override fun evaluate() {
|
||||
val binder = delegate.bindService(Intent(context, serviceClass))
|
||||
val walletService = IWalletService.Stub.asInterface(binder)
|
||||
_walletProvider = WalletServiceClient.withBoundService(context, walletService)
|
||||
|
||||
try {
|
||||
walletProvider.use { base.evaluate() }
|
||||
} finally {
|
||||
delegate.unbindService()
|
||||
}
|
||||
}
|
||||
}, description)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package im.molly.monero.sdk.e2etest
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.ServiceTestRule
|
||||
import im.molly.monero.sdk.InMemoryWalletDataStore
|
||||
import im.molly.monero.sdk.Mainnet
|
||||
import im.molly.monero.sdk.MoneroWallet
|
||||
import im.molly.monero.sdk.WalletDataStore
|
||||
import im.molly.monero.sdk.WalletProvider
|
||||
import im.molly.monero.sdk.internal.IWalletService
|
||||
import im.molly.monero.sdk.internal.WalletServiceClient
|
||||
import im.molly.monero.sdk.service.BaseWalletService
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
|
||||
abstract class WalletTestBase(private val serviceClass: Class<out BaseWalletService>) {
|
||||
|
||||
@get:Rule
|
||||
val walletServiceRule = ServiceTestRule()
|
||||
|
||||
protected lateinit var walletProvider: WalletProvider
|
||||
private set
|
||||
|
||||
protected val context: Context by lazy {
|
||||
InstrumentationRegistry.getInstrumentation().context
|
||||
}
|
||||
|
||||
private fun bindService(): IWalletService {
|
||||
val binder = walletServiceRule.bindService(Intent(context, serviceClass))
|
||||
return IWalletService.Stub.asInterface(binder)
|
||||
}
|
||||
|
||||
private fun unbindService() {
|
||||
walletServiceRule.unbindService()
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUpBase() {
|
||||
val walletService = bindService()
|
||||
walletProvider = WalletServiceClient.withBoundService(context, walletService)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDownBase() {
|
||||
runCatching {
|
||||
walletProvider.disconnect()
|
||||
}
|
||||
unbindService()
|
||||
}
|
||||
|
||||
protected suspend fun wallet(defaultStore: WalletDataStore? = null) =
|
||||
walletProvider.createNewWallet(Mainnet, defaultStore)
|
||||
|
||||
protected suspend fun withReopenedWallet(
|
||||
wallet: MoneroWallet,
|
||||
action: suspend (original: MoneroWallet, reopened: MoneroWallet) -> Unit,
|
||||
) {
|
||||
walletProvider.openWallet(
|
||||
network = wallet.network,
|
||||
dataStore = InMemoryWalletDataStore().apply {
|
||||
wallet.save(targetStore = this)
|
||||
},
|
||||
).use { reopened ->
|
||||
action(wallet, reopened)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
package im.molly.monero.sdk.internal
|
||||
|
||||
import android.os.ParcelFileDescriptor
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.molly.monero.sdk.WalletDataStore
|
||||
import io.mockk.clearAllMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.junit4.MockKRule
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
|
||||
class DataStoreAdapterTest {
|
||||
|
||||
@get:Rule
|
||||
val mockkRule = MockKRule(this)
|
||||
|
||||
private val dataStore = mockk<WalletDataStore>(relaxed = true)
|
||||
private val testDispatcher: TestDispatcher = StandardTestDispatcher()
|
||||
private val testIOException = IOException("Test IO Exception")
|
||||
|
||||
private lateinit var adapter: DataStoreAdapter
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
adapter = DataStoreAdapter(dataStore, ioDispatcher = testDispatcher)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun overwriteIsPassedToDataStore() = runTest(testDispatcher) {
|
||||
coEvery { dataStore.save(any(), any()) } returns Unit
|
||||
|
||||
listOf(true, false).forEach {
|
||||
adapter.saveWithFd(overwrite = it) {}
|
||||
coVerify(exactly = 1) { dataStore.save(any(), overwrite = it) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun propagatesIOExceptionWhenLoadFails() = runTest(testDispatcher) {
|
||||
coEvery { dataStore.load() } throws testIOException
|
||||
|
||||
val exception = runCatching {
|
||||
adapter.loadWithFd {}
|
||||
}.exceptionOrNull()
|
||||
|
||||
assertThat(exception).isEqualTo(testIOException)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun propagatesIOExceptionWhenSaveFails() = runTest(testDispatcher) {
|
||||
coEvery { dataStore.save(any(), any()) } throws testIOException
|
||||
|
||||
val exception = runCatching {
|
||||
adapter.saveWithFd(overwrite = true) {}
|
||||
}.exceptionOrNull()
|
||||
|
||||
assertThat(exception).isEqualTo(testIOException)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pipeIsAlwaysClosedAfterLoad() = runTest(testDispatcher) {
|
||||
val (readFd, writeFd) = mockPipe()
|
||||
|
||||
coEvery { dataStore.load() } returns ByteArrayInputStream(byteArrayOf(1, 2, 3))
|
||||
|
||||
adapter.loadWithFd({})
|
||||
coVerify { readFd.close() }
|
||||
coVerify { writeFd.close() }
|
||||
|
||||
clearAllMocks(answers = false)
|
||||
|
||||
runCatching {
|
||||
adapter.loadWithFd({ throw RuntimeException() })
|
||||
}
|
||||
coVerify { readFd.close() }
|
||||
coVerify { writeFd.close() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pipeIsAlwaysClosedAfterSave() = runTest(testDispatcher) {
|
||||
val (readFd, writeFd) = mockPipe()
|
||||
|
||||
coEvery { dataStore.save(any(), any()) } returns Unit
|
||||
|
||||
adapter.saveWithFd(overwrite = true, {})
|
||||
coVerify { readFd.close() }
|
||||
coVerify { writeFd.close() }
|
||||
|
||||
clearAllMocks(answers = false)
|
||||
|
||||
runCatching {
|
||||
adapter.saveWithFd(overwrite = true, { throw RuntimeException() })
|
||||
}
|
||||
coVerify { readFd.close() }
|
||||
coVerify { writeFd.close() }
|
||||
}
|
||||
|
||||
private fun mockPipe(): Pair<ParcelFileDescriptor, ParcelFileDescriptor> {
|
||||
val readFd = mockk<ParcelFileDescriptor>(relaxed = true)
|
||||
val writeFd = mockk<ParcelFileDescriptor>(relaxed = true)
|
||||
|
||||
mockkStatic(ParcelFileDescriptor::class)
|
||||
coEvery { ParcelFileDescriptor.createPipe() } returns arrayOf(readFd, writeFd)
|
||||
|
||||
return readFd to writeFd
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package im.molly.monero.sdk.internal
|
||||
|
||||
import androidx.test.filters.LargeTest
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.molly.monero.sdk.Mainnet
|
||||
import im.molly.monero.sdk.SecretKey
|
||||
import im.molly.monero.sdk.Stagenet
|
||||
import im.molly.monero.sdk.Testnet
|
||||
import im.molly.monero.sdk.randomSecretKey
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
class NativeWalletTest {
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
fun keyGenerationIsDeterministic() = runTest {
|
||||
assertThat(
|
||||
NativeWallet.localSyncWallet(
|
||||
networkId = Mainnet.id,
|
||||
secretSpendKey = SecretKey("d2ca26e22489bd9871c910c58dee3ab08e66b9d566825a064c8c0af061cd8706".hexToByteArray()),
|
||||
).publicAddress
|
||||
).isEqualTo("4AYjQM9HoAFNUeC3cvSfgeAN89oMMpMqiByvunzSzhn97cj726rJj3x8hCbH58UnMqQJShczCxbpWRiCJQ3HCUDHLiKuo4T")
|
||||
|
||||
assertThat(
|
||||
NativeWallet.localSyncWallet(
|
||||
networkId = Testnet.id,
|
||||
secretSpendKey = SecretKey("48a35268bc33227eea43ac1ecfd144d51efc023c115c26ca68a01cc6201e9900".hexToByteArray()),
|
||||
).publicAddress
|
||||
).isEqualTo("A1v6gVUcGgGE87c1uFRWB1KfPVik2qLLDJiZT3rhZ8qjF3BGA6oHzeDboD23dH8rFaFFcysyqwF6DBj8WUTBWwEhESB7nZz")
|
||||
|
||||
assertThat(
|
||||
NativeWallet.localSyncWallet(
|
||||
networkId = Stagenet.id,
|
||||
secretSpendKey = SecretKey("561a8d4e121ffca7321a7dc6af79679ceb4cdc8c0dcb0ef588b574586c5fac04".hexToByteArray()),
|
||||
).publicAddress
|
||||
).isEqualTo("54kPaUhYgGNBT72N8Bv2DFMqstLGJCEcWg1EAjwpxABkKL3uBtBLAh4VAPKvhWBdaD4ZpiftA8YWFLAxnWL4aQ9TD4vhY4W")
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
fun publicAddressesAreDistinct() = runTest {
|
||||
val publicAddress =
|
||||
NativeWallet.localSyncWallet(
|
||||
networkId = Mainnet.id,
|
||||
secretSpendKey = randomSecretKey(),
|
||||
).publicAddress
|
||||
|
||||
val anotherPublicAddress =
|
||||
NativeWallet.localSyncWallet(
|
||||
networkId = Mainnet.id,
|
||||
secretSpendKey = randomSecretKey(),
|
||||
).publicAddress
|
||||
|
||||
assertThat(publicAddress).isNotEqualTo(anotherPublicAddress)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun balanceIsZeroAtGenesis() = runTest {
|
||||
with(
|
||||
NativeWallet.localSyncWallet(
|
||||
networkId = Mainnet.id,
|
||||
secretSpendKey = randomSecretKey(),
|
||||
).getLedger()
|
||||
) {
|
||||
assertThat(transactions).isEmpty()
|
||||
assertThat(isBalanceZero).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package im.molly.monero.sdk.mnemonics
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
import java.util.Locale
|
||||
|
||||
class MoneroMnemonicTest {
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
data class TestCase(val key: String, val words: String, val language: String) {
|
||||
val entropy = key.hexToByteArray()
|
||||
}
|
||||
|
||||
private val testCases = listOf(
|
||||
TestCase(
|
||||
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 knownMnemonics() {
|
||||
testCases.forEach {
|
||||
validateMnemonicGeneration(it)
|
||||
validateEntropyRecovery(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun emptyEntropy() {
|
||||
MoneroMnemonic.generateMnemonic(ByteArray(0))
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun invalidEntropy() {
|
||||
MoneroMnemonic.generateMnemonic(ByteArray(2))
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun emptyWords() {
|
||||
MoneroMnemonic.recoverEntropy("")
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun invalidLanguage() {
|
||||
MoneroMnemonic.generateMnemonic(ByteArray(32), Locale("ZZ"))
|
||||
}
|
||||
|
||||
private fun validateMnemonicGeneration(testCase: TestCase) {
|
||||
val mnemonicCode =
|
||||
MoneroMnemonic.generateMnemonic(testCase.entropy, Locale(testCase.language))
|
||||
assertMnemonicCode(mnemonicCode, testCase)
|
||||
}
|
||||
|
||||
private fun validateEntropyRecovery(testCase: TestCase) {
|
||||
val mnemonicCode = MoneroMnemonic.recoverEntropy(testCase.words)
|
||||
assertMnemonicCode(mnemonicCode, testCase)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package im.molly.monero.sdk.service
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class WalletServiceSandboxingTest {
|
||||
|
||||
private val context: Context by lazy { InstrumentationRegistry.getInstrumentation().context }
|
||||
|
||||
@Test
|
||||
fun inProcessWalletServiceIsNotIsolated() = runTest {
|
||||
InProcessWalletService.connect(context).use { walletProvider ->
|
||||
assertThat(walletProvider.isServiceSandboxed()).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sandboxedWalletServiceIsIsolated() = runTest {
|
||||
SandboxedWalletService.connect(context).use { walletProvider ->
|
||||
assertThat(walletProvider.isServiceSandboxed()).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,11 +4,17 @@
|
|||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<application android:usesCleartextTraffic="true">
|
||||
<application
|
||||
android:largeHeap="true"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
||||
<service android:name=".service.InProcessWalletService" />
|
||||
|
||||
<service
|
||||
android:name=".WalletService"
|
||||
android:name=".service.SandboxedWalletService"
|
||||
android:isolatedProcess="true"
|
||||
android:process=":wallet_service" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
package im.molly.monero;
|
||||
|
||||
parcelable BlockchainTime;
|
|
@ -1,3 +0,0 @@
|
|||
package im.molly.monero;
|
||||
|
||||
parcelable HttpResponse;
|
|
@ -1,10 +0,0 @@
|
|||
package im.molly.monero;
|
||||
|
||||
import im.molly.monero.BlockchainTime;
|
||||
import im.molly.monero.internal.TxInfo;
|
||||
|
||||
oneway interface IBalanceListener {
|
||||
void onBalanceChanged(in List<TxInfo> txHistory, in String[] subAddresses, in BlockchainTime blockchainTime);
|
||||
void onRefresh(in BlockchainTime blockchainTime);
|
||||
void onSubAddressListUpdated(in String[] subAddresses);
|
||||
}
|
|
@ -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,6 +0,0 @@
|
|||
package im.molly.monero;
|
||||
|
||||
interface IStorageAdapter {
|
||||
boolean writeAsync(in ParcelFileDescriptor pfd);
|
||||
oneway void readAsync(in ParcelFileDescriptor pfd);
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
package im.molly.monero;
|
||||
|
||||
import im.molly.monero.IRemoteNodeClient;
|
||||
|
||||
interface IWalletClient {
|
||||
int getNetworkId();
|
||||
IRemoteNodeClient getRemoteNodeClient();
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
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;
|
||||
|
||||
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);
|
||||
void setListener(in IWalletServiceListener listener);
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
package im.molly.monero;
|
||||
|
||||
parcelable PaymentRequest;
|
|
@ -1,3 +0,0 @@
|
|||
package im.molly.monero;
|
||||
|
||||
parcelable PublicAddress;
|
|
@ -1,3 +0,0 @@
|
|||
package im.molly.monero;
|
||||
|
||||
parcelable RemoteNode;
|
|
@ -1,3 +0,0 @@
|
|||
package im.molly.monero;
|
||||
|
||||
parcelable SecretKey;
|
|
@ -1,3 +0,0 @@
|
|||
package im.molly.monero;
|
||||
|
||||
parcelable SweepRequest;
|
|
@ -1,3 +0,0 @@
|
|||
package im.molly.monero;
|
||||
|
||||
parcelable WalletConfig;
|
|
@ -1,3 +0,0 @@
|
|||
package im.molly.monero.internal;
|
||||
|
||||
parcelable TxInfo;
|
|
@ -0,0 +1,3 @@
|
|||
package im.molly.monero.sdk;
|
||||
|
||||
parcelable BlockchainTime;
|
|
@ -0,0 +1,3 @@
|
|||
package im.molly.monero.sdk;
|
||||
|
||||
parcelable PaymentRequest;
|
|
@ -0,0 +1,3 @@
|
|||
package im.molly.monero.sdk;
|
||||
|
||||
parcelable PublicAddress;
|
|
@ -0,0 +1,3 @@
|
|||
package im.molly.monero.sdk;
|
||||
|
||||
parcelable RemoteNode;
|
|
@ -0,0 +1,3 @@
|
|||
package im.molly.monero.sdk;
|
||||
|
||||
parcelable SecretKey;
|
|
@ -0,0 +1,3 @@
|
|||
package im.molly.monero.sdk;
|
||||
|
||||
parcelable SweepRequest;
|
|
@ -0,0 +1,3 @@
|
|||
package im.molly.monero.sdk.internal;
|
||||
|
||||
parcelable HttpRequest;
|
|
@ -0,0 +1,3 @@
|
|||
package im.molly.monero.sdk.internal;
|
||||
|
||||
parcelable HttpResponse;
|
|
@ -0,0 +1,11 @@
|
|||
package im.molly.monero.sdk.internal;
|
||||
|
||||
import im.molly.monero.sdk.BlockchainTime;
|
||||
import im.molly.monero.sdk.internal.TxInfo;
|
||||
|
||||
oneway interface IBalanceListener {
|
||||
void onBalanceUpdateFinalized(in List<TxInfo> txBatch, in String[] allSubAddresses, in BlockchainTime blockchainTime);
|
||||
void onBalanceUpdateChunk(in List<TxInfo> txBatch);
|
||||
void onWalletRefreshed(in BlockchainTime blockchainTime);
|
||||
void onSubAddressListUpdated(in String[] allSubAddresses);
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package im.molly.monero.sdk.internal;
|
||||
|
||||
import im.molly.monero.sdk.internal.HttpResponse;
|
||||
|
||||
oneway interface IHttpRequestCallback {
|
||||
void onResponse(in HttpResponse response);
|
||||
void onError();
|
||||
void onRequestCanceled();
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package im.molly.monero.sdk.internal;
|
||||
|
||||
import im.molly.monero.sdk.internal.HttpRequest;
|
||||
import im.molly.monero.sdk.internal.IHttpRequestCallback;
|
||||
|
||||
interface IHttpRpcClient {
|
||||
oneway void callAsync(in HttpRequest request, in IHttpRequestCallback callback, int callId);
|
||||
oneway void cancelAsync(int callId);
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
package im.molly.monero;
|
||||
package im.molly.monero.sdk.internal;
|
||||
|
||||
import im.molly.monero.ITransferCallback;
|
||||
import im.molly.monero.sdk.internal.ITransferCallback;
|
||||
|
||||
interface IPendingTransfer {
|
||||
long getAmount();
|
|
@ -1,6 +1,6 @@
|
|||
package im.molly.monero;
|
||||
package im.molly.monero.sdk.internal;
|
||||
|
||||
import im.molly.monero.IPendingTransfer;
|
||||
import im.molly.monero.sdk.internal.IPendingTransfer;
|
||||
|
||||
oneway interface ITransferCallback {
|
||||
void onTransferCreated(in IPendingTransfer pendingTransfer);
|
|
@ -1,13 +1,16 @@
|
|||
package im.molly.monero;
|
||||
package im.molly.monero.sdk.internal;
|
||||
|
||||
import im.molly.monero.IBalanceListener;
|
||||
import im.molly.monero.ITransferCallback;
|
||||
import im.molly.monero.IWalletCallbacks;
|
||||
import im.molly.monero.PaymentRequest;
|
||||
import im.molly.monero.SweepRequest;
|
||||
import im.molly.monero.sdk.PaymentRequest;
|
||||
import im.molly.monero.sdk.SecretKey;
|
||||
import im.molly.monero.sdk.SweepRequest;
|
||||
import im.molly.monero.sdk.internal.IBalanceListener;
|
||||
import im.molly.monero.sdk.internal.ITransferCallback;
|
||||
import im.molly.monero.sdk.internal.IWalletCallbacks;
|
||||
|
||||
interface IWallet {
|
||||
String getPublicAddress();
|
||||
SecretKey getSpendSecretKey();
|
||||
SecretKey getViewSecretKey();
|
||||
void addBalanceListener(in IBalanceListener listener);
|
||||
void removeBalanceListener(in IBalanceListener listener);
|
||||
oneway void addDetachedSubAddress(int accountIndex, int subAddressIndex, in IWalletCallbacks callback);
|
||||
|
@ -18,7 +21,7 @@ interface IWallet {
|
|||
oneway void resumeRefresh(boolean skipCoinbase, in IWalletCallbacks callback);
|
||||
oneway void cancelRefresh();
|
||||
oneway void setRefreshSince(long heightOrTimestamp);
|
||||
oneway void commit(in IWalletCallbacks callback);
|
||||
oneway void commit(in ParcelFileDescriptor outputFd, in IWalletCallbacks callback);
|
||||
oneway void createPayment(in PaymentRequest request, in ITransferCallback callback);
|
||||
oneway void createSweep(in SweepRequest request, in ITransferCallback callback);
|
||||
oneway void requestFees(in IWalletCallbacks callback);
|
|
@ -1,11 +1,12 @@
|
|||
package im.molly.monero;
|
||||
package im.molly.monero.sdk.internal;
|
||||
|
||||
import im.molly.monero.BlockchainTime;
|
||||
import im.molly.monero.sdk.BlockchainTime;
|
||||
|
||||
oneway interface IWalletCallbacks {
|
||||
void onRefreshResult(in BlockchainTime blockchainTime, int status);
|
||||
void onCommitResult(boolean success);
|
||||
void onSubAddressReady(String subAddress);
|
||||
void onSubAddressListReceived(in String[] subAddresses);
|
||||
void onAccountNotFound(int accountIndex);
|
||||
void onFeesReceived(in long[] fees);
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package im.molly.monero.sdk.internal;
|
||||
|
||||
import im.molly.monero.sdk.SecretKey;
|
||||
import im.molly.monero.sdk.internal.IHttpRpcClient;
|
||||
import im.molly.monero.sdk.internal.IWalletServiceCallbacks;
|
||||
import im.molly.monero.sdk.internal.IWalletServiceListener;
|
||||
import im.molly.monero.sdk.internal.WalletConfig;
|
||||
|
||||
interface IWalletService {
|
||||
oneway void createWallet(in WalletConfig config, in IHttpRpcClient rpcClient, in IWalletServiceCallbacks callback);
|
||||
oneway void restoreWallet(in WalletConfig config, in IHttpRpcClient rpcClient, in IWalletServiceCallbacks callback, in SecretKey spendSecretKey, long restorePoint);
|
||||
oneway void openWallet(in WalletConfig config, in IHttpRpcClient rpcClient, in IWalletServiceCallbacks callback, in ParcelFileDescriptor inputFd);
|
||||
void setListener(in IWalletServiceListener listener);
|
||||
boolean isServiceIsolated();
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
package im.molly.monero;
|
||||
package im.molly.monero.sdk.internal;
|
||||
|
||||
import im.molly.monero.IWallet;
|
||||
import im.molly.monero.sdk.internal.IWallet;
|
||||
|
||||
oneway interface IWalletServiceCallbacks {
|
||||
void onWalletResult(in IWallet wallet);
|
|
@ -1,4 +1,4 @@
|
|||
package im.molly.monero;
|
||||
package im.molly.monero.sdk.internal;
|
||||
|
||||
oneway interface IWalletServiceListener {
|
||||
void onLogMessage(int priority, String tag, String msg, String cause);
|
|
@ -0,0 +1,3 @@
|
|||
package im.molly.monero.sdk.internal;
|
||||
|
||||
parcelable TxInfo;
|
|
@ -0,0 +1,3 @@
|
|||
package im.molly.monero.sdk.internal;
|
||||
|
||||
parcelable WalletConfig;
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue