Compare commits

...

51 commits

Author SHA1 Message Date
Oscar Mira
4f99e95dee
lib: add export methods for wallet secret keys 2025-06-05 21:38:14 +02:00
Oscar Mira
0088b1b562
lib: add view/spend public keys to PublicAddress 2025-06-05 21:37:36 +02:00
Oscar Mira
48fc04a490
lib: prepare 1.0.1-SNAPSHOT 2025-05-31 11:55:58 +02:00
Oscar Mira
328a23edea
lib: bump version to 1.0.0 2025-05-31 11:23:52 +02:00
Oscar Mira
88c689b58d
ci: unify test and snapshot workflows into build.yml 2025-05-31 10:29:59 +02:00
Oscar Mira
dc3f362ec7
ci: fix snapshot version check 2025-05-31 01:08:28 +02:00
Oscar Mira
f38b35e792
ci: enable KVM on runner to fix emulator startup 2025-05-30 20:33:43 +02:00
Oscar Mira
dd9c2ca7f3
ci: add disk-cleanup action 2025-05-30 20:33:43 +02:00
Oscar Mira
cbe714ea35
ci: add workflow to publish snapshots to Maven Central 2025-05-30 20:33:43 +02:00
Oscar Mira
607dd440b8
ci: enable instrumented tests 2025-05-30 18:11:40 +02:00
Oscar Mira
a25334a18c
ci: skip signing for snapshots 2025-05-30 16:15:37 +02:00
Oscar Mira
ac021c2567
ci: use setup-gradle action in test workflow 2025-05-30 16:15:37 +02:00
Oscar Mira
8c2a27964b
doc: add README.md 2025-05-30 16:15:24 +02:00
Oscar Mira
b895d2c01c
lib: prepare for publishing 2025-05-28 22:20:28 +02:00
Oscar Mira
0ad0ade8e3
lib: rename package to im.molly.monero.sdk
Renamed package name from im.molly.monero to im.molly.monero.sdk to
align with project modularization and avoid namespace confusion.
2025-05-28 22:20:28 +02:00
Oscar Mira
0d7f98b622
lib: add more unit and E2E tests 2025-05-28 22:20:28 +02:00
Oscar Mira
40ca4b4237
monero: update submodule 2025-05-28 22:20:27 +02:00
Oscar Mira
59d2c7e402
lib: add keyImages property to Ledger 2025-05-28 22:20:27 +02:00
Oscar Mira
c030b6565c
lib: refactor MoneroNodeClient and RemoteNode
- Renamed MoneroNodeClient.forNetwork() to create()
- Added RemoteNode.singleNodeClient() extension
- Replaced uri with url:String in RemoteNode
- Added FirstRule load balancing strategy
2025-05-28 22:20:22 +02:00
Oscar Mira
0152cadc8f
lib: replace referential equality in Enote 2025-05-28 22:20:22 +02:00
Oscar Mira
a02241128a
lib: rename timeLockedAmounts to lockableAmounts 2025-05-28 22:20:22 +02:00
Oscar Mira
e9cae0b359
lib: add top-level network aliases 2025-05-28 22:20:00 +02:00
Oscar Mira
aeebd6ff32
lib: move more internal classes to package 2025-05-26 19:45:04 +02:00
Oscar Mira
adfe3c8d7a
lib: expand test coverage 2025-05-26 19:45:04 +02:00
Oscar Mira
44824c4991
lib: update libsodium to 1.0.20 2025-05-26 19:45:04 +02:00
Oscar Mira
c8331ea58b
build: upgrade to AGP 8.10.0 2025-05-26 19:44:58 +02:00
Oscar Mira
524314f391
lib: use large heap 2025-05-01 20:55:41 +02:00
Oscar Mira
d2dd6171fe
lib: improve DataStore API 2025-05-01 20:55:41 +02:00
Oscar Mira
e9d08f95d1
lib: move native classes to internal package 2025-04-20 01:35:37 +02:00
Oscar Mira
21e99c74c0
lib: add more unit tests 2025-04-19 22:04:22 +02:00
Oscar Mira
dcc3552daa
build: downgrade JDK to 17 2025-04-19 22:04:22 +02:00
Oscar Mira
c15ba3fb22
lib: add in-process wallet provider 2025-04-19 22:04:22 +02:00
Oscar Mira
5f6f67c7fc
lib: send balance updates in batches 2025-02-27 23:08:11 +01:00
Oscar Mira
f97854e424
lib: optimize hex string parceling 2025-02-26 12:40:42 +01:00
Oscar Mira
cb79e3421d
lib: add mnemonic tests 2025-01-30 23:20:07 +01:00
Oscar Mira
16ff7b06db
lib: deprecate parseHex and use hexToByteArray instead 2025-01-30 23:20:07 +01:00
Oscar Mira
d05a056698
build: add plugin for code coverage reporting 2025-01-30 14:01:50 +01:00
Oscar Mira
301f1efc1c
lib: compact TxInfo struct 2025-01-30 11:13:59 +01:00
Oscar Mira
a664ce1652
lib: fix instrumented tests 2025-01-29 18:06:44 +01:00
Oscar Mira
181b3dc442
build: convert modules to kts and update deps 2025-01-29 17:33:16 +01:00
Oscar Mira
9b71aae6c5
build: convert main .gradle to kts 2025-01-28 14:05:04 +01:00
Oscar Mira
9f54eadc61
build: migrate to version catalogs 2025-01-28 13:42:14 +01:00
Oscar Mira
8dbf0ffb6b
lib: rename function fullNode to localSyncWallet 2025-01-28 13:42:11 +01:00
Oscar Mira
874f777deb
demo: add lint config 2025-01-26 20:54:04 +01:00
Oscar Mira
87d70c1e4f
build: add test.yml workflow 2025-01-26 20:46:42 +01:00
Oscar Mira
194d6d184e
build: replace PCH with include directive to fix Android Studio bug 2025-01-23 00:35:52 +01:00
Oscar Mira
37b68d6062
build: fix boost download URL 2025-01-23 00:33:05 +01:00
Oscar Mira
6bf3174d52
build: upgrade to AGP 8.8.0 2025-01-23 00:32:43 +01:00
Oscar Mira
b3fb65b858
monero: rebase molly/release-v0.18 onto v0.18.3.3 2024-05-27 19:39:16 +02:00
Oscar Mira
30ef8b481b
lib: rename RemoteNodeClient to MoneroNodeClient and hide implementation 2024-05-15 19:27:43 +02:00
Oscar Mira
308031250b
build: disable jetifier 2024-05-14 09:29:24 +02:00
197 changed files with 3834 additions and 1749 deletions

