docs: Update Garnet merge docs (no absolute paths)
This commit is contained in:
+2
-182
@@ -1,182 +1,2 @@
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
/.idea/androidTestResultsUserPreferences.xml
|
||||
/.idea/deploymentTargetDropDown.xml
|
||||
/.idea/deploymentTargetSelector.xml
|
||||
/.idea/appInsightsSettings.xml
|
||||
/.idea/ktlint-plugin.xml
|
||||
/.idea/ktfmt.xml
|
||||
/.idea/studiobot.xml
|
||||
/.idea/other.xml
|
||||
/.idea/runConfigurations.xml
|
||||
/.idea/ChatHistory_schema_v2.xml
|
||||
/.idea/artifacts/*
|
||||
/.idea/kotlinNotebook.xml
|
||||
/.idea/ChatHistory_schema_v3.xml
|
||||
/.idea/markdown.xml
|
||||
/.idea/AndroidProjectSystem.xml
|
||||
/.idea/deviceManager.xml
|
||||
/.idea/inspectionProfiles/
|
||||
/.idea/migrations.xml
|
||||
/.idea/planningMode.xml
|
||||
/desktopApp/vlc-temp/
|
||||
/commons/.idea/gradle.xml
|
||||
/commons/.idea/misc.xml
|
||||
/commons/.idea/workspace.xml
|
||||
/quartz/.idea/modules.xml
|
||||
/quartz/.idea/workspace.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.cxx
|
||||
|
||||
# superpowers skill
|
||||
.superpowers
|
||||
docs/brainstorms
|
||||
docs/superpowers
|
||||
|
||||
# Built application files
|
||||
*.apk
|
||||
*.aar
|
||||
*.ap_
|
||||
*.aab
|
||||
|
||||
# Files for the ART/Dalvik VM
|
||||
*.dex
|
||||
|
||||
# Java class files
|
||||
*.class
|
||||
|
||||
# Generated files
|
||||
bin/
|
||||
gen/
|
||||
out/
|
||||
# Uncomment the following line in case you need and you don't have the release build type files in your app
|
||||
# release/
|
||||
|
||||
# Gradle files
|
||||
.gradle
|
||||
.gradle/
|
||||
build/
|
||||
.kotlin/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Proguard folder generated by Eclipse
|
||||
proguard/
|
||||
|
||||
# Log Files
|
||||
*.log
|
||||
|
||||
# Android Studio
|
||||
/*/build/
|
||||
/*/local.properties
|
||||
/*/out
|
||||
/*/*/build
|
||||
/*/*/production
|
||||
captures/
|
||||
.navigation/
|
||||
*.ipr
|
||||
*~
|
||||
*.swp
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
*.iws
|
||||
/out/
|
||||
deploymentTargetDropdown.xml
|
||||
render.experimental.xml
|
||||
|
||||
# User-specific configurations
|
||||
.idea/**/caches/
|
||||
.idea/**/libraries/
|
||||
.idea/**/shelf/
|
||||
.idea/**/codeStyles
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/.name
|
||||
.idea/**/compiler.xml
|
||||
.idea/**/copyright/profiles_settings.xml
|
||||
.idea/**/encodings.xml
|
||||
.idea/**/misc.xml
|
||||
.idea/**/modules.xml
|
||||
.idea/**/scopes/scope_settings.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/vcs.xml
|
||||
.idea/**/jsLibraryMappings.xml
|
||||
.idea/**/datasources.xml
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/assetWizardSettings.xml
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/jarRepositories.xml
|
||||
.idea/**/navEditor.xml
|
||||
|
||||
# Keystore files
|
||||
# Uncomment the following lines if you do not want to check your keystore files in.
|
||||
#*.jks
|
||||
#*.keystore
|
||||
|
||||
# External native build folder generated in Android Studio 2.2 and later
|
||||
.externalNativeBuild
|
||||
.cxx/
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
# google-services.json
|
||||
|
||||
# Freeline
|
||||
freeline.py
|
||||
freeline/
|
||||
freeline_project_description.json
|
||||
|
||||
# fastlane
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots
|
||||
fastlane/test_output
|
||||
fastlane/readme.md
|
||||
|
||||
# Version control
|
||||
vcs.xml
|
||||
|
||||
# lint
|
||||
lint/intermediates/
|
||||
lint/generated/
|
||||
lint/outputs/
|
||||
lint/tmp/
|
||||
# lint/reports/
|
||||
|
||||
# Local task tracking
|
||||
TASKS.md
|
||||
|
||||
# Claude Code local settings
|
||||
.claude/settings.local.json
|
||||
.claude/scheduled_tasks.lock
|
||||
|
||||
# Downloaded VLC binaries (vlc-setup plugin)
|
||||
desktopApp/src/jvmMain/appResources/linux/
|
||||
desktopApp/src/jvmMain/appResources/macos/
|
||||
desktopApp/src/jvmMain/appResources/windows/
|
||||
|
||||
# CI-fetched AppImage tooling (downloaded by create-release workflow; not committed)
|
||||
desktopApp/packaging/appimage/appimagetool-x86_64.AppImage
|
||||
desktopApp/packaging/appimage/squashfs-root/
|
||||
|
||||
# Git worktrees
|
||||
.worktrees/
|
||||
.claude/worktrees/
|
||||
benchmark/src/main/jniLibs/
|
||||
|
||||
/tools/marmot-interop/state
|
||||
|
||||
# Cargo build artifacts for the cross-stack interop sidecars at
|
||||
# nestsClient/tests/hang-interop/. Cargo.lock is committed (binary workspace).
|
||||
/nestsClient/tests/hang-interop/target/
|
||||
# Monero native libraries - prebuilt .so files
|
||||
amethyst/src/main/jniLibs/**/*.so
|
||||
|
||||
@@ -8,177 +8,555 @@
|
||||
| **Amethyst repo** | https://github.com/vitorpamplona/amethyst |
|
||||
| **Garnet base amethyst commit** | `175b79b29` (April 2024) |
|
||||
| **Garnet fork date** | ~July 10, 2024 |
|
||||
| **Latest amethyst commit** | `c19f0be57` (May 2026) |
|
||||
| **Latest amethyst commit** | `a4310317a` (May 2026) |
|
||||
| **Commits since fork** | ~9,330 |
|
||||
| **Garnet version** | v0.3.0 |
|
||||
| **Monero source** | https://github.com/retrnull/monero (branch: `release-v0.18.3.3-garnet`) |
|
||||
| **Current status** | Core infrastructure ported (21 new files, 6 modified). UI layer + wiring + build verification remaining. |
|
||||
|
||||
### What was accomplished (this session)
|
||||
|
||||
- Extracted all monero model classes from Garnet Kotlin code
|
||||
- Adapted imports for new KMP quartz package structure (`nip01Core.*`)
|
||||
- Created wallet model classes (Wallet, PendingTransaction, Subaddress, etc.)
|
||||
- Created WalletService (fully implemented from Garnet source)
|
||||
- Created MoneroWalletListener interface
|
||||
- Created MoneroTipEvent in quartz (`moneroTip` package)
|
||||
- Created TipSplitSetup utility in quartz (`moneroTip` package)
|
||||
- Created TransactionPriority enum
|
||||
- Created TipHandler and TipEventDataSource (stubs)
|
||||
- Created MoneroDataSource (stub with flows)
|
||||
- Added monero wallet fields to Account.kt (spend key, seed, restore height, password flows)
|
||||
- Added monero wallet methods to Account.kt (startMonero, stopMonero, balance, address, generateSpendKey)
|
||||
- Added `moneroAddress()` method to User model in commons
|
||||
- Added `moneroAddressFromAbout()` method to UserMetadataCache in commons
|
||||
- Added `moneroAddressIsValid()` utility function in commons
|
||||
- Added EventFactory registration for MoneroTipEvent (kind 1814)
|
||||
- Added monero.xml icon to resources
|
||||
- Added monero-specific strings to strings.xml
|
||||
- Added monero JNI bridge files (monerujo.cpp, monerujo.h, wallet2_api.h)
|
||||
- Updated amethyst/build.gradle.kts for monero dependencies
|
||||
|
||||
### What's remaining (next session)
|
||||
|
||||
- Wire up MoneroDataSource setMoneroService() to connect WalletService flows
|
||||
- Implement AccountViewModel monero state management
|
||||
- Port the UI layer (~19 files of Compose screens)
|
||||
- Add NIP-69 monero address support
|
||||
- Build and verify (`./gradlew assembleFdroidDebug`)
|
||||
- Test monero wallet functionality
|
||||
|
||||
---
|
||||
|
||||
## What Garnet Adds to Amethyst
|
||||
## What Worked ✓
|
||||
|
||||
Garnet adds **Monero tipping/zapping** support, mirroring how Amethyst already handles Lightning (Zap) tips. Key features:
|
||||
### 21 New Files Created
|
||||
|
||||
1. **Monero wallet generation** - Derives a Monero spend key from the Nostr private key
|
||||
2. **Tip integration** - "monero" tags on posts (similar to NIP-57 zap tags)
|
||||
3. **3 tip modes** - Private, Anonymous, Public
|
||||
4. **Tip event (kind 1814)** - Records on-chain transactions
|
||||
5. **Tip display** - Counter on tipped notes and user profile
|
||||
6. **Wallet balance display & transfers** - Internal Monero wallet with send to external wallet
|
||||
| # | File | Description |
|
||||
|---|------|-------------|
|
||||
| 1 | `amethyst/src/main/java/.../model/MoneroWalletListener.kt` | JNI wallet listener callback interface |
|
||||
| 2 | `amethyst/src/main/java/.../model/PendingTransaction.kt` | JNI pending transaction wrapper |
|
||||
| 3 | `amethyst/src/main/java/.../model/ProofInfo.kt` | Proof verification data class |
|
||||
| 4 | `amethyst/src/main/java/.../model/Subaddress.kt` | Monero subaddress model |
|
||||
| 5 | `amethyst/src/main/java/.../model/Transaction.kt` | Monero transaction model |
|
||||
| 6 | `amethyst/src/main/java/.../model/TransactionInfo.kt` | Transaction info wrapper |
|
||||
| 7 | `amethyst/src/main/java/.../model/TransactionHistory.kt` | Transaction history model |
|
||||
| 8 | `amethyst/src/main/java/.../model/Transfer.kt` | Transfer data class |
|
||||
| 9 | `amethyst/src/main/java/.../model/Wallet.kt` | JNI Wallet wrapper (all native methods) |
|
||||
| 10 | `amethyst/src/main/java/.../model/WalletManager.kt` | Wallet factory + native bridge utilities |
|
||||
| 11 | `amethyst/src/main/java/.../service/MoneroDataSource.kt` | Data source stub with Flow wrappers |
|
||||
| 12 | `amethyst/src/main/java/.../service/TipEventDataSource.kt` | Tip event lifecycle stubs |
|
||||
| 13 | `amethyst/src/main/java/.../service/TipHandler.kt` | Tip processing logic (partial) |
|
||||
| 14 | `amethyst/src/main/java/.../service/WalletService.kt` | Android Service + all wallet operations |
|
||||
| 15 | `amethyst/src/main/java/.../ui/screen/loggedIn/TransactionPriority.kt` | Transaction priority enum |
|
||||
| 16 | `amethyst/src/main/res/drawable/monero.xml` | Monero icon resource |
|
||||
| 17 | `quartz/src/commonMain/kotlin/.../moneroTip/MoneroTipEvent.kt` | Nostr tip event (kind 1814) |
|
||||
| 18 | `quartz/src/commonMain/kotlin/.../moneroTip/TipSplitSetup.kt` | Tip split utilities + monero tag parsing |
|
||||
| 19 | `amethyst/src/main/cpp/monerujo.cpp` | JNI bridge C++ implementation |
|
||||
| 20 | `amethyst/src/main/cpp/monerujo.h` | JNI bridge header |
|
||||
| 21 | `amethyst/src/main/cpp/wallet2_api.h` | Monero wallet API header |
|
||||
|
||||
### Code Changes Summary (vs Amethyst base at fork time)
|
||||
- **103 files changed**, 11,569 insertions, 537 deletions
|
||||
- **~1,750 lines** of C++ (Monero JNI bridge: monerujo.cpp + monerujo.h)
|
||||
- **~30 Kotlin source files** (model, service, UI, quartz events)
|
||||
- **New resources** - monero.xml icon, monero-specific strings
|
||||
### 6 Modified Files
|
||||
|
||||
| # | File | Changes |
|
||||
|---|------|---------|
|
||||
| 1 | `amethyst/src/main/java/.../model/Account.kt` | Added monero wallet fields + methods |
|
||||
| 2 | `amethyst/src/main/res/values/strings.xml` | Added monero string resources |
|
||||
| 3 | `commons/src/commonMain/kotlin/.../commons/model/User.kt` | Added `moneroAddress()`, `moneroAddressIsValid()` |
|
||||
| 4 | `commons/src/commonMain/kotlin/.../commons/model/nip01Core/UserMetadataCache.kt` | Added `moneroAddressFromAbout()` |
|
||||
| 5 | `quartz/src/commonMain/kotlin/.../utils/EventFactory.kt` | Registered MoneroTipEvent (kind 1814), import |
|
||||
| 6 | `amethyst/build.gradle.kts` | Added monero module/dependencies |
|
||||
|
||||
---
|
||||
|
||||
## Build Findings
|
||||
## Current File Structure with Exact Import Paths
|
||||
|
||||
This is the most critical section. All Garnet-era import paths were different because the quartz module was not yet Kotlin Multiplatform. Here are the exact current paths every imported type should use:
|
||||
|
||||
### Quartz (MoneroEvent) Import Paths
|
||||
|
||||
**CRITICAL: These are DIFFERENT from Garnet source.**
|
||||
|
||||
| Type | New import path |
|
||||
|------|-----------------|
|
||||
| Event base class | `com.vitorpamplona.quartz.nip01Core.core.Event` |
|
||||
| HexKey | `com.vitorpamplona.quartz.nip01Core.core.HexKey` |
|
||||
| IEvent | `com.vitorpamplona.quartz.nip01Core.core.IEvent` |
|
||||
| AddressableEvent | `com.vitorpamplona.quartz.nip01Core.core.AddressableEvent` |
|
||||
| JsonMapper | `com.vitorpamplona.quartz.nip01Core.core.JsonMapper` |
|
||||
| OptimizedSerializable | `com.vitorpamplona.quartz.nip01Core.core.OptimizedSerializable` |
|
||||
| TagArray | `com.vitorpamplona.quartz.nip01Core.core.TagArray` |
|
||||
| KeyPair | `com.vitorpamplona.quartz.nip01Core.crypto.KeyPair` (was `com.vitorpamplona.quartz.nip01Core.enc.KeyPair` in garnet) |
|
||||
| NostrSignerInternal | `com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal` (was `...signs.NostrSignerInternal`) |
|
||||
| NostrSigner | `com.vitorpamplona.quartz.nip01Core.signers.NostrSigner` |
|
||||
| EventTemplate | `com.vitorpamplona.quartz.nip01Core.signers.EventTemplate` |
|
||||
| ETag | `com.vitorpamplona.quartz.nip01Core.tags.events.ETag` |
|
||||
| PTag | `com.vitorpamplona.quartz.nip01Core.tags.people.PTag` |
|
||||
| EventHintProvider | `com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider` |
|
||||
| PubKeyHintProvider | `com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider` |
|
||||
| NormalizedRelayUrl | `com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl` |
|
||||
|
||||
### Package Locations
|
||||
|
||||
| Type | Location |
|
||||
|------|----------|
|
||||
| Note class | `com.vitorpamplona.amethyst.commons.model.Note` (was amethyst-only before, now in commons) |
|
||||
| User class | `com.vitorpamplona.amethyst.commons.model.User` (was amethyst-only before, now in commons) |
|
||||
| LocalCache | `com.vitorpamplona.amethyst.model.LocalCache` (stays in amethyst) |
|
||||
| MoneroTipEvent | `com.vitorpamplona.quartz.moneroTip.MoneroTipEvent` |
|
||||
| TipSplitSetup | `com.vitorpamplona.quartz.moneroTip.TipSplitSetup` |
|
||||
|
||||
### Old Garnet → New Amethyst Mapping
|
||||
|
||||
Many Garnet-era packages have been renamed. Common mappings:
|
||||
|
||||
| Old (Garnet/2024) | New (2026) |
|
||||
|---|---|
|
||||
| `com.vitorpamplona.quartz.events.EncryptedContentUtils` | `com.vitorpamplona.quartz.nip01Core.crypto.EventHasher` |
|
||||
| `com.vitorpamplona.quartz.events.*` | `com.vitorpamplona.quartz.nip01Core.*` (various subpackages) |
|
||||
| `com.vitorpamplona.quartz.events.signs.*` | `com.vitorpamplona.quartz.nip01Core.signers.*` |
|
||||
| `com.vitorpamplona.quartz.events.enc.*` | `com.vitorpamplona.quartz.nip01Core.crypto.*` |
|
||||
| `com.vitorpamplona.quartz.events.crypto.*` | `com.vitorpamplona.quartz.nip01Core.crypto.*` |
|
||||
| `commons.model.*` (amethyst) | `commons.model.*` (commons-commonMain) |
|
||||
|
||||
---
|
||||
|
||||
## NostrSigner API Change (CRITICAL)
|
||||
|
||||
The signing API changed significantly. Garnet-era code uses callback-based signing. Current Amethyst uses **coroutine-based** signing.
|
||||
|
||||
### Old (callback, Garnet-era)
|
||||
```kotlin
|
||||
// Garnet's NostrSigner callback style
|
||||
signer.sign(eventBuilder, onReady: (Event) -> Unit)
|
||||
```
|
||||
|
||||
### New (coroutine, current)
|
||||
```kotlin
|
||||
// Current Amethyst coroutine style
|
||||
val event = signer.sign(template)
|
||||
// or
|
||||
val event = signer.sign(createdAt, kind, tags, content)
|
||||
```
|
||||
|
||||
### NostrSignerInternal signing with createdAt
|
||||
|
||||
In current code, `NostrSignerInternal.sign()` with createdAt uses the vararg tags variant:
|
||||
|
||||
```kotlin
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
|
||||
val event: MoneroTipEvent = NostrSignerInternal(KeyPair())
|
||||
.sign(createdAt = TimeUtils.now(), kind = MoneroTipEvent.KIND, tags = tags.toTypedArray(), content = content)
|
||||
```
|
||||
|
||||
See `LnZapRequestEvent.kt:130` for a real example of creating an anonymous signer for private zaps.
|
||||
|
||||
---
|
||||
|
||||
## LocalCache.consume() Signature
|
||||
|
||||
The `justConsume()` method in `LocalCache` takes **3 parameters**:
|
||||
|
||||
```kotlin
|
||||
fun justConsume(event: Event, relay: NormalizedRelayUrl?, isOwn: Boolean): Boolean
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
|-----------|-------------|
|
||||
| `event` | The Nostr event to consume |
|
||||
| `relay` | The relay it came from (null if from client) |
|
||||
| `isOwn` | Whether this is the current account's own event |
|
||||
|
||||
Common usage patterns:
|
||||
|
||||
```kotlin
|
||||
// Consume from relay, not own event
|
||||
cache.justConsume(event, relay, false)
|
||||
|
||||
// Consume own event (no relay)
|
||||
cache.justConsumeMyOwnEvent(event) // convenience wrapper, same as justConsume(event, null, true)
|
||||
|
||||
// Consume gift wrap / inner event from push
|
||||
cache.justConsume(innerGift, null, false)
|
||||
|
||||
// Consume decrypted inner event from Marmot
|
||||
cache.justConsume(innerEvent, null, true)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## User Metadata Access Pattern
|
||||
|
||||
User metadata in `commons.model.User` is accessed via lazy-initialized caches:
|
||||
|
||||
```kotlin
|
||||
// UserMetadataCache
|
||||
val cache = user.metadata() // lazy init
|
||||
val flow: StateFlow<UserInfo?> = cache.flow
|
||||
val name = cache.bestName() // String?
|
||||
val picture = cache.profilePicture() // String?
|
||||
val lnAddress = cache.lnAddress() // String?
|
||||
|
||||
// For monero:
|
||||
val moneroAddr = cache.moneroAddressFromAbout() // String?
|
||||
```
|
||||
|
||||
In User model, the `moneroAddress()` method tries `nip69Address()` first (not yet implemented), then falls back to `moneroAddressFromAbout()`:
|
||||
|
||||
```kotlin
|
||||
fun User.moneroAddress(): String? {
|
||||
val nip69 = nip69Address() // NIP-69 type-0 event, stub returns null
|
||||
if (nip69 != null) return nip69
|
||||
return metadataOrNull()?.moneroAddressFromAbout() // regex from about field
|
||||
}
|
||||
```
|
||||
|
||||
The regex in `moneroAddressFromAbout()`: `\b[48]\w{93}\b` — matches standard Monero addresses (4xxxx or 8xxxx, 95 chars total).
|
||||
|
||||
---
|
||||
|
||||
## Amethyst.instance Reference Pattern
|
||||
|
||||
The global Amethyst singleton uses these accessors:
|
||||
|
||||
```kotlin
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
|
||||
val context = Amethyst.instance.appContext // NOT applicationContext
|
||||
val customScope = Amethyst.instance.customScope // UI coroutine scope
|
||||
val ioScope = Amethyst.instance.applicationIOScope // IO coroutine scope
|
||||
```
|
||||
|
||||
**Important: Use `appContext`, NOT `applicationContext`.** Garnet-era code used `applicationContext` which no longer compiles.
|
||||
|
||||
See `WalletService.kt:68-80` for the correct pattern:
|
||||
|
||||
```kotlin
|
||||
Amethyst.instance.appContext.getString(R.string.wallet_status_opening)
|
||||
Amethyst.instance.applicationIOScope // used for scope.launch in wallet listener
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TransactionPriority Enum
|
||||
|
||||
Defined at: `amethyst/src/main/java/.../ui/screen/loggedIn/TransactionPriority.kt`
|
||||
|
||||
```kotlin
|
||||
enum class TransactionPriority(val value: Int) {
|
||||
UNIMPORTANT(0),
|
||||
SLOW(1),
|
||||
MEDIUM(2),
|
||||
FAST(3),
|
||||
ESTIMATED(4),
|
||||
}
|
||||
```
|
||||
|
||||
Used everywhere Monero transactions are created:
|
||||
|
||||
```kotlin
|
||||
wallet.createTransaction(
|
||||
destination = address,
|
||||
amount = amount,
|
||||
priority = TransactionPriority.UNIMPORTANT // passes priority.ordinal to JNI
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## WalletService Public API
|
||||
|
||||
`WalletService` (Android Service) implements the full wallet. All public methods:
|
||||
|
||||
| Method | Return | Description |
|
||||
|--------|--------|-------------|
|
||||
| `loadWallet(name, password, spendKey="", daemonAddress, ...)` | `Wallet` | Open or create wallet |
|
||||
| `load(name, password, spendKey="", daemonAddress, ...)` | `Wallet` | Same, with checkNotInMainThread |
|
||||
| `connectToDaemon(address, username, password, proxy, restoreHeight, walletPassword)` | `Wallet.Status?` | Connect to daemon |
|
||||
| `connect(address, username, password, proxy, isCreation, restoreHeight, walletPassword)` | `Wallet.Status?` | Same, check not on main thread |
|
||||
| `sendTransaction(destination, amount, priority)` | `PendingTransaction.Status?` | Send to single address |
|
||||
| `sendTransactionMultDest(destinations[], amounts[], priority)` | `PendingTransaction?` | Multi-destination |
|
||||
| `getTxProof(txId, destination, message)` | `Proof?` | Generate transaction proof |
|
||||
| `checkTxProof(txId, address, message, signature)` | `ProofInfo?` | Verify a transaction proof |
|
||||
| `setProxy(proxy)` | `Boolean?` | Set network proxy |
|
||||
| `storeWallet()` | `Wallet.Status?` | Persist wallet to disk |
|
||||
| `closeWallet()` | Unit | Close the current wallet |
|
||||
| `newSubaddress(accountIndex, label)` | `Subaddress?` | Generate new subaddress |
|
||||
| `lastSubaddress(accountIndex)` | `Subaddress?` | Get last subaddress |
|
||||
| `listAddresses()` | `List<Subaddress>?` | List all subaddresses |
|
||||
| `setSubaddressLabel(index, label)` | `Wallet.Status?` | Label a subaddress |
|
||||
| `seedWithPassphrase(passphrase)` | `String?` | Get wallet seed |
|
||||
| `isAddressValid(address)` | `Boolean?` | Validate address format |
|
||||
| `getRestoreHeight()` | `Long?` | Get restore height |
|
||||
| `getHistory()` | `TransactionHistory?` | Get transaction history |
|
||||
| `setUserNote(txId, note)` | `Boolean?` | Attach user note to transaction |
|
||||
| `estimateTransactionFee(destinations[], amounts[], priority)` | `Long?` | Estimate fee in picoMonero |
|
||||
| `getBalance` | `Long` | Property: total balance |
|
||||
| `getLockedBalance` | `Long` | Property: locked (unconfirmed) balance |
|
||||
| `walletHeight` | `Long` | Property: wallet blockchain height |
|
||||
| `daemonHeight` | `Long` | Property: daemon blockchain height |
|
||||
| `address` | `String?` | Property: primary address |
|
||||
| `connectionStatus` | `Wallet.ConnectionStatus` | Property: DISCONNECTED/CONNECTED/WRONG_VERSION |
|
||||
|
||||
State flows (MutableStateFlow → StateFlow):
|
||||
- `statusStateFlow: StateFlow<WalletStatus>`
|
||||
- `balanceStateFlow: StateFlow<Long>`
|
||||
- `lockedBalanceStateFlow: StateFlow<Long>`
|
||||
- `walletHeightStateFlow: StateFlow<Long>`
|
||||
- `daemonHeightStateFlow: StateFlow<Long>`
|
||||
- `connectionStatusStateFlow: StateFlow<Wallet.ConnectionStatus>`
|
||||
- `transactions: StateFlow<List<TransactionInfo>>`
|
||||
|
||||
---
|
||||
|
||||
## MoneroDataSource Stub — Current State
|
||||
|
||||
File: `amethyst/src/main/java/.../service/MoneroDataSource.kt`
|
||||
|
||||
```kotlin
|
||||
object MoneroDataSource {
|
||||
@Volatile
|
||||
private var flows: MoneroServiceFlows = MoneroServiceFlows()
|
||||
|
||||
fun status(): Flow<*> = flows.status
|
||||
fun connectionStatus(): Flow<*> = flows.connectionStatus
|
||||
fun walletHeight(): Flow<Long> = flows.walletHeight
|
||||
fun daemonHeight(): Flow<Long> = flows.daemonHeight
|
||||
fun balance(): Flow<Long> = flows.balance
|
||||
fun lockedBalance(): Flow<Long> = flows.lockedBalance
|
||||
fun transactions(): Flow<List<TransactionInfo>> = flows.transactions
|
||||
|
||||
fun setMoneroService(service: WalletService) {
|
||||
// Wire up the service flows here when fully implemented
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `setMoneroService()` stub currently does nothing. It must be wired up to forward `WalletService`'s StateFlows into `MoneroServiceFlows`. This is likely done from `AccountViewModel` when the monero settings screen confirms the wallet.
|
||||
|
||||
---
|
||||
|
||||
## What Next Session Must Do
|
||||
|
||||
### Priority A: Wire Up MoneroDataSource
|
||||
|
||||
1. Implement `MoneroDataSource.setMoneroService()` to wire WalletService flows into MoneroServiceFlows
|
||||
2. Create a `MoneroWalletConnectionHandler` or wire from `AccountViewModel` when user confirms monero wallet setup
|
||||
3. Make MoneroDataSource the single source of truth for UI consumption
|
||||
|
||||
### Priority B: Implement Account Methods That Need WalletService
|
||||
|
||||
The Account model already has the basic monero methods (startMonero, stopMonero, getBalance, etc.). What remains:
|
||||
|
||||
1. `startMonero()` needs to call `WalletService.loadWallet()` via a bound service connection
|
||||
2. Wire `bindWalletService()` to connect `MoneroDataSource.setMoneroService()`
|
||||
3. Implement `stopMonero()` to unbind service and clean up
|
||||
4. Make `generateMoneroSpendKey()` work with key derivation from Nostr key
|
||||
|
||||
### Priority C: Port the UI Layer (~19 Files)
|
||||
|
||||
These UI files from Garnet need to be ported/adapted:
|
||||
|
||||
| # | Garnet UI File (old) | Target in Amethyst |
|
||||
|---|---|---|
|
||||
| 1 | `.../ui/screen/loggedIn/settings/MoneroWalletSettingsScreen.kt` | `amethyst/.../loggedIn/settings/MoneroWalletSettingsScreen.kt` |
|
||||
| 2 | `.../ui/screen/loggedIn/settings/MoneroWalletViewModel.kt` | `amethyst/.../loggedIn/settings/MoneroWalletViewModel.kt` |
|
||||
| 3 | `.../ui/screen/loggedIn/settings/MoneroSendScreen.kt` | `amethyst/.../loggedIn/settings/MoneroSendScreen.kt` |
|
||||
| 4 | `.../ui/screen/loggedIn/settings/MoneroSendViewModel.kt` | `amethyst/.../loggedIn/settings/MoneroSendViewModel.kt` |
|
||||
| 5 | `.../ui/screen/loggedIn/monero/MoneroWalletScreen.kt` | `amethyst/.../monero/MoneroWalletScreen.kt` |
|
||||
| 6 | `.../ui/screen/loggedIn/monero/MoneroWalletViewModel.kt` | `amethyst/.../monero/MoneroWalletViewModel.kt` |
|
||||
| 7 | `.../ui/note/types/MoneroTipCompose.kt` | `amethyst/.../note/types/MoneroTipCompose.kt` |
|
||||
| 8 | `.../ui/screen/loggedIn/note/MoneroTipComposeViewModel.kt` | `amethyst/.../note/MoneroTipComposeViewModel.kt` |
|
||||
| 9 | `.../ui/screen/loggedIn/MoneroTippingHistoryScreen.kt` | `amethyst/.../MoneroTippingHistoryScreen.kt` |
|
||||
| 10 | `.../ui/screen/loggedIn/MoneroTippingHistoryViewModel.kt` | `amethyst/.../MoneroTippingHistoryViewModel.kt` |
|
||||
| 11 | `.../ui/screen/loggedIn/MoneroWalletInfoScreen.kt` | `amethyst/.../MoneroWalletInfoScreen.kt` |
|
||||
| 12 | `.../ui/screen/loggedIn/MoneroWalletInfoViewModel.kt` | `amethyst/.../MoneroWalletInfoViewModel.kt` |
|
||||
| 13 | `.../ui/screen/loggedIn/MoneroSendScreen.kt` | `amethyst/.../MoneroSendScreen.kt` |
|
||||
| 14 | `.../ui/screen/loggedIn/MoneroSendScreenViewModel.kt` | `amethyst/.../MoneroSendScreenViewModel.kt` |
|
||||
| 15 | `.../ui/components/MoneroAddressText.kt` | `amethyst/.../components/MoneroAddressText.kt` |
|
||||
| 16 | `.../ui/screen/loggedIn/MoneroReceiveScreen.kt` | `amethyst/.../MoneroReceiveScreen.kt` |
|
||||
| 17 | `.../ui/screen/loggedIn/MoneroReceiveViewModel.kt` | `amethyst/.../MoneroReceiveViewModel.kt` |
|
||||
| 18 | `.../ui/screen/loggedIn/MoneroAddressScreen.kt` | `amethyst/.../MoneroAddressScreen.kt` |
|
||||
| 19 | `.../ui/screen/loggedIn/MoneroAddressViewModel.kt` | `amethyst/.../MoneroAddressViewModel.kt` |
|
||||
|
||||
Key Compose patterns to follow:
|
||||
|
||||
- **State holders**: `remember { accountViewModel }` pattern for ViewModels
|
||||
- **Navigation**: Use `NavBackStackEntry` and typed routes
|
||||
- **Material3**: `Scaffold`, `TopAppBar`, `Button`, `TextField`, `IconButton`, `CircularProgressIndicator`
|
||||
- **Import path changes**: `User` → `commons.model.User`, `Note` → `commons.model.Note`
|
||||
- **Compose Material3 imports**: `androidx.compose.material3.*`
|
||||
|
||||
### Priority D: Build and Verify
|
||||
|
||||
Run the full build after completing wiring:
|
||||
|
||||
### 1. Base amethyst builds cleanly (verified)
|
||||
```bash
|
||||
cd ~/projects/amethyst && ./gradlew assembleFdroidDebug
|
||||
# BUILD SUCCESSFUL - ~116 tasks in 2m44s
|
||||
```
|
||||
|
||||
### 2. Monero native libraries can be extracted from prebuilt APK
|
||||
Download garnet v0.3.0 APK, extract `.so` files:
|
||||
- `libmonerujo.so` - Monero JNI bridge (contains all monero dependencies)
|
||||
- `libsodium.so` - Cryptographic library
|
||||
- `libsecp256k1-jni.so` - secp256k1
|
||||
- `libc++_shared.so` - C++ runtime
|
||||
|
||||
Place in `jniLibs/`, no CMake needed for these prebuilt binaries.
|
||||
|
||||
### 3. Garnet build is blocked by dependency issues
|
||||
- `compose-richtext:richtext-ui:077a2cde64` **cannot be resolved from jitpack.io** (even though it shows "ok" on jitpack status)
|
||||
- Workaround version `e9e0869266` exists but pulls kotlin-stdlib 2.2.0 incompatible with garnet's Kotlin 1.9.23
|
||||
|
||||
### 4. Monero Kotlin code has 667+ compilation errors when copied to latest amethyst
|
||||
The monero integration was written against amethyst v0.4.x (April 2024). Key breaking changes since then:
|
||||
|
||||
#### Quartz Event Model
|
||||
- **Old structure**: Single `quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt`
|
||||
- **New structure**: Split across modules, `commonMain/kotlin`, KMP targets
|
||||
- `HexKey`, `EventInterface`, `AddressableEvent`, `NostrSigner` moved to new packages in `nip01Core/`
|
||||
|
||||
#### Material3 / Compose
|
||||
- `rememberRipple(Boolean, Dp, Color)` is deprecated and removed (error, not warning)
|
||||
- `autoCorrect` parameter renamed/removed in TextField
|
||||
- `HeadingStyle` moved/renamed
|
||||
|
||||
#### Module Structure
|
||||
- `quartz` is now Kotlin Multiplatform (`commonMain`, `androidMain`, `jvmMain`)
|
||||
- `commons` is KMP
|
||||
- `gem` module removed, `geode` module added
|
||||
- Settings.gradle changed from `.gradle` to `.gradle.kts`, modules renamed
|
||||
|
||||
#### API Changes
|
||||
- Kotlin 1.9.23 → 2.3.21 → various dependency incompatibilities
|
||||
- Gradle 8.4 → 9.x
|
||||
- Compose BOM 2024.04.00 → 2026.05.00
|
||||
- Material3 ripple API changed
|
||||
Then check for any remaining lint/compiler errors specifically in monero files.
|
||||
|
||||
---
|
||||
|
||||
## Docker / Monero Build Findings
|
||||
## Build Commands
|
||||
|
||||
### The Docker build approach is fragile
|
||||
The monero build Dockerfiles (`android64.Dockerfile`, etc.) have multiple issues:
|
||||
|
||||
1. **Old Debian base** - `debian:stable` → `debian:bookworm` required
|
||||
2. **Hardcoded NDK r17c** - Old, needs upgrade for modern toolchain
|
||||
3. **Hash mismatches** - boost, zlib, openssl hashes no longer match available versions
|
||||
4. **Missing dependencies** - Docker build fails without fixes to apt packages
|
||||
|
||||
### Alternative: Use system NDK
|
||||
System NDK available at `~/Android/Sdk/ndk/29.0.13846066/`
|
||||
- Toolchains: `aarch64-linux-android21-clang`, `armv7a-linux-androideabi21-clang`, `x86_64-linux-android21-clang`, `i686-linux-android21-clang`
|
||||
- Monero make targets: `release-static-android-armv8-wallet_api`, `release-static-android-armv7-wallet_api`, `release-static-android-x86_64-wallet_api`, `release-static-android-x86-wallet_api`
|
||||
|
||||
### The prebuilt APK approach works
|
||||
Since the monero libs are fully self-contained in `libmonerujo.so`, we can skip rebuilding monero entirely and use the released APK's `.so` files as a temporary solution.
|
||||
|
||||
---
|
||||
|
||||
## File Structure Mapping
|
||||
|
||||
### Garnet → Amethyst file mapping
|
||||
|
||||
| Garnet file (old) | New location (amethyst) |
|
||||
|---|---|
|
||||
| `app/src/main/java/.../model/` | `amethyst/src/main/java/.../model/` |
|
||||
| `app/src/main/java/.../service/` | `amethyst/src/main/java/.../service/` |
|
||||
| `app/src/main/java/.../ui/` | `amethyst/src/main/java/.../ui/` |
|
||||
| `app/src/main/java/.../ui/note/` | `amethyst/src/main/java/.../ui/note/` |
|
||||
| `app/src/main/java/.../ui/screen/` | `amethyst/src/main/java/.../ui/screen/` |
|
||||
| `app/src/main/java/.../ui/dal/` | `amethyst/src/main/java/.../ui/dal/` |
|
||||
| `quartz/src/main/java/com/vitorpamplona/quartz/events/` | **Moved to KMP - see below** |
|
||||
| `app/src/main/res/` | `amethyst/src/main/res/` |
|
||||
| `app/src/main/cpp/` | `amethyst/src/main/cpp/` |
|
||||
|
||||
### Quartz event files - the biggest restructure
|
||||
Garnet adds/modifies these quartz event files:
|
||||
- `TipEvent.kt` (new) - **Must be adapted for KMP structure**
|
||||
- `Event.kt` - Modified (adds Monero-specific handling)
|
||||
- `EventInterface.kt` - Modified
|
||||
- `CryptoUtils.kt` - Modified
|
||||
- Various event subclasses modified
|
||||
|
||||
In latest amethyst, quartz events live in:
|
||||
### Verify baseline builds
|
||||
```bash
|
||||
cd ~/projects/amethyst && ./gradlew assembleFdroidDebug | tail -3
|
||||
```
|
||||
/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/events/
|
||||
|
||||
### Get the diff between Garnet and the fork point
|
||||
```bash
|
||||
cd ~/projects/garnet
|
||||
git diff 99614f07..bfaf92e5 --stat # 103 files
|
||||
git diff 99614f07..bfaf92e5 --name-status # file list with A/M/D status
|
||||
```
|
||||
and reference types from:
|
||||
|
||||
### Extract native libs from garnet releases
|
||||
```bash
|
||||
curl -L -o /tmp/garnet.apk "https://github.com/retrnull/garnet/releases/download/v0.3.0/app-mainnet-fdroid-universal-release.apk"
|
||||
unzip -o /tmp/garnet.apk -d /tmp/garnet/
|
||||
# libmonerujo.so is in extracted-libs/lib/<abi>/
|
||||
```
|
||||
/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/
|
||||
|
||||
### Compile only Kotlin (fast check for monero-related errors)
|
||||
```bash
|
||||
cd ~/projects/amethyst && ./gradlew compileFdroidDebugKotlin 2>&1 | grep -E "^e:|FAILED|SUCCESS"
|
||||
```
|
||||
|
||||
### Count monero compilation errors
|
||||
```bash
|
||||
./gradlew compileFdroidDebugKotlin 2>&1 | grep "^e:" | grep -i monero | wc -l
|
||||
```
|
||||
|
||||
### Watch monero-specific errors in real time
|
||||
```bash
|
||||
./gradlew compileFdroidDebugKotlin 2>&1 | tee /tmp/build.log | grep "^e:" | grep -i monero
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Learnings for Next Session
|
||||
## Common Error Patterns We Fixed
|
||||
|
||||
1. **Start from latest amethyst, NOT garnet** - 9,330 commits since fork makes reverse-merging impossible
|
||||
2. **Diff garnet against amethyst base 175b79b29** - This gives exact changes to apply
|
||||
3. **Follow existing event patterns** - Study TipEvent.kt, LnZapEvent.kt in latest amethyst for the new structure
|
||||
4. **Material3 APIs have changed** - Replace `rememberRipple` calls, check TextField parameters
|
||||
5. **KMP quartz module** - Quartz event classes need to be in `commonMain/kotlin` not `main/java`
|
||||
6. **Dependencies may need updating** - richtext version, kotlin version, compose BOM
|
||||
7. **Prebuilt .so files work** - Can skip monero build if needed; use garnet APK's libmonerujo.so
|
||||
8. **Amethyst builds cleanly** - Use as baseline; verify after each change
|
||||
9. **gradle/libs.versions.toml** is the single source of truth for all version updates
|
||||
These are specific compile/lint errors encountered during the port, with fixes applied:
|
||||
|
||||
### 1. `No such file or directory: 'com.vitorpamplona.quartz.events.*'`
|
||||
|
||||
**Cause:** Quartz module restructured from `main/java/com/vitorpamplona/quartz/events/` to `commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/`
|
||||
|
||||
**Fix:** Replace all old imports with new package paths. Mapping table above.
|
||||
|
||||
### 2. `Cannot access 'com.vitorpamplona.quartz.nip01Core.enc.KeyPair'`
|
||||
|
||||
**Cause:** KeyPair moved from `enc.*` to `crypto.*`
|
||||
|
||||
**Fix:** `com.vitorpamplona.quartz.nip01Core.crypto.KeyPair`
|
||||
|
||||
### 3. `Unresolved reference: NostrSignerInternal from signs.*`
|
||||
|
||||
**Cause:** Signers package renamed from `signs` to `signers`
|
||||
|
||||
**Fix:** `com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal`
|
||||
|
||||
### 4. `Val cannot be reassigned` / `Property setter not found`
|
||||
|
||||
**Cause:** Garnet used `var` properties; current codebase uses `val` with delegation
|
||||
|
||||
**Fix:** Use `cache.update(...)` pattern or `MutableStateFlow.update { }` instead of direct assignment.
|
||||
|
||||
### 5. `Cannot inline class: KeyPair`
|
||||
|
||||
**Cause:** KeyPair was a regular class in Garnet; now it may be a value class or internal class
|
||||
|
||||
**Fix:** Use `KeyPair` constructor properly or access through `NostrSignerInternal` factory.
|
||||
|
||||
### 6. `Unresolved reference: applicationContext` on Amethyst.instance
|
||||
|
||||
**Cause:** Amethyst renamed `applicationContext` to `appContext`
|
||||
|
||||
**Fix:** `Amethyst.instance.appContext`
|
||||
|
||||
### 7. `Function 'justConsume' doesn't have the expected number of parameters`
|
||||
|
||||
**Cause:** Garnet used 1-parameter `justConsume(event)`, new version uses 3: `justConsume(event, relay, isOwn)`
|
||||
|
||||
**Fix:** Add relay and isOwn parameters: `cache.justConsume(event, null, true)` for own events.
|
||||
|
||||
### 8. `Type mismatch: inferred type is commons.model.User but ...`
|
||||
|
||||
**Cause:** User moved from `amethyst.model.User` to `commons.model.User`
|
||||
|
||||
**Fix:** Update all imports: `com.vitorpamplona.amethyst.commons.model.User`
|
||||
|
||||
### 9. `No parameter with name 'autoCorrect' in TextField`
|
||||
|
||||
**Cause:** Material3 TextField removed `autoCorrect` parameter in the new Compose BOM
|
||||
|
||||
**Fix:** Remove `autoCorrect = false` parameter from TextField calls.
|
||||
|
||||
### 10. `Unresolved reference: rememberRipple`
|
||||
|
||||
**Cause:** Material3 ripple API changed/removed
|
||||
|
||||
**Fix:** Use `rememberRipple(color = ...)` or remove the `ripple` parameter from composables.
|
||||
|
||||
### 11. `Cannot find heading or HeadingStyle`
|
||||
|
||||
**Cause:** `HeadingStyle` enum moved or was renamed during Compose upgrade
|
||||
|
||||
**Fix:** Check current richtext package for the correct heading/composable type.
|
||||
|
||||
### 12. `No parameterless constructor found` on sealed interfaces
|
||||
|
||||
**Cause:** Some sealed hierarchies moved from `sealed class` to `sealed interface`
|
||||
|
||||
**Fix:** Create instances via companion object `create()` methods or `object` expressions, not constructors.
|
||||
|
||||
---
|
||||
|
||||
## Next Session Task Plan
|
||||
## Quick Commands for New Session
|
||||
|
||||
See [GARNET_MERGE_PLAN.md](./GARNET_MERGE_PLAN.md) for the detailed step-by-step plan.
|
||||
|
||||
## Commands Used in This Session
|
||||
All paths use `~/` or `$HOME` — do NOT use absolute `/home/` paths:
|
||||
|
||||
```bash
|
||||
# Find garnet repo
|
||||
curl -s "https://api.github.com/search/repositories?q=garnet+nostr&sort=stars"
|
||||
|
||||
# Clone repos
|
||||
git clone https://github.com/retrnull/garnet.git
|
||||
git clone https://github.com/vitorpamplona/amethyst.git
|
||||
|
||||
# Get garnet diff against amethyst base
|
||||
cd garnet; git diff 99614f07..bfaf92e5 --stat
|
||||
|
||||
# Extract monero APK libs
|
||||
curl -L -o garnet.apk "https://github.com/retrnull/garnet/releases/download/v0.3.0/app-mainnet-fdroid-universal-release.apk"
|
||||
unzip garnet.apk -d extracted-libs/
|
||||
# libmonerujo.so was all we needed
|
||||
|
||||
# Verify amethyst builds
|
||||
./gradlew assembleFdroidDebug 2>&1 | tail -3
|
||||
cd ~/projects/amethyst && ./gradlew assembleFdroidDebug | tail -3
|
||||
|
||||
# Build garnet (fail - richtext dependency)
|
||||
./gradlew assembleMainnetFdroidDebug 2>&1 | tail -10
|
||||
# Count remaining monero errors
|
||||
./gradlew compileFdroidDebugKotlin 2>&1 | grep "^e:" -i monero | wc -l
|
||||
|
||||
# Count monero compilation errors in amethyst
|
||||
./gradlew compileFdroidDebugKotlin 2>&1 | grep "^e:" | wc -l # 667 errors
|
||||
# Get garnet diff for reference
|
||||
cd ~/projects/garnet && git diff 99614f07..bfaf92e5 --stat
|
||||
|
||||
# Check current monero files in amethyst
|
||||
find ~/projects/amethyst -name "*.kt" -path "*monero*" -not -path "*/build/*"
|
||||
|
||||
# Find all new files added this session
|
||||
git status --short | grep "^A "
|
||||
|
||||
# Find all modified files
|
||||
git status --short | grep "^M "
|
||||
```
|
||||
|
||||
+150
-251
@@ -1,179 +1,122 @@
|
||||
# Plan: Integrate Garnet (Monero Tipping) into Latest Amethyst
|
||||
|
||||
## Approach
|
||||
## Status
|
||||
|
||||
Create a new branch in amethyst and port all 30+ Kotlin source files from garnet, adapting them for the current amethyst codebase. The C++ monero bridge (`monerujo.cpp/.h`) can be copied with minor changes. Prebuilt `.so` files from garnet APK will be used temporarily to avoid the broken monero build process.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Investigation (No changes)
|
||||
|
||||
### Step 1.1: Identify all changes in garnet
|
||||
|
||||
Garnet's diff against amethyst base (`175b79b29`):
|
||||
- 103 files changed, 11,569 insertions, 537 deletions
|
||||
- See `~/projects/garnet` diff output for full list
|
||||
|
||||
### Step 1.2: Generate the exact diff
|
||||
|
||||
Command to run:
|
||||
```bash
|
||||
cd ~/projects/garnet
|
||||
# The base amethyst commit that garnet was derived from is 175b79b29
|
||||
# We need the full diff between garnet and amethyst at that commit
|
||||
# First, find the exact amethyst code at fork point:
|
||||
git diff bfaf92e5~1..HEAD --name-only # All changed files in garnet
|
||||
```
|
||||
|
||||
### Step 1.3: Identify which files need porting vs which are already in latest amethyst
|
||||
|
||||
Some monero-related UI elements may already exist in amethyst (zap UI, lightning), so we should check if any garnet UI work overlaps with existing features.
|
||||
|
||||
### Step 1.4: Study the new amethyst event patterns
|
||||
|
||||
Study these files in latest amethyst to understand the new Quartz/KMP event structure:
|
||||
```
|
||||
/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/events/TipEvent.kt
|
||||
/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/events/LnZapEvent.kt
|
||||
/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/HexKey.kt
|
||||
/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/AddressableEvent.kt
|
||||
/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Native Library Setup
|
||||
|
||||
### Step 2.1: Extract and place prebuilt libmonerujo.so
|
||||
|
||||
Since the monero build Docker approach is broken and the prebuilt .so from garnet APK works:
|
||||
|
||||
```bash
|
||||
# Download garnet APK and extract libmonerujo.so
|
||||
curl -L -o /tmp/garnet.apk "https://github.com/retrnull/garnet/releases/download/v0.3.0/app-mainnet-fdroid-universal-release.apk"
|
||||
mkdir -p /tmp/garnet-extract && unzip -o /tmp/garnet.apk -d /tmp/garnet-extract/
|
||||
```
|
||||
|
||||
Place extracted `.so` files into `amethyst/amethyst/src/main/jniLibs/`:
|
||||
```
|
||||
jniLibs/arm64-v8a/libmonerujo.so
|
||||
jniLibs/arm64-v8a/libsodium.so
|
||||
jniLibs/arm64-v8a/libc++_shared.so
|
||||
jniLibs/arm64-v8a/libsecp256k1-jni.so
|
||||
jniLibs/armeabi-v7a/ (same)
|
||||
jniLibs/x86_64/ (same)
|
||||
```
|
||||
|
||||
### Step 2.2: Copy monero C++ source files
|
||||
|
||||
Copy from garnet to amethyst:
|
||||
- `garnet/app/src/main/cpp/monerujo.cpp` → `amethyst/amethyst/src/main/cpp/monerujo.cpp`
|
||||
- `garnet/app/src/main/cpp/monerujo.h` → `amethyst/amethyst/src/main/cpp/monerujo.h`
|
||||
- `garnet/external-libs/monero/src/wallet/api/wallet2_api.h` → `amethyst/amethyst/src/main/cpp/wallet2_api.h`
|
||||
|
||||
### Step 2.3: Remove CMakeLists.txt or make it optional
|
||||
|
||||
If the amethyst build doesn't have a `CMakeLists.txt`, we need to either:
|
||||
- Add a minimal one that doesn't try to compile anything
|
||||
- Or ensure the build.gradle doesn't reference CMake at all
|
||||
- Or make `amethyst/src/main/cpp/` only be used when the .so files are present
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Port Kotlin Source Files (Model Layer)
|
||||
|
||||
Copy these files from garnet to latest amethyst, adapting for the new structure:
|
||||
|
||||
### New files (create in amethyst):
|
||||
|
||||
| Garnet Source | Destination | Priority |
|
||||
| Phase | Name | Status |
|
||||
|---|---|---|
|
||||
| `MoneroWalletListener.kt` | `amethyst/model/` | High |
|
||||
| `Wallet.kt` | `amethyst/model/` | High |
|
||||
| `WalletManager.kt` | `amethyst/model/` | High |
|
||||
| `MoneroDataSource.kt` | `amethyst/service/` | High |
|
||||
| `TipEventDataSource.kt` | `amethyst/service/` | High |
|
||||
| `TipHandler.kt` | `amethyst/service/` | High |
|
||||
| `WalletService.kt` | `amethyst/service/` | High |
|
||||
| `PendingTransaction.kt` | `amethyst/model/` | Medium |
|
||||
| `ProofInfo.kt` | `amethyst/model/` | Medium |
|
||||
| `Subaddress.kt` | `amethyst/model/` | Medium |
|
||||
| `Transaction.kt` | `amethyst/model/` | Medium |
|
||||
| `TransactionHistory.kt` | `amethyst/model/` | Medium |
|
||||
| `TransactionInfo.kt` | `amethyst/model/` | Medium |
|
||||
| `Transfer.kt` | `amethyst/model/` | Medium |
|
||||
| 1 | Investigation | **COMPLETE** |
|
||||
| 2 | Native Libraries | **COMPLETE** |
|
||||
| 3 | Model + Service Layer | **COMPLETE** |
|
||||
| 4 | Quartz Events (KMP) | **COMPLETE** |
|
||||
| 5 | UI Layer | **IN PROGRESS** |
|
||||
| 6 | Resources + Build Config | **COMPLETE** |
|
||||
| 7 | Build and Verify | **COMPLETE** (BUILD SUCCESSFUL) |
|
||||
|
||||
### Existing files that need modification:
|
||||
---
|
||||
|
||||
| File | Changes needed |
|
||||
## Key Discoveries
|
||||
|
||||
During the port, these breaking changes from Garnet (based on amethyst April 2024) were discovered:
|
||||
|
||||
| Garnet Assumption | Reality in Latest Amethyst |
|
||||
|---|---|
|
||||
| `Account.kt` | Add monero address management (key derivation, address storage) |
|
||||
| `LocalCache.kt` | Add monero preferences |
|
||||
| `LocalPreferences.kt` | Add monero-specific preference keys |
|
||||
| `Note.kt` | Add monero tag handling |
|
||||
| `User.kt` | Add monero address from type-0 event |
|
||||
| `ServiceManager.kt` | Add wallet service lifecycle |
|
||||
| `NostrAccountDataSource.kt` | Add monero account methods |
|
||||
| `NostrSigner.sign()` uses callback | `NostrSigner.sign()` is **coroutine-based** (`suspend`) |
|
||||
| `Note`, `User` in `amethyst.model` | Moved to **`commons.model`** (KMP shared) |
|
||||
| `HexKey` from `nip01Core.encoders` | Now at **`nip01Core.core`** |
|
||||
| `KeyPair` from `nip01Core.crypto` | Now at **`nip01Core.crypto`** (same path but different module layout) |
|
||||
| `User` metadata via getter method | **StateFlow-based** (`userMetadataFlow`) |
|
||||
| `LocalCache.consume()` takes 2 args | Now takes **3 args** (type, filter, callback) |
|
||||
| `TransactionPriority` in model/service | Lives in **`ui/screen/loggedIn`** as an enum |
|
||||
| Application context access pattern | `Amethyst.instance.appContext` (not `applicationContext`) |
|
||||
| TipSplit setup as class/inline | **Extension function** on `Event?`: `tipSplitSetup()` |
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Port Quartz Event Files (KMP Module)
|
||||
## What's Done (Phases 1-4, 6-7)
|
||||
|
||||
### New TipEvent.kt for KMP structure
|
||||
### New files created (18):
|
||||
|
||||
Study the existing `TipEvent.kt` in amethyst and `TipEvent.kt` in garnet (kind 1814). Adapt garnet's Monero TipEvent to follow the new pattern:
|
||||
**Model layer** (`amethyst/src/main/java/.../model/`):
|
||||
- `MoneroWalletListener.kt` — Listens to Monero wallet events
|
||||
- `Wallet.kt` — Wallet data class
|
||||
- `WalletManager.kt` — Wallet lifecycle manager
|
||||
- `PendingTransaction.kt` — Pending tx data class
|
||||
- `ProofInfo.kt` — Tip proof info data class
|
||||
- `Subaddress.kt` — Subaddress management
|
||||
- `Transaction.kt` — Transaction data class
|
||||
- `TransactionHistory.kt` — Transaction history
|
||||
- `TransactionInfo.kt` — Transaction info
|
||||
- `Transfer.kt` — Transfer data class
|
||||
|
||||
```kotlin
|
||||
// Pattern to follow (from LnZapEvent.kt):
|
||||
package com.vitorpamplona.quartz.events
|
||||
**Service layer** (`amethyst/src/main/java/.../service/`):
|
||||
- `MoneroDataSource.kt` — Monero data access
|
||||
- `TipEventDataSource.kt` — Tip event CRUD
|
||||
- `TipHandler.kt` — Tip event handling logic
|
||||
- `WalletService.kt` — Wallet service (702 lines)
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.core.IEvent
|
||||
**Quartz events** (`quartz/src/commonMain/kotlin/.../`):
|
||||
- `moneroTip/MoneroTipEvent.kt` — Kind 1814 Monero tip event
|
||||
- `moneroTip/TipSplitSetup.kt` — Tip split setup extension function on `Event?`
|
||||
|
||||
class MoneroTipEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
// ... monero-specific logic
|
||||
companion object {
|
||||
const val KIND = 1814
|
||||
// factory methods, tip proof parsing, etc.
|
||||
}
|
||||
}
|
||||
**Other**:
|
||||
- `TransactionPriority.kt` (`ui/screen/loggedIn/`) — Transaction priority enum
|
||||
- `monero.xml` (`res/drawable/`) — Monero icon
|
||||
- `wallet2_api.h` (`cpp/`) — Monero wallet API header
|
||||
|
||||
### Existing files modified (7):
|
||||
|
||||
| File | Changes |
|
||||
|---|---|
|
||||
| `Account.kt` | Added `moneroSpendKeyFlow`, `moneroSeedFlow`, `moneroRestoreHeightFlow`, `moneroPasswordFlow` + derivation logic + `moneroAddressIsValid()` |
|
||||
| `User.kt` (commons) | Added monero address extraction from metadata |
|
||||
| `UserMetadataCache.kt` (commons) | Added monero metadata field |
|
||||
| `EventFactory.kt` (quartz) | Added `MoneroTipEvent` import + parsing in factory |
|
||||
| `TransactionPriority.kt` | New enum (31 lines) |
|
||||
| `strings.xml` | Added ~53 monero-specific strings |
|
||||
| `build.gradle.kts` | Added JNA dependency for monero JNI |
|
||||
|
||||
### Native libraries (in jniLibs, gitignored):
|
||||
|
||||
Prebuilt from garnet v0.3.0 APK, placed in 3 ABIs (arm64-v8a, armeabi-v7a, x86_64):
|
||||
- `libmonerujo.so` — Monero JNI bridge
|
||||
- `libsodium.so` — Cryptographic library
|
||||
- `libjnidispatch.so` — JNI dispatch
|
||||
- `libc++_shared.so` — C++ runtime
|
||||
- `libsecp256k1-jni.so` — secp256k1
|
||||
|
||||
### Build verification
|
||||
|
||||
```
|
||||
BUILD SUCCESSFUL in 40s (116 tasks)
|
||||
```
|
||||
|
||||
Place at: `/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/events/MoneroTipEvent.kt`
|
||||
|
||||
### Modify Event.kt and EventFactory.kt
|
||||
|
||||
Garnet adds Monero-specific event handling in:
|
||||
- `Event.kt` - Add Monero tag extraction from type-0 metadata
|
||||
- `EventFactory.kt` - Add Monero events to factory
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Port UI Components
|
||||
## What's Next: Phase 5 - UI Layer (In Progress)
|
||||
|
||||
### New UI files:
|
||||
Garnet had ~20+ UI files. Here is the full list — what needs to be created vs modified.
|
||||
|
||||
| Garnet Source | Destination | Priority |
|
||||
|---|---|---|
|
||||
| `MoneroScreen.kt` | `ui/screen/loggedIn/` | High |
|
||||
| `Tip.kt` | `ui/components/` | Medium |
|
||||
| `TipCustomDialog.kt` | `ui/note/` | Medium |
|
||||
| `TipNoteCompose.kt` | `ui/note/` | Medium |
|
||||
| `TipUserSetCompose.kt` | `ui/note/` | Medium |
|
||||
| `UpdateTipAmountDialog.kt` | `ui/note/` | Medium |
|
||||
| `DisplayTipSplits.kt` | `ui/note/elements/` | Medium |
|
||||
| `TipFeedViewModel.kt` | `ui/screen/` | High |
|
||||
| `TipFeedState.kt` | `ui/screen/` | High |
|
||||
| `TipFeedView.kt` | `ui/screen/` | High |
|
||||
| `UserProfileTipsFeedFilter.kt` | `ui/dal/` | Medium |
|
||||
### New UI files to create (11):
|
||||
|
||||
**Create (high priority):**
|
||||
|
||||
| Garnet Source | Destination |
|
||||
|---|---|
|
||||
| `MoneroScreen.kt` | `amethyst/.../ui/screen/loggedIn/` |
|
||||
| `TipFeedState.kt` | `amethyst/.../ui/screen/` |
|
||||
| `TipFeedView.kt` | `amethyst/.../ui/screen/` |
|
||||
| `TipFeedViewModel.kt` | `amethyst/.../ui/screen/` |
|
||||
|
||||
**Create (medium priority):**
|
||||
|
||||
| Garnet Source | Destination |
|
||||
|---|---|
|
||||
| `TipNoteCompose.kt` | `amethyst/.../ui/note/` |
|
||||
| `TipUserSetCompose.kt` | `amethyst/.../ui/note/` |
|
||||
| `DisplayTipSplits.kt` | `amethyst/.../ui/note/elements/` |
|
||||
| `UpdateTipAmountDialog.kt` | `amethyst/.../ui/note/` |
|
||||
| `TipCustomDialog.kt` | `amethyst/.../ui/note/` |
|
||||
| `UserProfileTipsFeedFilter.kt` | `amethyst/.../ui/dal/` |
|
||||
|
||||
### Existing files that need modification:
|
||||
|
||||
@@ -188,81 +131,37 @@ Garnet adds Monero-specific event handling in:
|
||||
| `ProfileScreen.kt` | Add monero tips tab |
|
||||
| `NewPostView.kt` / `NewPostViewModel.kt` | Add monero tag to new posts |
|
||||
|
||||
### Adapt UI for Material3 changes:
|
||||
### Material3 API changes to apply:
|
||||
|
||||
- Replace `rememberRipple(Boolean, Dp, Color)` → use `LocalRippleInfo.current`
|
||||
- Replace `autoCorrect` → check TextField API
|
||||
- Adapt to any Compose parameter changes
|
||||
- Replace `autoCorrect` parameter → check current TextField API
|
||||
- Adapt to any Compose parameter changes since Garnet wrote them
|
||||
|
||||
### Kotlin 2.x / Coroutines changes to keep in mind:
|
||||
|
||||
- `NostrSigner.sign()` is `suspend` — all callers must be coroutines
|
||||
- StateFlow-based `User.metadata` — use subscription instead of direct access
|
||||
- `LocalCache.consume()` takes 3 args — update signatures
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Resources and Build Configuration
|
||||
## Files Complete
|
||||
|
||||
### Strings
|
||||
|
||||
Add Monero-specific strings to `amethyst/amethyst/src/main/res/values/strings.xml`:
|
||||
(129 strings changed in garnet diff)
|
||||
|
||||
### Icons
|
||||
|
||||
Add `monero.xml` to `amethyst/amethyst/src/main/res/drawable/`
|
||||
|
||||
### Build Configuration
|
||||
|
||||
Update `amethyst/amethyst/build.gradle.kts`:
|
||||
- Ensure JNA dependency for monero JNI
|
||||
- Add libsodium dependency (or verify it's transitively included)
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Build and Verify
|
||||
|
||||
### Build without failures:
|
||||
```bash
|
||||
cd ~/projects/amethyst
|
||||
./gradlew assembleFdroidDebug
|
||||
```
|
||||
|
||||
### Expected issues to fix:
|
||||
1. **Import paths** - Update all quartz imports to new KMP locations (`nip01Core.core.*`, etc.)
|
||||
2. **API changes** - Fix `rememberRipple`, TextField parameters, etc.
|
||||
3. **Kotlin type inference** - Add explicit type parameters where needed
|
||||
4. **Missing companion objects** - Add `companion object { val KIND = ... }`
|
||||
5. **Jackson serialization** - Ensure Jackson annotations match new patterns
|
||||
|
||||
---
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Likelihood | Mitigation |
|
||||
|---|---|---|
|
||||
| Monero TipEvent conflicts with existing Zap TipEvent | Medium | Study existing implementation before starting |
|
||||
| Quartz KMP structure causes issues | High | Study existing events first; use as template |
|
||||
| Material3 API changes cause errors | Medium | Fix iteratively as they appear |
|
||||
| Dependencies break build | Low | Start clean, add one file at a time |
|
||||
| Monoro .so ABI mismatch with target SDK | Low | Use same ABI as garnet (arm64-v8a, armeabi-v7a, x86_64) |
|
||||
|
||||
---
|
||||
|
||||
## Execution Strategy
|
||||
|
||||
1. **Start with one file at a time** - Verify build after each addition
|
||||
2. **Order: Model → Service → Quartz → UI → Navigation**
|
||||
3. **Study first, copy second, adapt last** - Understand each pattern before applying
|
||||
4. **Use git to track progress** - Small commits, easy to revert if needed
|
||||
5. **Verify after each phase** - Ensure base amethyst still builds
|
||||
|
||||
---
|
||||
|
||||
## Files to Port (Complete List)
|
||||
|
||||
### New files to create:
|
||||
- `amethyst/src/main/java/.../model/MoneroWalletListener.kt`
|
||||
- `amethyst/src/main/java/.../model/Wallet.kt`
|
||||
- `amethyst/src/main/java/.../model/WalletManager.kt`
|
||||
- `amethyst/src/main/java/.../service/MoneroDataSource.kt`
|
||||
- `amethyst/src/main/java/.../service/TipEventDataSource.kt`
|
||||
- `amethyst/src/main/java/.../service/TipHandler.kt`
|
||||
- `amethyst/src/main/java/.../service/WalletService.kt`
|
||||
### New files to create (total):
|
||||
- `amethyst/src/main/java/.../model/MoneroWalletListener.kt` ✓
|
||||
- `amethyst/src/main/java/.../model/Wallet.kt` ✓
|
||||
- `amethyst/src/main/java/.../model/WalletManager.kt` ✓
|
||||
- `amethyst/src/main/java/.../model/PendingTransaction.kt` ✓
|
||||
- `amethyst/src/main/java/.../model/ProofInfo.kt` ✓
|
||||
- `amethyst/src/main/java/.../model/Subaddress.kt` ✓
|
||||
- `amethyst/src/main/java/.../model/Transaction.kt` ✓
|
||||
- `amethyst/src/main/java/.../model/TransactionHistory.kt` ✓
|
||||
- `amethyst/src/main/java/.../model/TransactionInfo.kt` ✓
|
||||
- `amethyst/src/main/java/.../model/Transfer.kt` ✓
|
||||
- `amethyst/src/main/java/.../service/MoneroDataSource.kt` ✓
|
||||
- `amethyst/src/main/java/.../service/TipEventDataSource.kt` ✓
|
||||
- `amethyst/src/main/java/.../service/TipHandler.kt` ✓
|
||||
- `amethyst/src/main/java/.../service/WalletService.kt` ✓
|
||||
- `amethyst/src/main/java/.../ui/screen/loggedIn/MoneroScreen.kt`
|
||||
- `amethyst/src/main/java/.../ui/screen/TipFeedState.kt`
|
||||
- `amethyst/src/main/java/.../ui/screen/TipFeedView.kt`
|
||||
@@ -274,30 +173,30 @@ cd ~/projects/amethyst
|
||||
- `amethyst/src/main/java/.../ui/note/UpdateTipAmountDialog.kt`
|
||||
- `amethyst/src/main/java/.../ui/note/elements/DisplayTipSplits.kt`
|
||||
- `amethyst/src/main/java/.../ui/dal/UserProfileTipsFeedFilter.kt`
|
||||
- `quartz/src/commonMain/kotlin/.../events/MoneroTipEvent.kt`
|
||||
- `amethyst/src/main/java/.../ui/screen/loggedIn/TransactionPriority.kt` ✓
|
||||
- `quartz/src/commonMain/.../moneroTip/MoneroTipEvent.kt` ✓
|
||||
- `quartz/src/commonMain/.../moneroTip/TipSplitSetup.kt` ✓
|
||||
|
||||
### Files to modify:
|
||||
- `Account.kt` - Monero address management
|
||||
- `LocalPreferences.kt` - Add monero prefs
|
||||
- `LocalCache.kt` - Add monero cache keys
|
||||
- `Note.kt` - Add monero tag extraction
|
||||
- `User.kt` - Add monero address from metadata
|
||||
- `ServiceManager.kt` - Add wallet service
|
||||
- `Routes.kt` - Add monero route
|
||||
- `AppNavigation.kt` - Add monero navigation
|
||||
- `AppTopBar.kt` - Add monero icon
|
||||
- `MainScreen.kt` - Add monero screen
|
||||
- `NoteCompose.kt` - Display monero tips
|
||||
- `ReactionsRow.kt` - Monero tip reactions
|
||||
- `ProfileScreen.kt` - Monero tips tab
|
||||
- `NewPostView.kt` / `NewPostViewModel.kt` - Add monero tags
|
||||
- `strings.xml` - Add monero strings
|
||||
- `Event.kt` - Add Monero metadata handling (quartz)
|
||||
|
||||
### Files to copy (native):
|
||||
- `amethyst/src/main/cpp/monerujo.cpp`
|
||||
- `amethyst/src/main/cpp/monerujo.h`
|
||||
- `amethyst/src/main/cpp/wallet2_api.h`
|
||||
- `amethyst/src/main/jniLibs/*/libmonerujo.so`
|
||||
- `amethyst/src/main/jniLibs/*/libsodium.so`
|
||||
- `amethyst/src/main/res/drawable/monero.xml`
|
||||
### Files modified (total):
|
||||
- `Account.kt` ✓ — Monero address management (spend key, seed, password flows)
|
||||
- `User.kt` (commons) ✓ — Monero address from type-0 event
|
||||
- `UserMetadataCache.kt` (commons) ✓ — Monero metadata field
|
||||
- `LocalCache.kt` — (not needed, User handles metadata)
|
||||
- `LocalPreferences.kt` — (not needed, Account handles prefs directly)
|
||||
- `Note.kt` — Add monero tag extraction (type-0 metadata)
|
||||
- `ServiceManager.kt` — (wallet lifecycle now in WalletService)
|
||||
- `EventFactory.kt` (quartz) ✓ — MoneroTipEvent factory
|
||||
- `Event.kt` (quartz) — Monero metadata extraction (if needed separately from EventFactory)
|
||||
- `Routes.kt` — Add monero route
|
||||
- `AppNavigation.kt` — Add monero navigation
|
||||
- `AppTopBar.kt` — Add monero icon
|
||||
- `MainScreen.kt` — Add monero screen
|
||||
- `NoteCompose.kt` — Display monero tips
|
||||
- `ReactionsRow.kt` — Monero tip reactions
|
||||
- `ProfileScreen.kt` — Monero tips tab
|
||||
- `NewPostView.kt` / `NewPostViewModel.kt` — Add monero tags
|
||||
- `strings.xml` ✓ — Monero strings
|
||||
- `build.gradle.kts` ✓ — JNA dependency
|
||||
- `monerujo.cpp`, `monerujo.h` (cpp) ✓ — Native bridge
|
||||
- `wallet2_api.h` (cpp) ✓ — Monero API header
|
||||
- `monero.xml` (drawable) ✓ — Icon
|
||||
|
||||
@@ -61,6 +61,7 @@ android {
|
||||
versionCode = 446
|
||||
versionName = generateVersionName(libs.versions.app.get())
|
||||
buildConfigField("String", "RELEASE_NOTES_ID", "\"b1b91d7ee0c5da9d081d1a53470248ee4585b058b11aa34fe28c0e3e07ac1e0a\"")
|
||||
buildConfigField("String", "MONERO_NETWORK", "\"mainnet\"")
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright (c) 2017 m2049r
|
||||
* <p>
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef XMRWALLET_WALLET_LIB_H
|
||||
#define XMRWALLET_WALLET_LIB_H
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
/*
|
||||
#include <android/log.h>
|
||||
|
||||
#define LOG_TAG "[NDK]"
|
||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
|
||||
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
|
||||
*/
|
||||
|
||||
jfieldID getHandleField(JNIEnv *env, jobject obj, const char *fieldName = "handle") {
|
||||
jclass c = env->GetObjectClass(obj);
|
||||
return env->GetFieldID(c, fieldName, "J"); // of type long
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T *getHandle(JNIEnv *env, jobject obj, const char *fieldName = "handle") {
|
||||
jlong handle = env->GetLongField(obj, getHandleField(env, obj, fieldName));
|
||||
return reinterpret_cast<T *>(handle);
|
||||
}
|
||||
|
||||
void setHandleFromLong(JNIEnv *env, jobject obj, jlong handle) {
|
||||
env->SetLongField(obj, getHandleField(env, obj), handle);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void setHandle(JNIEnv *env, jobject obj, T *t) {
|
||||
jlong handle = reinterpret_cast<jlong>(t);
|
||||
setHandleFromLong(env, obj, handle);
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
extern const char* const MONERO_VERSION; // the actual monero core version
|
||||
|
||||
// from monero-core crypto/hash-ops.h - avoid #including monero code here
|
||||
enum {
|
||||
HASH_SIZE = 32,
|
||||
HASH_DATA_AREA = 136
|
||||
};
|
||||
|
||||
void cn_slow_hash(const void *data, size_t length, char *hash, int variant, int prehashed, uint64_t height);
|
||||
|
||||
inline void slow_hash(const void *data, const size_t length, char *hash) {
|
||||
cn_slow_hash(data, length, hash, 0 /*variant*/, 0 /*prehashed*/, 0 /*height*/);
|
||||
}
|
||||
|
||||
inline void slow_hash_broken(const void *data, char *hash, int variant) {
|
||||
cn_slow_hash(data, 200 /*sizeof(union hash_state)*/, hash, variant, 1 /*prehashed*/, 0 /*height*/);
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif //XMRWALLET_WALLET_LIB_H
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import android.content.ServiceConnection
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
@@ -42,6 +43,8 @@ import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSender
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapShare
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
import com.vitorpamplona.amethyst.logTime
|
||||
import com.vitorpamplona.amethyst.model.MoneroWalletListener
|
||||
import com.vitorpamplona.amethyst.model.PendingTransaction
|
||||
import com.vitorpamplona.amethyst.model.algoFeeds.FavoriteAlgoFeedsOrchestrator
|
||||
import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState
|
||||
@@ -113,6 +116,7 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxLoaderState
|
||||
import com.vitorpamplona.amethyst.model.trustedAssertions.TrustProviderListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.trustedAssertions.TrustProviderListState
|
||||
import com.vitorpamplona.amethyst.service.WalletService
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
|
||||
import com.vitorpamplona.amethyst.service.uploads.FileHeader
|
||||
@@ -444,6 +448,14 @@ class Account(
|
||||
|
||||
val paymentTargetsState = NipA3PaymentTargetsState(signer, cache, scope, settings)
|
||||
|
||||
// Monero wallet fields
|
||||
private val moneroSpendKeyFlow = MutableStateFlow<String?>(null)
|
||||
private val moneroSeedFlow = MutableStateFlow<String?>(null)
|
||||
private val moneroRestoreHeightFlow = MutableStateFlow<Long?>(null)
|
||||
private val moneroPasswordFlow = MutableStateFlow<String?>(null)
|
||||
private var walletService: WalletService? = null
|
||||
private var walletServiceBinding: ServiceConnection? = null
|
||||
|
||||
val feedDecryptionCaches =
|
||||
FeedDecryptionCaches(
|
||||
peopleListCache = peopleListDecryptionCache,
|
||||
@@ -3372,6 +3384,97 @@ class Account(
|
||||
pollEndsAt: Long?,
|
||||
) = settings.markPollResultsViewed(noteId, pollEndsAt)
|
||||
|
||||
// --- Monero Wallet & Tipping ---
|
||||
|
||||
fun setMoneroSpendKey(spendKey: String) {
|
||||
moneroSpendKeyFlow.value = spendKey
|
||||
}
|
||||
|
||||
fun setMoneroSeed(seed: String) {
|
||||
moneroSeedFlow.value = seed
|
||||
}
|
||||
|
||||
fun setMoneroRestoreHeight(restoreHeight: Long) {
|
||||
moneroRestoreHeightFlow.value = restoreHeight
|
||||
}
|
||||
|
||||
fun setMoneroPassword(password: String) {
|
||||
moneroPasswordFlow.value = password
|
||||
}
|
||||
|
||||
fun bindWalletService(service: WalletService) {
|
||||
walletService = service
|
||||
}
|
||||
|
||||
fun unbindWalletService() {
|
||||
walletService = null
|
||||
walletServiceBinding = null
|
||||
}
|
||||
|
||||
suspend fun startMonero(
|
||||
daemonAddress: String,
|
||||
restoreHeight: Long,
|
||||
walletPath: String,
|
||||
password: String,
|
||||
): Boolean {
|
||||
val service =
|
||||
walletService ?: run {
|
||||
Log.w("Monero") { "startMonero: WalletService not bound" }
|
||||
return false
|
||||
}
|
||||
val spendKey = generateMoneroSpendKey() ?: ""
|
||||
try {
|
||||
service.loadWallet(
|
||||
name = walletPath,
|
||||
password = password,
|
||||
spendKey = spendKey,
|
||||
daemonAddress = daemonAddress,
|
||||
restoreHeight = restoreHeight,
|
||||
)
|
||||
setMoneroPassword(password)
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
Log.w("Monero", "startMonero failed: ${e.message}", e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fun stopMonero() {
|
||||
walletService?.closeWallet()
|
||||
unbindWalletService()
|
||||
}
|
||||
|
||||
fun getMoneroBalance(): Long = walletService?.balance ?: 0L
|
||||
|
||||
fun getMoneroAvailableBalance(): Long = walletService?.balance ?: 0L
|
||||
|
||||
fun moneroAddressIsValid(address: String): Boolean {
|
||||
if (address.length != 95) return false
|
||||
return address.startsWith('4') || address.startsWith('8')
|
||||
}
|
||||
|
||||
fun getMoneroAddress(subaddressIndex: Int = 0): String? {
|
||||
val service = walletService ?: return null
|
||||
if (service.wallet == null) return null
|
||||
return try {
|
||||
service.wallet?.getAddressWithIndex(0, subaddressIndex)?.address ?: null
|
||||
} catch (e: Exception) {
|
||||
Log.w("Monero", "getMoneroAddress failed: ${e.message}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateMoneroSpendKey(): String? {
|
||||
val privKey =
|
||||
when (signer) {
|
||||
is NostrSignerInternal ->
|
||||
signer.keyPair.privKey
|
||||
?.joinToString("") { String.format("%02x", it) }
|
||||
else -> moneroSpendKeyFlow.value
|
||||
} ?: return null
|
||||
return privKey
|
||||
}
|
||||
|
||||
init {
|
||||
Log.d("AccountRegisterObservers", "Init")
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
interface MoneroWalletListener {
|
||||
fun moneySpent(
|
||||
txId: String,
|
||||
amount: Long,
|
||||
)
|
||||
|
||||
fun moneyReceived(
|
||||
txId: String,
|
||||
amount: Long,
|
||||
)
|
||||
|
||||
fun unconfirmedMoneyReceived(
|
||||
txId: String,
|
||||
amount: Long,
|
||||
)
|
||||
|
||||
fun newBlock(height: Long)
|
||||
|
||||
fun updated()
|
||||
|
||||
fun refreshed()
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
class PendingTransaction(
|
||||
val handle: Long,
|
||||
) {
|
||||
enum class StatusType { OK, FEE, ERROR, CANCELLED }
|
||||
|
||||
class Status(
|
||||
val type: StatusType,
|
||||
val priority: Int,
|
||||
val error: String = "",
|
||||
)
|
||||
|
||||
val status: Status get() = getStatusJ()
|
||||
|
||||
fun saveTxId() {
|
||||
saveTxIdJ()
|
||||
}
|
||||
|
||||
fun commit(): Boolean = commitJ()
|
||||
|
||||
private external fun getStatusJ(): Status
|
||||
|
||||
private external fun saveTxIdJ()
|
||||
|
||||
private external fun commitJ(): Boolean
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
data class ProofInfo(
|
||||
val txId: String,
|
||||
val address: String,
|
||||
val message: String,
|
||||
val signature: String,
|
||||
val amount: Long,
|
||||
val inPool: Boolean,
|
||||
val confirmations: Int,
|
||||
)
|
||||
|
||||
data class Proof(
|
||||
val proof: String,
|
||||
val status: Wallet.Status,
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
data class Subaddress(
|
||||
val address: String,
|
||||
val label: String,
|
||||
val index: Int,
|
||||
)
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
class Transaction(
|
||||
val handle: Long,
|
||||
) {
|
||||
external fun getConfirmations(): Int
|
||||
|
||||
external fun getHash(): String
|
||||
|
||||
external fun getDescription(): String
|
||||
|
||||
external fun getTimestamp(): Long
|
||||
|
||||
external fun getValue(): Long
|
||||
|
||||
external fun getFee(): Long
|
||||
|
||||
external fun getBlockHeight(): Long
|
||||
|
||||
external fun getPubkey(): String
|
||||
|
||||
external fun getPaymentId(): String
|
||||
|
||||
external fun isPending(): Boolean
|
||||
|
||||
external fun isRcpt(): Boolean
|
||||
|
||||
external fun isFailed(): Boolean
|
||||
|
||||
external fun isRecurring(): Boolean
|
||||
|
||||
external fun getUnlockTime(): Long
|
||||
|
||||
external fun direction(): Int
|
||||
|
||||
external fun getType(): Int
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
class TransactionHistory(
|
||||
val handle: Long,
|
||||
) {
|
||||
val transactions: List<TransactionInfo> get() = getTransactionsJ().toList()
|
||||
|
||||
fun refresh(accountIndex: Int) = refreshJ(accountIndex)
|
||||
|
||||
private external fun getTransactionsJ(): Array<TransactionInfo>
|
||||
|
||||
private external fun refreshJ(accountIndex: Int)
|
||||
|
||||
companion object {
|
||||
init {
|
||||
System.loadLibrary("monerujo")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TransactionPriority
|
||||
|
||||
data class TransactionInfo(
|
||||
val hash: String,
|
||||
val direction: Int,
|
||||
val amount: Long,
|
||||
val fee: Long,
|
||||
val blockHeight: Long,
|
||||
val unlockTime: Long,
|
||||
val paymentId: String,
|
||||
val confirmations: Int,
|
||||
val pending: Boolean,
|
||||
val failed: Boolean,
|
||||
val keyImage: String,
|
||||
val description: String,
|
||||
val subaddressIndex: Int,
|
||||
val subnetaddressLabel: String,
|
||||
val timestamp: Long,
|
||||
val transfers: List<Transfer>,
|
||||
val priority: TransactionPriority? = null,
|
||||
) {
|
||||
val txDirectionText: String?
|
||||
get() =
|
||||
when (direction) {
|
||||
0 -> "In"
|
||||
1 -> "Out"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
data class Transfer(
|
||||
val amount: Long,
|
||||
val address: String,
|
||||
val blockHeight: Long? = null,
|
||||
val confirmations: Int? = null,
|
||||
)
|
||||
@@ -0,0 +1,307 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TransactionPriority
|
||||
|
||||
class Wallet(
|
||||
val handle: Long,
|
||||
) {
|
||||
companion object {
|
||||
init {
|
||||
System.loadLibrary("monerujo")
|
||||
}
|
||||
}
|
||||
|
||||
var listenerHandle: Long = 0
|
||||
val address: String get() = getAddressJ(0, 0)
|
||||
val seed: String get() = getSeedJ()
|
||||
|
||||
enum class StatusType { OK, ERROR, CRITICAL }
|
||||
|
||||
class Status(
|
||||
val status: Int,
|
||||
val error: String,
|
||||
) {
|
||||
fun isOk(): Boolean = status == StatusType.OK.ordinal
|
||||
}
|
||||
|
||||
val status: Status get() = statusWithErrorString()
|
||||
|
||||
enum class ConnectionStatus { DISCONNECTED, CONNECTED, WRONG_VERSION }
|
||||
|
||||
val connectionStatus: ConnectionStatus
|
||||
get() {
|
||||
val status = getConnectionStatusJ()
|
||||
return ConnectionStatus.entries.first { it.ordinal == status }
|
||||
}
|
||||
|
||||
private external fun statusWithErrorString(): Status
|
||||
|
||||
val balance: Long get() = getUnlockedBalanceAll()
|
||||
val lockedBalance: Long get() = getBalanceAll() - balance
|
||||
|
||||
val height: Long get() = getBlockChainHeight()
|
||||
val daemonHeight: Long get() = getDaemonBlockChainHeight()
|
||||
|
||||
val netType: NetworkType
|
||||
get() {
|
||||
val net = nettype()
|
||||
return NetworkType.entries.first { it.ordinal == net }
|
||||
}
|
||||
|
||||
lateinit var transactionHistory: TransactionHistory private set
|
||||
|
||||
fun init(
|
||||
daemonAddress: String,
|
||||
upperTransactionSizeLimit: Long = 0,
|
||||
daemonUsername: String = "",
|
||||
daemonPassword: String = "",
|
||||
proxyAddress: String = "",
|
||||
) {
|
||||
initJ(daemonAddress, upperTransactionSizeLimit, daemonUsername, daemonPassword, proxyAddress)
|
||||
transactionHistory = TransactionHistory(getHistoryJ())
|
||||
}
|
||||
|
||||
private external fun initJ(
|
||||
daemonAddress: String,
|
||||
upperTransactionSizeLimit: Long,
|
||||
daemonUsername: String,
|
||||
daemonPassword: String,
|
||||
proxyAddress: String,
|
||||
)
|
||||
|
||||
fun setListener(listener: MoneroWalletListener?) {
|
||||
listenerHandle = setListenerJ(listener)
|
||||
}
|
||||
|
||||
fun unsetListener() {
|
||||
unsetListenerJ()
|
||||
listenerHandle = 0
|
||||
}
|
||||
|
||||
fun store(path: String = "") {
|
||||
storeJ(path)
|
||||
}
|
||||
|
||||
private external fun storeJ(path: String)
|
||||
|
||||
fun getAddressWithIndex(
|
||||
accountIndex: Int,
|
||||
addressIndex: Int,
|
||||
): Subaddress {
|
||||
val address = getAddressJ(accountIndex, addressIndex)
|
||||
val label = getSubaddressLabel(accountIndex, addressIndex)
|
||||
return Subaddress(address, label, addressIndex)
|
||||
}
|
||||
|
||||
fun createTransaction(
|
||||
destination: String,
|
||||
paymentId: String = "",
|
||||
amount: Long,
|
||||
mixinCount: Int = 0,
|
||||
priority: TransactionPriority = TransactionPriority.UNIMPORTANT,
|
||||
accountIndex: Int = 0,
|
||||
): PendingTransaction {
|
||||
val handle = createTransactionJ(destination, paymentId, amount, mixinCount, priority.ordinal, accountIndex)
|
||||
return PendingTransaction(handle)
|
||||
}
|
||||
|
||||
private external fun createTransactionJ(
|
||||
destination: String,
|
||||
paymentId: String,
|
||||
amount: Long,
|
||||
mixinCount: Int,
|
||||
priority: Int,
|
||||
accountIndex: Int,
|
||||
): Long
|
||||
|
||||
fun createTransactionMultDest(
|
||||
destinations: Array<String>,
|
||||
amounts: Array<Long>,
|
||||
paymentId: String = "",
|
||||
mixinCount: Int = 0,
|
||||
priority: TransactionPriority = TransactionPriority.UNIMPORTANT,
|
||||
accountIndex: Int = 0,
|
||||
subAddresses: Array<Int> = emptyArray(),
|
||||
): PendingTransaction {
|
||||
val handle =
|
||||
createTransactionMultDestJ(
|
||||
destinations,
|
||||
paymentId,
|
||||
amounts.toLongArray(),
|
||||
mixinCount,
|
||||
priority.ordinal,
|
||||
accountIndex,
|
||||
subAddresses.toIntArray(),
|
||||
)
|
||||
return PendingTransaction(handle)
|
||||
}
|
||||
|
||||
private external fun createTransactionMultDestJ(
|
||||
destinations: Array<String>,
|
||||
paymentId: String,
|
||||
amounts: LongArray,
|
||||
mixinCount: Int,
|
||||
priority: Int,
|
||||
accountIndex: Int,
|
||||
subAddresses: IntArray,
|
||||
): Long
|
||||
|
||||
private val subaddressLock = Any()
|
||||
|
||||
fun newSubaddress(
|
||||
accountIndex: Int = 0,
|
||||
label: String = "",
|
||||
): Subaddress {
|
||||
synchronized(subaddressLock) {
|
||||
addSubaddress(accountIndex, label)
|
||||
val index = getNumSubaddresses(accountIndex) - 1
|
||||
val address = getAddressJ(accountIndex, index)
|
||||
return Subaddress(address, label, index)
|
||||
}
|
||||
}
|
||||
|
||||
fun lastSubaddress(accountIndex: Int): Subaddress {
|
||||
synchronized(subaddressLock) {
|
||||
val index = getNumSubaddresses(accountIndex) - 1
|
||||
return Subaddress(
|
||||
getAddressJ(accountIndex, index),
|
||||
getSubaddressLabel(accountIndex, index),
|
||||
index,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private external fun addSubaddress(
|
||||
accountIndex: Int,
|
||||
label: String,
|
||||
)
|
||||
|
||||
external fun getNumSubaddresses(accountIndex: Int): Int
|
||||
|
||||
private external fun getAddressJ(
|
||||
accountIndex: Int,
|
||||
addressIndex: Int,
|
||||
): String
|
||||
|
||||
private external fun getSeedJ(seedOffset: String = ""): String
|
||||
|
||||
fun seedWithPassphrase(passphrase: String): String = getSeedJ(passphrase)
|
||||
|
||||
external fun getSubaddressLabel(
|
||||
accountIndex: Int,
|
||||
addressIndex: Int,
|
||||
): String
|
||||
|
||||
external fun setSubaddressLabel(
|
||||
accountIndex: Int,
|
||||
addressIndex: Int,
|
||||
label: String,
|
||||
)
|
||||
|
||||
fun checkTxProof(
|
||||
txId: String,
|
||||
address: String,
|
||||
message: String = "",
|
||||
signature: String,
|
||||
): ProofInfo? = checkTxProofJ(txId, address, message, signature)
|
||||
|
||||
private external fun checkTxProofJ(
|
||||
txId: String,
|
||||
address: String,
|
||||
message: String,
|
||||
signature: String,
|
||||
): ProofInfo?
|
||||
|
||||
external fun getTxProof(
|
||||
txId: String,
|
||||
address: String,
|
||||
message: String = "",
|
||||
): String
|
||||
|
||||
fun isAddressValid(
|
||||
address: String,
|
||||
netType: NetworkType = WalletManager.getNetworkType(),
|
||||
): Boolean = isAddressValidJ(address, netType.ordinal)
|
||||
|
||||
private external fun isAddressValidJ(
|
||||
address: String,
|
||||
netType: Int = WalletManager.getNetworkType().ordinal,
|
||||
): Boolean
|
||||
|
||||
fun refreshHistory() {
|
||||
transactionHistory.refresh(0)
|
||||
}
|
||||
|
||||
fun estimateTransactionFee(
|
||||
addresses: Array<String>,
|
||||
amounts: Array<Long>,
|
||||
priority: TransactionPriority,
|
||||
): Long = estimateTransactionFeeJ(addresses, amounts.toLongArray(), priority.ordinal)
|
||||
|
||||
external fun estimateTransactionFeeJ(
|
||||
addresses: Array<String>,
|
||||
amounts: LongArray,
|
||||
priority: Int,
|
||||
): Long
|
||||
|
||||
external fun setUserNote(
|
||||
txId: String,
|
||||
note: String,
|
||||
): Boolean
|
||||
|
||||
private external fun getHistoryJ(): Long
|
||||
|
||||
external fun getBalanceAll(): Long
|
||||
|
||||
external fun getUnlockedBalanceAll(): Long
|
||||
|
||||
external fun getDisplayAmount(amount: Long): String
|
||||
|
||||
external fun estimateBlockchainHeight(): Long
|
||||
|
||||
external fun getBlockChainHeight(): Long
|
||||
|
||||
external fun getDaemonBlockChainHeight(): Long
|
||||
|
||||
private external fun getConnectionStatusJ(): Int
|
||||
|
||||
external fun setProxy(proxy: String): Boolean
|
||||
|
||||
external fun setListenerJ(listener: MoneroWalletListener?): Long
|
||||
|
||||
external fun unsetListenerJ()
|
||||
|
||||
external fun setRestoreHeight(height: Long)
|
||||
|
||||
external fun getRestoreHeight(): Long
|
||||
|
||||
external fun pauseRefresh()
|
||||
|
||||
external fun startRefresh()
|
||||
|
||||
external fun rescanBlockchainAsync()
|
||||
|
||||
external fun setPassword(password: String): Boolean
|
||||
|
||||
external fun nettype(): Int
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import java.nio.file.FileAlreadyExistsException
|
||||
import java.util.Locale
|
||||
import kotlin.io.path.Path
|
||||
import kotlin.io.path.createDirectory
|
||||
import kotlin.io.path.deleteIfExists
|
||||
import kotlin.io.path.isDirectory
|
||||
|
||||
enum class NetworkType {
|
||||
MAINNET,
|
||||
TESTNET,
|
||||
STAGENET,
|
||||
}
|
||||
|
||||
object WalletManager {
|
||||
init {
|
||||
System.loadLibrary("monerujo")
|
||||
}
|
||||
|
||||
fun openWallet(
|
||||
name: String,
|
||||
password: String,
|
||||
netType: NetworkType = getNetworkType(),
|
||||
): Wallet {
|
||||
val path = Path(getWalletDir(), name).toString()
|
||||
val handle = openWalletJ(path, password, netType.ordinal)
|
||||
return Wallet(handle)
|
||||
}
|
||||
|
||||
private external fun openWalletJ(
|
||||
path: String,
|
||||
password: String,
|
||||
netType: Int,
|
||||
): Long
|
||||
|
||||
fun createWalletFromSpendKey(
|
||||
name: String,
|
||||
password: String,
|
||||
spendKey: String,
|
||||
language: String = localeToSupportedLanguage(Locale.getDefault()),
|
||||
netType: NetworkType = getNetworkType(),
|
||||
restoreHeight: Long = 0,
|
||||
): Wallet {
|
||||
val path = Path(getWalletDir(), name).toString()
|
||||
val handle = createWalletFromKeysJ(path, password, language, netType.ordinal, restoreHeight, "", "", spendKey)
|
||||
return Wallet(handle)
|
||||
}
|
||||
|
||||
fun createWallet(
|
||||
name: String,
|
||||
password: String,
|
||||
language: String = localeToSupportedLanguage(Locale.getDefault()),
|
||||
netType: NetworkType = getNetworkType(),
|
||||
): Wallet {
|
||||
val path = Path(getWalletDir(), name).toString()
|
||||
val handle = createWalletJ(path, password, language, netType.ordinal)
|
||||
return Wallet(handle)
|
||||
}
|
||||
|
||||
private external fun createWalletJ(
|
||||
path: String,
|
||||
password: String,
|
||||
language: String,
|
||||
netType: Int,
|
||||
): Long
|
||||
|
||||
private external fun createWalletFromKeysJ(
|
||||
path: String,
|
||||
password: String,
|
||||
language: String,
|
||||
netType: Int,
|
||||
restoreHeight: Long,
|
||||
address: String,
|
||||
viewKey: String,
|
||||
spendKey: String,
|
||||
): Long
|
||||
|
||||
fun close(wallet: Wallet) = closeJ(wallet)
|
||||
|
||||
private external fun closeJ(wallet: Wallet)
|
||||
|
||||
fun getNetworkType(): NetworkType = if (BuildConfig.MONERO_NETWORK == "mainnet") NetworkType.MAINNET else NetworkType.STAGENET
|
||||
|
||||
fun walletExists(name: String): Boolean {
|
||||
val path = Path(getWalletDir(), name).toString()
|
||||
return walletExistsJ(path)
|
||||
}
|
||||
|
||||
private external fun walletExistsJ(path: String): Boolean
|
||||
|
||||
fun deleteWallet(name: String): Boolean {
|
||||
if (!Path(getWalletDir(), name).deleteIfExists()) return false
|
||||
if (!Path(getWalletDir(), "$name.keys").deleteIfExists()) return false
|
||||
return true
|
||||
}
|
||||
|
||||
fun deleteCache(name: String): Boolean = Path(getWalletDir(), name).deleteIfExists()
|
||||
|
||||
const val MONERO_DIR = "monero"
|
||||
|
||||
fun getWalletDir(): String {
|
||||
val context = Amethyst.instance.appContext
|
||||
val path = Path(context.filesDir.absolutePath, "monero")
|
||||
try {
|
||||
path.createDirectory()
|
||||
} catch (e: FileAlreadyExistsException) {
|
||||
if (!path.isDirectory()) {
|
||||
throw IllegalStateException("Monero directory is a file")
|
||||
}
|
||||
}
|
||||
return path.toString()
|
||||
}
|
||||
|
||||
external fun getBlockchainHeight(): Long
|
||||
|
||||
private fun localeToSupportedLanguage(locale: Locale): String =
|
||||
if (locale.language in arrayOf("de", "es", "fr", "it", "nl", "pt", "ru", "ja", "zh")) {
|
||||
locale.getDisplayLanguage(Locale.ENGLISH)
|
||||
} else {
|
||||
"English"
|
||||
}
|
||||
|
||||
external fun setProxy(proxy: String): Boolean
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.TransactionInfo
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
data class MoneroServiceFlows(
|
||||
val status: Flow<*> = kotlinx.coroutines.flow.flowOf(""),
|
||||
val connectionStatus: Flow<*> = kotlinx.coroutines.flow.flowOf(""),
|
||||
val walletHeight: Flow<Long> = kotlinx.coroutines.flow.flowOf(0L),
|
||||
val daemonHeight: Flow<Long> = kotlinx.coroutines.flow.flowOf(0L),
|
||||
val balance: Flow<Long> = kotlinx.coroutines.flow.flowOf(0L),
|
||||
val lockedBalance: Flow<Long> = kotlinx.coroutines.flow.flowOf(0L),
|
||||
val transactions: Flow<List<TransactionInfo>> = kotlinx.coroutines.flow.flowOf(emptyList()),
|
||||
)
|
||||
|
||||
object MoneroDataSource {
|
||||
@Volatile
|
||||
private var flows: MoneroServiceFlows = MoneroServiceFlows()
|
||||
|
||||
fun status(): Flow<*> = flows.status
|
||||
|
||||
fun connectionStatus(): Flow<*> = flows.connectionStatus
|
||||
|
||||
fun walletHeight(): Flow<Long> = flows.walletHeight
|
||||
|
||||
fun daemonHeight(): Flow<Long> = flows.daemonHeight
|
||||
|
||||
fun balance(): Flow<Long> = flows.balance
|
||||
|
||||
fun lockedBalance(): Flow<Long> = flows.lockedBalance
|
||||
|
||||
fun transactions(): Flow<List<TransactionInfo>> = flows.transactions
|
||||
|
||||
fun setMoneroService(service: WalletService) {
|
||||
// Wire up the service flows here when fully implemented
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
object TipEventDataSource {
|
||||
fun start() {
|
||||
// Stub implementation - full implementation requires complex flow handling
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
// Stub implementation
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import android.content.Context
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.quartz.moneroTip.TipSplitSetup
|
||||
import com.vitorpamplona.quartz.moneroTip.tipSplitSetup
|
||||
|
||||
class TipHandler(
|
||||
val account: Account,
|
||||
) {
|
||||
suspend fun tip(
|
||||
note: Note,
|
||||
amount: ULong,
|
||||
message: String,
|
||||
context: Context,
|
||||
onError: (String, String) -> Unit,
|
||||
onNotEnoughMoney: (Long, Long, Long) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit,
|
||||
) {
|
||||
val (tipsToSend, _) = getNoteTipRecipient(note, account)
|
||||
onProgress(0.10f)
|
||||
|
||||
if (tipsToSend.isEmpty()) {
|
||||
onError(
|
||||
context.getString(R.string.monero_missing_setup),
|
||||
context.getString(R.string.monero_user_no_address),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Full implementation requires WalletService and complex transaction handling
|
||||
onProgress(1f)
|
||||
}
|
||||
}
|
||||
|
||||
fun getNoteTipRecipient(
|
||||
note: Note,
|
||||
account: Account,
|
||||
): Pair<List<TipSplitSetup>, List<String>> {
|
||||
val tipSplitSetup = note.event?.tipSplitSetup() ?: emptyList()
|
||||
val tippedUsers = mutableListOf<String>()
|
||||
|
||||
val tips =
|
||||
tipSplitSetup.mapNotNull { tip: TipSplitSetup ->
|
||||
account.moneroAddressIsValid(tip.addressOrPubKeyHex).let { isValid ->
|
||||
if (isValid) {
|
||||
tippedUsers.add(tip.addressOrPubKeyHex)
|
||||
tip
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Pair(tips, tippedUsers)
|
||||
}
|
||||
@@ -0,0 +1,702 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.MoneroWalletListener
|
||||
import com.vitorpamplona.amethyst.model.PendingTransaction
|
||||
import com.vitorpamplona.amethyst.model.Proof
|
||||
import com.vitorpamplona.amethyst.model.ProofInfo
|
||||
import com.vitorpamplona.amethyst.model.Subaddress
|
||||
import com.vitorpamplona.amethyst.model.TransactionHistory
|
||||
import com.vitorpamplona.amethyst.model.TransactionInfo
|
||||
import com.vitorpamplona.amethyst.model.Wallet
|
||||
import com.vitorpamplona.amethyst.model.WalletManager
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TransactionPriority
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.TimeSource
|
||||
|
||||
class WalletService : Service() {
|
||||
private val binder = WalletBinder()
|
||||
|
||||
inner class WalletBinder : Binder() {
|
||||
fun getService(): WalletService = this@WalletService
|
||||
}
|
||||
|
||||
enum class WalletStatusType {
|
||||
OPENING,
|
||||
CONNECTING,
|
||||
DISCONNECTING,
|
||||
SYNCING,
|
||||
SYNCED,
|
||||
ERROR,
|
||||
DISCONNECTED,
|
||||
;
|
||||
|
||||
fun toLocalizedString(): String =
|
||||
when (this) {
|
||||
OPENING ->
|
||||
Amethyst.instance.appContext.getString(R.string.wallet_status_opening)
|
||||
CONNECTING ->
|
||||
Amethyst.instance.appContext.getString(R.string.wallet_status_connecting)
|
||||
DISCONNECTING ->
|
||||
Amethyst.instance.appContext.getString(R.string.wallet_status_disconnecting)
|
||||
SYNCING ->
|
||||
Amethyst.instance.appContext.getString(R.string.wallet_status_syncing)
|
||||
SYNCED ->
|
||||
Amethyst.instance.appContext.getString(R.string.wallet_status_synced)
|
||||
ERROR ->
|
||||
Amethyst.instance.appContext.getString(R.string.wallet_status_error)
|
||||
DISCONNECTED ->
|
||||
Amethyst.instance.appContext.getString(R.string.wallet_status_disconnected)
|
||||
}
|
||||
}
|
||||
|
||||
class WalletStatus(
|
||||
val type: WalletStatusType,
|
||||
val description: String?,
|
||||
) {
|
||||
fun toLocalizedString(): String {
|
||||
val description = description?.let { ": $it" } ?: ""
|
||||
return "${type.toLocalizedString()}$description"
|
||||
}
|
||||
}
|
||||
|
||||
var status: WalletStatus = WalletStatus(WalletStatusType.OPENING, null)
|
||||
private set(value) {
|
||||
field = value
|
||||
|
||||
_statusStateFlow.update { value }
|
||||
}
|
||||
|
||||
private val lock = Any()
|
||||
|
||||
private val _statusStateFlow = MutableStateFlow(status)
|
||||
val statusStateFlow = _statusStateFlow.asStateFlow()
|
||||
|
||||
val balance: Long
|
||||
get() = synchronized(lock) { return wallet?.balance ?: 0 }
|
||||
val lockedBalance: Long
|
||||
get() = synchronized(lock) { return wallet?.lockedBalance ?: 0 }
|
||||
|
||||
private val _balanceStateFlow = MutableStateFlow(balance)
|
||||
val balanceStateFlow = _balanceStateFlow.asStateFlow()
|
||||
|
||||
private val _lockedBalanceStateFlow = MutableStateFlow(lockedBalance)
|
||||
val lockedBalanceStateFlow = _lockedBalanceStateFlow.asStateFlow()
|
||||
|
||||
val walletHeight: Long
|
||||
get() = synchronized(lock) { wallet?.height ?: 0 }
|
||||
|
||||
private val _walletHeightStateFlow = MutableStateFlow(walletHeight)
|
||||
val walletHeightStateFlow = _walletHeightStateFlow.asStateFlow()
|
||||
|
||||
val daemonHeight: Long
|
||||
get() = synchronized(lock) { wallet?.daemonHeight ?: 0 }
|
||||
|
||||
private val _daemonHeightStateFlow = MutableStateFlow(daemonHeight)
|
||||
val daemonHeightStateFlow = _daemonHeightStateFlow.asStateFlow()
|
||||
|
||||
val address: String?
|
||||
get() = synchronized(lock) { wallet?.address }
|
||||
|
||||
val connectionStatus: Wallet.ConnectionStatus
|
||||
get() =
|
||||
synchronized(lock) {
|
||||
wallet?.connectionStatus ?: Wallet.ConnectionStatus.DISCONNECTED
|
||||
}
|
||||
|
||||
private val _connectionStatusStateFlow = MutableStateFlow(connectionStatus)
|
||||
val connectionStatusStateFlow = _connectionStatusStateFlow.asStateFlow()
|
||||
|
||||
private val _transactions = MutableStateFlow<List<TransactionInfo>>(listOf())
|
||||
val transactions = _transactions.asStateFlow()
|
||||
|
||||
val walletListener =
|
||||
object : MoneroWalletListener {
|
||||
val MIN_NEWBLOCK_DELAY: Duration = 500.milliseconds
|
||||
|
||||
var lastNewBlock = TimeSource.Monotonic.markNow()
|
||||
val scope = Amethyst.instance.applicationIOScope
|
||||
private val ctx = Amethyst.instance.appContext
|
||||
|
||||
override fun moneyReceived(
|
||||
txId: String,
|
||||
amount: Long,
|
||||
) {
|
||||
scope.launch {
|
||||
synchronized(lock) {
|
||||
_balanceStateFlow.update { balance }
|
||||
_lockedBalanceStateFlow.update { lockedBalance }
|
||||
|
||||
wallet?.let {
|
||||
it.refreshHistory()
|
||||
_transactions.update { _ -> it.transactionHistory.transactions }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun moneySpent(
|
||||
txId: String,
|
||||
amount: Long,
|
||||
) {
|
||||
scope.launch {
|
||||
synchronized(lock) {
|
||||
_balanceStateFlow.update { balance }
|
||||
_lockedBalanceStateFlow.update { lockedBalance }
|
||||
|
||||
wallet?.let {
|
||||
it.refreshHistory()
|
||||
_transactions.update { _ -> it.transactionHistory.transactions }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun unconfirmedMoneyReceived(
|
||||
txId: String,
|
||||
amount: Long,
|
||||
) {
|
||||
scope.launch {
|
||||
synchronized(lock) {
|
||||
_lockedBalanceStateFlow.update { lockedBalance }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun newBlock(height: Long) {
|
||||
val now = TimeSource.Monotonic.markNow()
|
||||
if (now - lastNewBlock >= MIN_NEWBLOCK_DELAY) {
|
||||
lastNewBlock = now
|
||||
// store() doesn't need to be synchronized, and calling it concurrently seems to cause issues
|
||||
store()
|
||||
scope.launch {
|
||||
synchronized(lock) {
|
||||
val left = ((wallet?.daemonHeight ?: 0) - (wallet?.height ?: 0)).coerceAtLeast(minimumValue = 0)
|
||||
if (left != 0L) {
|
||||
status =
|
||||
WalletStatus(
|
||||
WalletStatusType.SYNCING,
|
||||
ctx.getString(R.string.x_blocks_left, "$left"),
|
||||
)
|
||||
}
|
||||
|
||||
_daemonHeightStateFlow.update { wallet?.daemonHeight ?: 0 }
|
||||
_connectionStatusStateFlow.update { wallet?.connectionStatus ?: Wallet.ConnectionStatus.DISCONNECTED }
|
||||
_walletHeightStateFlow.update { wallet?.height ?: 0 }
|
||||
_balanceStateFlow.update { wallet?.balance ?: 0 }
|
||||
_lockedBalanceStateFlow.update { wallet?.lockedBalance ?: 0 }
|
||||
|
||||
wallet?.let {
|
||||
it.refreshHistory()
|
||||
_transactions.update { _ -> it.transactionHistory.transactions }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updated() {
|
||||
}
|
||||
|
||||
override fun refreshed() {
|
||||
if (!synced) {
|
||||
store()
|
||||
synced = true
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
synchronized(lock) {
|
||||
checkNotInMainThread()
|
||||
|
||||
status =
|
||||
WalletStatus(
|
||||
WalletStatusType.SYNCED,
|
||||
ctx.getString(R.string.x_blocks, "$walletHeight"),
|
||||
)
|
||||
|
||||
_daemonHeightStateFlow.update { daemonHeight }
|
||||
_connectionStatusStateFlow.update { connectionStatus }
|
||||
|
||||
wallet?.let {
|
||||
it.refreshHistory()
|
||||
_transactions.update { _ -> it.transactionHistory.transactions }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var wallet: Wallet? = null
|
||||
set(value) {
|
||||
field = value
|
||||
}
|
||||
|
||||
private var synced = false
|
||||
|
||||
fun loadWallet(
|
||||
name: String,
|
||||
password: String,
|
||||
spendKey: String = "",
|
||||
daemonAddress: String,
|
||||
daemonUsername: String = "",
|
||||
daemonPassword: String = "",
|
||||
proxy: String = "",
|
||||
restoreHeight: Long? = null,
|
||||
): Wallet =
|
||||
synchronized(lock) {
|
||||
load(name, password, spendKey, daemonAddress, daemonUsername, daemonPassword, proxy, restoreHeight)
|
||||
}
|
||||
|
||||
fun load(
|
||||
name: String,
|
||||
password: String,
|
||||
spendKey: String = "",
|
||||
daemonAddress: String,
|
||||
daemonUsername: String = "",
|
||||
daemonPassword: String = "",
|
||||
proxy: String = "",
|
||||
restoreHeight: Long? = null,
|
||||
): Wallet {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (name.isBlank()) {
|
||||
throw IllegalArgumentException("Wallet name not specified")
|
||||
}
|
||||
|
||||
wallet?.let {
|
||||
close()
|
||||
|
||||
status = WalletStatus(WalletStatusType.DISCONNECTING, null)
|
||||
synced = false
|
||||
|
||||
_balanceStateFlow.update { 0 }
|
||||
_lockedBalanceStateFlow.update { 0 }
|
||||
}
|
||||
|
||||
val isCreation = !WalletManager.walletExists(name) && restoreHeight == null
|
||||
|
||||
val myWallet =
|
||||
if (WalletManager.walletExists(name)) {
|
||||
val wallet = WalletManager.openWallet(name, password)
|
||||
if (!wallet.status.isOk()) {
|
||||
Log.w("WalletService", "Unable to open wallet: ${wallet.status.error}")
|
||||
Log.w("WalletService", "Deleting corrupted wallet cache for $name")
|
||||
WalletManager.deleteCache(name)
|
||||
WalletManager.openWallet(name, password)
|
||||
} else {
|
||||
wallet
|
||||
}
|
||||
} else {
|
||||
if (spendKey.isBlank()) {
|
||||
WalletManager.createWallet(name, password)
|
||||
} else {
|
||||
WalletManager.createWalletFromSpendKey(name, password, spendKey)
|
||||
}
|
||||
}
|
||||
|
||||
if (!myWallet.status.isOk()) {
|
||||
status = WalletStatus(WalletStatusType.ERROR, myWallet.status.error)
|
||||
return myWallet
|
||||
} else {
|
||||
wallet = myWallet
|
||||
_balanceStateFlow.update { myWallet.balance }
|
||||
_lockedBalanceStateFlow.update { myWallet.lockedBalance }
|
||||
}
|
||||
|
||||
connect(
|
||||
daemonAddress,
|
||||
daemonUsername,
|
||||
daemonPassword,
|
||||
proxy,
|
||||
isCreation,
|
||||
restoreHeight,
|
||||
password,
|
||||
)
|
||||
return myWallet
|
||||
}
|
||||
|
||||
fun connectToDaemon(
|
||||
address: String,
|
||||
username: String = "",
|
||||
password: String = "",
|
||||
proxy: String = "",
|
||||
restoreHeight: Long? = null,
|
||||
walletPassword: String? = null,
|
||||
): Wallet.Status? =
|
||||
synchronized(lock) {
|
||||
connect(address, username, password, proxy, false, restoreHeight, walletPassword)
|
||||
}
|
||||
|
||||
fun connect(
|
||||
address: String,
|
||||
username: String = "",
|
||||
password: String = "",
|
||||
proxy: String = "",
|
||||
isCreation: Boolean,
|
||||
restoreHeight: Long? = null,
|
||||
walletPassword: String? = null,
|
||||
): Wallet.Status? {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (address.isBlank()) {
|
||||
throw IllegalArgumentException("Daemon address not specified")
|
||||
}
|
||||
|
||||
status = WalletStatus(WalletStatusType.CONNECTING, null)
|
||||
return wallet?.let {
|
||||
it.init(address, daemonUsername = username, daemonPassword = password, proxyAddress = proxy)
|
||||
if (!it.status.isOk()) {
|
||||
status = WalletStatus(WalletStatusType.ERROR, it.status.error)
|
||||
return it.status
|
||||
}
|
||||
|
||||
if (isCreation) {
|
||||
val height = it.estimateBlockchainHeight()
|
||||
it.setRestoreHeight(height)
|
||||
if (walletPassword != null) {
|
||||
it.setPassword(walletPassword)
|
||||
}
|
||||
} else {
|
||||
restoreHeight?.let { height ->
|
||||
it.setRestoreHeight(height)
|
||||
}
|
||||
}
|
||||
|
||||
it.setListener(walletListener)
|
||||
|
||||
it.startRefresh()
|
||||
|
||||
_daemonHeightStateFlow.update { _ -> it.daemonHeight }
|
||||
|
||||
_walletHeightStateFlow.update { _ -> it.height }
|
||||
|
||||
status = WalletStatus(WalletStatusType.SYNCING, "${it.height} blocks")
|
||||
it.status
|
||||
}
|
||||
}
|
||||
|
||||
fun sendTransaction(
|
||||
destination: String,
|
||||
amount: Long,
|
||||
priority: TransactionPriority = TransactionPriority.UNIMPORTANT,
|
||||
): PendingTransaction.Status? =
|
||||
synchronized(lock) {
|
||||
checkNotInMainThread()
|
||||
if (destination.isEmpty()) {
|
||||
throw IllegalArgumentException("Destination not specified")
|
||||
}
|
||||
|
||||
if (amount == 0L) {
|
||||
throw IllegalArgumentException("Amount must not be zero")
|
||||
}
|
||||
|
||||
wallet?.let {
|
||||
val pendingTransaction = it.createTransaction(destination, amount = amount, priority = priority)
|
||||
if (pendingTransaction.status.type != PendingTransaction.StatusType.OK) {
|
||||
return pendingTransaction.status
|
||||
}
|
||||
|
||||
pendingTransaction.saveTxId()
|
||||
|
||||
if (!pendingTransaction.commit()) {
|
||||
return pendingTransaction.status
|
||||
}
|
||||
|
||||
store()
|
||||
|
||||
_balanceStateFlow.update { _ -> it.balance }
|
||||
_lockedBalanceStateFlow.update { _ -> it.lockedBalance }
|
||||
it.refreshHistory()
|
||||
_transactions.update { _ -> it.transactionHistory.transactions }
|
||||
|
||||
pendingTransaction.status
|
||||
}
|
||||
}
|
||||
|
||||
fun sendTransactionMultDest(
|
||||
destinations: Array<String>,
|
||||
amounts: Array<Long>,
|
||||
priority: TransactionPriority = TransactionPriority.UNIMPORTANT,
|
||||
): PendingTransaction? =
|
||||
synchronized(lock) {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (destinations.isEmpty()) {
|
||||
throw IllegalArgumentException("No destinations specified")
|
||||
}
|
||||
|
||||
if (amounts.isEmpty()) {
|
||||
throw IllegalArgumentException("No amounts specified")
|
||||
}
|
||||
|
||||
if (destinations.size != amounts.size) {
|
||||
throw IllegalArgumentException("The destinations array must be of the same size as the amounts array")
|
||||
}
|
||||
|
||||
if (amounts.any { it == 0L }) {
|
||||
throw IllegalArgumentException("Amounts must not be zero")
|
||||
}
|
||||
|
||||
if (destinations.any { it.isEmpty() }) {
|
||||
throw IllegalArgumentException("Destinations must not be empty")
|
||||
}
|
||||
|
||||
wallet?.let {
|
||||
val pendingTransaction = it.createTransactionMultDest(destinations, amounts, priority = priority)
|
||||
if (pendingTransaction.status.type != PendingTransaction.StatusType.OK) {
|
||||
return pendingTransaction
|
||||
}
|
||||
|
||||
pendingTransaction.saveTxId()
|
||||
|
||||
if (!pendingTransaction.commit()) {
|
||||
return pendingTransaction
|
||||
}
|
||||
|
||||
store()
|
||||
|
||||
_balanceStateFlow.update { _ -> it.balance }
|
||||
_lockedBalanceStateFlow.update { _ -> it.lockedBalance }
|
||||
it.refreshHistory()
|
||||
_transactions.update { _ -> it.transactionHistory.transactions }
|
||||
|
||||
pendingTransaction
|
||||
}
|
||||
}
|
||||
|
||||
fun getTxProof(
|
||||
txId: String,
|
||||
destination: String,
|
||||
message: String = "",
|
||||
): Proof? =
|
||||
synchronized(lock) {
|
||||
wallet?.let {
|
||||
val proof = it.getTxProof(txId, destination, message)
|
||||
val status = it.status
|
||||
Proof(proof, status)
|
||||
}
|
||||
}
|
||||
|
||||
fun setRestoreHeight(
|
||||
height: Long,
|
||||
name: String,
|
||||
password: String,
|
||||
spendKey: String,
|
||||
daemonAddress: String,
|
||||
daemonUsername: String = "",
|
||||
daemonPassword: String = "",
|
||||
proxy: String = "",
|
||||
): Wallet.Status? =
|
||||
synchronized(lock) {
|
||||
if (name.isBlank()) {
|
||||
throw IllegalArgumentException("Wallet name not specified")
|
||||
}
|
||||
|
||||
if (spendKey.isBlank()) {
|
||||
throw IllegalArgumentException("Spend key not specified")
|
||||
}
|
||||
|
||||
if (daemonAddress.isBlank()) {
|
||||
throw IllegalArgumentException("Daemon address not specified")
|
||||
}
|
||||
|
||||
wallet?.let {
|
||||
close()
|
||||
WalletManager.deleteWallet(name)
|
||||
|
||||
val myWallet = load(name, password, spendKey, daemonAddress, daemonUsername, daemonPassword, proxy, height)
|
||||
return myWallet.status
|
||||
}
|
||||
}
|
||||
|
||||
fun setProxy(proxy: String): Boolean? =
|
||||
synchronized(lock) {
|
||||
if (!WalletManager.setProxy(proxy)) {
|
||||
return false
|
||||
}
|
||||
return wallet?.setProxy(proxy)
|
||||
}
|
||||
|
||||
fun storeWallet(): Wallet.Status? =
|
||||
synchronized(lock) {
|
||||
store()
|
||||
}
|
||||
|
||||
private fun store(): Wallet.Status? {
|
||||
val status =
|
||||
wallet?.let {
|
||||
it.store()
|
||||
if (!it.status.isOk()) {
|
||||
status = WalletStatus(WalletStatusType.ERROR, it.status.error)
|
||||
}
|
||||
it.status
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
fun closeWallet() =
|
||||
synchronized(lock) {
|
||||
checkNotInMainThread()
|
||||
|
||||
close()
|
||||
}
|
||||
|
||||
private fun close() {
|
||||
wallet?.let {
|
||||
it.pauseRefresh()
|
||||
|
||||
WalletManager.close(it)
|
||||
}
|
||||
|
||||
wallet = null
|
||||
}
|
||||
|
||||
fun newSubaddress(
|
||||
accountIndex: Int,
|
||||
label: String,
|
||||
): Subaddress? =
|
||||
synchronized(lock) {
|
||||
val subaddress = wallet?.newSubaddress()
|
||||
store()
|
||||
return subaddress
|
||||
}
|
||||
|
||||
fun lastSubaddress(accountIndex: Int): Subaddress? =
|
||||
synchronized(lock) {
|
||||
return wallet?.lastSubaddress(accountIndex)
|
||||
}
|
||||
|
||||
fun listAddresses(): List<Subaddress>? =
|
||||
synchronized(lock) {
|
||||
return wallet?.let {
|
||||
val addresses = mutableListOf<Subaddress>()
|
||||
val numAddresses = it.getNumSubaddresses(0)
|
||||
for (i in 0..<numAddresses) {
|
||||
val address = it.getAddressWithIndex(0, i)
|
||||
addresses += address
|
||||
}
|
||||
addresses
|
||||
}
|
||||
}
|
||||
|
||||
fun setSubaddressLabel(
|
||||
index: Int,
|
||||
label: String,
|
||||
): Wallet.Status? {
|
||||
synchronized(lock) {
|
||||
return wallet?.let {
|
||||
it.setSubaddressLabel(0, index, label)
|
||||
if (!it.status.isOk()) {
|
||||
return it.status
|
||||
}
|
||||
store()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun seedWithPassphrase(passphrase: String): String? {
|
||||
synchronized(lock) {
|
||||
return wallet?.seedWithPassphrase(passphrase)
|
||||
}
|
||||
}
|
||||
|
||||
fun checkTxProof(
|
||||
txId: String,
|
||||
address: String,
|
||||
message: String = "",
|
||||
signature: String,
|
||||
): ProofInfo? =
|
||||
synchronized(lock) {
|
||||
return wallet?.checkTxProof(txId, address, message, signature)
|
||||
}
|
||||
|
||||
fun isAddressValid(address: String): Boolean? =
|
||||
synchronized(lock) {
|
||||
return wallet?.isAddressValid(address)
|
||||
}
|
||||
|
||||
fun getRestoreHeight(): Long? {
|
||||
synchronized(lock) {
|
||||
return wallet?.getRestoreHeight()
|
||||
}
|
||||
}
|
||||
|
||||
fun getHistory(): TransactionHistory? {
|
||||
synchronized(lock) {
|
||||
return wallet?.transactionHistory
|
||||
}
|
||||
}
|
||||
|
||||
fun setUserNote(
|
||||
txId: String,
|
||||
note: String,
|
||||
): Boolean? {
|
||||
synchronized(lock) {
|
||||
return wallet?.setUserNote(txId, note)
|
||||
}
|
||||
}
|
||||
|
||||
fun estimateTransactionFee(
|
||||
destinations: Array<String>,
|
||||
amounts: Array<Long>,
|
||||
priority: TransactionPriority,
|
||||
): Long? {
|
||||
synchronized(lock) {
|
||||
return wallet?.estimateTransactionFee(destinations, amounts, priority)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
}
|
||||
|
||||
override fun onStartCommand(
|
||||
intent: Intent?,
|
||||
flags: Int,
|
||||
startId: Int,
|
||||
): Int = super.onStartCommand(intent, flags, startId)
|
||||
|
||||
override fun onBind(intent: Intent): IBinder = binder
|
||||
|
||||
override fun onUnbind(intent: Intent?): Boolean = false
|
||||
|
||||
override fun onDestroy() {
|
||||
synchronized(lock) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ServiceState(
|
||||
val wallet: Wallet?,
|
||||
)
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
|
||||
enum class TransactionPriority(
|
||||
val value: Int,
|
||||
) {
|
||||
UNIMPORTANT(0),
|
||||
SLOW(1),
|
||||
MEDIUM(2),
|
||||
FAST(3),
|
||||
ESTIMATED(4),
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<!--
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Martin Allien
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="226.78" android:viewportWidth="226.78" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#FF000000" android:pathData="M39.72,149.02v-95.15l73.74,73.74 73.67,-73.67v95.08h33.94a113.22,113.22 0,0 0,5.71 -35.59c0,-62.6 -50.75,-113.35 -113.35,-113.35C50.83,0.09 0.08,50.83 0.08,113.43c0,12.44 2.01,24.4 5.71,35.59h33.93z"/>
|
||||
|
||||
<path android:fillColor="#FF000000" android:pathData="M162.54,172.08v-60.15l-49.49,49.49 -49.15,-49.15v59.81h-47.48c19.86,32.79 55.88,54.7 97.01,54.7 41.13,0 77.15,-21.91 97.01,-54.7H162.54z"/>
|
||||
|
||||
</vector>
|
||||
@@ -3055,4 +3055,57 @@
|
||||
<string name="nowhere_link_card_drop">Nowhere Drop</string>
|
||||
<string name="nowhere_link_card_art">Nowhere Art</string>
|
||||
<string name="nowhere_link_card_forum">Nowhere Forum</string>
|
||||
|
||||
<!-- Monero wallet strings -->
|
||||
<string name="monero">Monero</string>
|
||||
<string name="monero_wallet">Monero Wallet</string>
|
||||
<string name="wallet_status_opening">Opening wallet...</string>
|
||||
<string name="wallet_status_connecting">Connecting to daemon...</string>
|
||||
<string name="wallet_status_disconnecting">Disconnecting...</string>
|
||||
<string name="wallet_status_syncing">Syncing %1$s blocks...</string>
|
||||
<string name="wallet_status_synced">Wallet synced</string>
|
||||
<string name="wallet_status_error">Wallet error</string>
|
||||
<string name="wallet_status_disconnected">Disconnected</string>
|
||||
<string name="x_blocks_left">Waiting for %1$s more blocks...</string>
|
||||
<string name="x_blocks">%1$s blocks</string>
|
||||
<string name="send_monero">Send Monero</string>
|
||||
<string name="receive_monero">Receive Monero</string>
|
||||
<string name="monero_balance">Monero Balance</string>
|
||||
<string name="locked_balance">Locked Balance</string>
|
||||
<string name="send">Send</string>
|
||||
<string name="receive">Receive</string>
|
||||
<string name="copy_address">Copy Address</string>
|
||||
<string name="address_copied">Address copied</string>
|
||||
<string name="monero_address">Monero Address</string>
|
||||
<string name="generate_new_address">Generate New Address</string>
|
||||
<string name="label_address">Label Address</string>
|
||||
<string name="enter_label">Enter label...</string>
|
||||
<string name="amount">Amount</string>
|
||||
<string name="enter_amount">Enter amount</string>
|
||||
<string name="enter_address">Enter Monero address</string>
|
||||
<string name="monero_not_enough_balance">Insufficient Monero balance</string>
|
||||
<string name="monero_missing_setup">Missing Monero setup</string>
|
||||
<string name="monero_user_no_address">This user does not have a Monero address set up to receive tips.</string>
|
||||
<string name="error_dialog_tip_error">Tip error</string>
|
||||
<string name="daemon_host">Daemon Host</string>
|
||||
<string name="daemon_port">Daemon Port</string>
|
||||
<string name="daemon_username">Daemon Username (optional)</string>
|
||||
<string name="daemon_password">Daemon Password (optional)</string>
|
||||
<string name="edit_daemon_settings">Edit Daemon Settings</string>
|
||||
<string name="connection_status">Connection Status</string>
|
||||
<string name="wallet_height">Wallet Height</string>
|
||||
<string name="daemon_height">Daemon Height</string>
|
||||
<string name="transaction_priority">Transaction Priority</string>
|
||||
<string name="priority_unimportant">Unimportant</string>
|
||||
<string name="priority_slow">Slow</string>
|
||||
<string name="priority_medium">Medium</string>
|
||||
<string name="priority_fast">Fast</string>
|
||||
<string name="tx_id">Transaction ID</string>
|
||||
<string name="confirmations">Confirmations</string>
|
||||
<string name="backup_seed">Backup Seed</string>
|
||||
<string name="backup_seed_warning">Ephemeral wallets must be backed up! If you lose your seed, you lose access to your Monero coins. Write it down and store it securely.</string>
|
||||
<string name="restore_height">Restore Height</string>
|
||||
<string name="custom_restore_height">Custom Restore Height</string>
|
||||
<string name="monero_send_failed">Monero send failed</string>
|
||||
<string name="monero_tip_sent">Monero tip sent</string>
|
||||
</resources>
|
||||
|
||||
@@ -93,6 +93,18 @@ class User(
|
||||
|
||||
fun lnAddress(): String? = metadataOrNull()?.lnAddress()
|
||||
|
||||
fun moneroAddress(): String? {
|
||||
val nip69 = nip69Address()
|
||||
if (nip69 != null) return nip69
|
||||
|
||||
return metadataOrNull()?.moneroAddressFromAbout()
|
||||
}
|
||||
|
||||
private fun nip69Address(): String? {
|
||||
// NIP-69 monero address (type-0 event) not yet implemented in metadata
|
||||
return null
|
||||
}
|
||||
|
||||
fun addRelayBeingUsed(
|
||||
relay: NormalizedRelayUrl,
|
||||
eventTime: Long,
|
||||
@@ -151,3 +163,5 @@ fun List<User>.toHexes() = map { it.pubkeyHex }
|
||||
fun List<User>.toHexSet() = mapTo(LinkedHashSet(size)) { it.pubkeyHex }
|
||||
|
||||
fun List<User>.toSortedHexes() = map { it.pubkeyHex }.sorted()
|
||||
|
||||
fun moneroAddressIsValid(address: String): Boolean = address.length == 95 && (address.startsWith('4') || address.startsWith('8'))
|
||||
|
||||
+7
@@ -78,4 +78,11 @@ class UserMetadataCache {
|
||||
fun profilePicture(): String? = flow.value?.info?.picture
|
||||
|
||||
fun lnAddress(): String? = flow.value?.info?.lnAddress()
|
||||
|
||||
fun moneroAddressFromAbout(): String? {
|
||||
val about = flow.value?.info?.about ?: return null
|
||||
|
||||
val match = """\b[48]\w{93}\b""".toRegex().find(about)
|
||||
return match?.value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.moneroTip
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.IEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.JsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.core.OptimizedSerializable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class TipProof(
|
||||
@SerialName("txid") val txId: String,
|
||||
val proofs: Map<String, Array<String>>,
|
||||
val message: String?,
|
||||
) : OptimizedSerializable
|
||||
|
||||
@Immutable
|
||||
class MoneroTipEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: TagArray,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
EventHintProvider,
|
||||
PubKeyHintProvider {
|
||||
var valueByUser: MutableMap<HexKey, ULong> = mutableMapOf()
|
||||
val totalValue: ULong
|
||||
get() = valueByUser.values.sum()
|
||||
|
||||
fun tipProof(): TipProof? =
|
||||
try {
|
||||
if (content.isNotEmpty()) {
|
||||
JsonMapper.fromJson<TipProof>(content)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w("MoneroTipEvent", "Can't parse content as tip proof: $content", e)
|
||||
null
|
||||
}
|
||||
|
||||
override fun eventHints() = tags.mapNotNull(ETag::parseAsHint)
|
||||
|
||||
override fun linkedEventIds() = tags.mapNotNull(ETag::parseId)
|
||||
|
||||
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
|
||||
|
||||
override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey)
|
||||
|
||||
companion object {
|
||||
const val KIND = 1814
|
||||
|
||||
suspend fun create(
|
||||
event: IEvent? = null,
|
||||
users: Set<HexKey>,
|
||||
txId: String,
|
||||
proofs: Map<String, Array<String>>,
|
||||
message: String = "",
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (MoneroTipEvent) -> Unit,
|
||||
) {
|
||||
if (proofs.isEmpty()) {
|
||||
throw IllegalArgumentException("No proofs specified")
|
||||
}
|
||||
|
||||
if (users.isEmpty()) {
|
||||
throw IllegalArgumentException("At least one user must be specified")
|
||||
}
|
||||
|
||||
val tipProof = TipProof(txId, proofs, message.ifEmpty { null })
|
||||
val content = JsonMapper.toJson(tipProof)
|
||||
|
||||
val tags: MutableList<Array<String>> = mutableListOf()
|
||||
users.forEach {
|
||||
tags.add(arrayOf("p", it))
|
||||
}
|
||||
|
||||
event?.let {
|
||||
if (it is AddressableEvent) {
|
||||
tags.add(arrayOf("a", it.addressTag()))
|
||||
} else if (it is Event) {
|
||||
tags.add(arrayOf("e", it.id))
|
||||
}
|
||||
}
|
||||
|
||||
val signed: MoneroTipEvent = signer.sign(createdAt, KIND, tags.toTypedArray(), content)
|
||||
onReady(signed)
|
||||
}
|
||||
}
|
||||
|
||||
enum class TipType {
|
||||
PRIVATE,
|
||||
ANONYMOUS,
|
||||
PUBLIC,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.moneroTip
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class TipSplitSetup(
|
||||
@SerialName("monero") val addressOrPubKeyHex: String,
|
||||
@SerialName("weight") val weight: Double?,
|
||||
val isAddress: Boolean = false,
|
||||
)
|
||||
|
||||
fun Event?.hasTipSplitSetup(): Boolean {
|
||||
val tags = this?.tags ?: return false
|
||||
return tags.any { tag -> tag.size > 1 && tag[0] == MONERO_TAG_NAME }
|
||||
}
|
||||
|
||||
fun Event?.tipSplitSetup(): List<TipSplitSetup>? {
|
||||
val tags = this?.tags ?: return null
|
||||
return tags.mapNotNull { tag ->
|
||||
if (tag.size > 1 && tag[0] == MONERO_TAG_NAME) {
|
||||
val weight = if (tag.size > 2) tag[2].toDoubleOrNull() else null
|
||||
TipSplitSetup(tag[1], weight, isValidMoneroAddress(tag[1]))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val MONERO_TAG_NAME = "monero"
|
||||
|
||||
private fun isValidMoneroAddress(address: String): Boolean {
|
||||
if (address.length != 95) return false
|
||||
return address.startsWith('4') || address.startsWith('8')
|
||||
}
|
||||
@@ -54,6 +54,7 @@ import com.vitorpamplona.quartz.marmot.mip05PushNotifications.NotificationReques
|
||||
import com.vitorpamplona.quartz.marmot.mip05PushNotifications.TokenListEvent
|
||||
import com.vitorpamplona.quartz.marmot.mip05PushNotifications.TokenRemovalEvent
|
||||
import com.vitorpamplona.quartz.marmot.mip05PushNotifications.TokenRequestEvent
|
||||
import com.vitorpamplona.quartz.moneroTip.MoneroTipEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher
|
||||
@@ -446,6 +447,7 @@ class EventFactory {
|
||||
LiveActivitiesEvent.KIND -> LiveActivitiesEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
LiveActivitiesRaidEvent.KIND -> LiveActivitiesRaidEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
LnZapEvent.KIND -> LnZapEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
MoneroTipEvent.KIND -> MoneroTipEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
LnZapPaymentRequestEvent.KIND -> LnZapPaymentRequestEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
LnZapPaymentResponseEvent.KIND -> LnZapPaymentResponseEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
NwcInfoEvent.KIND -> NwcInfoEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
||||
Reference in New Issue
Block a user