├── lib
└── android
│ ├── consumer-rules.pro
│ ├── src
│ ├── main
│ │ ├── aidl
│ │ │ └── im
│ │ │ │ └── molly
│ │ │ │ └── monero
│ │ │ │ └── sdk
│ │ │ │ ├── RemoteNode.aidl
│ │ │ │ ├── SecretKey.aidl
│ │ │ │ ├── PublicAddress.aidl
│ │ │ │ ├── SweepRequest.aidl
│ │ │ │ ├── BlockchainTime.aidl
│ │ │ │ ├── PaymentRequest.aidl
│ │ │ │ └── internal
│ │ │ │ ├── TxInfo.aidl
│ │ │ │ ├── HttpRequest.aidl
│ │ │ │ ├── HttpResponse.aidl
│ │ │ │ ├── WalletConfig.aidl
│ │ │ │ ├── IWalletServiceListener.aidl
│ │ │ │ ├── IWalletServiceCallbacks.aidl
│ │ │ │ ├── IHttpRequestCallback.aidl
│ │ │ │ ├── IPendingTransfer.aidl
│ │ │ │ ├── IHttpRpcClient.aidl
│ │ │ │ ├── IWalletCallbacks.aidl
│ │ │ │ ├── IBalanceListener.aidl
│ │ │ │ ├── IWalletService.aidl
│ │ │ │ ├── ITransferCallback.aidl
│ │ │ │ └── IWallet.aidl
│ │ ├── cpp
│ │ │ ├── monero
│ │ │ │ ├── wallet2
│ │ │ │ │ ├── i18n_override.cc
│ │ │ │ │ ├── include
│ │ │ │ │ │ └── boringssl_compat.h
│ │ │ │ │ ├── perf_timer_override.cc
│ │ │ │ │ └── mlog_override.cc
│ │ │ │ ├── lmdb
│ │ │ │ │ └── CMakeLists.txt
│ │ │ │ ├── randomx
│ │ │ │ │ └── CMakeLists.txt
│ │ │ │ ├── electrum_words
│ │ │ │ │ ├── mlog_override.cc
│ │ │ │ │ └── CMakeLists.txt
│ │ │ │ ├── easylogging
│ │ │ │ │ └── CMakeLists.txt
│ │ │ │ └── CMakeLists.txt
│ │ │ ├── common
│ │ │ │ ├── arraysize.h
│ │ │ │ ├── jvm.h
│ │ │ │ ├── eraser.h
│ │ │ │ └── debug.h
│ │ │ ├── boringssl
│ │ │ │ └── CMakeLists.txt
│ │ │ ├── mnemonics
│ │ │ │ ├── jni_loader.cc
│ │ │ │ ├── jni_cache.h
│ │ │ │ ├── jni_cache.cc
│ │ │ │ └── mnemonics.cc
│ │ │ ├── wallet
│ │ │ │ ├── transfer.cc
│ │ │ │ ├── jni_loader.cc
│ │ │ │ ├── transfer.h
│ │ │ │ ├── jni_cache.h
│ │ │ │ ├── logging.h
│ │ │ │ ├── fd.h
│ │ │ │ └── http_client.h
│ │ │ ├── boost
│ │ │ │ └── user-config
│ │ │ │ │ ├── boost_x86_64.jam.in
│ │ │ │ │ ├── boost_arm64-v8a.jam.in
│ │ │ │ │ └── boost_armeabi-v7a.jam.in
│ │ │ ├── libsodium
│ │ │ │ └── CMakeLists.txt
│ │ │ ├── unbound
│ │ │ │ └── CMakeLists.txt
│ │ │ ├── cmake
│ │ │ │ └── toolchain.cmake
│ │ │ └── CMakeLists.txt
│ │ ├── kotlin
│ │ │ └── im
│ │ │ │ └── molly
│ │ │ │ └── monero
│ │ │ │ └── sdk
│ │ │ │ ├── FeePriority.kt
│ │ │ │ ├── internal
│ │ │ │ ├── WalletConfig.kt
│ │ │ │ ├── CalledByNative.kt
│ │ │ │ ├── HttpResponse.kt
│ │ │ │ ├── WalletServiceLogListener.kt
│ │ │ │ ├── Binder.kt
│ │ │ │ ├── HexStringParceler.kt
│ │ │ │ ├── constants
│ │ │ │ │ └── Constants.kt
│ │ │ │ ├── NativeLoader.kt
│ │ │ │ ├── LedgerFactory.kt
│ │ │ │ ├── HttpRequest.kt
│ │ │ │ ├── Logger.kt
│ │ │ │ └── DataStoreAdapter.kt
│ │ │ │ ├── exceptions
│ │ │ │ ├── NoSuchAccountException.kt
│ │ │ │ └── InternalRuntimeException.kt
│ │ │ │ ├── ProtocolInfo.kt
│ │ │ │ ├── PaymentDetail.kt
│ │ │ │ ├── DynamicFeeRate.kt
│ │ │ │ ├── MoneroNetworkAliases.kt
│ │ │ │ ├── TransferRequest.kt
│ │ │ │ ├── service
│ │ │ │ ├── BaseWalletService.kt
│ │ │ │ ├── SandboxedWalletService.kt
│ │ │ │ └── InProcessWalletService.kt
│ │ │ │ ├── RemoteNode.kt
│ │ │ │ ├── WalletAccount.kt
│ │ │ │ ├── ContextUtils.kt
│ │ │ │ ├── HashDigest.kt
│ │ │ │ ├── PublicKey.kt
│ │ │ │ ├── Ledger.kt
│ │ │ │ ├── Block.kt
│ │ │ │ ├── Transaction.kt
│ │ │ │ ├── loadbalancer
│ │ │ │ ├── Rule.kt
│ │ │ │ └── LoadBalancer.kt
│ │ │ │ ├── MoneroNetwork.kt
│ │ │ │ ├── WalletProvider.kt
│ │ │ │ ├── WalletDataStore.kt
│ │ │ │ ├── RestorePoint.kt
│ │ │ │ ├── TimeLocked.kt
│ │ │ │ ├── Logging.kt
│ │ │ │ ├── PendingTransfer.kt
│ │ │ │ ├── Balance.kt
│ │ │ │ ├── AccountAddress.kt
│ │ │ │ ├── RetryBackoff.kt
│ │ │ │ ├── Enote.kt
│ │ │ │ ├── MoneroAmount.kt
│ │ │ │ ├── SecretKey.kt
│ │ │ │ ├── util
│ │ │ │ └── Base58.kt
│ │ │ │ ├── mnemonics
│ │ │ │ └── MnemonicCode.kt
│ │ │ │ └── MoneroCurrency.kt
│ │ ├── proto
│ │ │ └── ledger.proto
│ │ └── AndroidManifest.xml
│ ├── androidTest
│ │ └── kotlin
│ │ │ └── im
│ │ │ └── molly
│ │ │ └── monero
│ │ │ └── sdk
│ │ │ ├── SecretKeyParcelableTest.kt
│ │ │ ├── service
│ │ │ └── WalletServiceSandboxingTest.kt
│ │ │ ├── MoneroWalletSubject.kt
│ │ │ ├── LedgerSubject.kt
│ │ │ ├── e2etest
│ │ │ ├── WalletServiceRule.kt
│ │ │ ├── WalletTestBase.kt
│ │ │ └── WalletRefreshTest.kt
│ │ │ ├── mnemonics
│ │ │ └── MoneroMnemonicTest.kt
│ │ │ └── internal
│ │ │ └── NativeWalletTest.kt
│ └── test
│ │ └── kotlin
│ │ └── im
│ │ └── molly
│ │ └── monero
│ │ └── sdk
│ │ ├── TimeLockedTest.kt
│ │ ├── BalanceTest.kt
│ │ ├── SecretKeyTest.kt
│ │ └── WalletServiceClientTest.kt
│ └── proguard-rules.pro
├── .gitignore
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── test-libs.versions.toml
├── gradle.properties
├── demo
└── android
│ ├── lint.xml
│ ├── src
│ ├── main
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── values
│ │ │ │ ├── themes.xml
│ │ │ │ ├── strings.xml
│ │ │ │ └── colors.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ └── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ ├── kotlin
│ │ │ └── im
│ │ │ │ └── molly
│ │ │ │ └── monero
│ │ │ │ └── demo
│ │ │ │ ├── data
│ │ │ │ ├── model
│ │ │ │ │ ├── WalletTransaction.kt
│ │ │ │ │ ├── UserSettings.kt
│ │ │ │ │ ├── WalletConfig.kt
│ │ │ │ │ ├── WalletAddress.kt
│ │ │ │ │ ├── RemoteNode.kt
│ │ │ │ │ └── SocksProxy.kt
│ │ │ │ ├── entity
│ │ │ │ │ ├── PopulatedWallet.kt
│ │ │ │ │ ├── WalletEntity.kt
│ │ │ │ │ ├── WalletRemoteNodeXRef.kt
│ │ │ │ │ └── RemoteNodeEntity.kt
│ │ │ │ ├── AppDatabase.kt
│ │ │ │ ├── dao
│ │ │ │ │ ├── WalletDao.kt
│ │ │ │ │ └── RemoteNodeDao.kt
│ │ │ │ ├── SettingsRepository.kt
│ │ │ │ ├── RemoteNodeRepository.kt
│ │ │ │ └── WalletDataSource.kt
│ │ │ │ ├── ui
│ │ │ │ ├── theme
│ │ │ │ │ ├── Color.kt
│ │ │ │ │ ├── Icon.kt
│ │ │ │ │ ├── Type.kt
│ │ │ │ │ └── Theme.kt
│ │ │ │ ├── WalletCardList.kt
│ │ │ │ ├── navigation
│ │ │ │ │ ├── HistoryNavigation.kt
│ │ │ │ │ ├── HomeNavigation.kt
│ │ │ │ │ ├── TopLevelDestination.kt
│ │ │ │ │ ├── TransactionNavigation.kt
│ │ │ │ │ ├── SettingsNavigation.kt
│ │ │ │ │ └── NavGraph.kt
│ │ │ │ ├── AddressCardList.kt
│ │ │ │ ├── TransactionCardList.kt
│ │ │ │ ├── component
│ │ │ │ │ ├── Toolbar.kt
│ │ │ │ │ ├── CopyableText.kt
│ │ │ │ │ ├── RadioButtons.kt
│ │ │ │ │ └── SelectListBox.kt
│ │ │ │ ├── WalletListViewModel.kt
│ │ │ │ ├── HistoryScreen.kt
│ │ │ │ ├── PendingTransferView.kt
│ │ │ │ ├── preview
│ │ │ │ │ └── PreviewParameterData.kt
│ │ │ │ ├── DemoAppState.kt
│ │ │ │ ├── TransactionViewModel.kt
│ │ │ │ ├── AddressCard.kt
│ │ │ │ └── WalletCard.kt
│ │ │ │ ├── DefaultNodeList.kt
│ │ │ │ ├── common
│ │ │ │ └── Result.kt
│ │ │ │ ├── MainApplication.kt
│ │ │ │ ├── AppModule.kt
│ │ │ │ └── MainActivity.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── kotlin
│ │ │ └── im
│ │ │ └── molly
│ │ │ └── monero
│ │ │ └── demo
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── kotlin
│ │ └── im
│ │ └── molly
│ │ └── monero
│ │ └── demo
│ │ └── ExampleInstrumentedTest.kt
│ ├── proguard-rules.pro
│ └── build.gradle.kts
├── .gitmodules
├── .github
├── actions
│ └── disk-cleanup
│ │ └── action.yml
└── workflows
│ └── build.yml
└── settings.gradle.kts
/lib/android/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | .idea
4 | build
5 | captures
6 | .externalNativeBuild
7 | .cxx
8 | local.properties
9 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mollyim/monero-wallet-sdk/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/lib/android/src/main/aidl/im/molly/monero/sdk/RemoteNode.aidl:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk;
2 |
3 | parcelable RemoteNode;
4 |
--------------------------------------------------------------------------------
/lib/android/src/main/aidl/im/molly/monero/sdk/SecretKey.aidl:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk;
2 |
3 | parcelable SecretKey;
4 |
--------------------------------------------------------------------------------
/lib/android/src/main/aidl/im/molly/monero/sdk/PublicAddress.aidl:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk;
2 |
3 | parcelable PublicAddress;
4 |
--------------------------------------------------------------------------------
/lib/android/src/main/aidl/im/molly/monero/sdk/SweepRequest.aidl:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk;
2 |
3 | parcelable SweepRequest;
4 |
--------------------------------------------------------------------------------
/lib/android/src/main/aidl/im/molly/monero/sdk/BlockchainTime.aidl:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk;
2 |
3 | parcelable BlockchainTime;
4 |
--------------------------------------------------------------------------------
/lib/android/src/main/aidl/im/molly/monero/sdk/PaymentRequest.aidl:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk;
2 |
3 | parcelable PaymentRequest;
4 |
--------------------------------------------------------------------------------
/lib/android/src/main/aidl/im/molly/monero/sdk/internal/TxInfo.aidl:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal;
2 |
3 | parcelable TxInfo;
4 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
2 | android.useAndroidX=true
3 | android.native.buildOutput=verbose
4 |
--------------------------------------------------------------------------------
/lib/android/src/main/aidl/im/molly/monero/sdk/internal/HttpRequest.aidl:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal;
2 |
3 | parcelable HttpRequest;
4 |
--------------------------------------------------------------------------------
/lib/android/src/main/aidl/im/molly/monero/sdk/internal/HttpResponse.aidl:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal;
2 |
3 | parcelable HttpResponse;
4 |
--------------------------------------------------------------------------------
/lib/android/src/main/aidl/im/molly/monero/sdk/internal/WalletConfig.aidl:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal;
2 |
3 | parcelable WalletConfig;
4 |
--------------------------------------------------------------------------------
/demo/android/lint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/demo/android/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mollyim/monero-wallet-sdk/HEAD/demo/android/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/demo/android/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mollyim/monero-wallet-sdk/HEAD/demo/android/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/demo/android/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mollyim/monero-wallet-sdk/HEAD/demo/android/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/demo/android/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mollyim/monero-wallet-sdk/HEAD/demo/android/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/demo/android/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mollyim/monero-wallet-sdk/HEAD/demo/android/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/demo/android/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mollyim/monero-wallet-sdk/HEAD/demo/android/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/demo/android/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mollyim/monero-wallet-sdk/HEAD/demo/android/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/demo/android/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mollyim/monero-wallet-sdk/HEAD/demo/android/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/monero/wallet2/i18n_override.cc:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | const char* i18n_translate(const char* s, const std::string& context) {
4 | return s;
5 | }
6 |
--------------------------------------------------------------------------------
/demo/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mollyim/monero-wallet-sdk/HEAD/demo/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/demo/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mollyim/monero-wallet-sdk/HEAD/demo/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/demo/android/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/monero/wallet2/include/boringssl_compat.h:
--------------------------------------------------------------------------------
1 | // Missing functions in BoringSSL compared to OpenSSL.
2 | // Not used but symbols are needed to precompile.
3 |
4 | #define EC_GROUP_check(group, ctx) 0
5 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/FeePriority.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | enum class FeePriority(val priority: Int) {
4 | Low(1),
5 | Medium(2),
6 | High(3),
7 | Urgent(4),
8 | }
9 |
--------------------------------------------------------------------------------
/lib/android/src/main/aidl/im/molly/monero/sdk/internal/IWalletServiceListener.aidl:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal;
2 |
3 | oneway interface IWalletServiceListener {
4 | void onLogMessage(int priority, String tag, String msg, String cause);
5 | }
6 |
--------------------------------------------------------------------------------
/demo/android/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Monero SDK Demo
3 | Home
4 | History
5 | Settings
6 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/data/model/WalletTransaction.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.data.model
2 |
3 | import im.molly.monero.sdk.Transaction
4 |
5 | data class WalletTransaction(
6 | val walletId: Long,
7 | val transaction: Transaction,
8 | )
9 |
--------------------------------------------------------------------------------
/lib/android/src/main/aidl/im/molly/monero/sdk/internal/IWalletServiceCallbacks.aidl:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal;
2 |
3 | import im.molly.monero.sdk.internal.IWallet;
4 |
5 | oneway interface IWalletServiceCallbacks {
6 | void onWalletResult(in IWallet wallet);
7 | }
8 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/data/model/UserSettings.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.data.model
2 |
3 | import java.net.Proxy
4 |
5 | data class UserSettings(
6 | val socksProxy: SocksProxy?,
7 | ) {
8 | val activeProxy: Proxy = socksProxy ?: Proxy.NO_PROXY
9 | }
10 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/internal/WalletConfig.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 |
6 | @Parcelize
7 | internal data class WalletConfig(
8 | val networkId: Int,
9 | ) : Parcelable
10 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/data/model/WalletConfig.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.data.model
2 |
3 | data class WalletConfig(
4 | val id: Long,
5 | val publicAddress: String,
6 | val filename: String,
7 | val name: String,
8 | val remoteNodes: Set,
9 | )
10 |
--------------------------------------------------------------------------------
/demo/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/monero/lmdb/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | set(LMDB_SOURCE_DIR "${MONERO_DIR}/external/db_drivers/liblmdb")
2 |
3 | add_subdirectory("${LMDB_SOURCE_DIR}" "lmdb" EXCLUDE_FROM_ALL)
4 | add_library(Monero::lmdb ALIAS lmdb)
5 |
6 | set_target_properties(lmdb PROPERTIES
7 | INTERFACE_INCLUDE_DIRECTORIES "${LMDB_SOURCE_DIR}"
8 | )
9 |
--------------------------------------------------------------------------------
/demo/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/exceptions/NoSuchAccountException.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.exceptions
2 |
3 | class NoSuchAccountException(private val accountIndex: Int) : NoSuchElementException() {
4 | override val message: String
5 | get() = "No account was found with the specified index: $accountIndex"
6 | }
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionSha256Sum=d7042b3c11565c192041fc8c4703f541b888286404b4f267138c1d094d8ecdca
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-all.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/lib/android/src/main/aidl/im/molly/monero/sdk/internal/IHttpRequestCallback.aidl:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal;
2 |
3 | import im.molly.monero.sdk.internal.HttpResponse;
4 |
5 | oneway interface IHttpRequestCallback {
6 | void onResponse(in HttpResponse response);
7 | void onError();
8 | void onRequestCanceled();
9 | }
10 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/monero/randomx/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | set(RANDOMX_SOURCE_DIR "${MONERO_DIR}/external/randomx")
2 |
3 | add_subdirectory("${RANDOMX_SOURCE_DIR}" "randomx" EXCLUDE_FROM_ALL)
4 | add_library(Monero::randomx ALIAS randomx)
5 |
6 | set_target_properties(randomx PROPERTIES
7 | INTERFACE_INCLUDE_DIRECTORIES "${RANDOMX_SOURCE_DIR}/src"
8 | )
9 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/monero/electrum_words/mlog_override.cc:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | #include "easylogging++.h"
4 |
5 | void mlog_configure(const std::string& filename_base,
6 | bool console,
7 | const std::size_t max_log_file_size,
8 | const std::size_t max_log_files) {
9 | // No-op.
10 | }
11 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/common/arraysize.h:
--------------------------------------------------------------------------------
1 | #ifndef COMMON_ARRAYSIZE_H
2 | #define COMMON_ARRAYSIZE_H
3 |
4 | // The arraysize(arr) macro returns the # of elements in an array arr.
5 | template
6 | char (&ArraySizeHelper(T (&array)[N]))[N];
7 |
8 | #define arraysize(array) (sizeof(ArraySizeHelper(array)))
9 |
10 | #endif // COMMON_ARRAYSIZE_H
11 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/internal/CalledByNative.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal
2 |
3 | /**
4 | * Annotation that shows the method is called by JNI native code.
5 | */
6 | @Target(AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR)
7 | @Retention(AnnotationRetention.SOURCE)
8 | @MustBeDocumented
9 | annotation class CalledByNative
10 |
--------------------------------------------------------------------------------
/lib/android/src/main/aidl/im/molly/monero/sdk/internal/IPendingTransfer.aidl:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal;
2 |
3 | import im.molly.monero.sdk.internal.ITransferCallback;
4 |
5 | interface IPendingTransfer {
6 | long getAmount();
7 | long getFee();
8 | int getTxCount();
9 | oneway void commitAndClose(in ITransferCallback callback);
10 | oneway void close();
11 | }
12 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/ProtocolInfo.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | interface ProtocolInfo {
4 | val version: Int
5 | val perByteFee: Boolean
6 | val feeScaling2021: Boolean
7 | }
8 |
9 | data class MoneroReleaseInfo(override val version: Int) : ProtocolInfo {
10 | override val perByteFee = version >= 8
11 | override val feeScaling2021 = version >= 15
12 | }
13 |
--------------------------------------------------------------------------------
/lib/android/src/main/aidl/im/molly/monero/sdk/internal/IHttpRpcClient.aidl:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal;
2 |
3 | import im.molly.monero.sdk.internal.HttpRequest;
4 | import im.molly.monero.sdk.internal.IHttpRequestCallback;
5 |
6 | interface IHttpRpcClient {
7 | oneway void callAsync(in HttpRequest request, in IHttpRequestCallback callback, int callId);
8 | oneway void cancelAsync(int callId);
9 | }
10 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/PaymentDetail.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 |
6 | @Parcelize
7 | data class PaymentDetail(
8 | val amount: MoneroAmount,
9 | val recipientAddress: PublicAddress,
10 | ) : Parcelable {
11 | init {
12 | require(amount >= 0) { "Payment amount cannot be negative" }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/exceptions/InternalRuntimeException.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.exceptions
2 |
3 | class InternalRuntimeException(message: String, cause: Throwable? = null) : RuntimeException(
4 | buildString {
5 | append(message.trimEnd('.'))
6 | append(". This is likely a bug; please report the issue to the Monero SDK team on GitHub.")
7 | },
8 | cause
9 | )
10 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/boringssl/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | set(BORINGSSL_SOURCE_DIR "${VENDOR_DIR}/boringssl")
2 |
3 | add_subdirectory("${BORINGSSL_SOURCE_DIR}" "boringssl" EXCLUDE_FROM_ALL)
4 |
5 | add_library(OpenSSL::SSL ALIAS ssl)
6 | add_library(OpenSSL::Crypto ALIAS crypto)
7 |
8 | set(OPENSSL_SOURCE_DIR "${BORINGSSL_SOURCE_DIR}" PARENT_SCOPE)
9 | set(OPENSSL_INCLUDE_DIR "${BORINGSSL_SOURCE_DIR}/include" PARENT_SCOPE)
10 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/data/model/WalletAddress.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.data.model
2 |
3 | import im.molly.monero.sdk.AccountAddress
4 | import im.molly.monero.sdk.Enote
5 | import im.molly.monero.sdk.TimeLocked
6 |
7 | data class WalletAddress(
8 | val address: AccountAddress,
9 | val enotes: List>,
10 | val used: Boolean,
11 | val isLastForAccount: Boolean,
12 | )
13 |
--------------------------------------------------------------------------------
/demo/android/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/mnemonics/jni_loader.cc:
--------------------------------------------------------------------------------
1 | #include "jni_cache.h"
2 |
3 | #include "common/jvm.h"
4 |
5 | namespace monero {
6 |
7 | extern "C"
8 | JNIEXPORT jint
9 | JNI_OnLoad(JavaVM* vm, void* reserved) {
10 | JNIEnv* env = InitializeJvm(vm, JNI_VERSION_1_6);
11 | if (env == nullptr) {
12 | return JNI_ERR;
13 | }
14 |
15 | InitializeJniCache(env);
16 |
17 | return JNI_VERSION_1_6;
18 | }
19 |
20 | } // namespace monero
21 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/monero/easylogging/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | set(EASYLOGGING_SOURCE_DIR "${MONERO_DIR}/external/easylogging++")
2 |
3 | add_compile_definitions(AUTO_INITIALIZE_EASYLOGGINGPP)
4 |
5 | add_subdirectory("${EASYLOGGING_SOURCE_DIR}" "easylogging++" EXCLUDE_FROM_ALL)
6 | add_library(Monero::easylogging ALIAS easylogging)
7 |
8 | set_target_properties(easylogging PROPERTIES
9 | INTERFACE_INCLUDE_DIRECTORIES "${EASYLOGGING_SOURCE_DIR}"
10 | )
11 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "vendor/monero"]
2 | path = vendor/monero
3 | url = https://github.com/mollyim/monero.git
4 | [submodule "vendor/boringssl"]
5 | path = vendor/boringssl
6 | url = https://boringssl.googlesource.com/boringssl
7 | [submodule "vendor/unbound"]
8 | path = vendor/unbound
9 | url = https://github.com/NLnetLabs/unbound.git
10 | [submodule "vendor/libsodium"]
11 | path = vendor/libsodium
12 | url = https://github.com/jedisct1/libsodium.git
13 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
12 | val Red40 = Color(0xFFBA1B1B)
13 | val Blue40 = Color(0xFF1546F6)
14 |
--------------------------------------------------------------------------------
/demo/android/src/test/kotlin/im/molly/monero/demo/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/wallet/transfer.cc:
--------------------------------------------------------------------------------
1 | #include "transfer.h"
2 |
3 | #include "common/debug.h"
4 |
5 | #include "jni_cache.h"
6 |
7 | namespace monero {
8 |
9 | extern "C"
10 | JNIEXPORT void JNICALL
11 | Java_im_molly_monero_sdk_internal_NativeWallet_00024NativePendingTransfer_nativeDispose(
12 | JNIEnv* env,
13 | jobject thiz,
14 | jlong transfer_handle) {
15 | delete reinterpret_cast(transfer_handle);
16 | }
17 |
18 | } // namespace monero
19 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/internal/HttpResponse.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal
2 |
3 | import android.os.ParcelFileDescriptor
4 | import android.os.Parcelable
5 | import kotlinx.parcelize.Parcelize
6 |
7 | @Parcelize
8 | data class HttpResponse(
9 | val code: Int,
10 | val contentType: String? = null,
11 | val body: ParcelFileDescriptor? = null,
12 | ) : AutoCloseable, Parcelable {
13 | override fun close() {
14 | body?.close()
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/wallet/jni_loader.cc:
--------------------------------------------------------------------------------
1 | #include "jni_cache.h"
2 |
3 | #include "common/jvm.h"
4 |
5 | #include "logging.h"
6 |
7 | namespace monero {
8 |
9 | extern "C"
10 | JNIEXPORT jint
11 | JNI_OnLoad(JavaVM* vm, void* reserved) {
12 | JNIEnv* env = InitializeJvm(vm, JNI_VERSION_1_6);
13 | if (env == nullptr) {
14 | return JNI_ERR;
15 | }
16 |
17 | InitializeJniCache(env);
18 | InitializeEasyLogging();
19 |
20 | return JNI_VERSION_1_6;
21 | }
22 |
23 | } // namespace monero
24 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/internal/WalletServiceLogListener.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal
2 |
3 | internal object WalletServiceLogListener : IWalletServiceListener.Stub() {
4 | override fun onLogMessage(priority: Int, tag: String, msg: String, cause: String?) {
5 | if (Logger.adapter.isLoggable(priority, tag)) {
6 | val tr = if (cause != null) Throwable(cause) else null
7 | Logger.adapter.print(priority, tag, msg, tr)
8 | }
9 | }
10 | }
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/internal/Binder.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal
2 |
3 | import android.os.Build
4 | import android.os.IBinder
5 | import android.os.IInterface
6 |
7 | /**
8 | * Returns whether this interface is in a remote process.
9 | */
10 | fun IInterface.isRemote(): Boolean {
11 | return asBinder() !== this
12 | }
13 |
14 | fun getMaxIpcSize(): Int = if (Build.VERSION.SDK_INT >= 30) {
15 | IBinder.getSuggestedMaxIpcSizeBytes()
16 | } else {
17 | 64 * 1024
18 | }
19 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/internal/HexStringParceler.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal
2 |
3 | import android.os.Parcel
4 | import kotlinx.parcelize.Parceler
5 |
6 | @OptIn(ExperimentalStdlibApi::class)
7 | object HexStringParceler : Parceler {
8 | override fun create(parcel: Parcel): String? =
9 | parcel.createByteArray()?.toHexString()
10 |
11 | override fun String?.write(parcel: Parcel, flags: Int) {
12 | parcel.writeByteArray(this?.hexToByteArray())
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/DynamicFeeRate.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | import im.molly.monero.sdk.internal.constants.PER_KB_FEE_QUANTIZATION_DECIMALS
4 | import java.math.BigDecimal
5 |
6 | data class DynamicFeeRate(val feePerByte: Map) {
7 |
8 | val quantizationMask: MoneroAmount = BigDecimal.TEN.pow(PER_KB_FEE_QUANTIZATION_DECIMALS).xmr
9 |
10 | fun estimateFee(pendingTransfer: PendingTransfer): Map {
11 | TODO()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/lib/android/src/main/proto/ledger.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package monero.proto;
4 | option java_package = "im.molly.monero.sdk.proto";
5 | option java_multiple_files = true;
6 |
7 | option optimize_for = LITE_RUNTIME;
8 |
9 | message LedgerProto {
10 | string public_address = 1;
11 | uint64 block_height = 2;
12 |
13 | repeated OwnedTxOutProto owned_tx_outs = 3;
14 | }
15 |
16 | message OwnedTxOutProto {
17 | bytes tx_id = 1;
18 | uint64 amount = 2;
19 | uint64 block_height = 3;
20 | uint64 spent_height = 4;
21 | }
22 |
--------------------------------------------------------------------------------
/lib/android/src/main/aidl/im/molly/monero/sdk/internal/IWalletCallbacks.aidl:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal;
2 |
3 | import im.molly.monero.sdk.BlockchainTime;
4 |
5 | oneway interface IWalletCallbacks {
6 | void onRefreshResult(in BlockchainTime blockchainTime, int status);
7 | void onCommitResult(boolean success);
8 | void onSubAddressReady(String subAddress);
9 | void onSubAddressListReceived(in String[] subAddresses);
10 | void onAccountNotFound(int accountIndex);
11 | void onFeesReceived(in long[] fees);
12 | }
13 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/mnemonics/jni_cache.h:
--------------------------------------------------------------------------------
1 | #ifndef MNEMONICS_JNI_CACHE_H__
2 | #define MNEMONICS_JNI_CACHE_H__
3 |
4 | #include "common/java_native.h"
5 |
6 | namespace monero {
7 |
8 | // Initialize various classes and method pointers cached for use in JNI.
9 | void InitializeJniCache(JNIEnv* env);
10 |
11 | // im.molly.monero.sdk.mnemonics
12 | extern jmethodID MoneroMnemonic_buildMnemonicFromJNI;
13 | extern ScopedJavaGlobalRef MoneroMnemonicClass;
14 |
15 | } // namespace monero
16 |
17 | #endif // MNEMONICS_JNI_CACHE_H__
18 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/internal/constants/Constants.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal.constants
2 |
3 | internal const val CRYPTONOTE_DISPLAY_DECIMAL_POINT: Int = 12
4 | internal const val CRYPTONOTE_MAX_BLOCK_NUMBER: Int = 500_000_000
5 | internal const val CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE: Int = 10
6 | internal const val DIFFICULTY_TARGET_V1: Long = 60
7 | internal const val DIFFICULTY_TARGET_V2: Long = 120
8 | internal const val PER_KB_FEE_QUANTIZATION_DECIMALS: Int = 8
9 | internal const val BULLETPROOF_PLUS_MAX_OUTPUTS: Int = 16
10 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/data/model/RemoteNode.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.data.model
2 |
3 | import android.net.Uri
4 | import im.molly.monero.sdk.MoneroNetwork
5 |
6 | data class RemoteNode(
7 | val id: Long? = null,
8 | val network: MoneroNetwork,
9 | val uri: Uri,
10 | val username: String = "",
11 | val password: String = "",
12 | ) {
13 | companion object {
14 | val EMPTY = RemoteNode(network = DefaultMoneroNetwork, uri = Uri.EMPTY)
15 | }
16 | }
17 |
18 | val DefaultMoneroNetwork = MoneroNetwork.Mainnet
19 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/MoneroNetworkAliases.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | /**
4 | * Shorthand aliases for [MoneroNetwork] enum values.
5 | *
6 | * These aliases allow direct usage of `Mainnet`, `Testnet`, and `Stagenet`
7 | * without qualifying them with `MoneroNetwork`.
8 | *
9 | * Example usage:
10 | * ```
11 | * import im.molly.monero.sdk.Mainnet
12 | *
13 | * val network = Mainnet
14 | * ```
15 | */
16 |
17 | val Mainnet = MoneroNetwork.Mainnet
18 | val Testnet = MoneroNetwork.Testnet
19 | val Stagenet = MoneroNetwork.Stagenet
20 |
--------------------------------------------------------------------------------
/lib/android/src/main/aidl/im/molly/monero/sdk/internal/IBalanceListener.aidl:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal;
2 |
3 | import im.molly.monero.sdk.BlockchainTime;
4 | import im.molly.monero.sdk.internal.TxInfo;
5 |
6 | oneway interface IBalanceListener {
7 | void onBalanceUpdateFinalized(in List txBatch, in String[] allSubAddresses, in BlockchainTime blockchainTime);
8 | void onBalanceUpdateChunk(in List txBatch);
9 | void onWalletRefreshed(in BlockchainTime blockchainTime);
10 | void onSubAddressListUpdated(in String[] allSubAddresses);
11 | }
12 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/ui/WalletCardList.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.ui
2 |
3 | import androidx.compose.foundation.lazy.LazyListScope
4 | import androidx.compose.foundation.lazy.items
5 | import androidx.compose.ui.Modifier
6 |
7 | fun LazyListScope.walletCardItems(
8 | items: List,
9 | onItemClick: (walletId: Long) -> Unit,
10 | itemModifier: Modifier = Modifier,
11 | ) = items(
12 | items = items,
13 | key = { it },
14 | itemContent = {
15 | WalletCard(
16 | walletId = it,
17 | onClick = { onItemClick(it) },
18 | modifier = itemModifier,
19 | )
20 | },
21 | )
22 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/data/model/SocksProxy.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.data.model
2 |
3 | import java.net.InetSocketAddress
4 | import java.net.Proxy
5 | import java.net.SocketAddress
6 |
7 | data class SocksProxy(val socketAddress: SocketAddress) : Proxy(Type.SOCKS, socketAddress)
8 |
9 | fun String.toSocketAddress(): SocketAddress =
10 | try {
11 | val index = lastIndexOf(':')
12 | val host = substring(0, index)
13 | val port = substring(index + 1).toInt()
14 | InetSocketAddress.createUnresolved(host, port)
15 | } catch (t: IndexOutOfBoundsException) {
16 | throw IllegalArgumentException()
17 | }
18 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/TransferRequest.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 |
6 | sealed interface TransferRequest : Parcelable
7 |
8 | @Parcelize
9 | data class PaymentRequest(
10 | val paymentDetails: List,
11 | val spendingAccountIndex: Int,
12 | val feePriority: FeePriority? = null,
13 | ) : TransferRequest
14 |
15 | @Parcelize
16 | data class SweepRequest(
17 | val recipientAddress: PublicAddress,
18 | val splitCount: Int = 1,
19 | val keyImageHashes: List,
20 | val feePriority: FeePriority? = null,
21 | ) : TransferRequest
22 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/service/BaseWalletService.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.service
2 |
3 | import android.content.Intent
4 | import android.os.IBinder
5 | import androidx.lifecycle.LifecycleService
6 | import androidx.lifecycle.lifecycleScope
7 | import im.molly.monero.sdk.internal.IWalletService
8 | import im.molly.monero.sdk.internal.NativeWalletService
9 |
10 | abstract class BaseWalletService : LifecycleService() {
11 | private val service: IWalletService by lazy {
12 | NativeWalletService(this, lifecycleScope)
13 | }
14 |
15 | override fun onBind(intent: Intent): IBinder {
16 | super.onBind(intent)
17 | return service.asBinder()
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/mnemonics/jni_cache.cc:
--------------------------------------------------------------------------------
1 | #include "jni_cache.h"
2 |
3 | namespace monero {
4 |
5 | // im.molly.monero.sdk.mnemonics
6 | jmethodID MoneroMnemonic_buildMnemonicFromJNI;
7 | ScopedJavaGlobalRef MoneroMnemonicClass;
8 |
9 | void InitializeJniCache(JNIEnv* env) {
10 | jclass moneroMnemonic = GetClass(env, "im/molly/monero/sdk/mnemonics/MoneroMnemonic");
11 |
12 | MoneroMnemonic_buildMnemonicFromJNI = GetStaticMethodId(
13 | env, moneroMnemonic,
14 | "buildMnemonicFromJNI",
15 | "([B[BLjava/lang/String;)Lim/molly/monero/sdk/mnemonics/MnemonicCode;");
16 |
17 | MoneroMnemonicClass = ScopedJavaLocalRef(env, moneroMnemonic);
18 | }
19 |
20 | } // namespace monero
21 |
--------------------------------------------------------------------------------
/lib/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
10 |
11 |
12 |
13 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/ui/theme/Icon.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.ui.theme
2 |
3 | import androidx.compose.material.icons.Icons
4 | import androidx.compose.material.icons.filled.*
5 | import androidx.compose.material.icons.outlined.*
6 |
7 | object AppIcons {
8 | val ArrowBack = Icons.Filled.ArrowBack
9 | val MoreVert = Icons.Filled.MoreVert
10 | val Home = Icons.Filled.Home
11 | val HomeOutlined = Icons.Outlined.Home
12 | val History = Icons.Filled.List
13 | val HistoryOutlined = Icons.Outlined.List
14 | val Settings = Icons.Filled.Settings
15 | val SettingsOutlined = Icons.Outlined.Settings
16 | val AddWallet = Icons.Filled.Add
17 | val AddRemoteWallet = Icons.Outlined.AddCircle
18 | }
19 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/service/SandboxedWalletService.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.service
2 |
3 | import android.content.Context
4 | import im.molly.monero.sdk.WalletProvider
5 | import im.molly.monero.sdk.internal.WalletServiceClient
6 |
7 | /**
8 | * Provides wallet services using a sandboxed process.
9 | */
10 | class SandboxedWalletService : BaseWalletService() {
11 | companion object {
12 | /**
13 | * Connects to the sandboxed wallet service and returns a connected [WalletProvider].
14 | */
15 | suspend fun connect(context: Context): WalletProvider {
16 | return WalletServiceClient.bindService(context, SandboxedWalletService::class.java)
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/lib/android/src/androidTest/kotlin/im/molly/monero/sdk/SecretKeyParcelableTest.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | import android.os.Parcel
4 | import com.google.common.truth.Truth.assertThat
5 | import org.junit.Test
6 | import kotlin.random.Random
7 |
8 | class SecretKeyParcelableTest {
9 |
10 | @Test
11 | fun secretKeyIsParcelable() {
12 | val secret = Random.nextBytes(32)
13 | val originalKey = SecretKey(secret)
14 |
15 | val parcel = Parcel.obtain().apply {
16 | originalKey.writeToParcel(this, 0)
17 | setDataPosition(0)
18 | }
19 |
20 | val recreatedKey = SecretKey.CREATOR.createFromParcel(parcel)
21 |
22 | assertThat(recreatedKey).isEqualTo(originalKey)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/service/InProcessWalletService.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.service
2 |
3 | import android.content.Context
4 | import im.molly.monero.sdk.WalletProvider
5 | import im.molly.monero.sdk.internal.WalletServiceClient
6 |
7 | /**
8 | * Provides wallet services using an in-process bound service.
9 | */
10 | class InProcessWalletService : BaseWalletService() {
11 | companion object {
12 | /**
13 | * Connects to the in-process wallet service and returns a connected [WalletProvider].
14 | */
15 | suspend fun connect(context: Context): WalletProvider {
16 | return WalletServiceClient.bindService(context, InProcessWalletService::class.java)
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/monero/wallet2/perf_timer_override.cc:
--------------------------------------------------------------------------------
1 | #include "perf_timer.h"
2 |
3 | namespace tools {
4 |
5 | el::Level performance_timer_log_level = el::Level::Info;
6 |
7 | PerformanceTimer::PerformanceTimer(bool paused) {
8 | // No-op.
9 | }
10 |
11 | PerformanceTimer::~PerformanceTimer() {
12 | // No-op.
13 | }
14 |
15 | LoggingPerformanceTimer::LoggingPerformanceTimer(const std::string& s,
16 | const std::string& cat,
17 | uint64_t unit,
18 | el::Level l) {
19 | // No-op.
20 | }
21 |
22 | LoggingPerformanceTimer::~LoggingPerformanceTimer() {
23 | // No-op.
24 | }
25 |
26 | } // namespace tools
27 |
--------------------------------------------------------------------------------
/lib/android/src/test/kotlin/im/molly/monero/sdk/TimeLockedTest.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | import kotlin.test.Test
4 | import kotlin.test.assertFailsWith
5 |
6 | class TimeLockedTest {
7 |
8 | private val mainnetUnlockTime = UnlockTime.Block(100, Mainnet)
9 | private val locked = 1.xmr.lockedUntil(mainnetUnlockTime)
10 |
11 | @Test
12 | fun `isLocked throws on network mismatch`() {
13 | assertFailsWith {
14 | locked.isLocked(Stagenet.genesisTime)
15 | }
16 | }
17 |
18 | @Test
19 | fun `timeUntilUnlock throws on network mismatch`() {
20 | assertFailsWith {
21 | locked.timeUntilUnlock(Stagenet.genesisTime)
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/RemoteNode.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | import android.net.Uri
4 | import android.os.Parcelable
5 | import androidx.core.net.toUri
6 | import kotlinx.parcelize.Parcelize
7 |
8 | @Parcelize
9 | data class RemoteNode(
10 | val url: String,
11 | val network: MoneroNetwork,
12 | val username: String? = null,
13 | val password: String? = null,
14 | ) : Parcelable {
15 |
16 | fun uriForPath(path: String): Uri =
17 | url.toUri().buildUpon().appendPath(path.trimStart('/')).build()
18 |
19 | override fun toString(): String {
20 | val masked = if (password == null) "null" else "***"
21 | return "RemoteNode(url=$url, network=$network, username=$username, password=$masked)"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/internal/NativeLoader.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal
2 |
3 | import java.util.concurrent.atomic.AtomicBoolean
4 |
5 | internal object NativeLoader {
6 | private val wallet = AtomicBoolean()
7 | private val mnemonics = AtomicBoolean()
8 |
9 | fun loadWalletLibrary(logger: Logger) {
10 | if (wallet.getAndSet(true)) {
11 | return
12 | }
13 | System.loadLibrary("monero_wallet")
14 | nativeSetLogger(logger)
15 | }
16 |
17 | fun loadMnemonicsLibrary() {
18 | if (mnemonics.getAndSet(true)) {
19 | return
20 | }
21 | System.loadLibrary("monero_mnemonics")
22 | }
23 | }
24 |
25 | private external fun nativeSetLogger(logger: Logger)
26 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/ui/navigation/HistoryNavigation.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.ui.navigation
2 |
3 | import androidx.navigation.NavController
4 | import androidx.navigation.NavGraphBuilder
5 | import androidx.navigation.NavOptions
6 | import androidx.navigation.compose.composable
7 | import im.molly.monero.demo.ui.HistoryRoute
8 |
9 | const val historyNavRoute = "history"
10 |
11 | fun NavController.navigateToHistory(navOptions: NavOptions? = null) {
12 | navigate(historyNavRoute, navOptions)
13 | }
14 |
15 | fun NavGraphBuilder.historyScreen(
16 | navigateToTransaction: (String, Long) -> Unit,
17 | ) {
18 | composable(route = historyNavRoute) {
19 | HistoryRoute(
20 | navigateToTransaction = navigateToTransaction,
21 | )
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/WalletAccount.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | data class WalletAccount(
4 | val addresses: List,
5 | val accountIndex: Int,
6 | )
7 |
8 | fun Iterable.findAddressByIndex(
9 | accountIndex: Int,
10 | subAddressIndex: Int = 0,
11 | ): AccountAddress? {
12 | return flatMap { it.addresses }.find {
13 | it.accountIndex == accountIndex && it.subAddressIndex == subAddressIndex
14 | }
15 | }
16 |
17 | fun parseAndAggregateAddresses(addresses: Iterable): List {
18 | return addresses
19 | .map { AccountAddress.parseWithIndexes(it) }
20 | .groupBy { it.accountIndex }
21 | .toSortedMap()
22 | .map { (index, addresses) -> WalletAccount(addresses, index) }
23 | }
24 |
--------------------------------------------------------------------------------
/demo/android/src/androidTest/kotlin/im/molly/monero/demo/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("im.molly.monero.demo", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/data/entity/PopulatedWallet.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.data.entity
2 |
3 | import androidx.room.Embedded
4 | import androidx.room.Junction
5 | import androidx.room.Relation
6 |
7 | data class PopulatedWallet(
8 | @Embedded
9 | val wallet: WalletEntity,
10 |
11 | @Relation(
12 | parentColumn = "id",
13 | entityColumn = "id",
14 | associateBy = Junction(
15 | value = WalletRemoteNodeXRef::class,
16 | parentColumn = "wallet_id",
17 | entityColumn = "remote_node_id",
18 | )
19 | )
20 | val remoteNodes: Set,
21 | )
22 |
23 | fun PopulatedWallet.asExternalModel() = wallet.asExternalModel().copy(
24 | remoteNodes = remoteNodes.map(RemoteNodeEntity::asExternalModel).toSet()
25 | )
26 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/boost/user-config/boost_x86_64.jam.in:
--------------------------------------------------------------------------------
1 | using clang
2 | :
3 | ndk
4 | :
5 | "@NDK_CXX@"
6 | :
7 | "@NDK_AR@"
8 | "@NDK_RANLIB@"
9 | --sysroot="@SYSROOT@"
10 | -Werror=return-type
11 | -Werror=int-to-pointer-cast
12 | -Werror=pointer-to-int-cast
13 | -Werror=implicit-function-declaration
14 | -no-canonical-prefixes
15 | -fPIC
16 | -funwind-tables
17 | -fstack-protector-strong
18 | -frtti
19 | -fexceptions
20 | -flto
21 | -O2
22 | -g
23 | -fno-limit-debug-info
24 | -D_FORTIFY_SOURCE=2
25 | -DBOOST_ASIO_DISABLE_STD_ATOMIC
26 | -DBOOST_FILESYSTEM_DISABLE_STATX
27 | ;
28 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/ContextUtils.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | import android.app.ActivityManager
4 | import android.app.Application
5 | import android.content.Context.ACTIVITY_SERVICE
6 | import android.os.Build
7 | import android.os.Process
8 |
9 | fun Application.isIsolatedProcess(): Boolean {
10 | return if (Build.VERSION.SDK_INT >= 28) {
11 | Process.isIsolated()
12 | } else {
13 | try {
14 | val activityManager = getSystemService(ACTIVITY_SERVICE) as ActivityManager
15 | activityManager.runningAppProcesses
16 | false
17 | } catch (securityException: SecurityException) {
18 | securityException.message?.contains("isolated", true) ?: false
19 | } catch (_: Exception) {
20 | false
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/demo/android/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/boost/user-config/boost_arm64-v8a.jam.in:
--------------------------------------------------------------------------------
1 | using clang
2 | :
3 | ndk
4 | :
5 | "@NDK_CXX@"
6 | :
7 | "@NDK_AR@"
8 | "@NDK_RANLIB@"
9 | --sysroot="@SYSROOT@"
10 | -Werror=return-type
11 | -Werror=int-to-pointer-cast
12 | -Werror=pointer-to-int-cast
13 | -Werror=implicit-function-declaration
14 | -no-canonical-prefixes
15 | -fPIC
16 | -funwind-tables
17 | -fstack-protector-strong
18 | -frtti
19 | -fexceptions
20 | -flto
21 | -O2
22 | -g
23 | -fno-limit-debug-info
24 | -D_FORTIFY_SOURCE=2
25 | -DBOOST_ASIO_DISABLE_STD_ATOMIC
26 | -DBOOST_FILESYSTEM_DISABLE_STATX
27 | ;
28 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/HashDigest.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 |
6 | @JvmInline
7 | @Parcelize
8 | @OptIn(ExperimentalStdlibApi::class)
9 | value class HashDigest(private val hexString: String) : Parcelable {
10 | init {
11 | require(hexString.length == 64) {
12 | "Hash length must be 32 bytes (64 hex chars)"
13 | }
14 | require(hexString.all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' }) {
15 | "Hash must be a valid hex string"
16 | }
17 | }
18 |
19 | constructor(hashDigestBytes: ByteArray) : this(hashDigestBytes.toHexString())
20 |
21 | val bytes: ByteArray
22 | get() = hexString.hexToByteArray()
23 |
24 | override fun toString(): String = hexString
25 | }
26 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/PublicKey.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 |
6 | @Parcelize
7 | @JvmInline
8 | @OptIn(ExperimentalStdlibApi::class)
9 | value class PublicKey(private val hexString: String) : Parcelable {
10 | init {
11 | require(hexString.length == 64) {
12 | "Public key length must be 32 bytes (64 hex chars)"
13 | }
14 | require(hexString.all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' }) {
15 | "Public key must be a valid hex string"
16 | }
17 | }
18 |
19 | constructor(publicKeyBytes: ByteArray) : this(publicKeyBytes.toHexString())
20 |
21 | val bytes: ByteArray
22 | get() = hexString.hexToByteArray()
23 |
24 | override fun toString(): String = hexString
25 | }
26 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/Ledger.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | data class Ledger(
4 | val publicAddress: PublicAddress,
5 | val indexedAccounts: List,
6 | val transactionById: Map,
7 | val enoteSet: Set>,
8 | val checkedAt: BlockchainTime,
9 | ) {
10 | val transactions: Collection
11 | get() = transactionById.values
12 |
13 | val keyImages: Set
14 | get() = enoteSet.mapNotNull { it.value.keyImage }.toSet()
15 |
16 | val isBalanceZero: Boolean
17 | get() = getBalance().totalAmount.isZero
18 |
19 | fun getBalance(): Balance = enoteSet.calculateBalance()
20 |
21 | fun getBalanceForAccount(accountIndex: Int): Balance =
22 | enoteSet.calculateBalance { it.accountIndex == accountIndex }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/boost/user-config/boost_armeabi-v7a.jam.in:
--------------------------------------------------------------------------------
1 | using clang
2 | :
3 | ndk
4 | :
5 | "@NDK_CXX@"
6 | :
7 | "@NDK_AR@"
8 | "@NDK_RANLIB@"
9 | --sysroot="@SYSROOT@"
10 | -Werror=return-type
11 | -Werror=int-to-pointer-cast
12 | -Werror=pointer-to-int-cast
13 | -Werror=implicit-function-declaration
14 | -no-canonical-prefixes
15 | -fPIC
16 | -funwind-tables
17 | -fstack-protector-strong
18 | -frtti
19 | -fexceptions
20 | -flto
21 | -Oz
22 | -mthumb
23 | -g
24 | -fno-limit-debug-info
25 | -D_FORTIFY_SOURCE=2
26 | -DBOOST_ASIO_DISABLE_STD_ATOMIC
27 | -DBOOST_FILESYSTEM_DISABLE_STATX
28 | ;
29 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/ui/navigation/HomeNavigation.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.ui.navigation
2 |
3 | import androidx.navigation.NavController
4 | import androidx.navigation.NavGraphBuilder
5 | import androidx.navigation.NavOptions
6 | import androidx.navigation.compose.composable
7 | import im.molly.monero.demo.ui.HomeRoute
8 |
9 | const val homeNavRoute = "home"
10 |
11 | fun NavController.navigateToHome(navOptions: NavOptions? = null) {
12 | navigate(homeNavRoute, navOptions)
13 | }
14 |
15 | fun NavGraphBuilder.homeScreen(
16 | navigateToAddWalletWizard: () -> Unit,
17 | navigateToWallet: (Long) -> Unit,
18 | ) {
19 | composable(route = homeNavRoute) {
20 | HomeRoute(
21 | navigateToAddWalletWizard = navigateToAddWalletWizard,
22 | navigateToWallet = navigateToWallet,
23 | )
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/ui/AddressCardList.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.ui
2 |
3 | import androidx.compose.foundation.lazy.LazyListScope
4 | import androidx.compose.foundation.lazy.items
5 | import androidx.compose.ui.Modifier
6 | import im.molly.monero.demo.data.model.WalletAddress
7 |
8 | fun LazyListScope.addressCardItems(
9 | items: List,
10 | onCreateSubAddressClick: (accountIndex: Int) -> Unit,
11 | itemModifier: Modifier = Modifier,
12 | ) = items(
13 | items = items,
14 | key = { it.address },
15 | itemContent = {
16 | AddressCardExpanded(
17 | walletAddress = it,
18 | onClick = { },
19 | onCreateSubAddressClick = {
20 | onCreateSubAddressClick(it.address.accountIndex)
21 | },
22 | modifier = itemModifier,
23 | )
24 | },
25 | )
26 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/ui/TransactionCardList.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.ui
2 |
3 | import TransactionCardExpanded
4 | import androidx.compose.foundation.lazy.LazyListScope
5 | import androidx.compose.foundation.lazy.items
6 | import androidx.compose.ui.Modifier
7 | import im.molly.monero.demo.data.model.WalletTransaction
8 |
9 | fun LazyListScope.transactionCardItems(
10 | items: List,
11 | onTransactionClick: (txId: String, walletId: Long) -> Unit,
12 | itemModifier: Modifier = Modifier,
13 | ) = items(
14 | items = items,
15 | key = { it.transaction.txId },
16 | itemContent = {
17 | TransactionCardExpanded(
18 | transaction = it.transaction,
19 | onClick = { onTransactionClick(it.transaction.txId, it.walletId) },
20 | modifier = itemModifier,
21 | )
22 | },
23 | )
24 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/wallet/transfer.h:
--------------------------------------------------------------------------------
1 | #ifndef WALLET_TRANSFER_H_
2 | #define WALLET_TRANSFER_H_
3 |
4 | #include "wallet2.h"
5 |
6 | namespace monero {
7 |
8 | using wallet2 = tools::wallet2;
9 |
10 | struct PendingTransfer {
11 | std::vector m_ptxs;
12 |
13 | uint64_t fee() const {
14 | uint64_t n = 0;
15 | for (const auto& ptx: m_ptxs) {
16 | n += ptx.fee;
17 | }
18 | return n;
19 | }
20 |
21 | uint64_t amount() const {
22 | uint64_t n = 0;
23 | for (const auto& ptx: m_ptxs) {
24 | for (const auto& dest: ptx.dests) {
25 | n += dest.amount;
26 | }
27 | }
28 | return n;
29 | }
30 |
31 | int txCount() const {
32 | return m_ptxs.size();
33 | }
34 |
35 | PendingTransfer(const std::vector& ptxs)
36 | : m_ptxs(ptxs) {}
37 | };
38 |
39 | } // namespace monero
40 |
41 | #endif // WALLET_TRANSFER_H_
42 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/data/AppDatabase.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.data
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 | import im.molly.monero.demo.data.dao.RemoteNodeDao
6 | import im.molly.monero.demo.data.dao.WalletDao
7 | import im.molly.monero.demo.data.entity.RemoteNodeEntity
8 | import im.molly.monero.demo.data.entity.WalletEntity
9 | import im.molly.monero.demo.data.entity.WalletRemoteNodeXRef
10 |
11 | private const val DATABASE_VERSION = 1
12 |
13 | @Database(
14 | entities = [
15 | WalletEntity::class,
16 | RemoteNodeEntity::class,
17 | WalletRemoteNodeXRef::class,
18 | // TransactionEntity::class,
19 | ],
20 | version = DATABASE_VERSION,
21 | exportSchema = true,
22 | )
23 | abstract class AppDatabase : RoomDatabase() {
24 | abstract fun remoteNodeDao(): RemoteNodeDao
25 | abstract fun walletDao(): WalletDao
26 | }
27 |
--------------------------------------------------------------------------------
/lib/android/src/main/aidl/im/molly/monero/sdk/internal/IWalletService.aidl:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal;
2 |
3 | import im.molly.monero.sdk.SecretKey;
4 | import im.molly.monero.sdk.internal.IHttpRpcClient;
5 | import im.molly.monero.sdk.internal.IWalletServiceCallbacks;
6 | import im.molly.monero.sdk.internal.IWalletServiceListener;
7 | import im.molly.monero.sdk.internal.WalletConfig;
8 |
9 | interface IWalletService {
10 | oneway void createWallet(in WalletConfig config, in IHttpRpcClient rpcClient, in IWalletServiceCallbacks callback);
11 | oneway void restoreWallet(in WalletConfig config, in IHttpRpcClient rpcClient, in IWalletServiceCallbacks callback, in SecretKey spendSecretKey, long restorePoint);
12 | oneway void openWallet(in WalletConfig config, in IHttpRpcClient rpcClient, in IWalletServiceCallbacks callback, in ParcelFileDescriptor inputFd);
13 | void setListener(in IWalletServiceListener listener);
14 | boolean isServiceIsolated();
15 | }
16 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/ui/navigation/TopLevelDestination.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.ui.navigation
2 |
3 | import androidx.annotation.StringRes
4 | import androidx.compose.ui.graphics.vector.ImageVector
5 | import im.molly.monero.demo.R
6 | import im.molly.monero.demo.ui.theme.AppIcons
7 |
8 | enum class TopLevelDestination(
9 | val selectedIcon: ImageVector,
10 | val unselectedIcon: ImageVector,
11 | @StringRes val iconTextRes: Int,
12 | ) {
13 | HOME(
14 | selectedIcon = AppIcons.Home,
15 | unselectedIcon = AppIcons.HomeOutlined,
16 | iconTextRes = R.string.home,
17 | ),
18 | HISTORY(
19 | selectedIcon = AppIcons.History,
20 | unselectedIcon = AppIcons.HistoryOutlined,
21 | iconTextRes = R.string.history,
22 | ),
23 | SETTINGS(
24 | selectedIcon = AppIcons.History,
25 | unselectedIcon = AppIcons.HistoryOutlined,
26 | iconTextRes = R.string.settings,
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/lib/android/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
23 | # Keeps methods that are invoked by JNI.
24 | -keepclassmembers class * {
25 | @im.molly.monero.sdk.internal.CalledByNative *;
26 | }
27 |
--------------------------------------------------------------------------------
/lib/android/src/androidTest/kotlin/im/molly/monero/sdk/service/WalletServiceSandboxingTest.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.service
2 |
3 | import android.content.Context
4 | import androidx.test.platform.app.InstrumentationRegistry
5 | import com.google.common.truth.Truth.assertThat
6 | import kotlinx.coroutines.test.runTest
7 | import org.junit.Test
8 |
9 | class WalletServiceSandboxingTest {
10 |
11 | private val context: Context by lazy { InstrumentationRegistry.getInstrumentation().context }
12 |
13 | @Test
14 | fun inProcessWalletServiceIsNotIsolated() = runTest {
15 | InProcessWalletService.connect(context).use { walletProvider ->
16 | assertThat(walletProvider.isServiceSandboxed()).isFalse()
17 | }
18 | }
19 |
20 | @Test
21 | fun sandboxedWalletServiceIsIsolated() = runTest {
22 | SandboxedWalletService.connect(context).use { walletProvider ->
23 | assertThat(walletProvider.isServiceSandboxed()).isTrue()
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/gradle/test-libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | androidx-test-core = "1.6.1"
3 | androidx-test-junit = "1.2.1"
4 | androidx-test-runner = "1.6.2"
5 | androidx-test-truth = "1.6.0"
6 | junit = "4.13.2"
7 | mockk = "1.13.2"
8 | truth = "1.1.3"
9 |
10 | [libraries]
11 | androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test-core" }
12 | androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-junit" }
13 | androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test-core" }
14 | androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" }
15 | androidx-test-truth = { module = "androidx.test.ext:truth", version.ref = "androidx-test-truth" }
16 | junit = { module = "junit:junit", version.ref = "junit" }
17 | mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
18 | mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" }
19 | truth = { module = "com.google.truth:truth", version.ref = "truth" }
20 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/DefaultNodeList.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo
2 |
3 | import im.molly.monero.demo.data.AppDatabase
4 | import im.molly.monero.demo.data.entity.asEntity
5 | import im.molly.monero.demo.data.model.RemoteNode
6 | import im.molly.monero.sdk.MoneroNetwork
7 | import androidx.core.net.toUri
8 |
9 | val DefaultNodeList = listOf(
10 | MoneroNetwork.Mainnet to listOf(
11 | "http://node.monerodevs.org:18089",
12 | "http://node.sethforprivacy.com:18089",
13 | "http://xmr-node.cakewallet.com:18081",
14 | ),
15 | MoneroNetwork.Testnet to listOf(
16 | "http://node2.monerodevs.org:28089",
17 | ),
18 | )
19 |
20 | suspend fun addDefaultRemoteNodes(appDatabase: AppDatabase) {
21 | val dao = appDatabase.remoteNodeDao()
22 | val nodes = DefaultNodeList.flatMap { (network, urls) ->
23 | urls.map { url ->
24 | RemoteNode(network = network, uri = url.toUri()).asEntity()
25 | }
26 | }.toTypedArray()
27 | dao.upsert(*nodes)
28 | }
29 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/common/Result.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.common
2 |
3 | import kotlinx.coroutines.flow.Flow
4 | import kotlinx.coroutines.flow.catch
5 | import kotlinx.coroutines.flow.map
6 | import kotlinx.coroutines.flow.onStart
7 |
8 | /**
9 | * A generic class that holds a value with its loading status.
10 | * @param
11 | */
12 | sealed interface Result {
13 | data class Success(val data: T) : Result
14 | data class Error(val exception: Throwable? = null) : Result
15 | data object Loading : Result
16 | }
17 |
18 | /**
19 | * `true` if [Result] is of type [Result.Success] & holds non-null [Result.Success.data].
20 | */
21 | val Result<*>.succeeded
22 | get() = this is Result.Success && data != null
23 |
24 | fun Flow.asResult(): Flow> {
25 | return this
26 | .map> {
27 | Result.Success(it)
28 | }
29 | .onStart { emit(Result.Loading) }
30 | .catch { emit(Result.Error(it)) }
31 | }
32 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/Block.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | import im.molly.monero.sdk.internal.constants.CRYPTONOTE_MAX_BLOCK_NUMBER
4 | import java.time.Instant
5 |
6 | data class Block(
7 | // TODO: val hash: HashDigest,
8 | val header: BlockHeader,
9 | val minerRewardTxIndex: Int,
10 | val txs: Set,
11 | ) {
12 | // TODO: val blockId: String get() = hash.toString()
13 | }
14 |
15 | data class BlockHeader(
16 | val height: Int,
17 | val epochSecond: Long,
18 | // val version: ProtocolInfo,
19 | ) {
20 | val timestamp: Instant
21 | get() = Instant.ofEpochSecond(epochSecond)
22 |
23 | companion object {
24 | const val MAX_HEIGHT = CRYPTONOTE_MAX_BLOCK_NUMBER - 1
25 | }
26 | }
27 |
28 | fun isBlockHeightInRange(height: Long) = !(height < 0 || height > BlockHeader.MAX_HEIGHT)
29 |
30 | fun isBlockHeightInRange(height: Int) = isBlockHeightInRange(height.toLong())
31 |
32 | fun isBlockEpochInRange(epochSecond: Long) = epochSecond > BlockHeader.MAX_HEIGHT
33 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/ui/component/Toolbar.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.ui.component
2 |
3 | import androidx.compose.foundation.layout.RowScope
4 | import androidx.compose.material3.CenterAlignedTopAppBar
5 | import androidx.compose.material3.ExperimentalMaterial3Api
6 | import androidx.compose.material3.Text
7 | import androidx.compose.material3.TopAppBarScrollBehavior
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 |
11 | @OptIn(ExperimentalMaterial3Api::class)
12 | @Composable
13 | fun Toolbar(
14 | modifier: Modifier = Modifier,
15 | title: String? = null,
16 | navigationIcon: @Composable () -> Unit = {},
17 | actions: @Composable RowScope.() -> Unit = {},
18 | scrollBehavior: TopAppBarScrollBehavior? = null,
19 | ) {
20 | CenterAlignedTopAppBar(
21 | title = { Text(title ?: "") },
22 | modifier = modifier,
23 | navigationIcon = navigationIcon,
24 | actions = actions,
25 | scrollBehavior = scrollBehavior,
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/internal/LedgerFactory.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal
2 |
3 | import im.molly.monero.sdk.BlockchainTime
4 | import im.molly.monero.sdk.Ledger
5 | import im.molly.monero.sdk.WalletAccount
6 | import im.molly.monero.sdk.findAddressByIndex
7 |
8 | internal object LedgerFactory {
9 | fun createFromTxHistory(
10 | txList: List,
11 | accounts: List,
12 | blockchainTime: BlockchainTime,
13 | ): Ledger {
14 | val (txById, enotes) = txList.consolidateTransactions(
15 | accounts = accounts,
16 | blockchainContext = blockchainTime,
17 | )
18 | val publicAddress = accounts.findAddressByIndex(accountIndex = 0)
19 | checkNotNull(publicAddress)
20 | return Ledger(
21 | publicAddress = publicAddress,
22 | indexedAccounts = accounts,
23 | transactionById = txById,
24 | enoteSet = enotes,
25 | checkedAt = blockchainTime,
26 | )
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/common/jvm.h:
--------------------------------------------------------------------------------
1 | #ifndef COMMON_JVM_H_
2 | #define COMMON_JVM_H_
3 |
4 | #include
5 |
6 | #include
7 |
8 | namespace monero {
9 |
10 | // Get a JNIEnv* usable on this thread, regardless of whether it's a native
11 | // thread or a Java thread. Attach the thread to the JVM if necessary.
12 | JNIEnv* GetJniEnv();
13 |
14 | // This method should be called from JNI_OnLoad.
15 | JNIEnv* InitializeJvm(JavaVM* jvm, int version);
16 |
17 | // Checks for any Java exception and clears currently thrown exception.
18 | static bool inline CheckException(JNIEnv* env, bool log_exception = true) {
19 | if (env->ExceptionCheck()) {
20 | if (log_exception) env->ExceptionDescribe();
21 | env->ExceptionClear();
22 | return true;
23 | }
24 | return false;
25 | }
26 |
27 | static void inline ThrowRuntimeErrorOnException(JNIEnv* env) {
28 | if (CheckException(env, false)) {
29 | throw std::runtime_error("Uncaught exception returned from Java call");
30 | }
31 | }
32 |
33 | } // namespace monero
34 |
35 | #endif // COMMON_JVM_H_
36 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/Transaction.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | data class Transaction(
4 | val hash: HashDigest,
5 | val state: TxState,
6 | val network: MoneroNetwork,
7 | val timeLock: UnlockTime?,
8 | val sent: Set,
9 | val received: Set,
10 | val payments: List,
11 | val fee: MoneroAmount,
12 | val change: MoneroAmount,
13 | ) {
14 | val amount: MoneroAmount = received.sumOf { it.amount } - sent.sumOf { it.amount }
15 |
16 | val txId: String = hash.toString()
17 |
18 | private val blockHeader = (state as? TxState.OnChain)?.blockHeader
19 |
20 | val blockHeight = blockHeader?.height
21 |
22 | val blockTimestamp = blockHeader?.timestamp
23 | }
24 |
25 | sealed interface TxState {
26 | data class OnChain(
27 | val blockHeader: BlockHeader,
28 | ) : TxState
29 |
30 | data object BeingProcessed : TxState
31 |
32 | data object InMemoryPool : TxState
33 |
34 | data object Failed : TxState
35 |
36 | data object OffChain : TxState
37 | }
38 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/loadbalancer/Rule.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.loadbalancer
2 |
3 | import im.molly.monero.sdk.RemoteNode
4 | import java.util.concurrent.atomic.AtomicInteger
5 |
6 | interface Rule {
7 | /**
8 | * Returns one alive [RemoteNode] from the [loadBalancer] using its internal
9 | * node selection rule, or null if none are available.
10 | */
11 | fun chooseNode(loadBalancer: LoadBalancer): RemoteNode?
12 | }
13 |
14 | object FirstRule : Rule {
15 | override fun chooseNode(loadBalancer: LoadBalancer): RemoteNode? {
16 | return loadBalancer.onlineNodes.firstOrNull()
17 | }
18 | }
19 |
20 | class RoundRobinRule : Rule {
21 | private var currentIndex = AtomicInteger(0)
22 |
23 | override fun chooseNode(loadBalancer: LoadBalancer): RemoteNode? {
24 | val nodes = loadBalancer.onlineNodes
25 | return if (nodes.isNotEmpty()) {
26 | val index = currentIndex.getAndIncrement() % nodes.size
27 | nodes[index]
28 | } else {
29 | null
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/MoneroNetwork.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | import im.molly.monero.sdk.internal.constants.DIFFICULTY_TARGET_V1
4 | import im.molly.monero.sdk.internal.constants.DIFFICULTY_TARGET_V2
5 | import java.time.Duration
6 |
7 | /**
8 | * Monero network environments.
9 | *
10 | * Defined in cryptonote_config.h
11 | */
12 | enum class MoneroNetwork(val id: Int, val epoch: Long, val epochV2: Pair) {
13 | Mainnet(0, 1397818193, (1009827 to 1458748658)),
14 | Testnet(1, 1410295020, (624634 to 1448285909)),
15 | Stagenet(2, 1518932025, (32000 to 1520937818));
16 |
17 | companion object {
18 | fun fromId(value: Int) = entries.first { it.id == value }
19 |
20 | fun of(publicAddress: String) = PublicAddress.parse(publicAddress).network
21 | }
22 |
23 | fun avgBlockTime(height: Int): Duration {
24 | return if (height < epochV2.second) {
25 | Duration.ofSeconds(DIFFICULTY_TARGET_V1)
26 | } else {
27 | Duration.ofSeconds(DIFFICULTY_TARGET_V2)
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/data/entity/WalletEntity.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.data.entity
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.Index
6 | import androidx.room.PrimaryKey
7 | import im.molly.monero.demo.data.model.WalletConfig
8 |
9 | @Entity(
10 | tableName = "wallets",
11 | )
12 | data class WalletEntity(
13 | @PrimaryKey(autoGenerate = true)
14 | @ColumnInfo(name = "id")
15 | val id: Long = 0,
16 |
17 | @ColumnInfo(name = "public_address")
18 | val publicAddress: String,
19 |
20 | @ColumnInfo(name = "filename")
21 | val filename: String,
22 |
23 | @ColumnInfo(name = "name")
24 | val name: String = "",
25 | )
26 |
27 | fun WalletEntity.asExternalModel() = WalletConfig(
28 | id = id,
29 | publicAddress = publicAddress,
30 | filename = filename,
31 | name = name,
32 | remoteNodes = setOf(),
33 | )
34 |
35 | fun WalletConfig.asEntity() = WalletEntity(
36 | id = id,
37 | publicAddress = publicAddress,
38 | filename = filename,
39 | name = name
40 | )
41 |
--------------------------------------------------------------------------------
/lib/android/src/androidTest/kotlin/im/molly/monero/sdk/MoneroWalletSubject.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | import com.google.common.truth.FailureMetadata
4 | import com.google.common.truth.Subject
5 | import com.google.common.truth.Truth.assertAbout
6 | import kotlinx.coroutines.flow.first
7 |
8 | class MoneroWalletSubject private constructor(
9 | metadata: FailureMetadata,
10 | private val actual: MoneroWallet,
11 | ) : Subject(metadata, actual) {
12 |
13 | companion object {
14 | fun assertThat(wallet: MoneroWallet): MoneroWalletSubject {
15 | return assertAbout(factory).that(wallet)
16 | }
17 |
18 | private val factory = Factory { metadata, actual: MoneroWallet ->
19 | MoneroWalletSubject(metadata, actual)
20 | }
21 | }
22 |
23 | suspend fun matchesStateOf(expected: MoneroWallet) {
24 | with(actual) {
25 | check("publicAddress").that(publicAddress).isEqualTo(expected.publicAddress)
26 | check("ledger").that(ledger().first()).isEqualTo(expected.ledger().first())
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/WalletProvider.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | import java.io.Closeable
4 |
5 | interface WalletProvider : Closeable {
6 | suspend fun createNewWallet(
7 | network: MoneroNetwork,
8 | dataStore: WalletDataStore? = null,
9 | client: MoneroNodeClient? = null,
10 | ): MoneroWallet
11 |
12 | suspend fun restoreWallet(
13 | network: MoneroNetwork,
14 | dataStore: WalletDataStore? = null,
15 | client: MoneroNodeClient? = null,
16 | secretSpendKey: SecretKey,
17 | restorePoint: RestorePoint,
18 | ): MoneroWallet
19 |
20 | suspend fun openWallet(
21 | network: MoneroNetwork,
22 | dataStore: WalletDataStore,
23 | client: MoneroNodeClient? = null,
24 | ): MoneroWallet
25 |
26 | fun isServiceSandboxed(): Boolean
27 |
28 | fun disconnect()
29 |
30 | override fun close() {
31 | disconnect()
32 | }
33 |
34 | /** Exception thrown if the wallet service cannot be bound. */
35 | class ServiceNotBoundException : Exception()
36 | }
37 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/monero/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | set(MONERO_DIR "${VENDOR_DIR}/monero")
2 |
3 | set(ARCH_WIDTH "64")
4 |
5 | # Require C11/C++14 and disable extensions for all monero targets
6 | set(CMAKE_C_STANDARD 11)
7 | set(CMAKE_C_STANDARD_REQUIRED ON)
8 | set(CMAKE_C_EXTENSIONS OFF)
9 | set(CMAKE_CXX_STANDARD 14)
10 | set(CMAKE_CXX_STANDARD_REQUIRED ON)
11 | set(CMAKE_CXX_EXTENSIONS OFF)
12 |
13 | # Just an empty macro
14 | macro(monero_enable_coverage)
15 | endmacro()
16 |
17 | # From monero's root CMakeLists.txt
18 | macro(monero_find_all_headers headers_found module_root_dir)
19 | file(GLOB ${headers_found}
20 | "${module_root_dir}/*.h*" # h* will include hpps as well.
21 | "${module_root_dir}/**/*.h*" # Any number of subdirs will be included.
22 | "${module_root_dir}/*.inl" # .inl is typically template code and is being treated as headers (it's being included).
23 | "${module_root_dir}/**/*.inl"
24 | )
25 | endmacro()
26 |
27 | add_subdirectory(easylogging)
28 | add_subdirectory(electrum_words)
29 | add_subdirectory(lmdb)
30 | add_subdirectory(randomx)
31 | add_subdirectory(wallet2)
32 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/common/eraser.h:
--------------------------------------------------------------------------------
1 | #ifndef COMMON_ERASER_H_
2 | #define COMMON_ERASER_H_
3 |
4 | #include
5 |
6 | namespace monero {
7 |
8 | #pragma clang diagnostic push
9 | #pragma ide diagnostic ignored "NotImplementedFunctions"
10 |
11 | // Eraser clears buffers. Construct it with a buffer or object and the
12 | // destructor will ensure that it is zeroed.
13 | class Eraser {
14 | public:
15 | // Not implemented. If this gets used, we want a link error.
16 | template explicit Eraser(T* t);
17 |
18 | template
19 | explicit Eraser(T& t) : m_buf(reinterpret_cast(&t)), m_size(sizeof(t)) {}
20 |
21 | template explicit Eraser(char (&arr)[N]) : m_buf(arr), m_size(N) {}
22 |
23 | Eraser(void* buf, size_t size) : m_buf(static_cast(buf)), m_size(size) {}
24 | ~Eraser() { OPENSSL_cleanse(m_buf, m_size); }
25 |
26 | private:
27 | Eraser(const Eraser&);
28 | void operator=(const Eraser&);
29 |
30 | char* m_buf;
31 | size_t m_size;
32 | };
33 |
34 | #pragma clang diagnostic pop
35 |
36 | } // namespace monero
37 |
38 | #endif // COMMON_ERASER_H_
39 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/libsodium/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | include(ExternalProject)
2 |
3 | ExternalProject_Add(
4 | Libsodium
5 | URL "${VENDOR_DIR}/libsodium"
6 | CONFIGURE_COMMAND ./autogen.sh && env
7 | "AR=${NDK_AR}"
8 | "CC=${NDK_CC}"
9 | "AS=${NDK_AS}"
10 | "CXX=${NDK_CXX}"
11 | "LD=${NDK_LD}"
12 | "RANLIB=${NDK_RANLIB}"
13 | "STRIP=${NDK_STRIP}"
14 | "CFLAGS=${NDK_C_FLAGS}"
15 | ./configure
16 | "--prefix="
17 | --host=${TARGET_HOST}
18 | --enable-minimal
19 | --enable-static
20 | --disable-shared
21 | BUILD_IN_SOURCE 1
22 | BUILD_COMMAND "${NDK_MAKE}" install "-j${CORES}"
23 | BUILD_BYPRODUCTS
24 | "/lib/libsodium.a"
25 | )
26 |
27 | ExternalProject_Get_Property(Libsodium INSTALL_DIR)
28 |
29 | set(LIBSODIUM_INCLUDE_DIR "${INSTALL_DIR}/include" PARENT_SCOPE)
30 |
31 | add_library(libsodium STATIC IMPORTED GLOBAL)
32 | set_property(TARGET libsodium PROPERTY IMPORTED_LOCATION "${INSTALL_DIR}/lib/libsodium.a")
33 | add_dependencies(libsodium Libsodium)
34 |
35 | add_library(Libsodium::libsodium ALIAS libsodium)
36 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/data/dao/WalletDao.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.data.dao
2 |
3 | import androidx.room.*
4 | import im.molly.monero.demo.data.entity.PopulatedWallet
5 | import im.molly.monero.demo.data.entity.WalletEntity
6 | import im.molly.monero.demo.data.entity.WalletRemoteNodeXRef
7 | import kotlinx.coroutines.flow.Flow
8 |
9 | @Dao
10 | interface WalletDao {
11 | @Transaction
12 | @Query(
13 | value = """
14 | SELECT * FROM wallets
15 | WHERE id = :id
16 | """
17 | )
18 | fun findById(id: Long): Flow
19 |
20 | @Query(
21 | value = """
22 | SELECT id FROM wallets
23 | """
24 | )
25 | fun findAllIds(): Flow>
26 |
27 | @Insert(onConflict = OnConflictStrategy.ABORT)
28 | suspend fun insert(wallet: WalletEntity): Long
29 |
30 | @Insert(onConflict = OnConflictStrategy.ABORT)
31 | suspend fun insertRemoteNodeXRefEntities(walletRemoteNodeXRefs: List)
32 |
33 | @Update
34 | suspend fun update(wallet: WalletEntity)
35 |
36 | @Delete
37 | suspend fun delete(wallet: WalletEntity)
38 | }
39 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/data/entity/WalletRemoteNodeXRef.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.data.entity
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.ForeignKey
6 | import androidx.room.Index
7 |
8 | @Entity(
9 | tableName = "wallet_remote_nodes",
10 | primaryKeys = ["wallet_id", "remote_node_id"],
11 | foreignKeys = [
12 | ForeignKey(
13 | entity = WalletEntity::class,
14 | parentColumns = ["id"],
15 | childColumns = ["wallet_id"],
16 | onDelete = ForeignKey.CASCADE
17 | ),
18 | ForeignKey(
19 | entity = RemoteNodeEntity::class,
20 | parentColumns = ["id"],
21 | childColumns = ["remote_node_id"],
22 | onDelete = ForeignKey.CASCADE
23 | ),
24 | ],
25 | indices = [
26 | Index(value = ["wallet_id"]),
27 | Index(value = ["remote_node_id"]),
28 | ],
29 | )
30 | data class WalletRemoteNodeXRef(
31 | @ColumnInfo(name = "wallet_id")
32 | val walletId: Long,
33 |
34 | @ColumnInfo(name = "remote_node_id")
35 | val remoteNodeId: Long,
36 | )
37 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
35 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/ui/WalletListViewModel.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.ui
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import im.molly.monero.demo.AppModule
6 | import im.molly.monero.demo.data.WalletRepository
7 | import kotlinx.coroutines.flow.*
8 |
9 | class WalletListViewModel(
10 | walletRepository: WalletRepository = AppModule.walletRepository,
11 | ) : ViewModel() {
12 |
13 | val uiState: StateFlow =
14 | walletRepository.getWalletIdList().map { walletIds ->
15 | if (walletIds.isNotEmpty()) {
16 | WalletListUiState.Loaded(walletIds)
17 | } else {
18 | WalletListUiState.Empty
19 | }
20 | }.stateIn(
21 | scope = viewModelScope,
22 | started = SharingStarted.WhileSubscribed(5_000),
23 | initialValue = WalletListUiState.Loading,
24 | )
25 | }
26 |
27 | sealed interface WalletListUiState {
28 | data class Loaded(val walletIds: List) : WalletListUiState
29 | data object Loading : WalletListUiState
30 | data object Empty : WalletListUiState
31 | }
32 |
--------------------------------------------------------------------------------
/.github/actions/disk-cleanup/action.yml:
--------------------------------------------------------------------------------
1 | name: Disk cleanup
2 | description: "Free up disk space by removing unused pre-installed software."
3 | runs:
4 | using: composite
5 | steps:
6 | - name: Free up disk space
7 | shell: bash
8 | run: |
9 | echo "Disk space before cleanup:"
10 | df -h /
11 |
12 | echo "Removing unused software..."
13 | sudo rm -rf \
14 | /opt/az \
15 | /opt/google/chrome \
16 | /opt/hostedtoolcache/CodeQL \
17 | /opt/microsoft \
18 | /opt/pipx \
19 | /usr/lib/dotnet \
20 | /usr/lib/firefox \
21 | /usr/lib/google-cloud-sdk \
22 | /usr/lib/mono \
23 | /usr/local/.ghcup \
24 | /usr/local/aws-cli \
25 | /usr/local/julia* \
26 | /usr/local/share/chromium \
27 | /usr/local/share/powershell \
28 | /usr/local/share/vcpkg \
29 | /usr/local/aws-sam-cli \
30 | /usr/share/az_* \
31 | /usr/share/dotnet \
32 | /usr/share/man \
33 | /usr/share/miniconda \
34 | /usr/share/swift \
35 |
36 | echo "Disk space after cleanup:"
37 | df -h /
38 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/MainApplication.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import android.os.StrictMode
6 | import androidx.datastore.core.DataStore
7 | import androidx.datastore.preferences.core.Preferences
8 | import androidx.datastore.preferences.preferencesDataStore
9 | import im.molly.monero.demo.service.SyncService
10 | import im.molly.monero.sdk.isIsolatedProcess
11 |
12 | val Context.preferencesDataStore: DataStore by preferencesDataStore(name = "settings")
13 |
14 | class MainApplication : Application() {
15 | override fun onCreate() {
16 | super.onCreate()
17 | StrictMode.setVmPolicy(
18 | StrictMode.VmPolicy.Builder(StrictMode.getVmPolicy())
19 | .detectLeakedClosableObjects()
20 | .build()
21 | )
22 | if (isIsolatedProcess()) {
23 | return
24 | }
25 | AppModule.provide(
26 | application = this,
27 | populateInitialData = { db ->
28 | addDefaultRemoteNodes(db)
29 | },
30 | )
31 | SyncService.start(this)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/lib/android/src/main/aidl/im/molly/monero/sdk/internal/ITransferCallback.aidl:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal;
2 |
3 | import im.molly.monero.sdk.internal.IPendingTransfer;
4 |
5 | oneway interface ITransferCallback {
6 | void onTransferCreated(in IPendingTransfer pendingTransfer);
7 | void onTransferCommitted();
8 | // void onDaemonBusy();
9 | // void onNoConnectionToDaemon();
10 | // void onRPCError(String errorMessage);
11 | // void onFailedToGetOutputs();
12 | // void onNotEnoughUnlockedMoney(long available, long sentAmount);
13 | // void onNotEnoughMoney(long available, long sentAmount);
14 | // void onTransactionNotPossible(long available, long transactionAmount, long fee);
15 | // void onNotEnoughOutsToMix(int mixinCount, Map scantyOuts);
16 | // void onTransactionNotConstructed();
17 | // void onTransactionRejected(String transactionHash, int status);
18 | // void onTransactionSumOverflow(String errorMessage);
19 | // void onZeroDestination();
20 | // void onTransactionTooBig();
21 | // void onTransferError(String errorMessage);
22 | // void onWalletInternalError(String errorMessage);
23 | void onUnexpectedError(String message);
24 | }
25 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/wallet/jni_cache.h:
--------------------------------------------------------------------------------
1 | #ifndef WALLET_JNI_CACHE_H__
2 | #define WALLET_JNI_CACHE_H__
3 |
4 | #include "common/jvm.h"
5 | #include "common/java_native.h"
6 |
7 | namespace monero {
8 |
9 | // Initialize various classes and method pointers cached for use in JNI.
10 | void InitializeJniCache(JNIEnv* env);
11 |
12 | // im.molly.monero.sdk
13 | extern jmethodID HttpResponse_getBody;
14 | extern jmethodID HttpResponse_getCode;
15 | extern jmethodID HttpResponse_getContentType;
16 | extern jmethodID ITransferCallback_onTransferCreated;
17 | extern jmethodID ITransferCallback_onTransferCommitted;
18 | extern jmethodID ITransferCallback_onUnexpectedError;
19 | extern jmethodID Logger_logFromNative;
20 | extern jmethodID TxInfo_ctor;
21 | extern jmethodID NativeWallet_callRemoteNode;
22 | extern jmethodID NativeWallet_createPendingTransfer;
23 | extern jmethodID NativeWallet_onRefresh;
24 | extern jmethodID NativeWallet_onSuspendRefresh;
25 | extern ScopedJavaGlobalRef TxInfoClass;
26 |
27 | // android.os
28 | extern jmethodID ParcelFd_detachFd;
29 |
30 | // java.lang
31 | extern ScopedJavaGlobalRef StringClass;
32 |
33 | } // namespace monero
34 |
35 | #endif // WALLET_JNI_CACHE_H__
36 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/ui/HistoryScreen.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.ui
2 |
3 | import androidx.compose.material3.ExperimentalMaterial3Api
4 | import androidx.compose.material3.Scaffold
5 | import androidx.compose.material3.TopAppBarDefaults
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.input.nestedscroll.nestedScroll
9 | import im.molly.monero.demo.ui.component.Toolbar
10 |
11 | @Composable
12 | fun HistoryRoute(
13 | navigateToTransaction: (String, Long) -> Unit,
14 | ) {
15 | HistoryScreen(
16 | onTransactionClick = navigateToTransaction,
17 | )
18 | }
19 |
20 | @OptIn(ExperimentalMaterial3Api::class)
21 | @Composable
22 | private fun HistoryScreen(
23 | onTransactionClick: (String, Long) -> Unit,
24 | ) {
25 | val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
26 | Scaffold(
27 | modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
28 | topBar = {
29 | Toolbar(
30 | title = "Transaction history",
31 | scrollBehavior = scrollBehavior,
32 | )
33 | },
34 | ) { padding ->
35 | // TODO
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/data/dao/RemoteNodeDao.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.data.dao
2 |
3 | import androidx.room.*
4 | import im.molly.monero.demo.data.entity.RemoteNodeEntity
5 | import kotlinx.coroutines.flow.Flow
6 |
7 | @Dao
8 | interface RemoteNodeDao {
9 | @Query(
10 | value = """
11 | SELECT * FROM remote_nodes
12 | """
13 | )
14 | fun findAll(): Flow>
15 |
16 | @Query(
17 | value = """
18 | SELECT * FROM remote_nodes
19 | WHERE id = :id
20 | """
21 | )
22 | fun findById(id: Long): Flow
23 |
24 | @Query(
25 | value = """
26 | SELECT * FROM remote_nodes
27 | WHERE id IN (:ids)
28 | """
29 | )
30 | fun findByIds(ids: List): Flow>
31 |
32 | @Query(
33 | value = """
34 | SELECT * FROM remote_nodes
35 | WHERE net_type IN (:networkIds)
36 | """
37 | )
38 | fun findByNetworkIds(networkIds: Set): Flow>
39 |
40 | @Upsert
41 | suspend fun upsert(vararg remoteNodes: RemoteNodeEntity)
42 |
43 | @Delete
44 | suspend fun delete(vararg remoteNodes: RemoteNodeEntity)
45 | }
46 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/WalletDataStore.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | import java.io.ByteArrayInputStream
4 | import java.io.ByteArrayOutputStream
5 | import java.io.IOException
6 | import java.io.InputStream
7 | import java.io.OutputStream
8 |
9 | interface WalletDataStore {
10 | @Throws(IOException::class)
11 | suspend fun load(): InputStream
12 |
13 | @Throws(IOException::class)
14 | suspend fun save(writer: (OutputStream) -> Unit, overwrite: Boolean)
15 | }
16 |
17 | class InMemoryWalletDataStore() : WalletDataStore {
18 | private val data = ByteArrayOutputStream()
19 |
20 | constructor(byteArray: ByteArray) : this() {
21 | data.write(byteArray)
22 | }
23 |
24 | override suspend fun load(): InputStream {
25 | return ByteArrayInputStream(data.toByteArray())
26 | }
27 |
28 | override suspend fun save(writer: (OutputStream) -> Unit, overwrite: Boolean) {
29 | check(overwrite || data.size() == 0) { "Wallet data already exists" }
30 | data.reset()
31 | writer(data)
32 | }
33 |
34 | fun toByteArray(): ByteArray {
35 | return data.toByteArray()
36 | }
37 | }
38 |
39 | fun InMemoryWalletDataStore.copy() = InMemoryWalletDataStore(this.toByteArray())
40 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/ui/navigation/TransactionNavigation.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.ui.navigation
2 |
3 | import androidx.navigation.*
4 | import androidx.navigation.compose.composable
5 | import im.molly.monero.demo.ui.TransactionRoute
6 |
7 | const val transactionNavRoute = "tx"
8 |
9 | private const val txIdArg = "txId"
10 | private const val walletIdArg = "walletId"
11 |
12 | fun NavController.navigateToTransaction(txId: String, walletId: Long) {
13 | val route = "$transactionNavRoute/$txId/$walletId"
14 | navigate(route)
15 | }
16 |
17 | fun NavGraphBuilder.transactionScreen(
18 | onBackClick: () -> Unit,
19 | ) {
20 | composable(
21 | route = "$transactionNavRoute/{$txIdArg}/{$walletIdArg}",
22 | arguments = listOf(
23 | navArgument(txIdArg) { type = NavType.StringType },
24 | navArgument(walletIdArg) { type = NavType.LongType },
25 | )
26 | ) {
27 | val arguments = requireNotNull(it.arguments)
28 | val txId = arguments.getString(txIdArg)
29 | val walletId = arguments.getLong(walletIdArg)
30 | TransactionRoute(
31 | txId = txId!!,
32 | walletId = walletId,
33 | onBackClick = onBackClick,
34 | )
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/data/SettingsRepository.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.data
2 |
3 | import androidx.datastore.core.DataStore
4 | import androidx.datastore.preferences.core.Preferences
5 | import androidx.datastore.preferences.core.edit
6 | import androidx.datastore.preferences.core.stringPreferencesKey
7 | import im.molly.monero.demo.data.model.SocksProxy
8 | import im.molly.monero.demo.data.model.UserSettings
9 | import im.molly.monero.demo.data.model.toSocketAddress
10 | import kotlinx.coroutines.flow.map
11 |
12 | class SettingsRepository(
13 | private val dataStore: DataStore,
14 | ) {
15 | companion object {
16 | val PREF_SOCKS_PROXY = stringPreferencesKey("socks_proxy")
17 | }
18 |
19 | fun getUserSettings() = dataStore.data.map { prefs ->
20 | UserSettings(
21 | socksProxy = prefs[PREF_SOCKS_PROXY]?.let { SocksProxy(it.toSocketAddress()) }
22 | )
23 | }
24 |
25 | suspend fun setSocksProxy(socksProxy: SocksProxy?) {
26 | dataStore.edit { prefs ->
27 | if (socksProxy != null) {
28 | prefs[PREF_SOCKS_PROXY] = socksProxy.address().toString()
29 | } else {
30 | prefs.remove(PREF_SOCKS_PROXY)
31 | }
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/ui/component/CopyableText.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.ui.component
2 |
3 | import android.widget.Toast
4 | import androidx.compose.foundation.ExperimentalFoundationApi
5 | import androidx.compose.foundation.combinedClickable
6 | import androidx.compose.material3.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.platform.LocalClipboardManager
10 | import androidx.compose.ui.platform.LocalContext
11 | import androidx.compose.ui.text.AnnotatedString
12 | import androidx.compose.ui.text.TextStyle
13 |
14 | @OptIn(ExperimentalFoundationApi::class)
15 | @Composable
16 | fun CopyableText(
17 | text: String,
18 | style: TextStyle,
19 | modifier: Modifier = Modifier,
20 | ) {
21 | val context = LocalContext.current
22 | val clipboardManager = LocalClipboardManager.current
23 |
24 | Text(
25 | text = text,
26 | style = style,
27 | modifier = modifier.combinedClickable(
28 | onClick = {},
29 | onLongClick = {
30 | clipboardManager.setText(AnnotatedString(text))
31 | Toast.makeText(context, "Text copied to clipboard", Toast.LENGTH_SHORT).show()
32 | },
33 | ),
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/monero/wallet2/mlog_override.cc:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | #include "easylogging++.h"
4 |
5 | void mlog_configure(const std::string& filename_base,
6 | bool console,
7 | const std::size_t max_log_file_size,
8 | const std::size_t max_log_files) {
9 | // No-op.
10 | }
11 |
12 | extern "C" {
13 |
14 | // Log helpers for C code. Used only in module 'randomx' for simple error messages.
15 | // To make it easy, printf formatting is ignored and it's assumed the source
16 | // FILE and LINE will wrongly identify these functions instead of the callers.
17 |
18 | bool merror(const char* category, const char* format, ...) {
19 | CLOG(ERROR, category) << format;
20 | return true;
21 | }
22 |
23 | bool mwarning(const char* category, const char* format, ...) {
24 | CLOG(WARNING, category) << format;
25 | return true;
26 | }
27 |
28 | bool minfo(const char* category, const char* format, ...) {
29 | CLOG(INFO, category) << format;
30 | return true;
31 | }
32 |
33 | bool mdebug(const char* category, const char* format, ...) {
34 | CLOG(DEBUG, category) << format;
35 | return true;
36 | }
37 |
38 | bool mtrace(const char* category, const char* format, ...) {
39 | CLOG(TRACE, category) << format;
40 | return true;
41 | }
42 |
43 | } // extern C
44 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/wallet/logging.h:
--------------------------------------------------------------------------------
1 | #ifndef WALLET_LOGGING_H_
2 | #define WALLET_LOGGING_H_
3 |
4 | #include
5 |
6 | #include
7 |
8 | #include "common/scoped_java_ref.h"
9 |
10 | namespace monero {
11 |
12 | // Logging.kt priority levels.
13 | enum LoggingLevel {
14 | VERBOSE = ANDROID_LOG_VERBOSE,
15 | DEBUG = ANDROID_LOG_DEBUG,
16 | INFO = ANDROID_LOG_INFO,
17 | WARN = ANDROID_LOG_WARN,
18 | ERROR = ANDROID_LOG_ERROR,
19 | ASSERT = ANDROID_LOG_FATAL,
20 | };
21 |
22 | // Register easylogging++ post dispatcher and configure log format.
23 | void InitializeEasyLogging();
24 |
25 | // Log sink to send logs to JVM via Logging.kt API.
26 | class JvmLogSink {
27 | public:
28 | JvmLogSink(JvmLogSink&) = delete;
29 | void operator=(const JvmLogSink&) = delete;
30 |
31 | static JvmLogSink* instance() {
32 | static JvmLogSink ins;
33 | return &ins;
34 | }
35 |
36 | // This is called when a log message is dispatched by easylogging++.
37 | void write(const std::string& tag, LoggingLevel priority, const std::string& msg);
38 |
39 | void set_logger(JNIEnv* env, const JavaRef& logger);
40 |
41 | protected:
42 | JvmLogSink() = default;
43 |
44 | private:
45 | ScopedJavaGlobalRef m_logger;
46 | };
47 |
48 | } // namespace monero
49 |
50 | #endif // WALLET_LOGGING_H_
51 |
--------------------------------------------------------------------------------
/demo/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
15 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/data/entity/RemoteNodeEntity.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.data.entity
2 |
3 | import android.net.Uri
4 | import androidx.room.ColumnInfo
5 | import androidx.room.Entity
6 | import androidx.room.Index
7 | import androidx.room.PrimaryKey
8 | import im.molly.monero.demo.data.model.RemoteNode
9 | import im.molly.monero.sdk.MoneroNetwork
10 |
11 | @Entity(
12 | tableName = "remote_nodes",
13 | indices = [
14 | Index("net_type")
15 | ]
16 | )
17 | data class RemoteNodeEntity(
18 | @PrimaryKey(autoGenerate = true)
19 | @ColumnInfo(name = "id")
20 | val id: Long = 0,
21 |
22 | @ColumnInfo(name = "net_type")
23 | val networkId: Int,
24 |
25 | @ColumnInfo(name = "uri")
26 | val uri: String,
27 |
28 | @ColumnInfo(name = "user")
29 | val username: String,
30 |
31 | @ColumnInfo(name = "pwd")
32 | val password: String,
33 | )
34 |
35 | fun RemoteNodeEntity.asExternalModel() = RemoteNode(
36 | id = id,
37 | network = MoneroNetwork.fromId(networkId),
38 | uri = Uri.parse(uri),
39 | username = username,
40 | password = password,
41 | )
42 |
43 | fun RemoteNode.asEntity() = RemoteNodeEntity(
44 | id = id ?: 0,
45 | networkId = network.id,
46 | uri = uri.toString(),
47 | username = username,
48 | password = password,
49 | )
50 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/data/RemoteNodeRepository.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.data
2 |
3 | import im.molly.monero.demo.data.dao.RemoteNodeDao
4 | import im.molly.monero.demo.data.entity.*
5 | import im.molly.monero.demo.data.model.RemoteNode
6 | import kotlinx.coroutines.flow.Flow
7 | import kotlinx.coroutines.flow.map
8 |
9 | class RemoteNodeRepository(
10 | private val remoteNodeDao: RemoteNodeDao,
11 | ) {
12 | fun getRemoteNode(remoteNodeId: Long): Flow =
13 | remoteNodeDao.findById(remoteNodeId).map { it.asExternalModel() }
14 |
15 | fun getRemoteNodes(remoteNodeIds: List): Flow> =
16 | remoteNodeDao.findByIds(remoteNodeIds).map { it.map(RemoteNodeEntity::asExternalModel) }
17 |
18 | fun getAllRemoteNodes(filterNetworkIds: Set = emptySet()): Flow> =
19 | if (filterNetworkIds.isEmpty()) {
20 | remoteNodeDao.findAll()
21 | } else {
22 | remoteNodeDao.findByNetworkIds(filterNetworkIds)
23 | }.map { it.map(RemoteNodeEntity::asExternalModel) }
24 |
25 | suspend fun addOrUpdateRemoteNode(remoteNode: RemoteNode) =
26 | remoteNodeDao.upsert(remoteNode.asEntity())
27 |
28 | suspend fun deleteRemoteNode(remoteNode: RemoteNode) =
29 | remoteNodeDao.delete(remoteNode.asEntity())
30 | }
31 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/RestorePoint.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | import java.time.Instant
4 | import java.time.LocalDate
5 | import java.time.ZoneId
6 |
7 | interface RestorePoint {
8 | fun toLong(): Long
9 |
10 | companion object {
11 | val Genesis: RestorePoint = RestorePointValue(heightOrTimestamp = 0)
12 |
13 | fun blockHeight(height: Int): RestorePoint {
14 | require(isBlockHeightInRange(height))
15 | return RestorePointValue(heightOrTimestamp = height.toLong())
16 | }
17 |
18 | fun creationTime(localDate: LocalDate): RestorePoint = creationTime(
19 | epochSecond = localDate.atStartOfDay(ZoneId.systemDefault()).toEpochSecond()
20 | )
21 |
22 | fun creationTime(instant: Instant): RestorePoint = creationTime(
23 | epochSecond = instant.epochSecond
24 | )
25 |
26 | fun creationTime(epochSecond: Long): RestorePoint {
27 | require(epochSecond >= 1402185600) {
28 | "Monero accounts cannot be restored before June 8, 2014"
29 | }
30 | return RestorePointValue(heightOrTimestamp = epochSecond)
31 | }
32 | }
33 | }
34 |
35 | @JvmInline
36 | value class RestorePointValue(val heightOrTimestamp: Long) : RestorePoint {
37 | override fun toLong() = heightOrTimestamp
38 | }
39 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/TimeLocked.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | data class TimeLocked(val value: T, val unlockTime: UnlockTime? = null) {
4 | fun isLocked(currentTime: BlockchainTime): Boolean {
5 | val unlock = unlockTime ?: return false
6 | requireSameNetworkAs(currentTime)
7 | return unlock > currentTime
8 | }
9 |
10 | fun isUnlocked(currentTime: BlockchainTime) = !isLocked(currentTime)
11 |
12 | fun timeUntilUnlock(currentTime: BlockchainTime): BlockchainTimeSpan {
13 | if (unlockTime == null) return BlockchainTimeSpan.ZERO
14 |
15 | requireSameNetworkAs(currentTime)
16 |
17 | return if (unlockTime > currentTime) {
18 | unlockTime.blockchainTime - currentTime
19 | } else {
20 | BlockchainTimeSpan.ZERO
21 | }
22 | }
23 |
24 | private fun requireSameNetworkAs(other: BlockchainTime) {
25 | val expected = unlockTime?.blockchainTime?.network
26 | require(expected == other.network) {
27 | "BlockchainTime network mismatch: expected $expected, got ${other.network}"
28 | }
29 | }
30 | }
31 |
32 | fun MoneroAmount.lockedUntil(unlockTime: UnlockTime): TimeLocked =
33 | TimeLocked(this, unlockTime)
34 |
35 | fun MoneroAmount.unlocked(): TimeLocked =
36 | TimeLocked(this)
37 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/unbound/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | include(ExternalProject)
2 |
3 | ExternalProject_Add(
4 | Unbound
5 | URL "${VENDOR_DIR}/unbound"
6 | CONFIGURE_COMMAND env
7 | "AR=${NDK_AR}"
8 | "CC=${NDK_CC}"
9 | "AS=${NDK_AS}"
10 | "CXX=${NDK_CXX}"
11 | "LD=${NDK_LD}"
12 | "RANLIB=${NDK_RANLIB}"
13 | "STRIP=${NDK_STRIP}"
14 | "CFLAGS=${NDK_C_FLAGS}"
15 | "LDFLAGS=-L$ \
16 | -L$" # to pass autoconf SSL tests
17 | ./configure
18 | "--prefix="
19 | --host=${TARGET_HOST}
20 | --with-pic
21 | --with-libunbound-only
22 | "--with-ssl=${OPENSSL_SOURCE_DIR}"
23 | --enable-static
24 | --enable-pie
25 | --disable-shared
26 | --disable-gost
27 | BUILD_IN_SOURCE 1
28 | BUILD_COMMAND "${NDK_MAKE}" install "-j${CORES}"
29 | BUILD_BYPRODUCTS
30 | "/lib/libunbound.a"
31 | )
32 |
33 | add_dependencies(Unbound OpenSSL::Crypto OpenSSL::SSL)
34 |
35 | ExternalProject_Get_Property(Unbound INSTALL_DIR)
36 |
37 | set(UNBOUND_INCLUDE_DIR "${INSTALL_DIR}/include" PARENT_SCOPE)
38 |
39 | add_library(unbound STATIC IMPORTED GLOBAL)
40 | set_property(TARGET unbound PROPERTY IMPORTED_LOCATION "${INSTALL_DIR}/lib/libunbound.a")
41 | add_dependencies(unbound Unbound)
42 |
43 | add_library(Unbound::unbound ALIAS unbound)
44 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | includeBuild("build-logic")
3 | repositories {
4 | gradlePluginPortal()
5 | google()
6 | }
7 | }
8 |
9 | dependencyResolutionManagement {
10 | repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
11 | repositories {
12 | google {
13 | content {
14 | includeGroupByRegex("com\\.android(\\..*)?")
15 | includeGroupByRegex("com\\.google(\\..*)?")
16 | includeGroupByRegex("androidx?(\\..*)?")
17 | }
18 | }
19 | mavenCentral()
20 | }
21 | versionCatalogs {
22 | // "libs" is predefined by Gradle
23 | create("testLibs") {
24 | from(files("gradle/test-libs.versions.toml"))
25 | }
26 | }
27 | }
28 |
29 | check(JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17)) {
30 | """
31 | This project requires JDK 17+ but it is currently using JDK ${JavaVersion.current()}.
32 | Java Home: [${System.getProperty("java.home")}]
33 | https://developer.android.com/build/jdks#jdk-config-in-studio
34 | """.trimIndent()
35 | }
36 |
37 | includeProject("lib", "lib/android")
38 | includeProject("demo", "demo/android")
39 |
40 | fun includeProject(projectName: String, projectRoot: String) {
41 | val projectId = ":$projectName"
42 | include(projectId)
43 | project(projectId).projectDir = file(projectRoot)
44 | }
45 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/internal/HttpRequest.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 |
6 | @Parcelize
7 | data class HttpRequest(
8 | val method: String,
9 | val path: String,
10 | val header: String?,
11 | val bodyBytes: ByteArray?,
12 | ) : Parcelable {
13 |
14 | override fun toString(): String =
15 | "HttpRequest(method=$method, path=$path, headers=${header?.length}, body=${bodyBytes?.size})"
16 |
17 | override fun equals(other: Any?): Boolean {
18 | if (this === other) return true
19 | if (javaClass != other?.javaClass) return false
20 |
21 | other as HttpRequest
22 |
23 | if (method != other.method) return false
24 | if (path != other.path) return false
25 | if (header != other.header) return false
26 | if (bodyBytes != null) {
27 | if (other.bodyBytes == null) return false
28 | if (!bodyBytes.contentEquals(other.bodyBytes)) return false
29 | } else if (other.bodyBytes != null) return false
30 |
31 | return true
32 | }
33 |
34 | override fun hashCode(): Int {
35 | var result = method.hashCode()
36 | result = 31 * result + path.hashCode()
37 | result = 31 * result + (header?.hashCode() ?: 0)
38 | result = 31 * result + (bodyBytes?.contentHashCode() ?: 0)
39 | return result
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/Logging.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | import android.util.Log
4 | import im.molly.monero.sdk.internal.Logger
5 |
6 | /**
7 | * Adapter to output logs to the host application.
8 | *
9 | * Priority values matches Android framework [Log] priority levels.
10 | */
11 | interface LogAdapter {
12 | fun isLoggable(priority: Int, tag: String): Boolean = true
13 | fun print(priority: Int, tag: String, msg: String?, tr: Throwable?)
14 | }
15 |
16 | /**
17 | * Debug adapter outputs logs to system logger only in debug builds.
18 | */
19 | class DebugLogAdapter : LogAdapter {
20 | override fun isLoggable(priority: Int, tag: String): Boolean {
21 | return BuildConfig.DEBUG || (priority == Log.ASSERT)
22 | }
23 |
24 | override fun print(priority: Int, tag: String, msg: String?, tr: Throwable?) {
25 | when (priority) {
26 | Log.VERBOSE -> Log.v(tag, msg, tr)
27 | Log.DEBUG -> Log.d(tag, msg, tr)
28 | Log.INFO -> Log.i(tag, msg, tr)
29 | Log.WARN -> Log.w(tag, msg, tr)
30 | Log.ERROR -> Log.e(tag, msg, tr)
31 | Log.ASSERT -> Log.wtf(tag, msg, tr)
32 | }
33 | }
34 | }
35 |
36 | /**
37 | * Specifies the log adapter to use across the library.
38 | *
39 | * By default, the log adapter is [DebugLogAdapter].
40 | */
41 | fun setLoggingAdapter(logAdapter: LogAdapter) {
42 | Logger.adapter = logAdapter
43 | }
44 |
--------------------------------------------------------------------------------
/lib/android/src/androidTest/kotlin/im/molly/monero/sdk/LedgerSubject.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | import com.google.common.truth.FailureMetadata
4 | import com.google.common.truth.Subject
5 | import com.google.common.truth.Truth.assertAbout
6 | import java.math.BigDecimal
7 |
8 | class LedgerSubject private constructor(
9 | metadata: FailureMetadata,
10 | private val actual: Ledger,
11 | ) : Subject(metadata, actual) {
12 |
13 | companion object {
14 | fun assertThat(ledgerChain: Ledger): LedgerSubject {
15 | return assertAbout(factory).that(ledgerChain)
16 | }
17 |
18 | private val factory = Factory { metadata, actual: Ledger ->
19 | LedgerSubject(metadata, actual)
20 | }
21 | }
22 |
23 | fun isConsistent() {
24 | balanceIsNonNegative()
25 | }
26 |
27 | fun balanceIsNonNegative() {
28 | actual.indexedAccounts.forEach { account ->
29 | val accountIndex = account.accountIndex
30 | val balance = actual.getBalanceForAccount(accountIndex)
31 |
32 | val pending = balance.pendingAmount.xmr
33 | val confirmed = balance.confirmedAmount.xmr
34 |
35 | check("indexedAccounts[$accountIndex].pendingAmount.xmr").that(pending)
36 | .isAtLeast(BigDecimal.ZERO)
37 | check("indexedAccounts[$accountIndex].confirmedAmount.xmr").that(confirmed)
38 | .isAtLeast(BigDecimal.ZERO)
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/ui/PendingTransferView.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.ui
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.unit.dp
10 | import im.molly.monero.sdk.PendingTransfer
11 | import im.molly.monero.sdk.toFormattedString
12 |
13 | @Composable
14 | fun PendingTransferView(
15 | spendingAccountIndex: Int,
16 | pendingTransfer: PendingTransfer,
17 | modifier: Modifier = Modifier,
18 | ) {
19 | Column(modifier = modifier) {
20 | Text(
21 | text = "Confirm transfer",
22 | style = MaterialTheme.typography.titleLarge,
23 | modifier = Modifier.padding(bottom = 16.dp)
24 | )
25 | with(pendingTransfer) {
26 | TextRow("Sending account", "#$spendingAccountIndex")
27 | TextRow("Amount", amount.toFormattedString())
28 | TextRow("Fee", fee.toFormattedString())
29 | TextRow("Transactions", txCount.toString())
30 | }
31 | }
32 | }
33 |
34 | @Composable
35 | fun TextRow(label: String, text: String, modifier: Modifier = Modifier) {
36 | Text(
37 | text = "$label: $text",
38 | style = MaterialTheme.typography.bodyMedium,
39 | modifier = modifier.padding(bottom = 8.dp),
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/PendingTransfer.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | import im.molly.monero.sdk.internal.IPendingTransfer
4 | import im.molly.monero.sdk.internal.ITransferCallback
5 | import kotlinx.coroutines.ExperimentalCoroutinesApi
6 | import kotlinx.coroutines.suspendCancellableCoroutine
7 | import kotlin.coroutines.resumeWithException
8 |
9 | @OptIn(ExperimentalCoroutinesApi::class)
10 | class PendingTransfer internal constructor(
11 | private val pendingTransfer: IPendingTransfer,
12 | ) : AutoCloseable {
13 |
14 | val fee: MoneroAmount
15 | get() = pendingTransfer.fee.toAtomicUnits()
16 |
17 | val amount: MoneroAmount
18 | get() = pendingTransfer.amount.toAtomicUnits()
19 |
20 | val txCount: Int
21 | get() = pendingTransfer.txCount
22 |
23 | suspend fun commit(): Boolean = suspendCancellableCoroutine { continuation ->
24 | val callback = object : ITransferCallback.Stub() {
25 | override fun onTransferCreated(pendingTransfer: IPendingTransfer) = Unit
26 |
27 | override fun onTransferCommitted() {
28 | continuation.resume(true) {}
29 | }
30 |
31 | override fun onUnexpectedError(message: String) {
32 | continuation.resumeWithException(
33 | IllegalStateException(message)
34 | )
35 | }
36 | }
37 | pendingTransfer.commitAndClose(callback)
38 | }
39 |
40 | override fun close() = pendingTransfer.close()
41 | }
42 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/monero/electrum_words/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | set(ELECTRUM_WORDS_SOURCES
2 | contrib/epee/src/memwipe.c
3 | contrib/epee/src/mlocker.cpp
4 | contrib/epee/src/wipeable_string.cpp
5 | src/mnemonics/electrum-words.cpp
6 | )
7 |
8 | set(ELECTRUM_WORDS_OVERRIDES
9 | mlog_override.cc
10 | )
11 |
12 | set(ELECTRUM_WORDS_INCLUDES
13 | contrib/epee/include
14 | src
15 | src/mnemonics
16 | )
17 |
18 | list(TRANSFORM ELECTRUM_WORDS_SOURCES PREPEND "${MONERO_DIR}/")
19 | list(TRANSFORM ELECTRUM_WORDS_INCLUDES PREPEND "${MONERO_DIR}/")
20 |
21 | set(EASYLOGGING_SOURCE_DIR "${MONERO_DIR}/external/easylogging++")
22 |
23 | add_library(
24 | electrum_words STATIC ${ELECTRUM_WORDS_SOURCES} ${ELECTRUM_WORDS_OVERRIDES}
25 | )
26 |
27 | # Disable Easylogging++ logging
28 | add_definitions(-DELPP_DISABLE_LOGS)
29 |
30 | target_include_directories(
31 | electrum_words
32 | PUBLIC
33 | "${ELECTRUM_WORDS_INCLUDES}"
34 | )
35 |
36 | # Include external project header directories here. Workaround for:
37 | # https://gitlab.kitware.com/cmake/cmake/issues/15052
38 | target_include_directories(
39 | electrum_words
40 | SYSTEM PUBLIC
41 | "${BOOST_INCLUDE_DIR}"
42 | "${LIBSODIUM_INCLUDE_DIR}"
43 | )
44 |
45 | target_link_libraries(
46 | electrum_words
47 | PUBLIC
48 | PRIVATE
49 | Monero::easylogging
50 | Libsodium::libsodium
51 | )
52 |
53 | # Mnemonics depends on boost::crc that is a header-only library
54 | add_dependencies(electrum_words Boost)
55 |
56 | add_library(Monero::electrum_words ALIAS electrum_words)
57 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/ui/preview/PreviewParameterData.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.ui.preview
2 |
3 | import im.molly.monero.sdk.*
4 | import java.time.Instant
5 |
6 | object PreviewParameterData {
7 | val network = Mainnet
8 |
9 | val blockHeader = BlockHeader(height = 2999840, epochSecond = 1697792826)
10 |
11 | val recipients =
12 | listOf(PublicAddress.parse("888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H"))
13 |
14 | val transactions = listOf(
15 | Transaction(
16 | hash = HashDigest("e7a60483591378d536792d070f2bf6ccb7d0666df03b57f485ddaf66899a294b"),
17 | state = TxState.OnChain(blockHeader),
18 | network = network,
19 | timeLock = UnlockTime.Block(
20 | BlockchainTime(2999850, Instant.ofEpochSecond(1697792826), network)
21 | ),
22 | sent = emptySet(),
23 | received = emptySet(),
24 | payments = listOf(PaymentDetail((0.10).xmr, recipients.first())),
25 | fee = 0.00093088.xmr,
26 | change = MoneroAmount.ZERO,
27 | ),
28 | )
29 |
30 | val ledger = Ledger(
31 | publicAddress = PublicAddress.parse("4AYjQM9HoAFNUeC3cvSfgeAN89oMMpMqiByvunzSzhn97cj726rJj3x8hCbH58UnMqQJShczCxbpWRiCJQ3HCUDHLiKuo4T"),
32 | indexedAccounts = emptyList(),
33 | transactionById = transactions.associateBy { it.txId },
34 | enoteSet = emptySet(),
35 | checkedAt = BlockchainTime(blockHeader = blockHeader, network = network),
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/lib/android/src/main/aidl/im/molly/monero/sdk/internal/IWallet.aidl:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal;
2 |
3 | import im.molly.monero.sdk.PaymentRequest;
4 | import im.molly.monero.sdk.SecretKey;
5 | import im.molly.monero.sdk.SweepRequest;
6 | import im.molly.monero.sdk.internal.IBalanceListener;
7 | import im.molly.monero.sdk.internal.ITransferCallback;
8 | import im.molly.monero.sdk.internal.IWalletCallbacks;
9 |
10 | interface IWallet {
11 | String getPublicAddress();
12 | SecretKey getSpendSecretKey();
13 | SecretKey getViewSecretKey();
14 | void addBalanceListener(in IBalanceListener listener);
15 | void removeBalanceListener(in IBalanceListener listener);
16 | oneway void addDetachedSubAddress(int accountIndex, int subAddressIndex, in IWalletCallbacks callback);
17 | oneway void createAccount(in IWalletCallbacks callback);
18 | oneway void createSubAddressForAccount(int accountIndex, in IWalletCallbacks callback);
19 | oneway void getAddressesForAccount(int accountIndex, in IWalletCallbacks callback);
20 | oneway void getAllAddresses(in IWalletCallbacks callback);
21 | oneway void resumeRefresh(boolean skipCoinbase, in IWalletCallbacks callback);
22 | oneway void cancelRefresh();
23 | oneway void setRefreshSince(long heightOrTimestamp);
24 | oneway void commit(in ParcelFileDescriptor outputFd, in IWalletCallbacks callback);
25 | oneway void createPayment(in PaymentRequest request, in ITransferCallback callback);
26 | oneway void createSweep(in SweepRequest request, in ITransferCallback callback);
27 | oneway void requestFees(in IWalletCallbacks callback);
28 | void close();
29 | }
30 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/Balance.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | data class Balance(
4 | val lockableAmounts: List>,
5 | val pendingAmount: MoneroAmount = MoneroAmount.ZERO,
6 | ) {
7 | val confirmedAmount: MoneroAmount = lockableAmounts.sumOf { it.value }
8 | val totalAmount: MoneroAmount = confirmedAmount + pendingAmount
9 |
10 | companion object {
11 | val EMPTY = Balance(emptyList())
12 | }
13 |
14 | fun unlockedAmountAt(targetTime: BlockchainTime): MoneroAmount {
15 | return lockableAmounts
16 | .filter { it.isUnlocked(targetTime) }
17 | .sumOf { it.value }
18 | }
19 |
20 | fun lockedAmountsAt(targetTime: BlockchainTime): Map {
21 | return lockableAmounts
22 | .filter { it.isLocked(targetTime) }
23 | .groupBy({ it.timeUntilUnlock(targetTime) }, { it.value })
24 | .mapValues { it.value.sum() }
25 | }
26 | }
27 |
28 | fun Iterable>.calculateBalance(
29 | accountFilter: (owner: AccountAddress) -> Boolean = { true },
30 | ): Balance {
31 | val lockableAmounts = mutableListOf>()
32 |
33 | var pendingAmount = MoneroAmount.ZERO
34 |
35 | for (timeLocked in filter { !it.value.spent && accountFilter(it.value.owner) }) {
36 | if (timeLocked.value.age == 0) {
37 | pendingAmount += timeLocked.value.amount
38 | } else {
39 | lockableAmounts.add(TimeLocked(timeLocked.value.amount, timeLocked.unlockTime))
40 | }
41 | }
42 |
43 | return Balance(lockableAmounts, pendingAmount)
44 | }
45 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/loadbalancer/LoadBalancer.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.loadbalancer
2 |
3 | import im.molly.monero.sdk.RemoteNode
4 | import kotlinx.coroutines.CoroutineScope
5 | import kotlinx.coroutines.flow.Flow
6 | import kotlinx.coroutines.launch
7 | import kotlin.time.Duration
8 |
9 | class LoadBalancer(
10 | remoteNodes: Flow>,
11 | scope: CoroutineScope,
12 | ) {
13 | var onlineNodes: List = emptyList()
14 |
15 | init {
16 | scope.launch {
17 | remoteNodes.collect {
18 | updateNodes(it)
19 | }
20 | }
21 | }
22 |
23 | private fun updateNodes(nodeList: List) {
24 | onlineNodes = nodeList
25 | }
26 |
27 | fun onResponseTimeObservation(remoteNode: RemoteNode, responseTime: Duration) {
28 | // TODO
29 | }
30 | }
31 |
32 | sealed interface ConnectionState {
33 | /**
34 | * The remote node is currently online and able to handle requests.
35 | */
36 | data class Online(val responseTime: Duration) : ConnectionState
37 |
38 | /**
39 | * The client's request has timed out and no response has been received.
40 | */
41 | data class Timeout(val cause: Throwable?) : ConnectionState
42 |
43 | /**
44 | * Indicates that an error occurred while processing the client's request to the remote node.
45 | */
46 | sealed class Error(val message: String?) : ConnectionState
47 |
48 | /**
49 | * Indicates that the client is unauthorized to access the remote node, i.e. the client's credentials were invalid.
50 | */
51 | data object Unauthorized : Error("Unauthorized")
52 | }
53 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/data/WalletDataSource.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.data
2 |
3 | import im.molly.monero.demo.data.dao.WalletDao
4 | import im.molly.monero.demo.data.entity.WalletEntity
5 | import im.molly.monero.demo.data.entity.WalletRemoteNodeXRef
6 | import im.molly.monero.demo.data.entity.asEntity
7 | import im.molly.monero.demo.data.entity.asExternalModel
8 | import im.molly.monero.demo.data.model.WalletConfig
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.flow.map
11 |
12 | class WalletDataSource(
13 | private val walletDao: WalletDao,
14 | ) {
15 | fun readWalletIdList(): Flow> = walletDao.findAllIds()
16 |
17 | fun readWalletConfig(walletId: Long): Flow =
18 | walletDao.findById(walletId).map { it.asExternalModel() }
19 |
20 | suspend fun createWalletConfig(
21 | publicAddress: String,
22 | filename: String,
23 | name: String,
24 | remoteNodeIds: List,
25 | ): Long {
26 | val walletId = walletDao.insert(
27 | WalletEntity(
28 | publicAddress = publicAddress,
29 | filename = filename,
30 | name = name,
31 | )
32 | )
33 | val walletRemoteNodeXRef = remoteNodeIds.map { remoteNodeId ->
34 | WalletRemoteNodeXRef(
35 | walletId = walletId,
36 | remoteNodeId = remoteNodeId
37 | )
38 | }
39 | walletDao.insertRemoteNodeXRefEntities(walletRemoteNodeXRef)
40 | return walletId
41 | }
42 |
43 | suspend fun updateWalletConfig(walletConfig: WalletConfig) {
44 | walletDao.update(walletConfig.asEntity())
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/wallet/fd.h:
--------------------------------------------------------------------------------
1 | #ifndef WALLET_FD_H_
2 | #define WALLET_FD_H_
3 |
4 | #include
5 |
6 | #include
7 |
8 | #include
9 | #include
10 |
11 | #include "common/jvm.h"
12 |
13 | #include "jni_cache.h"
14 |
15 | namespace monero {
16 |
17 | // Utility class to hold a file descriptor and call 'close' automatically
18 | // on scope exit.
19 | class ScopedFd {
20 | public:
21 | ScopedFd() : m_fd(-1) {}
22 |
23 | explicit ScopedFd(int fd) : m_fd(fd) {}
24 |
25 | ScopedFd(ScopedFd&& other) : m_fd(other.m_fd) {
26 | other.m_fd = -1;
27 | }
28 |
29 | ScopedFd(JNIEnv* env, const JavaRef& parcel_file_descriptor) {
30 | if (!parcel_file_descriptor.is_null()) {
31 | m_fd = CallIntMethod(env, parcel_file_descriptor.obj(),
32 | ParcelFd_detachFd);
33 | } else {
34 | m_fd = -1;
35 | }
36 | }
37 |
38 | ~ScopedFd() {
39 | close();
40 | }
41 |
42 | int fd() const { return m_fd; }
43 |
44 | bool is_valid() const { return m_fd >= 0; }
45 |
46 | void close() {
47 | if (is_valid()) {
48 | int save_errno = errno;
49 | ::close(m_fd);
50 | m_fd = -1;
51 | errno = save_errno;
52 | }
53 | }
54 |
55 | void read(std::string* buf) const {
56 | using namespace boost::iostreams;
57 | stream stream(m_fd, never_close_handle);
58 | std::ostringstream ss;
59 | ss << stream.rdbuf();
60 | *buf = ss.str();
61 | }
62 |
63 | private:
64 | int m_fd;
65 |
66 | private:
67 | ScopedFd(const ScopedFd&) = delete;
68 | ScopedFd& operator=(const ScopedFd&) = delete;
69 | };
70 |
71 | } // namespace monero
72 |
73 | #endif // WALLET_FD_H_
74 |
--------------------------------------------------------------------------------
/lib/android/src/androidTest/kotlin/im/molly/monero/sdk/e2etest/WalletServiceRule.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.e2etest
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import androidx.test.platform.app.InstrumentationRegistry
6 | import androidx.test.rule.ServiceTestRule
7 | import im.molly.monero.sdk.WalletProvider
8 | import im.molly.monero.sdk.internal.IWalletService
9 | import im.molly.monero.sdk.internal.WalletServiceClient
10 | import im.molly.monero.sdk.service.BaseWalletService
11 | import org.junit.rules.TestRule
12 | import org.junit.runner.Description
13 | import org.junit.runners.model.Statement
14 |
15 | class WalletServiceRule(private val serviceClass: Class) : TestRule {
16 |
17 | val walletProvider: WalletProvider
18 | get() = _walletProvider ?: error("WalletService not bound yet")
19 |
20 | private var _walletProvider: WalletProvider? = null
21 |
22 | private val context: Context by lazy { InstrumentationRegistry.getInstrumentation().context }
23 |
24 | private val delegate = ServiceTestRule()
25 |
26 | override fun apply(base: Statement, description: Description): Statement {
27 | return delegate.apply(object : Statement() {
28 | override fun evaluate() {
29 | val binder = delegate.bindService(Intent(context, serviceClass))
30 | val walletService = IWalletService.Stub.asInterface(binder)
31 | _walletProvider = WalletServiceClient.withBoundService(context, walletService)
32 |
33 | try {
34 | walletProvider.use { base.evaluate() }
35 | } finally {
36 | delegate.unbindService()
37 | }
38 | }
39 | }, description)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/ui/navigation/SettingsNavigation.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.ui.navigation
2 |
3 | import androidx.navigation.*
4 | import androidx.navigation.compose.composable
5 | import androidx.navigation.compose.dialog
6 | import im.molly.monero.demo.ui.EditRemoteNodeRoute
7 | import im.molly.monero.demo.ui.SettingsRoute
8 |
9 | const val settingsNavRoute = "settings"
10 | const val settingsRemoteNodeNavRoute = "$settingsNavRoute/remote_node"
11 |
12 | private const val queryId = "id"
13 |
14 | fun NavController.navigateToSettings(navOptions: NavOptions? = null) {
15 | navigate(settingsNavRoute, navOptions)
16 | }
17 |
18 | fun NavController.navigateToEditRemoteNode(remoteNodeId: Long?) {
19 | val route = settingsRemoteNodeNavRoute +
20 | if (remoteNodeId != null) "?$queryId=$remoteNodeId" else ""
21 | navigate(route)
22 | }
23 |
24 | fun NavGraphBuilder.settingsScreen(
25 | navigateToEditRemoteNode: (Long?) -> Unit,
26 | ) {
27 | composable(route = settingsNavRoute) {
28 | SettingsRoute(
29 | navigateToEditRemoteNode = navigateToEditRemoteNode,
30 | )
31 | }
32 | }
33 |
34 | fun NavGraphBuilder.editRemoteNodeDialog(
35 | onBackClick: () -> Unit,
36 | ) {
37 | dialog(
38 | route = "$settingsRemoteNodeNavRoute?$queryId={$queryId}",
39 | arguments = listOf(
40 | navArgument(queryId) {
41 | type = NavType.StringType
42 | nullable = true
43 | }
44 | )
45 | ) {
46 | val arguments = requireNotNull(it.arguments)
47 | val remoteNodeId = arguments.getString(queryId)?.toLongOrNull()
48 | EditRemoteNodeRoute(
49 | remoteNodeId = remoteNodeId,
50 | onBackClick = onBackClick,
51 | )
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/demo/android/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/ui/navigation/NavGraph.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.ui.navigation
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.Modifier
5 | import androidx.navigation.NavHostController
6 | import androidx.navigation.compose.NavHost
7 |
8 | @Composable
9 | fun NavGraph(
10 | navController: NavHostController,
11 | onBackClick: () -> Unit,
12 | modifier: Modifier = Modifier,
13 | startDestination: String = homeNavRoute,
14 | ) {
15 | NavHost(
16 | navController,
17 | startDestination,
18 | modifier,
19 | ) {
20 | homeScreen(
21 | navigateToWallet = { walletId ->
22 | navController.navigateToWallet(walletId)
23 | },
24 | navigateToAddWalletWizard = {
25 | navController.navigateToAddWalletWizardGraph()
26 | },
27 | )
28 | historyScreen(
29 | navigateToTransaction = { txId, walletId ->
30 | navController.navigateToTransaction(txId, walletId)
31 | },
32 | )
33 | settingsScreen(
34 | navigateToEditRemoteNode = { remoteNodeId ->
35 | navController.navigateToEditRemoteNode(remoteNodeId)
36 | },
37 | )
38 | walletScreen(
39 | navigateToTransaction = { txId, walletId ->
40 | navController.navigateToTransaction(txId, walletId)
41 | },
42 | onBackClick = onBackClick,
43 | )
44 | transactionScreen(
45 | onBackClick = onBackClick,
46 | )
47 | editRemoteNodeDialog(
48 | onBackClick = onBackClick,
49 | )
50 | addWalletWizardGraph(
51 | navController = navController,
52 | onBackClick = onBackClick,
53 | )
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/ui/component/RadioButtons.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.ui.component
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.foundation.selection.selectable
5 | import androidx.compose.foundation.selection.selectableGroup
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.RadioButton
8 | import androidx.compose.material3.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Alignment
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.semantics.Role
13 | import androidx.compose.ui.unit.dp
14 |
15 | @Composable
16 | fun RadioButtons(
17 | radioOptions: List,
18 | selectedOption: String,
19 | onOptionSelected: (String) -> Unit,
20 | modifier: Modifier = Modifier,
21 | ) {
22 | Column(modifier.selectableGroup()) {
23 | radioOptions.forEach { text ->
24 | Row(
25 | Modifier
26 | .fillMaxWidth()
27 | .height(56.dp)
28 | .selectable(
29 | selected = (text == selectedOption),
30 | onClick = { onOptionSelected(text) },
31 | role = Role.RadioButton,
32 | )
33 | .padding(horizontal = 16.dp),
34 | verticalAlignment = Alignment.CenterVertically,
35 | ) {
36 | RadioButton(
37 | selected = (text == selectedOption),
38 | onClick = null,
39 | )
40 | Text(
41 | text = text,
42 | style = MaterialTheme.typography.bodyLarge,
43 | modifier = Modifier.padding(start = 16.dp),
44 | )
45 | }
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/AccountAddress.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | import android.annotation.SuppressLint
4 |
5 | @SuppressLint("ParcelCreator")
6 | data class AccountAddress(
7 | val publicAddress: PublicAddress,
8 | val accountIndex: Int = 0,
9 | val subAddressIndex: Int = 0,
10 | ) : PublicAddress by publicAddress {
11 |
12 | val isPrimaryAddress: Boolean
13 | get() = subAddressIndex == 0
14 |
15 | init {
16 | when (publicAddress) {
17 | is StandardAddress -> require(accountIndex == 0 && subAddressIndex == 0) {
18 | "Only the account address 0/0 is a standard address"
19 | }
20 |
21 | is SubAddress -> require(accountIndex != -1 && subAddressIndex != -1) {
22 | "Invalid subaddress indices"
23 | }
24 |
25 | else -> throw IllegalArgumentException("Unsupported address type")
26 | }
27 | }
28 |
29 | fun isAddressUsed(transactions: Iterable): Boolean {
30 | return transactions.any { tx ->
31 | tx.sent.any { enote ->
32 | enote.owner == this
33 | } || tx.received.any { enote ->
34 | enote.owner == this
35 | }
36 | }
37 | }
38 |
39 | companion object {
40 | fun parseWithIndexes(addressString: String): AccountAddress {
41 | val parts = addressString.split("/")
42 | require(parts.size == 3) { "Invalid account address format" }
43 | val accountIndex = parts[0].toInt()
44 | val subAddressIndex = parts[1].toInt()
45 | val publicAddress = PublicAddress.parse(parts[2])
46 | return AccountAddress(publicAddress, accountIndex, subAddressIndex)
47 | }
48 | }
49 |
50 | override fun toString(): String = "$accountIndex/$subAddressIndex/$publicAddress"
51 | }
52 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/cmake/toolchain.cmake:
--------------------------------------------------------------------------------
1 | # Definitions for Android NDK r23 single clang-toolchain
2 |
3 | set(TOOLCHAIN "${CMAKE_ANDROID_NDK_TOOLCHAIN_UNIFIED}")
4 | set(PREBUILTS "${CMAKE_ANDROID_NDK}/prebuilt/${CMAKE_ANDROID_NDK_TOOLCHAIN_HOST_TAG}")
5 | set(SYSROOT "${CMAKE_SYSROOT}")
6 |
7 | set(TOOLCHAIN_BIN "${TOOLCHAIN}/bin")
8 | set(PREBUILTS_BIN "${PREBUILTS}/bin")
9 |
10 | set(TARGET_HOST ${CMAKE_ANDROID_ARCH_TRIPLE})
11 | set(TARGET_API ${CMAKE_SYSTEM_VERSION})
12 |
13 | set(CLANG_CC ${TARGET_HOST}${TARGET_API}-clang)
14 | set(CLANG_AS ${TARGET_HOST}${TARGET_API}-clang)
15 | set(CLANG_CXX ${TARGET_HOST}${TARGET_API}-clang++)
16 |
17 | # https://developer.android.com/ndk/guides/other_build_systems#overview
18 | if(ANDROID_ABI STREQUAL "armeabi-v7a")
19 | string(REPLACE "arm" "armv7a" CLANG_CC "${CLANG_CC}")
20 | string(REPLACE "arm" "armv7a" CLANG_AS "${CLANG_AS}")
21 | string(REPLACE "arm" "armv7a" CLANG_CXX "${CLANG_CXX}")
22 | endif()
23 |
24 | find_program(NDK_AR llvm-ar PATHS "${TOOLCHAIN_BIN}" NO_DEFAULT_PATH REQUIRED)
25 | find_program(NDK_CC ${CLANG_CC} PATHS "${TOOLCHAIN_BIN}" NO_DEFAULT_PATH REQUIRED)
26 | find_program(NDK_AS ${CLANG_AS} PATHS "${TOOLCHAIN_BIN}" NO_DEFAULT_PATH REQUIRED)
27 | find_program(NDK_CXX ${CLANG_CXX} PATHS "${TOOLCHAIN_BIN}" NO_DEFAULT_PATH REQUIRED)
28 | find_program(NDK_LD ld PATHS "${TOOLCHAIN_BIN}" NO_DEFAULT_PATH REQUIRED)
29 | find_program(NDK_RANLIB llvm-ranlib PATHS "${TOOLCHAIN_BIN}" NO_DEFAULT_PATH REQUIRED)
30 | find_program(NDK_STRIP llvm-strip PATHS "${TOOLCHAIN_BIN}" NO_DEFAULT_PATH REQUIRED)
31 | find_program(NDK_MAKE make PATHS "${PREBUILTS_BIN}" NO_DEFAULT_PATH REQUIRED)
32 |
33 | # Common compiler flags for current build config (debug or release)
34 | string(TOUPPER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_UPPER)
35 | set(NDK_C_FLAGS "${CMAKE_C_FLAGS} ${CMAKE_C_FLAGS_${CMAKE_BUILD_TYPE_UPPER}}")
36 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/RetryBackoff.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | import kotlin.math.pow
4 | import kotlin.random.Random
5 | import kotlin.time.Duration
6 | import kotlin.time.Duration.Companion.seconds
7 | import kotlin.time.DurationUnit
8 | import kotlin.time.toDuration
9 |
10 | interface BackoffPolicy {
11 | /**
12 | * Returns the amount of time to wait before performing a retry or a reconnect, based on the current [retryCount].
13 | */
14 | fun waitTime(retryCount: Int): Duration
15 | }
16 |
17 | /**
18 | * A [BackoffPolicy] based on exponential backoff and jitter.
19 | *
20 | * @param minBackoff Set the minimum [Duration] for the first backoff.
21 | * @param maxBackoff Set a hard maximum [Duration] for exponential backoff.
22 | */
23 | class ExponentialBackoff(
24 | private val minBackoff: Duration,
25 | private val maxBackoff: Duration,
26 | private val multiplier: Double,
27 | private val jitter: Double,
28 | ) : BackoffPolicy {
29 | init {
30 | require(minBackoff.isPositive())
31 | require(maxBackoff.isPositive())
32 | require(multiplier > 1.0)
33 | require(jitter < 1.0)
34 | }
35 |
36 | override fun waitTime(retryCount: Int): Duration =
37 | if (retryCount > 0) {
38 | addJitter((minBackoff * multiplier.pow(retryCount)).coerceAtMost(maxBackoff))
39 | } else {
40 | Duration.ZERO
41 | }
42 |
43 | private fun addJitter(waitTime: Duration): Duration {
44 | val jitterAmount = waitTime.inWholeMilliseconds * jitter
45 | val jitter = Random.nextDouble(-jitterAmount, jitterAmount)
46 | return waitTime + jitter.toDuration(DurationUnit.MILLISECONDS)
47 | }
48 |
49 | companion object {
50 | val Default = ExponentialBackoff(
51 | minBackoff = 1.seconds,
52 | maxBackoff = 20.seconds,
53 | multiplier = 1.6,
54 | jitter = 0.2,
55 | )
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/ui/DemoAppState.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.ui
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.Stable
5 | import androidx.compose.runtime.remember
6 | import androidx.navigation.NavDestination
7 | import androidx.navigation.NavGraph.Companion.findStartDestination
8 | import androidx.navigation.NavHostController
9 | import androidx.navigation.compose.currentBackStackEntryAsState
10 | import androidx.navigation.compose.rememberNavController
11 | import androidx.navigation.navOptions
12 | import im.molly.monero.demo.ui.navigation.*
13 |
14 | @Composable
15 | fun rememberDemoAppState(
16 | navController: NavHostController = rememberNavController(),
17 | ): DemoAppState {
18 | return remember(navController) {
19 | DemoAppState(navController)
20 | }
21 | }
22 |
23 | @Stable
24 | class DemoAppState(
25 | val navController: NavHostController,
26 | ) {
27 | val currentDestination: NavDestination?
28 | @Composable get() = navController
29 | .currentBackStackEntryAsState().value?.destination
30 |
31 | val topLevelDestinations = TopLevelDestination.values().asList()
32 |
33 | fun navigateToTopLevelDestination(topLevelDestination: TopLevelDestination) {
34 | val topLevelNavOptions = navOptions {
35 | popUpTo(navController.graph.findStartDestination().id) {
36 | saveState = true
37 | }
38 | launchSingleTop = true
39 | restoreState = true
40 | }
41 |
42 | when (topLevelDestination) {
43 | TopLevelDestination.HOME -> navController.navigateToHome(topLevelNavOptions)
44 | TopLevelDestination.HISTORY -> navController.navigateToHistory(topLevelNavOptions)
45 | TopLevelDestination.SETTINGS -> navController.navigateToSettings(topLevelNavOptions)
46 | }
47 | }
48 |
49 | fun onBackClick() {
50 | navController.popBackStack()
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.ui.theme
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.isSystemInDarkTheme
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.darkColorScheme
7 | import androidx.compose.material3.dynamicDarkColorScheme
8 | import androidx.compose.material3.dynamicLightColorScheme
9 | import androidx.compose.material3.lightColorScheme
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.platform.LocalContext
12 |
13 | private val DarkColorScheme = darkColorScheme(
14 | primary = Purple80,
15 | secondary = PurpleGrey80,
16 | tertiary = Pink80
17 | )
18 |
19 | private val LightColorScheme = lightColorScheme(
20 | primary = Purple40,
21 | secondary = PurpleGrey40,
22 | tertiary = Pink40,
23 |
24 | /* Other default colors to override
25 | surface = Color(0xFFFFFBFE),
26 | onPrimary = Color.White,
27 | onSecondary = Color.White,
28 | onTertiary = Color.White,
29 | onBackground = Color(0xFF1C1B1F),
30 | onSurface = Color(0xFF1C1B1F),
31 | */
32 | )
33 |
34 | @Composable
35 | fun AppTheme(
36 | darkTheme: Boolean = isSystemInDarkTheme(),
37 | // Dynamic color is available on Android 12+
38 | dynamicColor: Boolean = true,
39 | content: @Composable () -> Unit
40 | ) {
41 | val colorScheme = pickColorScheme(dynamicColor, darkTheme)
42 |
43 | MaterialTheme(
44 | colorScheme = colorScheme,
45 | typography = Typography,
46 | content = content
47 | )
48 | }
49 |
50 | @Composable
51 | private fun pickColorScheme(
52 | dynamicColor: Boolean,
53 | darkTheme: Boolean
54 | ) = when {
55 | dynamicColor && Build.VERSION.SDK_INT >= 31 -> {
56 | val context = LocalContext.current
57 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
58 | }
59 | darkTheme -> DarkColorScheme
60 | else -> LightColorScheme
61 | }
62 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/Enote.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | class Enote(
4 | val amount: MoneroAmount,
5 | val owner: AccountAddress,
6 | val key: PublicKey?,
7 | val keyImage: HashDigest?,
8 | val age: Int,
9 | val origin: EnoteOrigin,
10 | ) {
11 | init {
12 | require(amount > 0) { "Amount must be greater than 0" }
13 | require(age >= 0) { "Age cannot be negative" }
14 | }
15 |
16 | var spent: Boolean = false
17 | private set
18 |
19 | fun markAsSpent(): Enote {
20 | spent = true
21 | return this
22 | }
23 |
24 | val sourceTxId: String?
25 | get() = (origin as? EnoteOrigin.TxOut)?.txId
26 |
27 | override fun toString(): String {
28 | return "Enote(" +
29 | "amount=${amount.xmr}" +
30 | ", age=$age" +
31 | ", spent=$spent" +
32 | ", owner=$owner" +
33 | ", key=$key" +
34 | ", keyImage=$keyImage" +
35 | ", origin=$origin" +
36 | ")"
37 | }
38 |
39 | override fun equals(other: Any?): Boolean {
40 | if (this === other) return true
41 | if (other !is Enote) return false
42 |
43 | return amount == other.amount &&
44 | owner == other.owner &&
45 | key == other.key &&
46 | keyImage == other.keyImage &&
47 | age == other.age &&
48 | origin == other.origin
49 | }
50 |
51 | override fun hashCode(): Int {
52 | var result = age
53 | result = 31 * result + amount.hashCode()
54 | result = 31 * result + owner.hashCode()
55 | result = 31 * result + (key?.hashCode() ?: 0)
56 | result = 31 * result + (keyImage?.hashCode() ?: 0)
57 | result = 31 * result + origin.hashCode()
58 | return result
59 | }
60 | }
61 |
62 | sealed interface EnoteOrigin {
63 | data class TxOut(val txId: String, val index: Int) : EnoteOrigin
64 | }
65 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/common/debug.h:
--------------------------------------------------------------------------------
1 | #ifndef COMMON_DEBUG_H_
2 | #define COMMON_DEBUG_H_
3 |
4 | #include
5 | #include
6 |
7 | // Default local tag
8 | #ifndef LOG_TAG
9 | #define LOG_TAG "MoneroJNI"
10 | #endif
11 |
12 | // Low-level debug macros. Log messages are not scrubbed.
13 | #define LOGD(...) ((void)__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__))
14 | #define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__))
15 | #define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__))
16 | #define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__))
17 | // For release builds, disable verbose logging.
18 | #ifndef NDEBUG
19 | #define LOGV(...) ((void)__android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__))
20 | #else
21 | #define LOGV(...) ((void)0)
22 | #endif
23 |
24 | // Log a fatal error. If the given condition fails, this stops program
25 | // execution like a normal assertion, but also generating the given message.
26 | // It always evaluates its argument and it is NOT stripped from release builds,
27 | // so it is OK for `cond` to have side effects. Note that the condition
28 | // test is -inverted- from the normal assert() semantics.
29 | #define LOG_FATAL_IF(cond, ...) \
30 | ( (__builtin_expect((cond)!=0, 0)) \
31 | ? ((void) __print_assert(#cond, LOG_TAG, ## __VA_ARGS__)) \
32 | : (void)0 )
33 |
34 | #define LOG_FATAL(...) \
35 | ( ((void) __print_assert(NULL, LOG_TAG, ## __VA_ARGS__)) )
36 |
37 | // Assertion that generates a log message when the assertion fails.
38 | // Stripped out of release builds.
39 | #ifndef NDEBUG
40 | #define LOG_ASSERT(cond, ...) LOG_FATAL_IF(!(cond), ## __VA_ARGS__)
41 | #else
42 | #define LOG_ASSERT(cond, ...) ((void)0)
43 | #endif
44 |
45 | #define __second(dummy, second, ...) second
46 | #define __rest(first, ...) , ## __VA_ARGS__
47 |
48 | #define __print_assert(cond, tag, fmt...) \
49 | __android_log_assert(cond, tag, __second(0, ## fmt, NULL) __rest(fmt))
50 |
51 | #endif // COMMON_DEBUG_H_
52 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/AppModule.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo
2 |
3 | import android.app.Application
4 | import androidx.room.Room
5 | import androidx.room.RoomDatabase
6 | import androidx.sqlite.db.SupportSQLiteDatabase
7 | import im.molly.monero.demo.data.*
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.launch
10 |
11 | /**
12 | * Naive container of global instances.
13 | *
14 | * A complex app should use Koin or Hilt for dependencies.
15 | */
16 | object AppModule {
17 | private lateinit var application: Application
18 | private lateinit var populateInitialData: suspend (AppDatabase) -> Unit
19 |
20 | private val applicationScope = kotlinx.coroutines.MainScope()
21 |
22 | private val database: AppDatabase by lazy {
23 | Room.databaseBuilder(
24 | application, AppDatabase::class.java, "monero-demo.db"
25 | ).addCallback(object : RoomDatabase.Callback() {
26 | override fun onCreate(db: SupportSQLiteDatabase) {
27 | applicationScope.launch(Dispatchers.IO) {
28 | populateInitialData(database)
29 | }
30 | }
31 | }).build()
32 | }
33 |
34 | private val walletDataSource: WalletDataSource by lazy {
35 | WalletDataSource(database.walletDao())
36 | }
37 |
38 | private val moneroSdkClient: MoneroSdkClient by lazy {
39 | MoneroSdkClient(application)
40 | }
41 |
42 | val settingsRepository: SettingsRepository by lazy {
43 | SettingsRepository(application.preferencesDataStore)
44 | }
45 |
46 | val remoteNodeRepository: RemoteNodeRepository by lazy {
47 | RemoteNodeRepository(database.remoteNodeDao())
48 | }
49 |
50 | val walletRepository: WalletRepository by lazy {
51 | WalletRepository(moneroSdkClient, walletDataSource, settingsRepository, applicationScope)
52 | }
53 |
54 | fun provide(application: Application, populateInitialData: suspend (AppDatabase) -> Unit) {
55 | this.application = application
56 | this.populateInitialData = populateInitialData
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/ui/TransactionViewModel.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.ui
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import androidx.lifecycle.viewmodel.initializer
6 | import androidx.lifecycle.viewmodel.viewModelFactory
7 | import im.molly.monero.demo.AppModule
8 | import im.molly.monero.demo.common.Result
9 | import im.molly.monero.demo.common.asResult
10 | import im.molly.monero.demo.data.WalletRepository
11 | import im.molly.monero.sdk.Transaction
12 | import kotlinx.coroutines.flow.SharingStarted
13 | import kotlinx.coroutines.flow.StateFlow
14 | import kotlinx.coroutines.flow.map
15 | import kotlinx.coroutines.flow.stateIn
16 |
17 | class TransactionViewModel(
18 | txId: String,
19 | walletId: Long,
20 | walletRepository: WalletRepository = AppModule.walletRepository,
21 | ) : ViewModel() {
22 |
23 | val uiState: StateFlow =
24 | walletRepository.getTransaction(walletId, txId)
25 | .asResult()
26 | .map { result ->
27 | when (result) {
28 | is Result.Success -> result.data?.let { tx ->
29 | TxUiState.Loaded(tx)
30 | } ?: TxUiState.NotFound
31 |
32 | is Result.Error -> TxUiState.Error
33 | is Result.Loading -> TxUiState.Loading
34 | }
35 | }.stateIn(
36 | scope = viewModelScope,
37 | started = SharingStarted.WhileSubscribed(5_000),
38 | initialValue = TxUiState.Loading
39 | )
40 |
41 | companion object {
42 | fun factory(txId: String, walletId: Long) = viewModelFactory {
43 | initializer {
44 | TransactionViewModel(txId, walletId)
45 | }
46 | }
47 |
48 | fun key(txId: String, walletId: Long): String = "tx_$txId:$walletId"
49 | }
50 | }
51 |
52 | sealed interface TxUiState {
53 | data class Loaded(val transaction: Transaction) : TxUiState
54 | data object Error : TxUiState
55 | data object Loading : TxUiState
56 | data object NotFound : TxUiState
57 | }
58 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/ui/component/SelectListBox.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.ui.component
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.material3.*
5 | import androidx.compose.runtime.*
6 | import androidx.compose.ui.Modifier
7 |
8 | @OptIn(ExperimentalMaterial3Api::class)
9 | @Composable
10 | fun SelectListBox(
11 | label: String,
12 | options: Map,
13 | selectedOption: T,
14 | onOptionClick: (T) -> Unit,
15 | modifier: Modifier = Modifier,
16 | enabled: Boolean = true,
17 | ) {
18 | var expanded by remember { mutableStateOf(false) }
19 |
20 | ExposedDropdownMenuBox(
21 | expanded = expanded,
22 | onExpandedChange = {
23 | expanded = !expanded
24 | },
25 | modifier = modifier,
26 | ) {
27 | OutlinedTextField(
28 | readOnly = true,
29 | value = options.getValue(selectedOption),
30 | onValueChange = { },
31 | enabled = enabled,
32 | modifier = Modifier
33 | .fillMaxWidth()
34 | .menuAnchor(),
35 | label = { Text(label) },
36 | trailingIcon = {
37 | if (enabled) {
38 | ExposedDropdownMenuDefaults.TrailingIcon(
39 | expanded = expanded
40 | )
41 | }
42 | },
43 | colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
44 | )
45 | DropdownMenu(
46 | expanded = expanded,
47 | onDismissRequest = {
48 | expanded = false
49 | },
50 | modifier = Modifier.exposedDropdownSize(),
51 | ) {
52 | options.forEach { (key, text) ->
53 | DropdownMenuItem(
54 | text = {
55 | Text(text)
56 | },
57 | onClick = {
58 | onOptionClick(key)
59 | expanded = false
60 | },
61 | )
62 | }
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/lib/android/src/test/kotlin/im/molly/monero/sdk/BalanceTest.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | import com.google.common.truth.Truth.assertThat
4 | import org.junit.Test
5 | import java.time.Instant
6 |
7 | class BalanceTest {
8 |
9 | @Test
10 | fun `confirmed and total amounts are sum of time-locked values`() {
11 | val balance = Balance(
12 | lockableAmounts = listOf(1.xmr, 2.xmr, 3.xmr).map { it.unlocked() },
13 | )
14 |
15 | assertThat(balance.confirmedAmount).isEqualTo(6.xmr)
16 | assertThat(balance.totalAmount).isEqualTo(6.xmr)
17 | }
18 |
19 | @Test
20 | fun `total amount includes pending`() {
21 | val balance = Balance(
22 | lockableAmounts = listOf(2.xmr.unlocked()),
23 | pendingAmount = 4.xmr,
24 | )
25 |
26 | assertThat(balance.totalAmount).isEqualTo(6.xmr)
27 | }
28 |
29 | @Test
30 | fun `empty balance returns zero amounts`() {
31 | assertThat(Balance.EMPTY.totalAmount).isEqualTo(0.xmr)
32 | }
33 |
34 | @Test
35 | fun `excludes locked amounts from unlocked sum`() {
36 | val current = BlockchainTime(100, Instant.now(), Mainnet)
37 |
38 | val allUnlocked = Balance(
39 | listOf(
40 | TimeLocked(2.xmr, Mainnet.unlockAtBlock(50)),
41 | TimeLocked(3.xmr, Mainnet.unlockAtBlock(50))
42 | )
43 | )
44 | assertThat(allUnlocked.unlockedAmountAt(current)).isEqualTo(5.xmr)
45 |
46 | val allLocked = Balance(
47 | listOf(
48 | TimeLocked(2.xmr, Mainnet.unlockAtBlock(150)),
49 | TimeLocked(3.xmr, Mainnet.unlockAtBlock(200))
50 | )
51 | )
52 | assertThat(allLocked.unlockedAmountAt(current)).isEqualTo(0.xmr)
53 |
54 | val partial = Balance(
55 | listOf(
56 | TimeLocked(1.xmr, Mainnet.unlockAtBlock(50)),
57 | TimeLocked(2.xmr, Mainnet.unlockAtBlock(100)),
58 | TimeLocked(5.xmr, Mainnet.unlockAtBlock(150)),
59 | )
60 | )
61 | assertThat(partial.unlockedAmountAt(current)).isEqualTo(3.xmr)
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - '**'
8 | paths-ignore:
9 | - 'README.md'
10 | - '.github/FUNDING.yml'
11 | - '.github/ISSUE_TEMPLATE/**'
12 |
13 | permissions:
14 | contents: read
15 |
16 | jobs:
17 | build:
18 | name: Build & Run tests
19 | runs-on: ubuntu-24.04
20 |
21 | steps:
22 | - uses: actions/checkout@v4
23 | with:
24 | submodules: recursive
25 |
26 | - name: Free up disk space in runner
27 | uses: ./.github/actions/disk-cleanup
28 |
29 | - name: Setup Java
30 | uses: actions/setup-java@v4
31 | with:
32 | distribution: temurin
33 | java-version: 21
34 |
35 | - name: Setup Gradle
36 | uses: gradle/actions/setup-gradle@v4
37 | with:
38 | build-scan-publish: true
39 | build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
40 | build-scan-terms-of-use-agree: 'yes'
41 |
42 | - name: Enable KVM
43 | run: |
44 | sudo tee /etc/udev/rules.d/99-kvm4all.rules >/dev/null <> $GITHUB_OUTPUT
55 |
56 | - name: Compile modules
57 | run: ./gradlew assemble --scan
58 |
59 | - name: Run unit tests
60 | run: ./gradlew check
61 |
62 | - name: Run instrumented tests on emulator
63 | run: ./gradlew ciGroupDebugAndroidTest
64 |
65 | - name: Publish Snapshot
66 | if: "github.ref_name == 'main' && endsWith(steps.meta.outputs.version, 'SNAPSHOT')"
67 | run: ./gradlew publishToMavenCentral
68 | env:
69 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }}
70 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }}
71 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/MoneroAmount.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | import android.os.Parcelable
4 | import im.molly.monero.sdk.internal.constants.CRYPTONOTE_DISPLAY_DECIMAL_POINT
5 | import kotlinx.parcelize.Parcelize
6 | import java.math.BigDecimal
7 |
8 | @JvmInline
9 | @Parcelize
10 | value class MoneroAmount(val atomicUnits: Long) : Parcelable {
11 |
12 | companion object {
13 | const val ATOMIC_UNIT_SCALE: Int = CRYPTONOTE_DISPLAY_DECIMAL_POINT
14 |
15 | val ZERO = MoneroAmount(0)
16 | }
17 |
18 | val xmr: BigDecimal
19 | get() = BigDecimal.valueOf(atomicUnits, ATOMIC_UNIT_SCALE)
20 |
21 | val isZero: Boolean get() = atomicUnits == 0L
22 |
23 | override fun toString() = atomicUnits.toString()
24 |
25 | operator fun plus(other: MoneroAmount) =
26 | MoneroAmount(Math.addExact(this.atomicUnits, other.atomicUnits))
27 |
28 | operator fun minus(other: MoneroAmount) =
29 | MoneroAmount(Math.subtractExact(this.atomicUnits, other.atomicUnits))
30 |
31 | operator fun compareTo(other: Int): Int = atomicUnits.compareTo(other)
32 | }
33 |
34 | fun Long.toAtomicUnits(): MoneroAmount = MoneroAmount(this)
35 |
36 | fun Int.toAtomicUnits(): MoneroAmount = MoneroAmount(this.toLong())
37 |
38 | inline val BigDecimal.xmr: MoneroAmount
39 | get() {
40 | val atomicUnits = times(BigDecimal.TEN.pow(MoneroAmount.ATOMIC_UNIT_SCALE)).toLong()
41 | return MoneroAmount(atomicUnits)
42 | }
43 |
44 | inline val Number.xmr: MoneroAmount get() = (this as BigDecimal).xmr
45 |
46 | inline val Double.xmr: MoneroAmount get() = BigDecimal(this).xmr
47 |
48 | inline val Long.xmr: MoneroAmount get() = BigDecimal(this).xmr
49 |
50 | inline val Int.xmr: MoneroAmount get() = BigDecimal(this).xmr
51 |
52 | inline fun Iterable.sumOf(selector: (T) -> MoneroAmount): MoneroAmount {
53 | var sum: MoneroAmount = MoneroAmount.ZERO
54 | for (element in this) {
55 | sum += selector(element)
56 | }
57 | return sum
58 | }
59 |
60 | fun Iterable.sum(): MoneroAmount {
61 | var sum: MoneroAmount = MoneroAmount.ZERO
62 | for (element in this) {
63 | sum += element
64 | }
65 | return sum
66 | }
67 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/mnemonics/mnemonics.cc:
--------------------------------------------------------------------------------
1 | #include "common/eraser.h"
2 |
3 | #include "jni_cache.h"
4 |
5 | #include "electrum-words.h"
6 |
7 | namespace monero {
8 |
9 | extern "C"
10 | JNIEXPORT jobject JNICALL
11 | Java_im_molly_monero_sdk_mnemonics_MoneroMnemonicKt_nativeElectrumWordsGenerateMnemonic(
12 | JNIEnv* env,
13 | jclass clazz,
14 | jbyteArray j_entropy,
15 | jstring j_language) {
16 | std::vector entropy = JavaToNativeByteArray(env, j_entropy);
17 | Eraser entropy_eraser(entropy);
18 |
19 | std::string language = JavaToNativeString(env, j_language);
20 |
21 | epee::wipeable_string words;
22 | bool success =
23 | crypto::ElectrumWords::bytes_to_words(entropy.data(), entropy.size(),
24 | words, language);
25 | if (!success) {
26 | return nullptr;
27 | }
28 |
29 | jobject j_mnemonic_code = CallStaticObjectMethod(
30 | env, MoneroMnemonicClass.obj(),
31 | MoneroMnemonic_buildMnemonicFromJNI,
32 | j_entropy,
33 | NativeToJavaByteArray(env, words.data(), words.size()),
34 | j_language);
35 |
36 | return j_mnemonic_code;
37 | }
38 |
39 | extern "C"
40 | JNIEXPORT jobject JNICALL
41 | Java_im_molly_monero_sdk_mnemonics_MoneroMnemonicKt_nativeElectrumWordsRecoverEntropy(
42 | JNIEnv* env,
43 | jclass clazz,
44 | jbyteArray j_source
45 | ) {
46 | std::vector words = JavaToNativeByteArray(env, j_source);
47 | Eraser words_eraser(words);
48 |
49 | epee::wipeable_string entropy, w_words(words.data(), words.size());
50 | std::string language;
51 | bool success =
52 | crypto::ElectrumWords::words_to_bytes(w_words,
53 | entropy,
54 | 0, /* len */
55 | true, /* duplicate */
56 | language);
57 | if (!success) {
58 | return nullptr;
59 | }
60 |
61 | jobject j_mnemonic_code = CallStaticObjectMethod(
62 | env, MoneroMnemonicClass.obj(),
63 | MoneroMnemonic_buildMnemonicFromJNI,
64 | NativeToJavaByteArray(env, entropy.data(), entropy.size()),
65 | j_source,
66 | NativeToJavaString(env, language));
67 |
68 | return j_mnemonic_code;
69 | }
70 |
71 | } // namespace monero
72 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/SecretKey.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import java.io.Closeable
6 | import java.security.MessageDigest
7 | import java.security.SecureRandom
8 | import javax.security.auth.Destroyable
9 |
10 | /**
11 | * Elliptic curve secret key.
12 | *
13 | * SecretKey wraps a secret scalar value, helping to prevent accidental exposure and securely
14 | * erasing the value from memory.
15 | *
16 | * This class is not thread-safe.
17 | */
18 | class SecretKey : Destroyable, Closeable, Parcelable {
19 |
20 | private val secret = ByteArray(32)
21 |
22 | constructor() {
23 | SecureRandom().nextBytes(secret)
24 | }
25 |
26 | constructor(secretScalar: ByteArray) {
27 | require(secretScalar.size == 32) { "Secret key must be 32 bytes" }
28 | secretScalar.copyInto(secret)
29 | }
30 |
31 | private constructor(parcel: Parcel) {
32 | parcel.readByteArray(secret)
33 | }
34 |
35 | internal val isNonZero
36 | get() = !MessageDigest.isEqual(secret, ByteArray(secret.size))
37 |
38 | val bytes: ByteArray
39 | get() {
40 | check(!destroyed) { "Secret key has been already destroyed" }
41 | check(isNonZero) { "Secret key cannot be zero" }
42 | return secret.clone()
43 | }
44 |
45 | var destroyed = false
46 | private set
47 |
48 | override fun destroy() {
49 | if (!destroyed) {
50 | secret.fill(0)
51 | }
52 | destroyed = true
53 | }
54 |
55 | override fun writeToParcel(parcel: Parcel, flags: Int) {
56 | parcel.writeByteArray(secret)
57 | }
58 |
59 | override fun describeContents(): Int = 0
60 |
61 | companion object CREATOR : Parcelable.Creator {
62 | override fun createFromParcel(parcel: Parcel): SecretKey {
63 | return SecretKey(parcel)
64 | }
65 |
66 | override fun newArray(size: Int): Array {
67 | return arrayOfNulls(size)
68 | }
69 | }
70 |
71 | override fun equals(other: Any?): Boolean =
72 | this === other || other is SecretKey && MessageDigest.isEqual(secret, other.secret)
73 |
74 | override fun hashCode(): Int = secret.contentHashCode()
75 |
76 | override fun close() = destroy()
77 |
78 | protected fun finalize() = destroy()
79 | }
80 |
81 | fun randomSecretKey(): SecretKey = SecretKey()
82 |
--------------------------------------------------------------------------------
/lib/android/src/androidTest/kotlin/im/molly/monero/sdk/e2etest/WalletTestBase.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.e2etest
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import androidx.test.platform.app.InstrumentationRegistry
6 | import androidx.test.rule.ServiceTestRule
7 | import im.molly.monero.sdk.InMemoryWalletDataStore
8 | import im.molly.monero.sdk.Mainnet
9 | import im.molly.monero.sdk.MoneroWallet
10 | import im.molly.monero.sdk.WalletDataStore
11 | import im.molly.monero.sdk.WalletProvider
12 | import im.molly.monero.sdk.internal.IWalletService
13 | import im.molly.monero.sdk.internal.WalletServiceClient
14 | import im.molly.monero.sdk.service.BaseWalletService
15 | import org.junit.After
16 | import org.junit.Before
17 | import org.junit.Rule
18 |
19 | abstract class WalletTestBase(private val serviceClass: Class) {
20 |
21 | @get:Rule
22 | val walletServiceRule = ServiceTestRule()
23 |
24 | protected lateinit var walletProvider: WalletProvider
25 | private set
26 |
27 | protected val context: Context by lazy {
28 | InstrumentationRegistry.getInstrumentation().context
29 | }
30 |
31 | private fun bindService(): IWalletService {
32 | val binder = walletServiceRule.bindService(Intent(context, serviceClass))
33 | return IWalletService.Stub.asInterface(binder)
34 | }
35 |
36 | private fun unbindService() {
37 | walletServiceRule.unbindService()
38 | }
39 |
40 | @Before
41 | fun setUpBase() {
42 | val walletService = bindService()
43 | walletProvider = WalletServiceClient.withBoundService(context, walletService)
44 | }
45 |
46 | @After
47 | fun tearDownBase() {
48 | runCatching {
49 | walletProvider.disconnect()
50 | }
51 | unbindService()
52 | }
53 |
54 | protected suspend fun wallet(defaultStore: WalletDataStore? = null) =
55 | walletProvider.createNewWallet(Mainnet, defaultStore)
56 |
57 | protected suspend fun withReopenedWallet(
58 | wallet: MoneroWallet,
59 | action: suspend (original: MoneroWallet, reopened: MoneroWallet) -> Unit,
60 | ) {
61 | walletProvider.openWallet(
62 | network = wallet.network,
63 | dataStore = InMemoryWalletDataStore().apply {
64 | wallet.save(targetStore = this)
65 | },
66 | ).use { reopened ->
67 | action(wallet, reopened)
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/demo/android/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application)
3 | alias(libs.plugins.androidx.room)
4 | alias(libs.plugins.kotlin.android)
5 | alias(libs.plugins.kotlin.compose)
6 | alias(libs.plugins.ksp)
7 | }
8 |
9 | android {
10 | namespace = "im.molly.monero.demo"
11 | compileSdk = 35
12 |
13 | defaultConfig {
14 | applicationId = "im.molly.monero.demo"
15 | minSdk = 26
16 | targetSdk = 34
17 | versionCode = 1
18 | versionName = "1.0"
19 |
20 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
21 | }
22 |
23 | buildTypes {
24 | release {
25 | isMinifyEnabled = false
26 |
27 | proguardFiles(
28 | getDefaultProguardFile("proguard-android-optimize.txt"),
29 | "proguard-rules.pro"
30 | )
31 | }
32 | }
33 |
34 | compileOptions {
35 | sourceCompatibility = JavaVersion.VERSION_11
36 | targetCompatibility = JavaVersion.VERSION_11
37 | }
38 |
39 | kotlinOptions {
40 | jvmTarget = "11"
41 | }
42 |
43 | buildFeatures {
44 | compose = true
45 | }
46 |
47 | packaging {
48 | resources {
49 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
50 | merges += "META-INF/LICENSE.md"
51 | merges += "META-INF/LICENSE-notice.md"
52 | }
53 | }
54 |
55 | room {
56 | schemaDirectory("$projectDir/schemas")
57 | }
58 | }
59 |
60 | dependencies {
61 | implementation(project(":lib"))
62 |
63 | implementation(libs.androidx.activity.compose)
64 | implementation(platform(libs.androidx.compose.bom))
65 | implementation(libs.androidx.core.ktx)
66 | implementation(libs.androidx.datastore.preferences)
67 | implementation(libs.androidx.lifecycle.runtime.ktx)
68 | implementation(libs.androidx.lifecycle.service)
69 | implementation(libs.androidx.material3)
70 | implementation(libs.androidx.navigation.compose)
71 | implementation(libs.androidx.ui)
72 | implementation(libs.androidx.ui.graphics)
73 | implementation(libs.androidx.ui.tooling.preview)
74 |
75 | implementation(libs.androidx.room.ktx)
76 | runtimeOnly(libs.androidx.room.runtime)
77 | ksp(libs.androidx.room.compiler)
78 |
79 | testImplementation(testLibs.junit)
80 |
81 | androidTestImplementation(testLibs.androidx.test.core)
82 | androidTestImplementation(testLibs.androidx.test.junit)
83 | androidTestImplementation(testLibs.androidx.test.runner)
84 | }
85 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/util/Base58.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.util
2 |
3 | import java.nio.ByteBuffer
4 | import java.nio.charset.Charset
5 |
6 | const val ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
7 |
8 | object Decoder {
9 | private val decodingTable = IntArray(128) { ALPHABET.indexOf(it.toChar()) }
10 |
11 | private val blockSizes = listOf(0, -1, 1, 2, -1, 3, 4, 5, -1, 6, 7, 8)
12 |
13 | fun decode(input: ByteArray): ByteBuffer {
14 | val size = input.size
15 | val needed = 8 * (size / 11) + findOutputBlockSize(size % 11)
16 | val out = ByteBuffer.allocate(needed)
17 | var pos = 0
18 | while (pos + 11 <= size) {
19 | decodeBlock(input, pos, 11, out)
20 | pos += 11
21 | }
22 | val remain = size - pos
23 | if (remain > 0) {
24 | decodeBlock(input, pos, remain, out)
25 | }
26 | out.flip()
27 | return out
28 | }
29 |
30 | private fun decodeBlock(block: ByteArray, offset: Int, len: Int, out: ByteBuffer) {
31 | val blockSize = findOutputBlockSize(len)
32 | val newOutPos = out.position() + blockSize
33 |
34 | var num = 0uL
35 | var base = 1uL
36 | var zeroes = 0
37 |
38 | for (i in (offset + len - 1) downTo offset) {
39 | val c = block[i].toInt()
40 | val digit = decodingTable.getOrElse(c) { -1 }.toULong()
41 | require(digit >= 0uL) { "Invalid symbol" }
42 | if (digit == 0uL) {
43 | zeroes++
44 | } else {
45 | while (zeroes > 0) {
46 | base *= 58u
47 | zeroes--
48 | }
49 | val prod = digit * base
50 | val lastNum = num
51 | num += prod
52 | require((prod / base == digit) && (num > lastNum)) { "Overflow" }
53 | base *= 58u // Never overflows, 58^10 < 2^64
54 | }
55 | }
56 | for (j in 1..blockSize) {
57 | out.put(newOutPos - j, num.toByte())
58 | num = num shr 8
59 | }
60 | require(num == 0uL) { "Overflow" }
61 | out.position(newOutPos)
62 | }
63 |
64 | private fun findOutputBlockSize(blockSize: Int): Int =
65 | blockSizes[blockSize].also {
66 | require(it >= 0) { "Invalid block size" }
67 | }
68 | }
69 |
70 | fun String.decodeBase58(): ByteArray =
71 | Decoder.decode(this.toByteArray(Charset.defaultCharset())).array()
72 |
--------------------------------------------------------------------------------
/lib/android/src/androidTest/kotlin/im/molly/monero/sdk/e2etest/WalletRefreshTest.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.e2etest
2 |
3 | import androidx.test.filters.LargeTest
4 | import im.molly.monero.sdk.LedgerChainSubject
5 | import im.molly.monero.sdk.Mainnet
6 | import im.molly.monero.sdk.MoneroWalletSubject
7 | import im.molly.monero.sdk.RemoteNode
8 | import im.molly.monero.sdk.RestorePoint
9 | import im.molly.monero.sdk.SecretKey
10 | import im.molly.monero.sdk.service.BaseWalletService
11 | import im.molly.monero.sdk.service.InProcessWalletService
12 | import im.molly.monero.sdk.service.SandboxedWalletService
13 | import im.molly.monero.sdk.singleNodeClient
14 | import kotlinx.coroutines.cancelAndJoin
15 | import kotlinx.coroutines.flow.takeWhile
16 | import kotlinx.coroutines.flow.toList
17 | import kotlinx.coroutines.launch
18 | import kotlinx.coroutines.runBlocking
19 | import kotlinx.coroutines.withTimeout
20 | import org.junit.Test
21 | import kotlin.time.Duration.Companion.minutes
22 |
23 | @OptIn(ExperimentalStdlibApi::class)
24 | abstract class WalletRefreshTest(
25 | serviceClass: Class,
26 | ) : WalletTestBase(serviceClass) {
27 |
28 | @Test
29 | fun restoredWalletEmitsExpectedLedgerOnRefresh(): Unit = runBlocking {
30 | val key =
31 | SecretKey("148d78d2aba7dbca5cd8f6abcfb0b3c009ffbdbea1ff373d50ed94d78286640e".hexToByteArray())
32 | val node = RemoteNode("http://node.monerodevs.org:18089", Mainnet)
33 | val restorePoint = RestorePoint.blockHeight(2861767)
34 |
35 | val wallet = walletProvider.restoreWallet(
36 | network = Mainnet,
37 | client = node.singleNodeClient(),
38 | secretSpendKey = key,
39 | restorePoint = restorePoint,
40 | )
41 |
42 | val refreshJob = launch {
43 | wallet.awaitRefresh()
44 | }
45 |
46 | val ledgers = withTimeout(5.minutes) {
47 | wallet.ledger()
48 | .takeWhile { it.checkedAt.height < 2862121 }
49 | .toList()
50 | }
51 |
52 | refreshJob.cancelAndJoin()
53 |
54 | LedgerChainSubject.assertThat(ledgers).hasValidWalletHistory()
55 |
56 | withReopenedWallet(wallet) { original, reopened ->
57 | MoneroWalletSubject.assertThat(reopened).matchesStateOf(original)
58 | }
59 | }
60 | }
61 |
62 | @LargeTest
63 | class WalletRefreshInProcessTest : WalletRefreshTest(InProcessWalletService::class.java)
64 |
65 | @LargeTest
66 | class WalletRefreshSandboxedTest : WalletRefreshTest(SandboxedWalletService::class.java)
67 |
--------------------------------------------------------------------------------
/lib/android/src/androidTest/kotlin/im/molly/monero/sdk/mnemonics/MoneroMnemonicTest.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.mnemonics
2 |
3 | import com.google.common.truth.Truth.assertThat
4 | import org.junit.Test
5 | import java.util.Locale
6 |
7 | class MoneroMnemonicTest {
8 |
9 | @OptIn(ExperimentalStdlibApi::class)
10 | data class TestCase(val key: String, val words: String, val language: String) {
11 | val entropy = key.hexToByteArray()
12 | }
13 |
14 | private val testCases = listOf(
15 | TestCase(
16 | key = "3b094ca7218f175e91fa2402b4ae239a2fe8262792a3e718533a1a357a1e4109",
17 | 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",
18 | language = "en",
19 | ),
20 | )
21 |
22 | @Test
23 | fun knownMnemonics() {
24 | testCases.forEach {
25 | validateMnemonicGeneration(it)
26 | validateEntropyRecovery(it)
27 | }
28 | }
29 |
30 | @Test(expected = IllegalArgumentException::class)
31 | fun emptyEntropy() {
32 | MoneroMnemonic.generateMnemonic(ByteArray(0))
33 | }
34 |
35 | @Test(expected = IllegalArgumentException::class)
36 | fun invalidEntropy() {
37 | MoneroMnemonic.generateMnemonic(ByteArray(2))
38 | }
39 |
40 | @Test(expected = IllegalArgumentException::class)
41 | fun emptyWords() {
42 | MoneroMnemonic.recoverEntropy("")
43 | }
44 |
45 | @Test(expected = IllegalArgumentException::class)
46 | fun invalidLanguage() {
47 | MoneroMnemonic.generateMnemonic(ByteArray(32), Locale("ZZ"))
48 | }
49 |
50 | private fun validateMnemonicGeneration(testCase: TestCase) {
51 | val mnemonicCode =
52 | MoneroMnemonic.generateMnemonic(testCase.entropy, Locale(testCase.language))
53 | assertMnemonicCode(mnemonicCode, testCase)
54 | }
55 |
56 | private fun validateEntropyRecovery(testCase: TestCase) {
57 | val mnemonicCode = MoneroMnemonic.recoverEntropy(testCase.words)
58 | assertMnemonicCode(mnemonicCode, testCase)
59 | }
60 |
61 | private fun assertMnemonicCode(mnemonicCode: MnemonicCode?, testCase: TestCase) {
62 | assertThat(mnemonicCode).isNotNull()
63 | with(mnemonicCode!!) {
64 | assertThat(entropy).isEqualTo(testCase.entropy)
65 | assertThat(String(words)).isEqualTo(testCase.words)
66 | assertThat(locale.language).isEqualTo(testCase.language)
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/internal/Logger.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal
2 |
3 | import android.util.Log
4 | import im.molly.monero.sdk.DebugLogAdapter
5 | import im.molly.monero.sdk.LogAdapter
6 |
7 | internal open class Logger(val tag: String) : LogAdapter {
8 | fun v(msg: String? = null, tr: Throwable? = null) = log(Log.VERBOSE, tag, msg, tr)
9 | fun d(msg: String? = null, tr: Throwable? = null) = log(Log.DEBUG, tag, msg, tr)
10 | fun i(msg: String? = null, tr: Throwable? = null) = log(Log.INFO, tag, msg, tr)
11 | fun w(msg: String? = null, tr: Throwable? = null) = log(Log.WARN, tag, msg, tr)
12 | fun e(msg: String? = null, tr: Throwable? = null) = log(Log.ERROR, tag, msg, tr)
13 | fun wtf(msg: String? = null, tr: Throwable? = null) = log(Log.ASSERT, tag, msg, tr)
14 |
15 | fun log(priority: Int, tag: String, msg: String?, tr: Throwable? = null) {
16 | if (isLoggable(priority, tag)) {
17 | print(priority, tag, msg, tr)
18 | }
19 | }
20 |
21 | inline fun v(lazyMsg: () -> String?) = log(Log.VERBOSE, tag, lazyMsg)
22 | inline fun d(lazyMsg: () -> String?) = log(Log.DEBUG, tag, lazyMsg)
23 | inline fun i(lazyMsg: () -> String?) = log(Log.INFO, tag, lazyMsg)
24 | inline fun w(lazyMsg: () -> String?) = log(Log.WARN, tag, lazyMsg)
25 | inline fun e(lazyMsg: () -> String?) = log(Log.ERROR, tag, lazyMsg)
26 | inline fun wtf(lazyMsg: () -> String?) = log(Log.ASSERT, tag, lazyMsg)
27 |
28 | inline fun log(priority: Int, tag: String, lazyMsg: () -> String?) {
29 | if (isLoggable(priority, tag)) {
30 | print(priority, tag, lazyMsg(), null)
31 | }
32 | }
33 |
34 | /**
35 | * Log method called from native code.
36 | */
37 | @CalledByNative
38 | fun logFromNative(priority: Int, tag: String, msg: String?) {
39 | val pri = if (priority in Log.VERBOSE.rangeTo(Log.ASSERT)) priority else Log.ASSERT
40 | val jniTag = "MoneroJNI.$tag"
41 | log(pri, jniTag, msg, null)
42 | }
43 |
44 | companion object {
45 | var adapter: LogAdapter = DebugLogAdapter()
46 | }
47 |
48 | override fun print(priority: Int, tag: String, msg: String?, tr: Throwable?) {
49 | adapter.print(priority, tag, msg, tr)
50 | }
51 | }
52 |
53 | internal inline fun loggerFor(): Logger = Logger(getTag(T::class.java))
54 |
55 | private fun getTag(clazz: Class<*>): String {
56 | val tag = clazz.simpleName
57 | return if (tag.length <= 23) {
58 | tag
59 | } else {
60 | tag.substring(0, 23)
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/mnemonics/MnemonicCode.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.mnemonics
2 |
3 | import im.molly.monero.sdk.SecretKey
4 | import java.io.Closeable
5 | import java.nio.CharBuffer
6 | import java.security.MessageDigest
7 | import java.util.Locale
8 | import javax.security.auth.Destroyable
9 |
10 | class MnemonicCode private constructor(
11 | private val _entropy: ByteArray,
12 | private val _words: CharArray,
13 | val locale: Locale,
14 | ) : Destroyable, Closeable, Iterable {
15 |
16 | constructor(entropy: ByteArray, words: CharBuffer, locale: Locale = Locale.ENGLISH) : this(
17 | entropy.clone(),
18 | words.array().copyOfRange(words.position(), words.remaining()),
19 | locale,
20 | )
21 |
22 | internal val isNonZero
23 | get() = !MessageDigest.isEqual(_entropy, ByteArray(_entropy.size))
24 |
25 | val entropy: ByteArray
26 | get() = checkNotDestroyed { _entropy.clone() }
27 |
28 | val words: CharArray
29 | get() = checkNotDestroyed { _words.clone() }
30 |
31 | override fun iterator(): Iterator = object : Iterator {
32 | private var cursor: Int = 0
33 |
34 | override fun hasNext(): Boolean = checkNotDestroyed { cursor < _words.size }
35 |
36 | override fun next(): CharArray {
37 | if (!hasNext()) {
38 | throw NoSuchElementException()
39 | }
40 |
41 | val endIndex = findNextWordEnd(cursor)
42 | val currentWord = _words.copyOfRange(cursor, endIndex)
43 | cursor = endIndex + 1
44 |
45 | return currentWord
46 | }
47 |
48 | private fun findNextWordEnd(startIndex: Int): Int {
49 | var endIndex = startIndex
50 | while (endIndex < _words.size && _words[endIndex] != ' ') {
51 | endIndex++
52 | }
53 | return endIndex
54 | }
55 | }
56 |
57 | var destroyed = false
58 | private set
59 |
60 | override fun destroy() {
61 | if (!destroyed) {
62 | _entropy.fill(0)
63 | _words.fill(0.toChar())
64 | }
65 | destroyed = true
66 | }
67 |
68 | override fun close() = destroy()
69 |
70 | protected fun finalize() = destroy()
71 |
72 | override fun equals(other: Any?): Boolean =
73 | this === other || (other is MnemonicCode && MessageDigest.isEqual(_entropy, other._entropy))
74 |
75 | override fun hashCode(): Int = _entropy.contentHashCode()
76 |
77 | private inline fun checkNotDestroyed(block: () -> T): T {
78 | check(!destroyed) { "MnemonicCode has already been destroyed" }
79 | return block()
80 | }
81 | }
82 |
83 | fun MnemonicCode.toSecretKey(): SecretKey = SecretKey(entropy)
84 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/MoneroCurrency.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | import java.text.DecimalFormat
4 | import java.util.Locale
5 | import kotlin.math.absoluteValue
6 |
7 | object MoneroCurrency {
8 | const val SYMBOL = "XMR"
9 |
10 | const val MAX_PRECISION = MoneroAmount.ATOMIC_UNIT_SCALE
11 |
12 | open class Format(
13 | val precision: Int,
14 | val locale: Locale = Locale.US,
15 | ) {
16 | init {
17 | require(precision in 0..MAX_PRECISION) {
18 | "Precision must be between 0 and $MAX_PRECISION"
19 | }
20 | }
21 |
22 | private val numberFormat = (DecimalFormat.getInstance(locale) as DecimalFormat).apply {
23 | minimumFractionDigits = precision
24 | isParseBigDecimal = true
25 | }
26 |
27 | open fun format(amount: MoneroAmount): String {
28 | return numberFormat.format(amount.xmr)
29 | }
30 |
31 | /**
32 | * @throw ParseException
33 | */
34 | open fun parse(source: String): MoneroAmount {
35 | return numberFormat.parse(source)?.xmr ?: MoneroAmount.ZERO
36 | }
37 | }
38 |
39 | val ExactFormat = object : Format(MoneroAmount.ATOMIC_UNIT_SCALE) {
40 | override fun format(amount: MoneroAmount) = buildString {
41 | if (amount.atomicUnits < 0) {
42 | append('-')
43 | }
44 |
45 | val num = amount.atomicUnits.absoluteValue.toString()
46 |
47 | if (precision < num.length) {
48 | val point = num.length - precision
49 | append(num.substring(0, point))
50 | append('.')
51 | append(num.substring(point))
52 | } else {
53 | append("0.")
54 | for (i in 1..(precision - num.length)) {
55 | append('0')
56 | }
57 | append(num)
58 | }
59 | }
60 | }
61 |
62 | fun format(amount: MoneroAmount, outputFormat: Format = ExactFormat): String {
63 | return outputFormat.format(amount)
64 | }
65 |
66 | fun format(amount: MoneroAmount, precision: Int, appendSymbol: Boolean = false): String {
67 | val formatted = Format(precision = precision).format(amount)
68 | return if (appendSymbol) "$formatted $SYMBOL" else formatted
69 | }
70 |
71 | /**
72 | * @throw ParseException
73 | */
74 | fun parse(source: String, outputFormat: Format = ExactFormat): MoneroAmount {
75 | return outputFormat.parse(source)
76 | }
77 | }
78 |
79 | fun MoneroAmount.toFormattedString(precision: Int = 5, appendSymbol: Boolean = false): String =
80 | MoneroCurrency.format(amount = this, precision = precision, appendSymbol = appendSymbol)
81 |
--------------------------------------------------------------------------------
/lib/android/src/androidTest/kotlin/im/molly/monero/sdk/internal/NativeWalletTest.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal
2 |
3 | import androidx.test.filters.LargeTest
4 | import com.google.common.truth.Truth.assertThat
5 | import im.molly.monero.sdk.Mainnet
6 | import im.molly.monero.sdk.SecretKey
7 | import im.molly.monero.sdk.Stagenet
8 | import im.molly.monero.sdk.Testnet
9 | import im.molly.monero.sdk.randomSecretKey
10 | import kotlinx.coroutines.test.runTest
11 | import org.junit.Test
12 |
13 | @OptIn(ExperimentalStdlibApi::class)
14 | class NativeWalletTest {
15 |
16 | @LargeTest
17 | @Test
18 | fun keyGenerationIsDeterministic() = runTest {
19 | assertThat(
20 | NativeWallet.localSyncWallet(
21 | networkId = Mainnet.id,
22 | secretSpendKey = SecretKey("d2ca26e22489bd9871c910c58dee3ab08e66b9d566825a064c8c0af061cd8706".hexToByteArray()),
23 | ).publicAddress
24 | ).isEqualTo("4AYjQM9HoAFNUeC3cvSfgeAN89oMMpMqiByvunzSzhn97cj726rJj3x8hCbH58UnMqQJShczCxbpWRiCJQ3HCUDHLiKuo4T")
25 |
26 | assertThat(
27 | NativeWallet.localSyncWallet(
28 | networkId = Testnet.id,
29 | secretSpendKey = SecretKey("48a35268bc33227eea43ac1ecfd144d51efc023c115c26ca68a01cc6201e9900".hexToByteArray()),
30 | ).publicAddress
31 | ).isEqualTo("A1v6gVUcGgGE87c1uFRWB1KfPVik2qLLDJiZT3rhZ8qjF3BGA6oHzeDboD23dH8rFaFFcysyqwF6DBj8WUTBWwEhESB7nZz")
32 |
33 | assertThat(
34 | NativeWallet.localSyncWallet(
35 | networkId = Stagenet.id,
36 | secretSpendKey = SecretKey("561a8d4e121ffca7321a7dc6af79679ceb4cdc8c0dcb0ef588b574586c5fac04".hexToByteArray()),
37 | ).publicAddress
38 | ).isEqualTo("54kPaUhYgGNBT72N8Bv2DFMqstLGJCEcWg1EAjwpxABkKL3uBtBLAh4VAPKvhWBdaD4ZpiftA8YWFLAxnWL4aQ9TD4vhY4W")
39 | }
40 |
41 | @LargeTest
42 | @Test
43 | fun publicAddressesAreDistinct() = runTest {
44 | val publicAddress =
45 | NativeWallet.localSyncWallet(
46 | networkId = Mainnet.id,
47 | secretSpendKey = randomSecretKey(),
48 | ).publicAddress
49 |
50 | val anotherPublicAddress =
51 | NativeWallet.localSyncWallet(
52 | networkId = Mainnet.id,
53 | secretSpendKey = randomSecretKey(),
54 | ).publicAddress
55 |
56 | assertThat(publicAddress).isNotEqualTo(anotherPublicAddress)
57 | }
58 |
59 | @Test
60 | fun balanceIsZeroAtGenesis() = runTest {
61 | with(
62 | NativeWallet.localSyncWallet(
63 | networkId = Mainnet.id,
64 | secretSpendKey = randomSecretKey(),
65 | ).getLedger()
66 | ) {
67 | assertThat(transactions).isEmpty()
68 | assertThat(isBalanceZero).isTrue()
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/lib/android/src/test/kotlin/im/molly/monero/sdk/SecretKeyTest.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | import com.google.common.truth.Truth.assertThat
4 | import org.junit.Test
5 | import kotlin.random.Random
6 | import kotlin.test.assertFailsWith
7 |
8 | class SecretKeyTest {
9 |
10 | @Test
11 | fun `secret keys are 256 bits`() {
12 | for (size in 0..64) {
13 | val secret = Random.nextBytes(size)
14 | if (size == 32) {
15 | assertThat(SecretKey(secret).bytes).hasLength(size)
16 | } else {
17 | assertFailsWith { SecretKey(secret) }
18 | }
19 | }
20 | }
21 |
22 | @Test
23 | fun `secret key copies buffer`() {
24 | val secretBytes = Random.nextBytes(32)
25 | val key = SecretKey(secretBytes)
26 |
27 | assertThat(key.bytes).isEqualTo(secretBytes)
28 | secretBytes.fill(0)
29 | assertThat(key.bytes).isNotEqualTo(secretBytes)
30 | }
31 |
32 | @Test
33 | fun `secret keys cannot be zero`() {
34 | assertFailsWith { SecretKey(ByteArray(32)).bytes }
35 | }
36 |
37 | @Test
38 | fun `when key is destroyed secret is zeroed`() {
39 | val secretBytes = Random.nextBytes(32)
40 | val key = SecretKey(secretBytes)
41 |
42 | assertThat(key.destroyed).isFalse()
43 | assertThat(key.bytes).isEqualTo(secretBytes)
44 |
45 | key.destroy()
46 |
47 | assertThat(key.destroyed).isTrue()
48 | assertThat(key.isNonZero).isFalse()
49 | assertFailsWith { key.bytes }
50 | }
51 |
52 | @Test
53 | fun `two keys with same secret are equal`() {
54 | val secret = Random.nextBytes(32)
55 |
56 | val key = SecretKey(secret)
57 | val sameKey = SecretKey(secret)
58 | val differentKey = randomSecretKey()
59 |
60 | assertThat(key).isEqualTo(sameKey)
61 | assertThat(sameKey).isNotEqualTo(differentKey)
62 | assertThat(differentKey).isNotEqualTo(key)
63 | }
64 |
65 | @Test
66 | fun `randomly generated keys are distinct`() {
67 | val times = 100_000
68 | val randomKeys = generateSequence { randomSecretKey() }.take(times).toSet()
69 |
70 | assertThat(randomKeys).hasSize(times)
71 | }
72 |
73 | @Test
74 | fun `keys are not equal to their destroyed versions`() {
75 | val secret = Random.nextBytes(32)
76 | val key = SecretKey(secret)
77 | val destroyed = SecretKey(secret).also { it.destroy() }
78 |
79 | assertThat(key).isNotEqualTo(destroyed)
80 | }
81 |
82 | @Test
83 | fun `destroyed keys are equal`() {
84 | val destroyed1 = randomSecretKey().also { it.destroy() }
85 | val destroyed2 = randomSecretKey().also { it.destroy() }
86 |
87 | assertThat(destroyed1).isEqualTo(destroyed2)
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/ui/AddressCard.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.ui
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.fillMaxWidth
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.Text
8 | import androidx.compose.material3.TextButton
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.draw.alpha
12 | import androidx.compose.ui.unit.dp
13 | import im.molly.monero.demo.data.model.WalletAddress
14 | import im.molly.monero.demo.ui.component.CopyableText
15 | import im.molly.monero.sdk.calculateBalance
16 | import im.molly.monero.sdk.toFormattedString
17 |
18 | @Composable
19 | fun AddressCardExpanded(
20 | walletAddress: WalletAddress,
21 | onClick: () -> Unit,
22 | onCreateSubAddressClick: () -> Unit,
23 | modifier: Modifier = Modifier,
24 | ) {
25 | Column(
26 | modifier = modifier
27 | .fillMaxWidth()
28 | .padding(horizontal = 16.dp, vertical = 8.dp)
29 | ) {
30 | val enotesCount = walletAddress.enotes.count()
31 | val unspentCount = walletAddress.enotes.count { !it.value.spent }
32 | val totalAmount = walletAddress.enotes.calculateBalance().totalAmount
33 |
34 | with(walletAddress.address) {
35 | val addressText = if (isPrimaryAddress) {
36 | "Account #$accountIndex Primary address"
37 | } else {
38 | "Account #$accountIndex Subaddress #$subAddressIndex"
39 | }
40 |
41 | val markedUsed = walletAddress.used || isPrimaryAddress
42 |
43 | Text(
44 | text = addressText,
45 | style = MaterialTheme.typography.bodyMedium,
46 | )
47 | Text(
48 | text = "Total balance: ${totalAmount.toFormattedString(appendSymbol = true)}",
49 | style = MaterialTheme.typography.bodySmall,
50 | )
51 | Text(
52 | text = "Total owned enotes: $enotesCount",
53 | style = MaterialTheme.typography.bodySmall,
54 | )
55 | Text(
56 | text = "Unspent enotes: $unspentCount",
57 | style = MaterialTheme.typography.bodySmall,
58 | )
59 | CopyableText(
60 | text = address,
61 | style = MaterialTheme.typography.bodyMedium,
62 | modifier = if (markedUsed) Modifier.alpha(0.5f) else Modifier,
63 | )
64 | if (walletAddress.isLastForAccount) {
65 | TextButton(onClick = onCreateSubAddressClick) {
66 | Text(
67 | text = "Add subaddress",
68 | style = MaterialTheme.typography.bodyMedium,
69 | )
70 | }
71 | }
72 |
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo
2 |
3 | import android.content.ComponentName
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.content.ServiceConnection
7 | import android.os.Bundle
8 | import android.os.IBinder
9 | import androidx.activity.ComponentActivity
10 | import androidx.activity.SystemBarStyle
11 | import androidx.activity.compose.setContent
12 | import androidx.activity.enableEdgeToEdge
13 | import androidx.compose.foundation.isSystemInDarkTheme
14 | import androidx.compose.runtime.DisposableEffect
15 | import androidx.core.view.WindowCompat
16 | import im.molly.monero.demo.service.SyncService
17 | import im.molly.monero.demo.ui.DemoApp
18 | import im.molly.monero.demo.ui.theme.AppTheme
19 | import kotlinx.coroutines.ExperimentalCoroutinesApi
20 | import kotlinx.coroutines.suspendCancellableCoroutine
21 |
22 |
23 | class MainActivity : ComponentActivity() {
24 | override fun onCreate(savedInstanceState: Bundle?) {
25 | super.onCreate(savedInstanceState)
26 |
27 | WindowCompat.setDecorFitsSystemWindows(window, false)
28 |
29 | setContent {
30 | val darkTheme = isSystemInDarkTheme()
31 |
32 | DisposableEffect(darkTheme) {
33 | enableEdgeToEdge(
34 | statusBarStyle = SystemBarStyle.auto(
35 | android.graphics.Color.TRANSPARENT,
36 | android.graphics.Color.TRANSPARENT,
37 | ) { darkTheme },
38 | navigationBarStyle = SystemBarStyle.auto(
39 | lightScrim,
40 | darkScrim,
41 | ) { darkTheme },
42 | )
43 | onDispose {}
44 | }
45 |
46 | AppTheme(
47 | darkTheme = darkTheme,
48 | ) {
49 | DemoApp()
50 | }
51 | }
52 | }
53 |
54 | @OptIn(ExperimentalCoroutinesApi::class)
55 | suspend fun connectToSyncService(): SyncService.LocalBinder =
56 | suspendCancellableCoroutine { continuation ->
57 | val connection = object : ServiceConnection {
58 | override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
59 | val binder = service as SyncService.LocalBinder
60 | continuation.resume(binder) {
61 | unbindService(this)
62 | }
63 | }
64 |
65 | override fun onServiceDisconnected(name: ComponentName?) {
66 | }
67 | }
68 |
69 | Intent(this@MainActivity, SyncService::class.java).also { intent ->
70 | bindService(intent, connection, Context.BIND_AUTO_CREATE)
71 | }
72 | }
73 | }
74 |
75 | private val lightScrim = android.graphics.Color.argb(0xe6, 0xFF, 0xFF, 0xFF)
76 |
77 | private val darkScrim = android.graphics.Color.argb(0x80, 0x1b, 0x1b, 0x1b)
78 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/wallet/http_client.h:
--------------------------------------------------------------------------------
1 | #ifndef WALLET_HTTP_CLIENT_H_
2 | #define WALLET_HTTP_CLIENT_H_
3 |
4 | #include "common/jvm.h"
5 |
6 | #include "fd.h"
7 |
8 | #include "net/abstract_http_client.h"
9 |
10 | namespace monero {
11 |
12 | using AbstractHttpClient = epee::net_utils::http::abstract_http_client;
13 |
14 | class RemoteNodeClient : public AbstractHttpClient {
15 | public:
16 | RemoteNodeClient(JNIEnv* env, const JavaRef& wallet_native) :
17 | m_wallet_native(env, wallet_native) {}
18 |
19 | bool set_proxy(const std::string& address) override;
20 | void set_server(std::string host,
21 | std::string port,
22 | boost::optional user,
23 | epee::net_utils::ssl_options_t ssl_options) override;
24 | void set_auto_connect(bool auto_connect) override;
25 | bool connect(std::chrono::milliseconds timeout) override;
26 | bool disconnect() override;
27 | bool is_connected(bool* ssl) override;
28 | bool invoke(const boost::string_ref uri,
29 | const boost::string_ref method,
30 | const boost::string_ref body,
31 | std::chrono::milliseconds timeout,
32 | const epee::net_utils::http::http_response_info** ppresponse_info,
33 | const epee::net_utils::http::fields_list& additional_params) override;
34 | bool invoke_get(const boost::string_ref uri,
35 | std::chrono::milliseconds timeout,
36 | const std::string& body,
37 | const epee::net_utils::http::http_response_info** ppresponse_info,
38 | const epee::net_utils::http::fields_list& additional_params) override;
39 | bool invoke_post(const boost::string_ref uri,
40 | const std::string& body,
41 | std::chrono::milliseconds timeout,
42 | const epee::net_utils::http::http_response_info** ppresponse_info,
43 | const epee::net_utils::http::fields_list& additional_params) override;
44 | uint64_t get_bytes_sent() const override;
45 | uint64_t get_bytes_received() const override;
46 |
47 | public:
48 | struct HttpResponse {
49 | int code;
50 | std::string content_type;
51 | ScopedFd body;
52 | };
53 |
54 | private:
55 | const ScopedJavaGlobalRef m_wallet_native;
56 | epee::net_utils::http::http_response_info m_response_info;
57 | };
58 |
59 | using HttpClientFactory = epee::net_utils::http::http_client_factory;
60 |
61 | class RemoteNodeClientFactory : public HttpClientFactory {
62 | public:
63 | RemoteNodeClientFactory(JNIEnv* env, const JavaRef& wallet_native) :
64 | m_wallet_native(env, wallet_native) {}
65 |
66 | std::unique_ptr create() override {
67 | return std::unique_ptr(
68 | new RemoteNodeClient(GetJniEnv(), m_wallet_native));
69 | }
70 |
71 | private:
72 | const ScopedJavaGlobalRef m_wallet_native;
73 | };
74 |
75 | } // namespace monero
76 |
77 | #endif // WALLET_HTTP_CLIENT_H_
78 |
--------------------------------------------------------------------------------
/lib/android/src/main/cpp/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.22)
2 |
3 | # Set the name of the final library
4 | project(monero_jni NONE)
5 |
6 | enable_language(C)
7 | enable_language(CXX)
8 | enable_language(ASM)
9 |
10 | # Number of cores
11 | cmake_host_system_information(RESULT NPROC QUERY NUMBER_OF_PHYSICAL_CORES)
12 | set(CORES "${NPROC}"
13 | CACHE STRING "Number of available processor cores.")
14 | mark_as_advanced(CORES)
15 |
16 | message(STATUS "CMake version ${CMAKE_VERSION} with ${CORES} processor cores available")
17 |
18 | # Location where external projects will be downloaded
19 | set(DOWNLOAD_CACHE ""
20 | CACHE PATH "Location where external projects will be downloaded.")
21 |
22 | # ABI-specific flags
23 | if(ANDROID_ABI STREQUAL "x86_64")
24 | # Equivalent to CMAKE_INTERPROCEDURAL_OPTIMIZATION
25 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -maes -flto")
26 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -maes -flto")
27 | else()
28 | # TODO: message(FATAL_ERROR "Unknown ABI:" ${ANDROID_ABI})
29 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -flto")
30 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -flto")
31 | endif()
32 |
33 | # Prevent locale issues
34 | set(ENV{LC_ALL} C)
35 |
36 | # Common definitions across builds
37 | include(cmake/toolchain.cmake)
38 |
39 | # Project dependencies
40 | add_subdirectory(boringssl)
41 | add_subdirectory(boost)
42 | add_subdirectory(libsodium)
43 | add_subdirectory(unbound)
44 | add_subdirectory(monero)
45 |
46 | # Hide all symbols not marked with JNIEXPORT
47 | set(CMAKE_C_VISIBILITY_PRESET hidden)
48 | set(CMAKE_CXX_VISIBILITY_PRESET hidden)
49 | set(CMAKE_VISIBILITY_INLINES_HIDDEN true)
50 |
51 | ### Project libraries
52 |
53 | set(COMMON_SOURCES
54 | common/jvm.cc
55 | common/java_native.cc
56 | )
57 |
58 | set(WALLET_SOURCES
59 | wallet/http_client.cc
60 | wallet/jni_cache.cc
61 | wallet/jni_loader.cc
62 | wallet/logging.cc
63 | wallet/transfer.cc
64 | wallet/wallet.cc
65 | )
66 |
67 | add_library(monero_wallet SHARED ${COMMON_SOURCES} ${WALLET_SOURCES})
68 |
69 | target_link_libraries(
70 | monero_wallet
71 | PRIVATE
72 | Monero::wallet2
73 | log
74 | )
75 |
76 | set(MNEMONICS_SOURCES
77 | mnemonics/jni_cache.cc
78 | mnemonics/jni_loader.cc
79 | mnemonics/mnemonics.cc
80 | )
81 |
82 | add_library(monero_mnemonics SHARED ${COMMON_SOURCES} ${MNEMONICS_SOURCES})
83 |
84 | target_link_libraries(
85 | monero_mnemonics
86 | PUBLIC
87 | Monero::easylogging
88 | PRIVATE
89 | Monero::electrum_words
90 | OpenSSL::SSL
91 | log
92 | )
93 |
94 | target_include_directories(monero_wallet PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
95 | target_include_directories(monero_mnemonics PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
96 |
97 | # Hide symbols from statically-linked dependencies
98 | set_target_properties(monero_wallet PROPERTIES LINK_FLAGS "-Wl,--exclude-libs,ALL")
99 | set_target_properties(monero_mnemonics PROPERTIES LINK_FLAGS "-Wl,--exclude-libs,ALL")
100 |
--------------------------------------------------------------------------------
/lib/android/src/test/kotlin/im/molly/monero/sdk/WalletServiceClientTest.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk
2 |
3 | import android.content.Context
4 | import android.content.ServiceConnection
5 | import im.molly.monero.sdk.internal.IWalletService
6 | import im.molly.monero.sdk.internal.WalletServiceClient
7 | import io.mockk.every
8 | import io.mockk.impl.annotations.MockK
9 | import io.mockk.junit4.MockKRule
10 | import io.mockk.mockk
11 | import io.mockk.verify
12 | import kotlinx.coroutines.runBlocking
13 | import org.junit.Before
14 | import org.junit.Rule
15 | import org.junit.Test
16 | import kotlin.test.assertFailsWith
17 |
18 | class WalletServiceClientTest {
19 |
20 | @get:Rule
21 | val mockkRule = MockKRule(this)
22 |
23 | @MockK(relaxed = true)
24 | lateinit var context: Context
25 |
26 | @MockK
27 | lateinit var service: IWalletService
28 |
29 | @MockK
30 | lateinit var serviceConnection: ServiceConnection
31 |
32 | private lateinit var client: WalletServiceClient
33 |
34 | @Before
35 | fun setUp() {
36 | client = WalletServiceClient(context, service, serviceConnection)
37 | }
38 |
39 | @Test
40 | fun `throws on mismatched client network`(): Unit = runBlocking {
41 | val mismatchedNodeClient = mockk {
42 | every { network } returns Testnet
43 | }
44 | val dataStore = mockk()
45 |
46 | assertFailsWith {
47 | client.createNewWallet(
48 | network = Mainnet,
49 | dataStore = null,
50 | client = mismatchedNodeClient,
51 | )
52 | }
53 | assertFailsWith {
54 | client.openWallet(
55 | network = Mainnet,
56 | dataStore = dataStore,
57 | client = mismatchedNodeClient,
58 | )
59 | }
60 | assertFailsWith {
61 | client.restoreWallet(
62 | network = Mainnet,
63 | dataStore = null,
64 | client = mismatchedNodeClient,
65 | secretSpendKey = randomSecretKey(),
66 | restorePoint = RestorePoint.blockHeight(1),
67 | )
68 | }
69 | }
70 |
71 | @Test
72 | fun `throws on mismatched restore point network`(): Unit = runBlocking {
73 | val nodeClient = mockk {
74 | every { network } returns Mainnet
75 | }
76 |
77 | assertFailsWith {
78 | client.restoreWallet(
79 | network = Mainnet,
80 | dataStore = null,
81 | client = nodeClient,
82 | secretSpendKey = randomSecretKey(),
83 | restorePoint = Testnet.genesisTime,
84 | )
85 | }
86 | }
87 |
88 | @Test
89 | fun `unbinds service on disconnect`() {
90 | client.disconnect()
91 | verify(exactly = 1) { context.unbindService(serviceConnection) }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/lib/android/src/main/kotlin/im/molly/monero/sdk/internal/DataStoreAdapter.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.sdk.internal
2 |
3 | import android.os.ParcelFileDescriptor
4 | import im.molly.monero.sdk.WalletDataStore
5 | import kotlinx.coroutines.CoroutineDispatcher
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.launch
8 | import kotlinx.coroutines.sync.Mutex
9 | import kotlinx.coroutines.sync.withLock
10 | import kotlinx.coroutines.withContext
11 | import java.io.FileInputStream
12 | import java.io.FileOutputStream
13 | import java.io.InputStream
14 | import java.io.OutputStream
15 |
16 | internal class DataStoreAdapter(
17 | private val dataStore: WalletDataStore,
18 | private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
19 | ) {
20 | private val mutex = Mutex()
21 | private val storeName: String? = dataStore::class.simpleName
22 | private val logger = loggerFor()
23 |
24 | suspend fun loadWithFd(
25 | block: suspend (ParcelFileDescriptor) -> T,
26 | ): T = withContext(ioDispatcher) {
27 | val (readFd, writeFd) = ParcelFileDescriptor.createPipe()
28 |
29 | val writerJob = launch {
30 | FileOutputStream(writeFd.fileDescriptor).use { output ->
31 | load(output)
32 | }
33 | }
34 |
35 | writerJob.invokeOnCompletion {
36 | writeFd.close()
37 | }
38 |
39 | try {
40 | readFd.use { block(readFd) }
41 | } finally {
42 | writerJob.join()
43 | }
44 | }
45 |
46 | suspend fun saveWithFd(
47 | overwrite: Boolean,
48 | block: suspend (ParcelFileDescriptor) -> T,
49 | ): T = withContext(ioDispatcher) {
50 | val (readFd, writeFd) = ParcelFileDescriptor.createPipe()
51 |
52 | val readerJob = launch {
53 | FileInputStream(readFd.fileDescriptor).use { input ->
54 | save(input, overwrite)
55 | }
56 | }
57 |
58 | readerJob.invokeOnCompletion {
59 | readFd.close()
60 | }
61 |
62 | try {
63 | writeFd.use { block(writeFd) }
64 | } finally {
65 | readerJob.join()
66 | }
67 | }
68 |
69 | private suspend fun load(output: OutputStream) {
70 | try {
71 | mutex.withLock {
72 | dataStore.load().use { input -> input.copyTo(output) }
73 | }
74 | } catch (t: Throwable) {
75 | logger.e("Error loading data from WalletDataStore ($storeName)", t)
76 | throw t
77 | }
78 | }
79 |
80 | private suspend fun save(input: InputStream, overwrite: Boolean) {
81 | try {
82 | mutex.withLock {
83 | dataStore.save(
84 | writer = { output -> input.copyTo(output) },
85 | overwrite = overwrite,
86 | )
87 | }
88 | } catch (t: Throwable) {
89 | logger.e("Error saving data to WalletDataStore ($storeName)", t)
90 | throw t
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/demo/android/src/main/kotlin/im/molly/monero/demo/ui/WalletCard.kt:
--------------------------------------------------------------------------------
1 | package im.molly.monero.demo.ui
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.layout.wrapContentSize
8 | import androidx.compose.material3.Card
9 | import androidx.compose.material3.ExperimentalMaterial3Api
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.getValue
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.unit.dp
16 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
17 | import androidx.lifecycle.viewmodel.compose.viewModel
18 | import im.molly.monero.demo.ui.component.LoadingWheel
19 |
20 | @OptIn(ExperimentalMaterial3Api::class)
21 | @Composable
22 | fun WalletCard(
23 | walletId: Long,
24 | onClick: () -> Unit,
25 | modifier: Modifier = Modifier,
26 | viewModel: WalletViewModel = viewModel(
27 | factory = WalletViewModel.factory(walletId),
28 | key = WalletViewModel.key(walletId),
29 | ),
30 | ) {
31 | val uiState: WalletUiState by viewModel.uiState.collectAsStateWithLifecycle()
32 |
33 | Card(
34 | onClick = onClick,
35 | modifier = modifier
36 | .fillMaxWidth()
37 | .padding(top = 8.dp, start = 8.dp, end = 8.dp),
38 | ) {
39 | Column(
40 | modifier = Modifier.padding(14.dp),
41 | ) {
42 | when (uiState) {
43 | is WalletUiState.Loaded -> WalletCardExpanded(uiState as WalletUiState.Loaded)
44 | WalletUiState.Error -> WalletCardError()
45 | WalletUiState.Loading -> WalletCardLoading()
46 | }
47 | }
48 | }
49 | }
50 |
51 | @Composable
52 | fun WalletCardExpanded(
53 | uiState: WalletUiState.Loaded,
54 | ) {
55 | Row {
56 | Text(
57 | text = "Wallet ID #${uiState.config.id} (${uiState.config.name}) ${uiState.network.name}",
58 | style = MaterialTheme.typography.bodyMedium,
59 | )
60 | }
61 | Row {
62 | Text(
63 | text = uiState.config.publicAddress,
64 | style = MaterialTheme.typography.bodyMedium,
65 | )
66 | }
67 | Row {
68 | Text(
69 | text = uiState.blockchainTime.toString(),
70 | style = MaterialTheme.typography.bodyMedium,
71 | )
72 | }
73 | }
74 |
75 | @Composable
76 | fun WalletCardError() {
77 | Text(text = "Error") // TODO
78 | }
79 |
80 | @Composable
81 | fun WalletCardLoading() {
82 | LoadingWheel(
83 | modifier = Modifier
84 | .fillMaxWidth()
85 | .wrapContentSize(),
86 | contentDesc = "Loading wallet",
87 | )
88 | }
89 |
90 | //@Preview
91 | //@Composable
92 | //private fun WalletCardExpandedPreview() {
93 | // AppTheme {
94 | // Surface {
95 | // WalletCardExpanded()
96 | // }
97 | // }
98 | //}
99 |
--------------------------------------------------------------------------------