37
.github/actions/disk-cleanup/action.yml vendored Normal file
View 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
View 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
View file

@ -0,0 +1,96 @@
# Monero Wallet SDK for Android
[![Build](https://github.com/mollyim/monero-wallet-sdk/actions/workflows/build.yml/badge.svg)](https://github.com/mollyim/monero-wallet-sdk/actions/workflows/build.yml)
![Maven Central](https://img.shields.io/maven-central/v/im.molly/monero-wallet-sdk)
![Snapshot](https://img.shields.io/maven-metadata/v?color=orange&label=snapshot&metadataUrl=https%3A%2F%2Fcentral.sonatype.com%2Frepository%2Fmaven-snapshots%2Fim%2Fmolly%2Fmonero-wallet-sdk%2Fmaven-metadata.xml)
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).

View 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"
}
}
}

View 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 },
)
}
}

View 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")

View 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"

View file

@ -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
View 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
}
}

View file

@ -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"
}

View 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
View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<issue id="UnusedMaterial3ScaffoldPaddingParameter" severity="ignore" />
</lint>

View file

@ -1,10 +1,10 @@
package im.molly.monero.demo
import android.net.Uri
import im.molly.monero.MoneroNetwork
import im.molly.monero.demo.data.AppDatabase
import im.molly.monero.demo.data.entity.asEntity
import im.molly.monero.demo.data.model.RemoteNode
import im.molly.monero.sdk.MoneroNetwork
import androidx.core.net.toUri
val DefaultNodeList = listOf(
MoneroNetwork.Mainnet to listOf(
@ -21,7 +21,7 @@ suspend fun addDefaultRemoteNodes(appDatabase: AppDatabase) {
val dao = appDatabase.remoteNodeDao()
val nodes = DefaultNodeList.flatMap { (network, urls) ->
urls.map { url ->
RemoteNode(network = network, uri = Uri.parse(url)).asEntity()
RemoteNode(network = network, uri = url.toUri()).asEntity()
}
}.toTypedArray()
dao.upsert(*nodes)

View file

@ -7,12 +7,12 @@ import android.content.ServiceConnection
import android.os.Bundle
import android.os.IBinder
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.graphics.Color
import androidx.core.view.WindowCompat
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import im.molly.monero.demo.service.SyncService
import im.molly.monero.demo.ui.DemoApp
import im.molly.monero.demo.ui.theme.AppTheme
@ -27,15 +27,19 @@ class MainActivity : ComponentActivity() {
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
val systemUiController = rememberSystemUiController()
val darkTheme = isSystemInDarkTheme()
DisposableEffect(systemUiController, darkTheme) {
systemUiController.setSystemBarsColor(
color = Color.Transparent,
darkIcons = !darkTheme,
DisposableEffect(darkTheme) {
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.auto(
android.graphics.Color.TRANSPARENT,
android.graphics.Color.TRANSPARENT,
) { darkTheme },
navigationBarStyle = SystemBarStyle.auto(
lightScrim,
darkScrim,
) { darkTheme },
)
onDispose {}
}
@ -67,3 +71,7 @@ class MainActivity : ComponentActivity() {
}
}
}
private val lightScrim = android.graphics.Color.argb(0xe6, 0xFF, 0xFF, 0xFF)
private val darkScrim = android.graphics.Color.argb(0x80, 0x1b, 0x1b, 0x1b)

View file

@ -7,7 +7,7 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import im.molly.monero.demo.service.SyncService
import im.molly.monero.isIsolatedProcess
import im.molly.monero.sdk.isIsolatedProcess
val Context.preferencesDataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

View file

@ -2,8 +2,9 @@ package im.molly.monero.demo.data
import android.content.Context
import android.util.AtomicFile
import im.molly.monero.*
import im.molly.monero.loadbalancer.RoundRobinRule
import im.molly.monero.sdk.*
import im.molly.monero.sdk.loadbalancer.RoundRobinRule
import im.molly.monero.sdk.service.SandboxedWalletService
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import okhttp3.OkHttpClient
@ -15,7 +16,7 @@ import java.io.OutputStream
class MoneroSdkClient(private val context: Context) {
private val providerDeferred = CoroutineScope(Dispatchers.IO).async {
WalletProvider.connect(context)
SandboxedWalletService.connect(context)
}
suspend fun createWallet(network: MoneroNetwork, filename: String): MoneroWallet {
@ -24,7 +25,7 @@ class MoneroSdkClient(private val context: Context) {
network = network,
dataStore = WalletDataStoreFile(filename, newFile = true),
).also { wallet ->
wallet.commit()
wallet.save()
}
}
@ -41,7 +42,7 @@ class MoneroSdkClient(private val context: Context) {
secretSpendKey = secretSpendKey,
restorePoint = restorePoint,
).also { wallet ->
wallet.commit()
wallet.save()
}
}
@ -52,7 +53,7 @@ class MoneroSdkClient(private val context: Context) {
httpClient: OkHttpClient,
): MoneroWallet {
val dataStore = WalletDataStoreFile(filename)
val client = RemoteNodeClient.forNetwork(
val client = MoneroNodeClient.create(
network = network,
remoteNodes = remoteNodes,
loadBalancerRule = RoundRobinRule(),
@ -86,7 +87,7 @@ class MoneroSdkClient(private val context: Context) {
throw IOException("Cannot create wallet data directory: ${walletDataDir.path}")
}
override suspend fun write(writer: (OutputStream) -> Unit) {
override suspend fun save(writer: (OutputStream) -> Unit, overwrite: Boolean) {
val output = file.startWrite()
try {
writer(output)
@ -97,7 +98,7 @@ class MoneroSdkClient(private val context: Context) {
}
}
override suspend fun read(): InputStream {
override suspend fun load(): InputStream {
return file.openRead()
}
}

View file

@ -1,7 +1,7 @@
package im.molly.monero.demo.data
import im.molly.monero.*
import im.molly.monero.demo.data.model.WalletConfig
import im.molly.monero.sdk.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import okhttp3.OkHttpClient
@ -33,7 +33,8 @@ class WalletRepository(
remoteNodes = configFlow.map {
it.remoteNodes.map { node ->
RemoteNode(
uri = node.uri,
url = node.uri.toString(),
network = node.network,
username = node.username,
password = node.password,
)
@ -51,8 +52,8 @@ class WalletRepository(
fun getWalletIdList() = walletDataSource.readWalletIdList()
fun getRemoteClients(): Flow<List<RemoteNodeClient>> =
getWalletIdList().map { it.mapNotNull { walletId -> getWallet(walletId).remoteNodeClient } }
fun getMoneroNodeClients(): Flow<List<MoneroNodeClient>> =
getWalletIdList().map { it.mapNotNull { walletId -> getWallet(walletId).moneroNodeClient } }
fun getWalletConfig(walletId: Long) = walletDataSource.readWalletConfig(walletId)

View file

@ -5,8 +5,8 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import im.molly.monero.MoneroNetwork
import im.molly.monero.demo.data.model.RemoteNode
import im.molly.monero.sdk.MoneroNetwork
@Entity(
tableName = "remote_nodes",

View file

@ -1,7 +1,7 @@
package im.molly.monero.demo.data.model
import android.net.Uri
import im.molly.monero.MoneroNetwork
import im.molly.monero.sdk.MoneroNetwork
data class RemoteNode(
val id: Long? = null,

View file

@ -1,8 +1,8 @@
package im.molly.monero.demo.data.model
import im.molly.monero.AccountAddress
import im.molly.monero.Enote
import im.molly.monero.TimeLocked
import im.molly.monero.sdk.AccountAddress
import im.molly.monero.sdk.Enote
import im.molly.monero.sdk.TimeLocked
data class WalletAddress(
val address: AccountAddress,

View file

@ -1,6 +1,6 @@
package im.molly.monero.demo.data.model
import im.molly.monero.Transaction
import im.molly.monero.sdk.Transaction
data class WalletTransaction(
val walletId: Long,

View file

@ -38,7 +38,7 @@ class SyncService(
syncedWalletIds.map {
walletRepository.getWallet(it)
}.forEach { wallet ->
wallet.commit()
wallet.save()
}
}
delay(60.seconds)
@ -56,7 +56,7 @@ class SyncService(
if (result.isError()) {
// TODO: Handle non-recoverable errors
}
wallet.commit()
wallet.save()
delay(10.seconds)
}
}

View file

@ -3,23 +3,21 @@ package im.molly.monero.demo.ui
import androidx.compose.runtime.mutableStateMapOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import im.molly.monero.MoneroNetwork
import im.molly.monero.RestorePoint
import im.molly.monero.SecretKey
import im.molly.monero.demo.AppModule
import im.molly.monero.demo.data.RemoteNodeRepository
import im.molly.monero.demo.data.WalletRepository
import im.molly.monero.demo.data.model.DefaultMoneroNetwork
import im.molly.monero.demo.data.model.RemoteNode
import im.molly.monero.mnemonics.MoneroMnemonic
import im.molly.monero.util.parseHex
import im.molly.monero.util.toHex
import im.molly.monero.sdk.MoneroNetwork
import im.molly.monero.sdk.RestorePoint
import im.molly.monero.sdk.SecretKey
import im.molly.monero.sdk.mnemonics.MoneroMnemonic
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import java.time.LocalDate
@OptIn(ExperimentalCoroutinesApi::class)
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalStdlibApi::class)
class AddWalletViewModel(
private val remoteNodeRepository: RemoteNodeRepository = AppModule.remoteNodeRepository,
private val walletRepository: WalletRepository = AppModule.walletRepository,
@ -66,7 +64,7 @@ class AddWalletViewModel(
MoneroMnemonic.recoverEntropy(words)?.use { mnemonicCode ->
val secretKey = SecretKey(mnemonicCode.entropy)
viewModelState.update {
it.copy(secretSpendKeyHex = secretKey.bytes.toHex())
it.copy(secretSpendKeyHex = secretKey.bytes.toHexString())
}
secretKey.destroy()
return true
@ -85,7 +83,7 @@ class AddWalletViewModel(
fun validateSecretSpendKeyHex(): Boolean =
with(viewModelState.value) {
return secretSpendKeyHex.length == 64 && runCatching {
secretSpendKeyHex.parseHex()
secretSpendKeyHex.hexToByteArray()
}.isSuccess
}
@ -125,7 +123,7 @@ class AddWalletViewModel(
else -> RestorePoint.Genesis
}
SecretKey(state.secretSpendKeyHex.parseHex()).use { secretSpendKey ->
SecretKey(state.secretSpendKeyHex.hexToByteArray()).use { secretSpendKey ->
walletRepository.restoreWallet(
state.network,
state.walletName,

View file

@ -16,13 +16,13 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import im.molly.monero.MoneroNetwork
import im.molly.monero.demo.data.model.DefaultMoneroNetwork
import im.molly.monero.demo.data.model.RemoteNode
import im.molly.monero.demo.ui.component.SelectListBox
import im.molly.monero.demo.ui.component.Toolbar
import im.molly.monero.demo.ui.theme.AppIcons
import im.molly.monero.demo.ui.theme.AppTheme
import im.molly.monero.sdk.MoneroNetwork
@Composable
fun AddWalletFirstStepRoute(

View file

@ -10,10 +10,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.unit.dp
import im.molly.monero.calculateBalance
import im.molly.monero.demo.data.model.WalletAddress
import im.molly.monero.demo.ui.component.CopyableText
import im.molly.monero.toFormattedString
import im.molly.monero.sdk.calculateBalance
import im.molly.monero.sdk.toFormattedString
@Composable
fun AddressCardExpanded(

View file

@ -15,10 +15,10 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import im.molly.monero.MoneroNetwork
import im.molly.monero.demo.data.model.RemoteNode
import im.molly.monero.demo.ui.component.SelectListBox
import im.molly.monero.demo.ui.theme.AppTheme
import im.molly.monero.sdk.MoneroNetwork
import kotlinx.coroutines.flow.first
@Composable

View file

@ -7,8 +7,8 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import im.molly.monero.PendingTransfer
import im.molly.monero.toFormattedString
import im.molly.monero.sdk.PendingTransfer
import im.molly.monero.sdk.toFormattedString
@Composable
fun PendingTransferView(

View file

@ -17,7 +17,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import im.molly.monero.MoneroCurrency
import im.molly.monero.sdk.MoneroCurrency
@Composable
fun EditableRecipientList(

View file

@ -4,15 +4,15 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import im.molly.monero.FeePriority
import im.molly.monero.MoneroCurrency
import im.molly.monero.PaymentDetail
import im.molly.monero.PaymentRequest
import im.molly.monero.PendingTransfer
import im.molly.monero.PublicAddress
import im.molly.monero.TransferRequest
import im.molly.monero.demo.AppModule
import im.molly.monero.demo.data.WalletRepository
import im.molly.monero.sdk.FeePriority
import im.molly.monero.sdk.MoneroCurrency
import im.molly.monero.sdk.PaymentDetail
import im.molly.monero.sdk.PaymentRequest
import im.molly.monero.sdk.PendingTransfer
import im.molly.monero.sdk.PublicAddress
import im.molly.monero.sdk.TransferRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.getAndUpdate

View file

@ -82,7 +82,7 @@ class SettingsViewModel(
}
private suspend fun onProxyChanged(newProxy: Proxy) {
walletRepository.getRemoteClients().first().forEach { client ->
walletRepository.getMoneroNodeClients().first().forEach { client ->
val current = client.httpClient.proxy
if (current != newProxy) {
val builder = client.httpClient.newBuilder()

View file

@ -15,12 +15,12 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import im.molly.monero.MoneroCurrency
import im.molly.monero.Transaction
import im.molly.monero.demo.ui.preview.PreviewParameterData
import im.molly.monero.demo.ui.theme.AppTheme
import im.molly.monero.demo.ui.theme.Blue40
import im.molly.monero.demo.ui.theme.Red40
import im.molly.monero.sdk.MoneroCurrency
import im.molly.monero.sdk.Transaction
import java.time.ZoneId
import java.time.format.DateTimeFormatter

View file

@ -22,12 +22,12 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import im.molly.monero.MoneroCurrency
import im.molly.monero.Transaction
import im.molly.monero.demo.ui.component.Toolbar
import im.molly.monero.demo.ui.preview.PreviewParameterData
import im.molly.monero.demo.ui.theme.AppIcons
import im.molly.monero.demo.ui.theme.AppTheme
import im.molly.monero.sdk.MoneroCurrency
import im.molly.monero.sdk.Transaction
@Composable

View file

@ -4,11 +4,11 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import im.molly.monero.Transaction
import im.molly.monero.demo.AppModule
import im.molly.monero.demo.common.Result
import im.molly.monero.demo.common.asResult
import im.molly.monero.demo.data.WalletRepository
import im.molly.monero.sdk.Transaction
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map

View file

@ -19,15 +19,15 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import im.molly.monero.MoneroAmount
import im.molly.monero.Balance
import im.molly.monero.BlockchainTime
import im.molly.monero.MoneroCurrency
import im.molly.monero.MoneroNetwork
import im.molly.monero.TimeLocked
import im.molly.monero.sdk.MoneroAmount
import im.molly.monero.sdk.Balance
import im.molly.monero.sdk.BlockchainTime
import im.molly.monero.sdk.Mainnet
import im.molly.monero.sdk.MoneroCurrency
import im.molly.monero.sdk.TimeLocked
import im.molly.monero.demo.ui.theme.AppTheme
import im.molly.monero.genesisTime
import im.molly.monero.xmr
import im.molly.monero.sdk.genesisTime
import im.molly.monero.sdk.xmr
import kotlinx.coroutines.delay
import java.math.BigDecimal
import java.time.Duration
@ -120,13 +120,13 @@ fun WalletBalanceDetailsPreview() {
WalletBalanceView(
balance = Balance(
pendingAmount = 5.xmr,
timeLockedAmounts = listOf(
lockableAmounts = listOf(
TimeLocked(10.xmr, null),
TimeLocked(BigDecimal("0.000000000001").xmr, null),
TimeLocked(30.xmr, null)
),
),
blockchainTime = MoneroNetwork.Mainnet.genesisTime,
blockchainTime = Mainnet.genesisTime,
)
}
}

View file

@ -25,17 +25,17 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import im.molly.monero.FeePriority
import im.molly.monero.Ledger
import im.molly.monero.MoneroCurrency
import im.molly.monero.PendingTransfer
import im.molly.monero.demo.data.model.WalletConfig
import im.molly.monero.demo.ui.component.SelectListBox
import im.molly.monero.demo.ui.component.Toolbar
import im.molly.monero.demo.ui.preview.PreviewParameterData
import im.molly.monero.demo.ui.theme.AppIcons
import im.molly.monero.demo.ui.theme.AppTheme
import im.molly.monero.toFormattedString
import im.molly.monero.sdk.FeePriority
import im.molly.monero.sdk.Ledger
import im.molly.monero.sdk.MoneroCurrency
import im.molly.monero.sdk.PendingTransfer
import im.molly.monero.sdk.toFormattedString
import kotlinx.coroutines.delay
import java.time.Instant
import kotlin.time.Duration.Companion.seconds

View file

@ -4,9 +4,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import im.molly.monero.Balance
import im.molly.monero.BlockchainTime
import im.molly.monero.MoneroNetwork
import im.molly.monero.demo.AppModule
import im.molly.monero.demo.common.Result
import im.molly.monero.demo.common.asResult
@ -14,6 +11,9 @@ import im.molly.monero.demo.data.WalletRepository
import im.molly.monero.demo.data.model.WalletAddress
import im.molly.monero.demo.data.model.WalletConfig
import im.molly.monero.demo.data.model.WalletTransaction
import im.molly.monero.sdk.Balance
import im.molly.monero.sdk.BlockchainTime
import im.molly.monero.sdk.MoneroNetwork
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import java.time.Instant

View file

@ -1,21 +1,10 @@
package im.molly.monero.demo.ui.preview
import im.molly.monero.BlockHeader
import im.molly.monero.BlockchainTime
import im.molly.monero.HashDigest
import im.molly.monero.Ledger
import im.molly.monero.MoneroAmount
import im.molly.monero.MoneroNetwork
import im.molly.monero.PaymentDetail
import im.molly.monero.PublicAddress
import im.molly.monero.Transaction
import im.molly.monero.TxState
import im.molly.monero.UnlockTime
import im.molly.monero.xmr
import im.molly.monero.sdk.*
import java.time.Instant
object PreviewParameterData {
val network = MoneroNetwork.Mainnet
val network = Mainnet
val blockHeader = BlockHeader(height = 2999840, epochSecond = 1697792826)

View file

@ -1,4 +1,3 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
android.enableJetifier=true
android.native.buildOutput=verbose

50
gradle/libs.versions.toml Normal file
View 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" }

View 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" }

Binary file not shown.

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=47a5bfed9ef814f90f8debcbbb315e8e7c654109acd224595ea39fca95c5d4da
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip
distributionSha256Sum=d7042b3c11565c192041fc8c4703f541b888286404b4f267138c1d094d8ecdca
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

37
gradlew vendored
View file

@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -80,13 +82,11 @@ do
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@ -133,22 +133,29 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@ -193,11 +200,15 @@ if "$cygwin" || "$msys" ; then
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \

23
gradlew.bat vendored
View file

@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@ -26,6 +28,7 @@ if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@ -42,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@ -56,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail

View file

@ -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"
}

View 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)
}
}

View file

@ -22,5 +22,5 @@
# Keeps methods that are invoked by JNI.
-keepclassmembers class * {
@im.molly.monero.UsedByNative *;
@im.molly.monero.sdk.internal.CalledByNative *;
}

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

@ -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())
}
}
}

View file

@ -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)
}
}
}

View file

@ -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) }
}
}

View file

@ -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)
}
}
}

View file

@ -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())
}
}
}

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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)

View file

@ -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)
}
}

View file

@ -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)
}
}
}

