mirror of
https://github.com/mollyim/monero-wallet-sdk.git
synced 2025-09-21 21:14:39 -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
|
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.AppDatabase
|
||||||
import im.molly.monero.demo.data.entity.asEntity
|
import im.molly.monero.demo.data.entity.asEntity
|
||||||
import im.molly.monero.demo.data.model.RemoteNode
|
import im.molly.monero.demo.data.model.RemoteNode
|
||||||
|
import im.molly.monero.sdk.MoneroNetwork
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
|
||||||
val DefaultNodeList = listOf(
|
val DefaultNodeList = listOf(
|
||||||
MoneroNetwork.Mainnet to listOf(
|
MoneroNetwork.Mainnet to listOf(
|
||||||
|
@ -21,7 +21,7 @@ suspend fun addDefaultRemoteNodes(appDatabase: AppDatabase) {
|
||||||
val dao = appDatabase.remoteNodeDao()
|
val dao = appDatabase.remoteNodeDao()
|
||||||
val nodes = DefaultNodeList.flatMap { (network, urls) ->
|
val nodes = DefaultNodeList.flatMap { (network, urls) ->
|
||||||
urls.map { url ->
|
urls.map { url ->
|
||||||
RemoteNode(network = network, uri = Uri.parse(url)).asEntity()
|
RemoteNode(network = network, uri = url.toUri()).asEntity()
|
||||||
}
|
}
|
||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
dao.upsert(*nodes)
|
dao.upsert(*nodes)
|
||||||
|
|
|
@ -7,12 +7,12 @@ import android.content.ServiceConnection
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.SystemBarStyle
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
|
||||||
import im.molly.monero.demo.service.SyncService
|
import im.molly.monero.demo.service.SyncService
|
||||||
import im.molly.monero.demo.ui.DemoApp
|
import im.molly.monero.demo.ui.DemoApp
|
||||||
import im.molly.monero.demo.ui.theme.AppTheme
|
import im.molly.monero.demo.ui.theme.AppTheme
|
||||||
|
@ -27,15 +27,19 @@ class MainActivity : ComponentActivity() {
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val systemUiController = rememberSystemUiController()
|
|
||||||
val darkTheme = isSystemInDarkTheme()
|
val darkTheme = isSystemInDarkTheme()
|
||||||
|
|
||||||
DisposableEffect(systemUiController, darkTheme) {
|
DisposableEffect(darkTheme) {
|
||||||
systemUiController.setSystemBarsColor(
|
enableEdgeToEdge(
|
||||||
color = Color.Transparent,
|
statusBarStyle = SystemBarStyle.auto(
|
||||||
darkIcons = !darkTheme,
|
android.graphics.Color.TRANSPARENT,
|
||||||
|
android.graphics.Color.TRANSPARENT,
|
||||||
|
) { darkTheme },
|
||||||
|
navigationBarStyle = SystemBarStyle.auto(
|
||||||
|
lightScrim,
|
||||||
|
darkScrim,
|
||||||
|
) { darkTheme },
|
||||||
)
|
)
|
||||||
|
|
||||||
onDispose {}
|
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.core.Preferences
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
import im.molly.monero.demo.service.SyncService
|
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")
|
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.content.Context
|
||||||
import android.util.AtomicFile
|
import android.util.AtomicFile
|
||||||
import im.molly.monero.*
|
import im.molly.monero.sdk.*
|
||||||
import im.molly.monero.loadbalancer.RoundRobinRule
|
import im.molly.monero.sdk.loadbalancer.RoundRobinRule
|
||||||
|
import im.molly.monero.sdk.service.SandboxedWalletService
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
@ -15,7 +16,7 @@ import java.io.OutputStream
|
||||||
class MoneroSdkClient(private val context: Context) {
|
class MoneroSdkClient(private val context: Context) {
|
||||||
|
|
||||||
private val providerDeferred = CoroutineScope(Dispatchers.IO).async {
|
private val providerDeferred = CoroutineScope(Dispatchers.IO).async {
|
||||||
WalletProvider.connect(context)
|
SandboxedWalletService.connect(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun createWallet(network: MoneroNetwork, filename: String): MoneroWallet {
|
suspend fun createWallet(network: MoneroNetwork, filename: String): MoneroWallet {
|
||||||
|
@ -24,7 +25,7 @@ class MoneroSdkClient(private val context: Context) {
|
||||||
network = network,
|
network = network,
|
||||||
dataStore = WalletDataStoreFile(filename, newFile = true),
|
dataStore = WalletDataStoreFile(filename, newFile = true),
|
||||||
).also { wallet ->
|
).also { wallet ->
|
||||||
wallet.commit()
|
wallet.save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,7 +42,7 @@ class MoneroSdkClient(private val context: Context) {
|
||||||
secretSpendKey = secretSpendKey,
|
secretSpendKey = secretSpendKey,
|
||||||
restorePoint = restorePoint,
|
restorePoint = restorePoint,
|
||||||
).also { wallet ->
|
).also { wallet ->
|
||||||
wallet.commit()
|
wallet.save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +53,7 @@ class MoneroSdkClient(private val context: Context) {
|
||||||
httpClient: OkHttpClient,
|
httpClient: OkHttpClient,
|
||||||
): MoneroWallet {
|
): MoneroWallet {
|
||||||
val dataStore = WalletDataStoreFile(filename)
|
val dataStore = WalletDataStoreFile(filename)
|
||||||
val client = RemoteNodeClient.forNetwork(
|
val client = MoneroNodeClient.create(
|
||||||
network = network,
|
network = network,
|
||||||
remoteNodes = remoteNodes,
|
remoteNodes = remoteNodes,
|
||||||
loadBalancerRule = RoundRobinRule(),
|
loadBalancerRule = RoundRobinRule(),
|
||||||
|
@ -86,7 +87,7 @@ class MoneroSdkClient(private val context: Context) {
|
||||||
throw IOException("Cannot create wallet data directory: ${walletDataDir.path}")
|
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()
|
val output = file.startWrite()
|
||||||
try {
|
try {
|
||||||
writer(output)
|
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()
|
return file.openRead()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package im.molly.monero.demo.data
|
package im.molly.monero.demo.data
|
||||||
|
|
||||||
import im.molly.monero.*
|
|
||||||
import im.molly.monero.demo.data.model.WalletConfig
|
import im.molly.monero.demo.data.model.WalletConfig
|
||||||
|
import im.molly.monero.sdk.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
@ -33,7 +33,8 @@ class WalletRepository(
|
||||||
remoteNodes = configFlow.map {
|
remoteNodes = configFlow.map {
|
||||||
it.remoteNodes.map { node ->
|
it.remoteNodes.map { node ->
|
||||||
RemoteNode(
|
RemoteNode(
|
||||||
uri = node.uri,
|
url = node.uri.toString(),
|
||||||
|
network = node.network,
|
||||||
username = node.username,
|
username = node.username,
|
||||||
password = node.password,
|
password = node.password,
|
||||||
)
|
)
|
||||||
|
@ -51,8 +52,8 @@ class WalletRepository(
|
||||||
|
|
||||||
fun getWalletIdList() = walletDataSource.readWalletIdList()
|
fun getWalletIdList() = walletDataSource.readWalletIdList()
|
||||||
|
|
||||||
fun getRemoteClients(): Flow<List<RemoteNodeClient>> =
|
fun getMoneroNodeClients(): Flow<List<MoneroNodeClient>> =
|
||||||
getWalletIdList().map { it.mapNotNull { walletId -> getWallet(walletId).remoteNodeClient } }
|
getWalletIdList().map { it.mapNotNull { walletId -> getWallet(walletId).moneroNodeClient } }
|
||||||
|
|
||||||
fun getWalletConfig(walletId: Long) = walletDataSource.readWalletConfig(walletId)
|
fun getWalletConfig(walletId: Long) = walletDataSource.readWalletConfig(walletId)
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,8 @@ import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.Index
|
import androidx.room.Index
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import im.molly.monero.MoneroNetwork
|
|
||||||
import im.molly.monero.demo.data.model.RemoteNode
|
import im.molly.monero.demo.data.model.RemoteNode
|
||||||
|
import im.molly.monero.sdk.MoneroNetwork
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "remote_nodes",
|
tableName = "remote_nodes",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package im.molly.monero.demo.data.model
|
package im.molly.monero.demo.data.model
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import im.molly.monero.MoneroNetwork
|
import im.molly.monero.sdk.MoneroNetwork
|
||||||
|
|
||||||
data class RemoteNode(
|
data class RemoteNode(
|
||||||
val id: Long? = null,
|
val id: Long? = null,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package im.molly.monero.demo.data.model
|
package im.molly.monero.demo.data.model
|
||||||
|
|
||||||
import im.molly.monero.AccountAddress
|
import im.molly.monero.sdk.AccountAddress
|
||||||
import im.molly.monero.Enote
|
import im.molly.monero.sdk.Enote
|
||||||
import im.molly.monero.TimeLocked
|
import im.molly.monero.sdk.TimeLocked
|
||||||
|
|
||||||
data class WalletAddress(
|
data class WalletAddress(
|
||||||
val address: AccountAddress,
|
val address: AccountAddress,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package im.molly.monero.demo.data.model
|
package im.molly.monero.demo.data.model
|
||||||
|
|
||||||
import im.molly.monero.Transaction
|
import im.molly.monero.sdk.Transaction
|
||||||
|
|
||||||
data class WalletTransaction(
|
data class WalletTransaction(
|
||||||
val walletId: Long,
|
val walletId: Long,
|
||||||
|
|
|
@ -38,7 +38,7 @@ class SyncService(
|
||||||
syncedWalletIds.map {
|
syncedWalletIds.map {
|
||||||
walletRepository.getWallet(it)
|
walletRepository.getWallet(it)
|
||||||
}.forEach { wallet ->
|
}.forEach { wallet ->
|
||||||
wallet.commit()
|
wallet.save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delay(60.seconds)
|
delay(60.seconds)
|
||||||
|
@ -56,7 +56,7 @@ class SyncService(
|
||||||
if (result.isError()) {
|
if (result.isError()) {
|
||||||
// TODO: Handle non-recoverable errors
|
// TODO: Handle non-recoverable errors
|
||||||
}
|
}
|
||||||
wallet.commit()
|
wallet.save()
|
||||||
delay(10.seconds)
|
delay(10.seconds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,23 +3,21 @@ package im.molly.monero.demo.ui
|
||||||
import androidx.compose.runtime.mutableStateMapOf
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.AppModule
|
||||||
import im.molly.monero.demo.data.RemoteNodeRepository
|
import im.molly.monero.demo.data.RemoteNodeRepository
|
||||||
import im.molly.monero.demo.data.WalletRepository
|
import im.molly.monero.demo.data.WalletRepository
|
||||||
import im.molly.monero.demo.data.model.DefaultMoneroNetwork
|
import im.molly.monero.demo.data.model.DefaultMoneroNetwork
|
||||||
import im.molly.monero.demo.data.model.RemoteNode
|
import im.molly.monero.demo.data.model.RemoteNode
|
||||||
import im.molly.monero.mnemonics.MoneroMnemonic
|
import im.molly.monero.sdk.MoneroNetwork
|
||||||
import im.molly.monero.util.parseHex
|
import im.molly.monero.sdk.RestorePoint
|
||||||
import im.molly.monero.util.toHex
|
import im.molly.monero.sdk.SecretKey
|
||||||
|
import im.molly.monero.sdk.mnemonics.MoneroMnemonic
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalStdlibApi::class)
|
||||||
class AddWalletViewModel(
|
class AddWalletViewModel(
|
||||||
private val remoteNodeRepository: RemoteNodeRepository = AppModule.remoteNodeRepository,
|
private val remoteNodeRepository: RemoteNodeRepository = AppModule.remoteNodeRepository,
|
||||||
private val walletRepository: WalletRepository = AppModule.walletRepository,
|
private val walletRepository: WalletRepository = AppModule.walletRepository,
|
||||||
|
@ -66,7 +64,7 @@ class AddWalletViewModel(
|
||||||
MoneroMnemonic.recoverEntropy(words)?.use { mnemonicCode ->
|
MoneroMnemonic.recoverEntropy(words)?.use { mnemonicCode ->
|
||||||
val secretKey = SecretKey(mnemonicCode.entropy)
|
val secretKey = SecretKey(mnemonicCode.entropy)
|
||||||
viewModelState.update {
|
viewModelState.update {
|
||||||
it.copy(secretSpendKeyHex = secretKey.bytes.toHex())
|
it.copy(secretSpendKeyHex = secretKey.bytes.toHexString())
|
||||||
}
|
}
|
||||||
secretKey.destroy()
|
secretKey.destroy()
|
||||||
return true
|
return true
|
||||||
|
@ -85,7 +83,7 @@ class AddWalletViewModel(
|
||||||
fun validateSecretSpendKeyHex(): Boolean =
|
fun validateSecretSpendKeyHex(): Boolean =
|
||||||
with(viewModelState.value) {
|
with(viewModelState.value) {
|
||||||
return secretSpendKeyHex.length == 64 && runCatching {
|
return secretSpendKeyHex.length == 64 && runCatching {
|
||||||
secretSpendKeyHex.parseHex()
|
secretSpendKeyHex.hexToByteArray()
|
||||||
}.isSuccess
|
}.isSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,7 +123,7 @@ class AddWalletViewModel(
|
||||||
|
|
||||||
else -> RestorePoint.Genesis
|
else -> RestorePoint.Genesis
|
||||||
}
|
}
|
||||||
SecretKey(state.secretSpendKeyHex.parseHex()).use { secretSpendKey ->
|
SecretKey(state.secretSpendKeyHex.hexToByteArray()).use { secretSpendKey ->
|
||||||
walletRepository.restoreWallet(
|
walletRepository.restoreWallet(
|
||||||
state.network,
|
state.network,
|
||||||
state.walletName,
|
state.walletName,
|
||||||
|
|
|
@ -16,13 +16,13 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
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.DefaultMoneroNetwork
|
||||||
import im.molly.monero.demo.data.model.RemoteNode
|
import im.molly.monero.demo.data.model.RemoteNode
|
||||||
import im.molly.monero.demo.ui.component.SelectListBox
|
import im.molly.monero.demo.ui.component.SelectListBox
|
||||||
import im.molly.monero.demo.ui.component.Toolbar
|
import im.molly.monero.demo.ui.component.Toolbar
|
||||||
import im.molly.monero.demo.ui.theme.AppIcons
|
import im.molly.monero.demo.ui.theme.AppIcons
|
||||||
import im.molly.monero.demo.ui.theme.AppTheme
|
import im.molly.monero.demo.ui.theme.AppTheme
|
||||||
|
import im.molly.monero.sdk.MoneroNetwork
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AddWalletFirstStepRoute(
|
fun AddWalletFirstStepRoute(
|
||||||
|
|
|
@ -10,10 +10,10 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import im.molly.monero.calculateBalance
|
|
||||||
import im.molly.monero.demo.data.model.WalletAddress
|
import im.molly.monero.demo.data.model.WalletAddress
|
||||||
import im.molly.monero.demo.ui.component.CopyableText
|
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
|
@Composable
|
||||||
fun AddressCardExpanded(
|
fun AddressCardExpanded(
|
||||||
|
|
|
@ -15,10 +15,10 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import im.molly.monero.MoneroNetwork
|
|
||||||
import im.molly.monero.demo.data.model.RemoteNode
|
import im.molly.monero.demo.data.model.RemoteNode
|
||||||
import im.molly.monero.demo.ui.component.SelectListBox
|
import im.molly.monero.demo.ui.component.SelectListBox
|
||||||
import im.molly.monero.demo.ui.theme.AppTheme
|
import im.molly.monero.demo.ui.theme.AppTheme
|
||||||
|
import im.molly.monero.sdk.MoneroNetwork
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
@ -7,8 +7,8 @@ import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import im.molly.monero.PendingTransfer
|
import im.molly.monero.sdk.PendingTransfer
|
||||||
import im.molly.monero.toFormattedString
|
import im.molly.monero.sdk.toFormattedString
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PendingTransferView(
|
fun PendingTransferView(
|
||||||
|
|
|
@ -17,7 +17,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import im.molly.monero.MoneroCurrency
|
import im.molly.monero.sdk.MoneroCurrency
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EditableRecipientList(
|
fun EditableRecipientList(
|
||||||
|
|
|
@ -4,15 +4,15 @@ import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.initializer
|
import androidx.lifecycle.viewmodel.initializer
|
||||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
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.AppModule
|
||||||
import im.molly.monero.demo.data.WalletRepository
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.getAndUpdate
|
import kotlinx.coroutines.flow.getAndUpdate
|
||||||
|
|
|
@ -82,7 +82,7 @@ class SettingsViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun onProxyChanged(newProxy: Proxy) {
|
private suspend fun onProxyChanged(newProxy: Proxy) {
|
||||||
walletRepository.getRemoteClients().first().forEach { client ->
|
walletRepository.getMoneroNodeClients().first().forEach { client ->
|
||||||
val current = client.httpClient.proxy
|
val current = client.httpClient.proxy
|
||||||
if (current != newProxy) {
|
if (current != newProxy) {
|
||||||
val builder = client.httpClient.newBuilder()
|
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.PreviewParameter
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import androidx.compose.ui.unit.dp
|
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.preview.PreviewParameterData
|
||||||
import im.molly.monero.demo.ui.theme.AppTheme
|
import im.molly.monero.demo.ui.theme.AppTheme
|
||||||
import im.molly.monero.demo.ui.theme.Blue40
|
import im.molly.monero.demo.ui.theme.Blue40
|
||||||
import im.molly.monero.demo.ui.theme.Red40
|
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.ZoneId
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
|
|
@ -22,12 +22,12 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
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.component.Toolbar
|
||||||
import im.molly.monero.demo.ui.preview.PreviewParameterData
|
import im.molly.monero.demo.ui.preview.PreviewParameterData
|
||||||
import im.molly.monero.demo.ui.theme.AppIcons
|
import im.molly.monero.demo.ui.theme.AppIcons
|
||||||
import im.molly.monero.demo.ui.theme.AppTheme
|
import im.molly.monero.demo.ui.theme.AppTheme
|
||||||
|
import im.molly.monero.sdk.MoneroCurrency
|
||||||
|
import im.molly.monero.sdk.Transaction
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
@ -4,11 +4,11 @@ import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.initializer
|
import androidx.lifecycle.viewmodel.initializer
|
||||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||||
import im.molly.monero.Transaction
|
|
||||||
import im.molly.monero.demo.AppModule
|
import im.molly.monero.demo.AppModule
|
||||||
import im.molly.monero.demo.common.Result
|
import im.molly.monero.demo.common.Result
|
||||||
import im.molly.monero.demo.common.asResult
|
import im.molly.monero.demo.common.asResult
|
||||||
import im.molly.monero.demo.data.WalletRepository
|
import im.molly.monero.demo.data.WalletRepository
|
||||||
|
import im.molly.monero.sdk.Transaction
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
|
@ -19,15 +19,15 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import im.molly.monero.MoneroAmount
|
import im.molly.monero.sdk.MoneroAmount
|
||||||
import im.molly.monero.Balance
|
import im.molly.monero.sdk.Balance
|
||||||
import im.molly.monero.BlockchainTime
|
import im.molly.monero.sdk.BlockchainTime
|
||||||
import im.molly.monero.MoneroCurrency
|
import im.molly.monero.sdk.Mainnet
|
||||||
import im.molly.monero.MoneroNetwork
|
import im.molly.monero.sdk.MoneroCurrency
|
||||||
import im.molly.monero.TimeLocked
|
import im.molly.monero.sdk.TimeLocked
|
||||||
import im.molly.monero.demo.ui.theme.AppTheme
|
import im.molly.monero.demo.ui.theme.AppTheme
|
||||||
import im.molly.monero.genesisTime
|
import im.molly.monero.sdk.genesisTime
|
||||||
import im.molly.monero.xmr
|
import im.molly.monero.sdk.xmr
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
@ -120,13 +120,13 @@ fun WalletBalanceDetailsPreview() {
|
||||||
WalletBalanceView(
|
WalletBalanceView(
|
||||||
balance = Balance(
|
balance = Balance(
|
||||||
pendingAmount = 5.xmr,
|
pendingAmount = 5.xmr,
|
||||||
timeLockedAmounts = listOf(
|
lockableAmounts = listOf(
|
||||||
TimeLocked(10.xmr, null),
|
TimeLocked(10.xmr, null),
|
||||||
TimeLocked(BigDecimal("0.000000000001").xmr, null),
|
TimeLocked(BigDecimal("0.000000000001").xmr, null),
|
||||||
TimeLocked(30.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.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
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.data.model.WalletConfig
|
||||||
import im.molly.monero.demo.ui.component.SelectListBox
|
import im.molly.monero.demo.ui.component.SelectListBox
|
||||||
import im.molly.monero.demo.ui.component.Toolbar
|
import im.molly.monero.demo.ui.component.Toolbar
|
||||||
import im.molly.monero.demo.ui.preview.PreviewParameterData
|
import im.molly.monero.demo.ui.preview.PreviewParameterData
|
||||||
import im.molly.monero.demo.ui.theme.AppIcons
|
import im.molly.monero.demo.ui.theme.AppIcons
|
||||||
import im.molly.monero.demo.ui.theme.AppTheme
|
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 kotlinx.coroutines.delay
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
|
@ -4,9 +4,6 @@ import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.initializer
|
import androidx.lifecycle.viewmodel.initializer
|
||||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
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.AppModule
|
||||||
import im.molly.monero.demo.common.Result
|
import im.molly.monero.demo.common.Result
|
||||||
import im.molly.monero.demo.common.asResult
|
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.WalletAddress
|
||||||
import im.molly.monero.demo.data.model.WalletConfig
|
import im.molly.monero.demo.data.model.WalletConfig
|
||||||
import im.molly.monero.demo.data.model.WalletTransaction
|
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.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
|
@ -1,21 +1,10 @@
|
||||||
package im.molly.monero.demo.ui.preview
|
package im.molly.monero.demo.ui.preview
|
||||||
|
|
||||||
import im.molly.monero.BlockHeader
|
import im.molly.monero.sdk.*
|
||||||
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 java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
object PreviewParameterData {
|
object PreviewParameterData {
|
||||||
val network = MoneroNetwork.Mainnet
|
val network = Mainnet
|
||||||
|
|
||||||
val blockHeader = BlockHeader(height = 2999840, epochSecond = 1697792826)
|
val blockHeader = BlockHeader(height = 2999840, epochSecond = 1697792826)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
|
||||||
android.native.buildOutput=verbose
|
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
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionSha256Sum=47a5bfed9ef814f90f8debcbbb315e8e7c654109acd224595ea39fca95c5d4da
|
distributionSha256Sum=d7042b3c11565c192041fc8c4703f541b888286404b4f267138c1d094d8ecdca
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-all.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
37
gradlew
vendored
37
gradlew
vendored
|
@ -15,6 +15,8 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
|
@ -55,7 +57,7 @@
|
||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (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.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
@ -80,13 +82,11 @@ do
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
APP_NAME="Gradle"
|
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
|
@ -133,22 +133,29 @@ location of your Java installation."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
JAVACMD=java
|
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
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
# Increase the maximum file descriptors if we can.
|
||||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
max*)
|
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 ) ||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
warn "Could not query maximum file descriptor limit"
|
warn "Could not query maximum file descriptor limit"
|
||||||
esac
|
esac
|
||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
'' | soft) :;; #(
|
'' | 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" ||
|
ulimit -n "$MAX_FD" ||
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
esac
|
esac
|
||||||
|
@ -193,11 +200,15 @@ if "$cygwin" || "$msys" ; then
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Collect all arguments for the java command;
|
|
||||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
# shell script including quotes and variable substitutions, so put them in
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
# double quotes to make sure that they get re-expanded; and
|
|
||||||
# * put everything else in single quotes, so that it's not re-expanded.
|
# 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 -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-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 See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
@ -26,6 +28,7 @@ if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@ -42,11 +45,11 @@ set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation.
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
|
@ -56,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation.
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
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.
|
# Keeps methods that are invoked by JNI.
|
||||||
-keepclassmembers class * {
|
-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.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<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
|
<service
|
||||||
android:name=".WalletService"
|
android:name=".service.SandboxedWalletService"
|
||||||
android:isolatedProcess="true"
|
android:isolatedProcess="true"
|
||||||
android:process=":wallet_service" />
|
android:process=":wallet_service" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</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 {
|
interface IPendingTransfer {
|
||||||
long getAmount();
|
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 {
|
oneway interface ITransferCallback {
|
||||||
void onTransferCreated(in IPendingTransfer pendingTransfer);
|
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.sdk.PaymentRequest;
|
||||||
import im.molly.monero.ITransferCallback;
|
import im.molly.monero.sdk.SecretKey;
|
||||||
import im.molly.monero.IWalletCallbacks;
|
import im.molly.monero.sdk.SweepRequest;
|
||||||
import im.molly.monero.PaymentRequest;
|
import im.molly.monero.sdk.internal.IBalanceListener;
|
||||||
import im.molly.monero.SweepRequest;
|
import im.molly.monero.sdk.internal.ITransferCallback;
|
||||||
|
import im.molly.monero.sdk.internal.IWalletCallbacks;
|
||||||
|
|
||||||
interface IWallet {
|
interface IWallet {
|
||||||
String getPublicAddress();
|
String getPublicAddress();
|
||||||
|
SecretKey getSpendSecretKey();
|
||||||
|
SecretKey getViewSecretKey();
|
||||||
void addBalanceListener(in IBalanceListener listener);
|
void addBalanceListener(in IBalanceListener listener);
|
||||||
void removeBalanceListener(in IBalanceListener listener);
|
void removeBalanceListener(in IBalanceListener listener);
|
||||||
oneway void addDetachedSubAddress(int accountIndex, int subAddressIndex, in IWalletCallbacks callback);
|
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 resumeRefresh(boolean skipCoinbase, in IWalletCallbacks callback);
|
||||||
oneway void cancelRefresh();
|
oneway void cancelRefresh();
|
||||||
oneway void setRefreshSince(long heightOrTimestamp);
|
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 createPayment(in PaymentRequest request, in ITransferCallback callback);
|
||||||
oneway void createSweep(in SweepRequest request, in ITransferCallback callback);
|
oneway void createSweep(in SweepRequest request, in ITransferCallback callback);
|
||||||
oneway void requestFees(in IWalletCallbacks 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 {
|
oneway interface IWalletCallbacks {
|
||||||
void onRefreshResult(in BlockchainTime blockchainTime, int status);
|
void onRefreshResult(in BlockchainTime blockchainTime, int status);
|
||||||
void onCommitResult(boolean success);
|
void onCommitResult(boolean success);
|
||||||
void onSubAddressReady(String subAddress);
|
void onSubAddressReady(String subAddress);
|
||||||
void onSubAddressListReceived(in String[] subAddresses);
|
void onSubAddressListReceived(in String[] subAddresses);
|
||||||
|
void onAccountNotFound(int accountIndex);
|
||||||
void onFeesReceived(in long[] fees);
|
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 {
|
oneway interface IWalletServiceCallbacks {
|
||||||
void onWalletResult(in IWallet wallet);
|
void onWalletResult(in IWallet wallet);
|
|
@ -1,4 +1,4 @@
|
||||||
package im.molly.monero;
|
package im.molly.monero.sdk.internal;
|
||||||
|
|
||||||
oneway interface IWalletServiceListener {
|
oneway interface IWalletServiceListener {
|
||||||
void onLogMessage(int priority, String tag, String msg, String cause);
|
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