View file

@ -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
}
}

View file

@ -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()
}
}
}

View file

@ -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)
}
}
}

View file

@ -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()
}
}
}

View file

@ -4,11 +4,17 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application android:usesCleartextTraffic="true">
<application
android:largeHeap="true"
android:usesCleartextTraffic="true">
<service android:name=".service.InProcessWalletService" />
<service
android:name=".WalletService"
android:name=".service.SandboxedWalletService"
android:isolatedProcess="true"
android:process=":wallet_service" />
</application>
</manifest>

View file

@ -1,3 +0,0 @@
package im.molly.monero;
parcelable BlockchainTime;

View file

@ -1,3 +0,0 @@
package im.molly.monero;
parcelable HttpResponse;

View file

@ -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);
}

View file

@ -1,6 +0,0 @@
package im.molly.monero;
oneway interface IHttpRequestCallback {
void onResponse(int code, String contentType, in ParcelFileDescriptor body);
void onFailure();
}

View file

@ -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);
}

View file

@ -1,6 +0,0 @@
package im.molly.monero;
interface IStorageAdapter {
boolean writeAsync(in ParcelFileDescriptor pfd);
oneway void readAsync(in ParcelFileDescriptor pfd);
}

View file

@ -1,8 +0,0 @@
package im.molly.monero;
import im.molly.monero.IRemoteNodeClient;
interface IWalletClient {
int getNetworkId();
IRemoteNodeClient getRemoteNodeClient();
}

View file

@ -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);
}

View file

@ -1,3 +0,0 @@
package im.molly.monero;
parcelable PaymentRequest;

View file

@ -1,3 +0,0 @@
package im.molly.monero;
parcelable PublicAddress;

View file

@ -1,3 +0,0 @@
package im.molly.monero;
parcelable RemoteNode;

View file

@ -1,3 +0,0 @@
package im.molly.monero;
parcelable SecretKey;

View file

@ -1,3 +0,0 @@
package im.molly.monero;
parcelable SweepRequest;

View file

@ -1,3 +0,0 @@
package im.molly.monero;
parcelable WalletConfig;

View file

@ -1,3 +0,0 @@
package im.molly.monero.internal;
parcelable TxInfo;

View file

@ -0,0 +1,3 @@
package im.molly.monero.sdk;
parcelable BlockchainTime;

View file

@ -0,0 +1,3 @@
package im.molly.monero.sdk;
parcelable PaymentRequest;

View file

@ -0,0 +1,3 @@
package im.molly.monero.sdk;
parcelable PublicAddress;

View file

@ -0,0 +1,3 @@
package im.molly.monero.sdk;
parcelable RemoteNode;

View file

@ -0,0 +1,3 @@
package im.molly.monero.sdk;
parcelable SecretKey;

View file

@ -0,0 +1,3 @@
package im.molly.monero.sdk;
parcelable SweepRequest;

View file

@ -0,0 +1,3 @@
package im.molly.monero.sdk.internal;
parcelable HttpRequest;

View file

@ -0,0 +1,3 @@
package im.molly.monero.sdk.internal;
parcelable HttpResponse;

View file

@ -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);
}

View file

@ -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();
}

View file

@ -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);
}

View file

@ -1,6 +1,6 @@
package im.molly.monero;
package im.molly.monero.sdk.internal;
import im.molly.monero.ITransferCallback;
import im.molly.monero.sdk.internal.ITransferCallback;
interface IPendingTransfer {
long getAmount();

View file

@ -1,6 +1,6 @@
package im.molly.monero;
package im.molly.monero.sdk.internal;
import im.molly.monero.IPendingTransfer;
import im.molly.monero.sdk.internal.IPendingTransfer;
oneway interface ITransferCallback {
void onTransferCreated(in IPendingTransfer pendingTransfer);

View file

@ -1,13 +1,16 @@
package im.molly.monero;
package im.molly.monero.sdk.internal;
import im.molly.monero.IBalanceListener;
import im.molly.monero.ITransferCallback;
import im.molly.monero.IWalletCallbacks;
import im.molly.monero.PaymentRequest;
import im.molly.monero.SweepRequest;
import im.molly.monero.sdk.PaymentRequest;
import im.molly.monero.sdk.SecretKey;
import im.molly.monero.sdk.SweepRequest;
import im.molly.monero.sdk.internal.IBalanceListener;
import im.molly.monero.sdk.internal.ITransferCallback;
import im.molly.monero.sdk.internal.IWalletCallbacks;
interface IWallet {
String getPublicAddress();
SecretKey getSpendSecretKey();
SecretKey getViewSecretKey();
void addBalanceListener(in IBalanceListener listener);
void removeBalanceListener(in IBalanceListener listener);
oneway void addDetachedSubAddress(int accountIndex, int subAddressIndex, in IWalletCallbacks callback);
@ -18,7 +21,7 @@ interface IWallet {
oneway void resumeRefresh(boolean skipCoinbase, in IWalletCallbacks callback);
oneway void cancelRefresh();
oneway void setRefreshSince(long heightOrTimestamp);
oneway void commit(in IWalletCallbacks callback);
oneway void commit(in ParcelFileDescriptor outputFd, in IWalletCallbacks callback);
oneway void createPayment(in PaymentRequest request, in ITransferCallback callback);
oneway void createSweep(in SweepRequest request, in ITransferCallback callback);
oneway void requestFees(in IWalletCallbacks callback);

View file

@ -1,11 +1,12 @@
package im.molly.monero;
package im.molly.monero.sdk.internal;
import im.molly.monero.BlockchainTime;
import im.molly.monero.sdk.BlockchainTime;
oneway interface IWalletCallbacks {
void onRefreshResult(in BlockchainTime blockchainTime, int status);
void onCommitResult(boolean success);
void onSubAddressReady(String subAddress);
void onSubAddressListReceived(in String[] subAddresses);
void onAccountNotFound(int accountIndex);
void onFeesReceived(in long[] fees);
}

View file

@ -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();
}

View file

@ -1,6 +1,6 @@
package im.molly.monero;
package im.molly.monero.sdk.internal;
import im.molly.monero.IWallet;
import im.molly.monero.sdk.internal.IWallet;
oneway interface IWalletServiceCallbacks {
void onWalletResult(in IWallet wallet);

View file

@ -1,4 +1,4 @@
package im.molly.monero;
package im.molly.monero.sdk.internal;
oneway interface IWalletServiceListener {
void onLogMessage(int priority, String tag, String msg, String cause);

View file

@ -0,0 +1,3 @@
package im.molly.monero.sdk.internal;
parcelable TxInfo;

View file

@ -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