Forgot to actually push the converted services

This commit is contained in:
pokkst 2023-12-06 12:47:26 -06:00
parent 08b989eaab
commit 97d39a3cb4
No known key found for this signature in database
GPG Key ID: EC4FAAA66859FAA4
18 changed files with 733 additions and 719 deletions

View File

@ -1,49 +0,0 @@
package net.mynero.wallet.service;
import net.mynero.wallet.data.Subaddress;
import net.mynero.wallet.model.Wallet;
import net.mynero.wallet.model.WalletManager;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
public class AddressService extends ServiceBase {
public static AddressService instance = null;
private int latestAddressIndex = 1;
public AddressService(MoneroHandlerThread thread) {
super(thread);
instance = this;
}
public static AddressService getInstance() {
return instance;
}
public void refreshAddresses() {
latestAddressIndex = WalletManager.getInstance().getWallet().getNumSubaddresses();
}
public int getLatestAddressIndex() {
return latestAddressIndex;
}
public String getPrimaryAddress() {
return WalletManager.getInstance().getWallet().getAddress();
}
public Subaddress freshSubaddress() {
String timeStamp = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss", Locale.US).format(new Date());
Wallet wallet = WalletManager.getInstance().getWallet();
wallet.addSubaddress(wallet.getAccountIndex(), timeStamp);
refreshAddresses();
wallet.store();
return wallet.getSubaddressObject(latestAddressIndex);
}
public Subaddress currentSubaddress() {
Wallet wallet = WalletManager.getInstance().getWallet();
return wallet.getSubaddressObject(latestAddressIndex);
}
}

View File

@ -0,0 +1,39 @@
package net.mynero.wallet.service
import net.mynero.wallet.data.Subaddress
import net.mynero.wallet.model.WalletManager
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class AddressService(thread: MoneroHandlerThread) : ServiceBase(thread) {
var latestAddressIndex = 1
private set
init {
instance = this
}
fun refreshAddresses() {
WalletManager.instance?.wallet?.numSubaddresses?.let { latestAddressIndex = it }
}
fun freshSubaddress(): Subaddress? {
val timeStamp = SimpleDateFormat("yyyy-MM-dd-HH:mm:ss", Locale.US).format(Date())
val wallet = WalletManager.instance?.wallet
wallet?.addSubaddress(wallet.getAccountIndex(), timeStamp)
refreshAddresses()
wallet?.store()
return wallet?.getSubaddressObject(latestAddressIndex)
}
fun currentSubaddress(): Subaddress? {
val wallet = WalletManager.instance?.wallet
return wallet?.getSubaddressObject(latestAddressIndex)
}
companion object {
@JvmField
var instance: AddressService? = null
}
}

View File

@ -1,52 +0,0 @@
package net.mynero.wallet.service;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import net.mynero.wallet.model.BalanceInfo;
import net.mynero.wallet.model.CoinsInfo;
public class BalanceService extends ServiceBase {
public static BalanceService instance = null;
private final MutableLiveData<BalanceInfo> _balanceInfo = new MutableLiveData<>(null);
public LiveData<BalanceInfo> balanceInfo = _balanceInfo;
public BalanceService(MoneroHandlerThread thread) {
super(thread);
instance = this;
}
public static BalanceService getInstance() {
return instance;
}
public void refreshBalance() {
long rawUnlocked = getUnlockedBalanceRaw();
long rawLocked = getLockedBalanceRaw();
_balanceInfo.postValue(new BalanceInfo(rawUnlocked, rawLocked));
}
public long getUnlockedBalanceRaw() {
long unlocked = 0;
for (CoinsInfo coinsInfo : UTXOService.getInstance().getUtxos()) {
if (!coinsInfo.isSpent() && !coinsInfo.isFrozen() && coinsInfo.isUnlocked() && !UTXOService.getInstance().isCoinFrozen(coinsInfo)) {
unlocked += coinsInfo.amount;
}
}
return unlocked;
}
public long getTotalBalanceRaw() {
long total = 0;
for (CoinsInfo coinsInfo : UTXOService.getInstance().getUtxos()) {
if (!coinsInfo.isSpent() && !coinsInfo.isFrozen() && !UTXOService.getInstance().isCoinFrozen(coinsInfo)) {
total += coinsInfo.amount;
}
}
return total;
}
public long getLockedBalanceRaw() {
return getTotalBalanceRaw() - getUnlockedBalanceRaw();
}
}

View File

@ -0,0 +1,58 @@
package net.mynero.wallet.service
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import net.mynero.wallet.model.BalanceInfo
class BalanceService(thread: MoneroHandlerThread) : ServiceBase(thread) {
private val _balanceInfo = MutableLiveData<BalanceInfo?>(null)
@JvmField
var balanceInfo: LiveData<BalanceInfo?> = _balanceInfo
init {
instance = this
}
fun refreshBalance() {
val rawUnlocked = unlockedBalanceRaw
val rawLocked = lockedBalanceRaw
_balanceInfo.postValue(BalanceInfo(rawUnlocked, rawLocked))
}
val unlockedBalanceRaw: Long
get() {
var unlocked: Long = 0
val utxos = UTXOService.instance?.getUtxos() ?: emptyList()
for (coinsInfo in utxos) {
if (!coinsInfo.isSpent && !coinsInfo.isFrozen && coinsInfo.isUnlocked && UTXOService.instance?.isCoinFrozen(
coinsInfo
) == false
) {
unlocked += coinsInfo.amount
}
}
return unlocked
}
val totalBalanceRaw: Long
get() {
var total: Long = 0
val utxos = UTXOService.instance?.getUtxos() ?: emptyList()
for (coinsInfo in utxos) {
if (!coinsInfo.isSpent && !coinsInfo.isFrozen && UTXOService.instance?.isCoinFrozen(
coinsInfo
) == false
) {
total += coinsInfo.amount
}
}
return total
}
val lockedBalanceRaw: Long
get() = totalBalanceRaw - unlockedBalanceRaw
companion object {
@JvmField
var instance: BalanceService? = null
}
}

View File

@ -1,55 +0,0 @@
package net.mynero.wallet.service;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import net.mynero.wallet.model.Wallet;
import net.mynero.wallet.model.WalletManager;
public class BlockchainService extends ServiceBase {
public static BlockchainService instance = null;
private final MutableLiveData<Long> _currentHeight = new MutableLiveData<>(0L);
private final MutableLiveData<Wallet.ConnectionStatus> _connectionStatus = new MutableLiveData<>(Wallet.ConnectionStatus.ConnectionStatus_Disconnected);
public LiveData<Long> height = _currentHeight;
public LiveData<Wallet.ConnectionStatus> connectionStatus = _connectionStatus;
private long daemonHeight = 0;
private long lastDaemonHeightUpdateTimeMs = 0;
public BlockchainService(MoneroHandlerThread thread) {
super(thread);
instance = this;
}
public static BlockchainService getInstance() {
return instance;
}
public void refreshBlockchain() {
_currentHeight.postValue(getCurrentHeight());
}
public long getCurrentHeight() {
return WalletManager.getInstance().getWallet().getBlockChainHeight();
}
public long getDaemonHeight() {
return this.daemonHeight;
}
public void setDaemonHeight(long height) {
long t = System.currentTimeMillis();
if (height > 0) {
daemonHeight = height;
lastDaemonHeightUpdateTimeMs = t;
} else {
if (t - lastDaemonHeightUpdateTimeMs > 120000) {
daemonHeight = WalletManager.getInstance().getWallet().getDaemonBlockChainHeight();
lastDaemonHeightUpdateTimeMs = t;
}
}
}
public void setConnectionStatus(Wallet.ConnectionStatus status) {
_connectionStatus.postValue(status);
}
}

View File

@ -0,0 +1,51 @@
package net.mynero.wallet.service
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import net.mynero.wallet.model.Wallet.ConnectionStatus
import net.mynero.wallet.model.WalletManager
class BlockchainService(thread: MoneroHandlerThread) : ServiceBase(thread) {
private val _currentHeight = MutableLiveData(0L)
private val _connectionStatus = MutableLiveData(ConnectionStatus.ConnectionStatus_Disconnected)
@JvmField
var height: LiveData<Long> = _currentHeight
@JvmField
var connectionStatus: LiveData<ConnectionStatus> = _connectionStatus
var daemonHeight: Long = 0
set(height) {
val t = System.currentTimeMillis()
if (height > 0) {
field = height
lastDaemonHeightUpdateTimeMs = t
} else {
if (t - lastDaemonHeightUpdateTimeMs > 120000) {
field = WalletManager.instance!!.wallet!!.getDaemonBlockChainHeight()
lastDaemonHeightUpdateTimeMs = t
}
}
}
private var lastDaemonHeightUpdateTimeMs: Long = 0
init {
instance = this
}
fun refreshBlockchain() {
_currentHeight.postValue(currentHeight)
}
val currentHeight: Long
get() = WalletManager.instance!!.wallet!!.getBlockChainHeight()
fun setConnectionStatus(status: ConnectionStatus) {
_connectionStatus.postValue(status)
}
companion object {
@JvmField
var instance: BlockchainService? = null
}
}

View File

@ -1,32 +0,0 @@
package net.mynero.wallet.service;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import net.mynero.wallet.model.TransactionInfo;
import net.mynero.wallet.model.WalletManager;
import java.util.List;
public class HistoryService extends ServiceBase {
private static HistoryService instance = null;
private final MutableLiveData<List<TransactionInfo>> _history = new MutableLiveData<>();
public LiveData<List<TransactionInfo>> history = _history;
public HistoryService(MoneroHandlerThread thread) {
super(thread);
instance = this;
}
public static HistoryService getInstance() {
return instance;
}
public void refreshHistory() {
_history.postValue(getHistory());
}
private List<TransactionInfo> getHistory() {
return WalletManager.getInstance().getWallet().getHistory().getAll();
}
}

View File

@ -0,0 +1,31 @@
package net.mynero.wallet.service
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import net.mynero.wallet.model.TransactionInfo
import net.mynero.wallet.model.WalletManager
class HistoryService(thread: MoneroHandlerThread) : ServiceBase(thread) {
private val _history = MutableLiveData<List<TransactionInfo>>()
@JvmField
var history: LiveData<List<TransactionInfo>> = _history
init {
instance = this
}
fun refreshHistory() {
_history.postValue(getHistory())
}
private fun getHistory(): List<TransactionInfo> {
return WalletManager.instance!!.wallet!!.history!!.all
}
companion object {
@JvmStatic
var instance: HistoryService? = null
private set
}
}

View File

@ -1,238 +0,0 @@
/*
* Copyright (C) 2006 The Android Open Source Project
* Copyright (c) 2017 m2049r
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
package net.mynero.wallet.service;
import net.mynero.wallet.data.Node;
import net.mynero.wallet.model.CoinsInfo;
import net.mynero.wallet.model.PendingTransaction;
import net.mynero.wallet.model.TransactionOutput;
import net.mynero.wallet.model.Wallet;
import net.mynero.wallet.model.WalletListener;
import net.mynero.wallet.model.WalletManager;
import net.mynero.wallet.util.Constants;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import kotlin.Pair;
/**
* Handy class for starting a new thread that has a looper. The looper can then be
* used to create handler classes. Note that start() must still be called.
* The started Thread has a stck size of STACK_SIZE (=5MB)
*/
public class MoneroHandlerThread extends Thread implements WalletListener {
// from src/cryptonote_config.h
static public final long THREAD_STACK_SIZE = 5 * 1024 * 1024;
private final Wallet wallet;
int triesLeft = 5;
private Listener listener = null;
public MoneroHandlerThread(String name, Listener listener, Wallet wallet) {
super(null, null, name, THREAD_STACK_SIZE);
this.listener = listener;
this.wallet = wallet;
}
@Override
public synchronized void start() {
super.start();
this.listener.onRefresh(false);
}
@Override
public void run() {
PrefService prefService = PrefService.getInstance();
boolean usesTor = prefService.getBoolean(Constants.PREF_USES_TOR, false);
Node currentNode = prefService.getNode();
boolean isLocalIp = currentNode.getAddress().startsWith("10.") || currentNode.getAddress().startsWith("192.168.") || currentNode.getAddress().equals("localhost") || currentNode.getAddress().equals("127.0.0.1");
if (usesTor && !isLocalIp) {
String proxy = prefService.getProxy();
WalletManager.getInstance().setProxy(proxy);
wallet.setProxy(proxy);
}
WalletManager.getInstance().setDaemon(currentNode);
wallet.init(0);
wallet.setListener(this);
wallet.startRefresh();
}
@Override
public void moneySpent(String txId, long amount) {
}
@Override
public void moneyReceived(String txId, long amount) {
}
@Override
public void unconfirmedMoneyReceived(String txId, long amount) {
}
@Override
public void newBlock(long height) {
refresh(false);
BlockchainService.getInstance().setDaemonHeight(wallet.isSynchronized() ? height : 0);
}
@Override
public void updated() {
refresh(false);
}
@Override
public void refreshed() {
Wallet.ConnectionStatus status = wallet.getFullStatus().connectionStatus;
if (status == Wallet.ConnectionStatus.ConnectionStatus_Disconnected || status == null) {
if (triesLeft > 0) {
wallet.startRefresh();
triesLeft--;
} else {
listener.onConnectionFail();
}
} else {
BlockchainService.getInstance().setDaemonHeight(wallet.getDaemonBlockChainHeight());
wallet.setSynchronized();
wallet.store();
refresh(true);
}
BlockchainService.getInstance().setConnectionStatus(status);
}
private void refresh(boolean walletSynced) {
wallet.refreshHistory();
if (walletSynced) {
wallet.refreshCoins();
}
listener.onRefresh(walletSynced);
}
public PendingTransaction createTx(String address, String amountStr, boolean sendAll, PendingTransaction.Priority feePriority, ArrayList<String> selectedUtxos) throws Exception {
ArrayList<Pair<String, String>> dests = new ArrayList<>();
dests.add(new Pair(address, amountStr));
return createTx(dests, sendAll, feePriority, selectedUtxos);
}
public PendingTransaction createTx(List<Pair<String, String>> dests, boolean sendAll, PendingTransaction.Priority feePriority, ArrayList<String> selectedUtxos) throws Exception {
long totalAmount = 0;
ArrayList<TransactionOutput> outputs = new ArrayList<>();
for (Pair<String, String> dest : dests) {
long amount = Wallet.getAmountFromString(dest.component2());
totalAmount += amount;
outputs.add(new TransactionOutput(dest.component1(), amount));
}
ArrayList<String> preferredInputs;
if (selectedUtxos.isEmpty()) {
// no inputs manually selected, we are sending from home screen most likely, or user somehow broke the app
preferredInputs = UTXOService.getInstance().selectUtxos(totalAmount, sendAll, feePriority);
} else {
preferredInputs = selectedUtxos;
checkSelectedAmounts(preferredInputs, totalAmount, sendAll);
}
if (sendAll) {
Pair<String, String> dest = dests.get(0);
String address = dest.component1();
return wallet.createSweepTransaction(address, feePriority, preferredInputs);
}
List<TransactionOutput> finalOutputs = maybeAddDonationOutputs(totalAmount, outputs, preferredInputs);
return wallet.createTransactionMultDest(finalOutputs, feePriority, preferredInputs);
}
private List<TransactionOutput> maybeAddDonationOutputs(long amount, List<TransactionOutput> outputs, List<String> preferredInputs) throws Exception {
TransactionOutput mainDestination = outputs.get(0); // at this point, for now, we should only have one item in the list. TODO: add multi-dest/pay-to-many feature in the UI
String paymentId = Wallet.getPaymentIdFromAddress(mainDestination.destination, WalletManager.getInstance().networkType.value);
ArrayList<TransactionOutput> newOutputs = new ArrayList<>(outputs);
boolean donatePerTx = PrefService.getInstance().getBoolean(Constants.PREF_DONATE_PER_TX, false);
if (donatePerTx && paymentId.isEmpty()) { // only attach donation when no payment id is needed (i.e. integrated address)
SecureRandom rand = new SecureRandom();
float randomDonatePct = getRandomDonateAmount(0.005f, 0.015f); // occasionally attaches a 0.5% to 1.5% donation. It is random so that not even I know how much exactly you are sending.
/*
It's also not entirely "per tx". It won't always attach it so as to not have a consistently uncommon fingerprint on-chain. When it does attach a donation,
it will periodically split it up into multiple outputs instead of one.
*/
int attachDonationRoll = rand.nextInt(100);
if (attachDonationRoll > 90) { // 10% chance of being added
int splitDonationRoll = rand.nextInt(100);
long donateAmount = (long) (amount * randomDonatePct);
if (splitDonationRoll > 50) { // 50% chance of being split
// split
int split = genRandomDonationSplit(1, 4); // splits into at most 4 outputs, for a total of 6 outputs in the transaction (real dest + change. we don't add donations to send-all/sweep transactions)
long splitAmount = donateAmount / split;
for (int i = 0; i < split; i++) {
// TODO this can be expanded upon into the future to perform an auto-splitting/auto-churning for the user if their wallet is fresh and has few utxos.
// randomly split between multiple wallets
int randomDonationAddress = rand.nextInt(Constants.DONATION_ADDRESSES.length);
String donationAddress = Constants.DONATION_ADDRESSES[randomDonationAddress];
newOutputs.add(new TransactionOutput(donationAddress, splitAmount));
}
} else {
// just add one output, for a total of 3 (real dest + change)
newOutputs.add(new TransactionOutput(Constants.DONATE_ADDRESS, donateAmount));
}
long total = amount + donateAmount;
checkSelectedAmounts(preferredInputs, total, false); // check that the selected UTXOs satisfy the new amount total
}
}
Collections.shuffle(newOutputs); // shuffle the outputs just in case. i think the monero library handles this for us anyway
return newOutputs;
}
private void checkSelectedAmounts(List<String> selectedUtxos, long amount, boolean sendAll) throws Exception {
if (!sendAll) {
long amountSelected = 0;
for (CoinsInfo coinsInfo : UTXOService.getInstance().getUtxos()) {
if (selectedUtxos.contains(coinsInfo.keyImage)) {
amountSelected += coinsInfo.amount;
}
}
if (amountSelected <= amount) {
throw new Exception("insufficient wallet balance");
}
}
}
public boolean sendTx(PendingTransaction pendingTx) {
return pendingTx.commit("", true);
}
private float getRandomDonateAmount(float min, float max) {
SecureRandom rand = new SecureRandom();
return rand.nextFloat() * (max - min) + min;
}
private int genRandomDonationSplit(int min, int max) {
SecureRandom rand = new SecureRandom();
return rand.nextInt(max) + min;
}
public interface Listener {
void onRefresh(boolean walletSynced);
void onConnectionFail();
}
}

View File

@ -0,0 +1,250 @@
/*
* Copyright (C) 2006 The Android Open Source Project
* Copyright (c) 2017 m2049r
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
package net.mynero.wallet.service
import net.mynero.wallet.model.PendingTransaction
import net.mynero.wallet.model.TransactionOutput
import net.mynero.wallet.model.Wallet
import net.mynero.wallet.model.Wallet.Companion.getAmountFromString
import net.mynero.wallet.model.Wallet.Companion.getPaymentIdFromAddress
import net.mynero.wallet.model.Wallet.ConnectionStatus
import net.mynero.wallet.model.WalletListener
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.util.Constants
import java.security.SecureRandom
/**
* Handy class for starting a new thread that has a looper. The looper can then be
* used to create handler classes. Note that start() must still be called.
* The started Thread has a stck size of STACK_SIZE (=5MB)
*/
class MoneroHandlerThread(name: String?, val listener: Listener?, wallet: Wallet) :
Thread(null, null, name, THREAD_STACK_SIZE), WalletListener {
private val wallet: Wallet
var triesLeft = 5
init {
this.wallet = wallet
}
@Synchronized
override fun start() {
super.start()
listener?.onRefresh(false)
}
override fun run() {
val prefService = PrefService.instance ?: return
val usesTor = prefService.getBoolean(Constants.PREF_USES_TOR, false)
val currentNode = prefService.node
val isLocalIp =
currentNode?.address?.startsWith("10.") == true ||
currentNode?.address?.startsWith("192.168.") == true ||
currentNode?.address == "localhost" ||
currentNode?.address == "127.0.0.1"
if (usesTor && !isLocalIp) {
val proxy = prefService.proxy
proxy?.let { WalletManager.instance?.setProxy(it) }
wallet.setProxy(proxy)
}
WalletManager.instance?.setDaemon(currentNode)
wallet.init(0)
wallet.setListener(this)
wallet.startRefresh()
}
override fun moneySpent(txId: String?, amount: Long) {}
override fun moneyReceived(txId: String?, amount: Long) {}
override fun unconfirmedMoneyReceived(txId: String?, amount: Long) {}
override fun newBlock(height: Long) {
refresh(false)
BlockchainService.instance?.daemonHeight = if (wallet.isSynchronized) height else 0
}
override fun updated() {
refresh(false)
}
override fun refreshed() {
val status = wallet.fullStatus.connectionStatus
if (status === ConnectionStatus.ConnectionStatus_Disconnected || status == null) {
if (triesLeft > 0) {
wallet.startRefresh()
triesLeft--
} else {
listener?.onConnectionFail()
}
} else {
BlockchainService.instance?.daemonHeight = wallet.getDaemonBlockChainHeight()
wallet.setSynchronized()
wallet.store()
refresh(true)
}
status?.let { BlockchainService.instance?.setConnectionStatus(it) }
}
private fun refresh(walletSynced: Boolean) {
wallet.refreshHistory()
if (walletSynced) {
wallet.refreshCoins()
}
listener?.onRefresh(walletSynced)
}
@Throws(Exception::class)
fun createTx(
address: String,
amountStr: String,
sendAll: Boolean,
feePriority: PendingTransaction.Priority,
selectedUtxos: ArrayList<String>
): PendingTransaction? {
val dests = ArrayList<Pair<String, String>>()
dests.add(Pair(address, amountStr))
return createTx(dests, sendAll, feePriority, selectedUtxos)
}
@Throws(Exception::class)
fun createTx(
dests: List<Pair<String, String>>,
sendAll: Boolean,
feePriority: PendingTransaction.Priority,
selectedUtxos: ArrayList<String>
): PendingTransaction? {
var totalAmount: Long = 0
val outputs = ArrayList<TransactionOutput>()
for (dest in dests) {
val amount = getAmountFromString(dest.component2())
totalAmount += amount
outputs.add(TransactionOutput(dest.component1(), amount))
}
val preferredInputs: ArrayList<String>
if (selectedUtxos.isEmpty()) {
// no inputs manually selected, we are sending from home screen most likely, or user somehow broke the app
preferredInputs =
UTXOService.instance?.selectUtxos(totalAmount, sendAll, feePriority) ?: ArrayList()
} else {
preferredInputs = selectedUtxos
checkSelectedAmounts(preferredInputs, totalAmount, sendAll)
}
if (sendAll) {
val dest = dests[0]
val address = dest.component1()
return wallet.createSweepTransaction(address, feePriority, preferredInputs)
}
val finalOutputs = maybeAddDonationOutputs(totalAmount, outputs, preferredInputs)
return wallet.createTransactionMultDest(finalOutputs, feePriority, preferredInputs)
}
@Throws(Exception::class)
private fun maybeAddDonationOutputs(
amount: Long,
outputs: List<TransactionOutput>,
preferredInputs: List<String>
): List<TransactionOutput> {
val newOutputs = ArrayList(outputs)
val networkType = WalletManager.instance?.networkType ?: return newOutputs
val mainDestination =
outputs[0] // at this point, for now, we should only have one item in the list. TODO: add multi-dest/pay-to-many feature in the UI
val paymentId =
getPaymentIdFromAddress(mainDestination.destination, networkType.value)
val donatePerTx = PrefService.instance?.getBoolean(Constants.PREF_DONATE_PER_TX, false)
if (donatePerTx == true && paymentId?.isEmpty() == true) { // only attach donation when no payment id is needed (i.e. integrated address)
val rand = SecureRandom()
val randomDonatePct = getRandomDonateAmount(
0.005f,
0.015f
) // occasionally attaches a 0.5% to 1.5% donation. It is random so that not even I know how much exactly you are sending.
/*
It's also not entirely "per tx". It won't always attach it so as to not have a consistently uncommon fingerprint on-chain. When it does attach a donation,
it will periodically split it up into multiple outputs instead of one.
*/
val attachDonationRoll = rand.nextInt(100)
if (attachDonationRoll > 90) { // 10% chance of being added
val splitDonationRoll = rand.nextInt(100)
val donateAmount = (amount * randomDonatePct).toLong()
if (splitDonationRoll > 50) { // 50% chance of being split
// split
val split = genRandomDonationSplit(
1,
4
) // splits into at most 4 outputs, for a total of 6 outputs in the transaction (real dest + change. we don't add donations to send-all/sweep transactions)
val splitAmount = donateAmount / split
for (i in 0 until split) {
// TODO this can be expanded upon into the future to perform an auto-splitting/auto-churning for the user if their wallet is fresh and has few utxos.
// randomly split between multiple wallets
val randomDonationAddress = rand.nextInt(Constants.DONATION_ADDRESSES.size)
val donationAddress = Constants.DONATION_ADDRESSES[randomDonationAddress]
newOutputs.add(TransactionOutput(donationAddress, splitAmount))
}
} else {
// just add one output, for a total of 3 (real dest + change)
newOutputs.add(TransactionOutput(Constants.DONATE_ADDRESS, donateAmount))
}
val total = amount + donateAmount
checkSelectedAmounts(
preferredInputs,
total,
false
) // check that the selected UTXOs satisfy the new amount total
}
}
newOutputs.shuffle() // shuffle the outputs just in case. i think the monero library handles this for us anyway
return newOutputs
}
@Throws(Exception::class)
private fun checkSelectedAmounts(selectedUtxos: List<String?>, amount: Long, sendAll: Boolean) {
if (!sendAll) {
var amountSelected: Long = 0
val utxos = UTXOService.instance?.getUtxos() ?: emptyList()
for (coinsInfo in utxos) {
if (selectedUtxos.contains(coinsInfo.keyImage)) {
amountSelected += coinsInfo.amount
}
}
if (amountSelected <= amount) {
throw Exception("insufficient wallet balance")
}
}
}
fun sendTx(pendingTx: PendingTransaction): Boolean {
return pendingTx.commit("", true)
}
private fun getRandomDonateAmount(min: Float, max: Float): Float {
val rand = SecureRandom()
return rand.nextFloat() * (max - min) + min
}
private fun genRandomDonationSplit(min: Int, max: Int): Int {
val rand = SecureRandom()
return rand.nextInt(max) + min
}
interface Listener {
fun onRefresh(walletSynced: Boolean)
fun onConnectionFail()
}
companion object {
// from src/cryptonote_config.h
const val THREAD_STACK_SIZE = (5 * 1024 * 1024).toLong()
}
}

View File

@ -1,110 +0,0 @@
package net.mynero.wallet.service;
import android.content.Context;
import android.content.SharedPreferences;
import net.mynero.wallet.MoneroApplication;
import net.mynero.wallet.data.DefaultNodes;
import net.mynero.wallet.data.Node;
import net.mynero.wallet.util.Constants;
import org.json.JSONException;
import org.json.JSONObject;
public class PrefService extends ServiceBase {
private static SharedPreferences preferences = null;
private static PrefService instance = null;
public PrefService(MoneroApplication application) {
super(null);
preferences = application.getSharedPreferences(application.getApplicationInfo().packageName, Context.MODE_PRIVATE);
instance = this;
}
public static PrefService getInstance() {
return instance;
}
public SharedPreferences.Editor edit() {
return preferences.edit();
}
public Node getNode() {
boolean usesProxy = getBoolean(Constants.PREF_USES_TOR, false);
DefaultNodes defaultNode = DefaultNodes.SAMOURAI;
if (usesProxy) {
String proxyPort = getProxyPort();
if (!proxyPort.isEmpty()) {
int port = Integer.parseInt(proxyPort);
if (port == 4447) {
defaultNode = DefaultNodes.MYNERO_I2P;
} else {
defaultNode = DefaultNodes.MYNERO_ONION;
}
}
}
String nodeString = getString(Constants.PREF_NODE_2, defaultNode.getNodeString());
try {
JSONObject nodeJson = new JSONObject(nodeString);
return Node.fromJson(nodeJson);
} catch (JSONException e) {
// stored node is not json format, upgrade if possible
return upgradeOldNode(nodeString);
}
}
private Node upgradeOldNode(String nodeString) {
if (!nodeString.isEmpty()) {
Node node = Node.fromString(nodeString);
if (node != null) {
edit().putString(Constants.PREF_NODE_2, node.toJson().toString()).apply();
return node;
}
}
return null;
}
public String getProxy() {
return PrefService.getInstance().getString(Constants.PREF_PROXY, "");
}
public boolean hasProxySet() {
String proxyString = getProxy();
return proxyString.contains(":");
}
public String getProxyAddress() {
if (hasProxySet()) {
String proxyString = getProxy();
return proxyString.split(":")[0];
}
return "";
}
public String getProxyPort() {
if (hasProxySet()) {
String proxyString = getProxy();
return proxyString.split(":")[1];
}
return "";
}
public String getString(String key, String defaultValue) {
String value = preferences.getString(key, "");
if (value.isEmpty() && !defaultValue.isEmpty()) {
edit().putString(key, defaultValue).apply();
return defaultValue;
}
return value;
}
public boolean getBoolean(String key, boolean defaultValue) {
boolean containsKey = preferences.contains(key);
boolean value = preferences.getBoolean(key, false);
if (!value && defaultValue && !containsKey) {
edit().putBoolean(key, true).apply();
return true;
}
return value;
}
}

View File

@ -0,0 +1,118 @@
package net.mynero.wallet.service
import android.content.Context
import android.content.SharedPreferences
import net.mynero.wallet.MoneroApplication
import net.mynero.wallet.data.DefaultNodes
import net.mynero.wallet.data.Node
import net.mynero.wallet.data.Node.Companion.fromJson
import net.mynero.wallet.data.Node.Companion.fromString
import net.mynero.wallet.util.Constants
import org.json.JSONException
import org.json.JSONObject
class PrefService(application: MoneroApplication) : ServiceBase(null) {
init {
preferences = application.getSharedPreferences(
application.applicationInfo.packageName,
Context.MODE_PRIVATE
)
instance = this
}
fun edit(): SharedPreferences.Editor? {
return preferences?.edit()
}
val node: Node?
get() {
val usesProxy = getBoolean(Constants.PREF_USES_TOR, false)
var defaultNode = DefaultNodes.SAMOURAI
if (usesProxy) {
val proxyPort = proxyPort
if (proxyPort.isNotEmpty()) {
val port = proxyPort.toInt()
defaultNode = if (port == 4447) {
DefaultNodes.MYNERO_I2P
} else {
DefaultNodes.MYNERO_ONION
}
}
}
val nodeString = getString(Constants.PREF_NODE_2, defaultNode.nodeString)
return try {
val nodeJson = nodeString?.let { JSONObject(it) }
fromJson(nodeJson)
} catch (e: JSONException) {
// stored node is not json format, upgrade if possible
nodeString?.let { upgradeOldNode(it) }
}
}
private fun upgradeOldNode(nodeString: String): Node? {
if (nodeString.isNotEmpty()) {
val node = fromString(nodeString)
if (node != null) {
edit()?.putString(Constants.PREF_NODE_2, node.toJson().toString())?.apply()
return node
}
}
return null
}
val proxy: String?
get() = instance?.getString(Constants.PREF_PROXY, "")
fun hasProxySet(): Boolean {
val proxyString = proxy
return proxyString?.contains(":") == true
}
val proxyAddress: String
get() {
if (hasProxySet()) {
val proxyString = proxy
return proxyString?.split(":".toRegex())?.dropLastWhile { it.isEmpty() }
?.toTypedArray()
?.get(0) ?: ""
}
return ""
}
val proxyPort: String
get() {
if (hasProxySet()) {
val proxyString = proxy
return proxyString?.split(":".toRegex())?.dropLastWhile { it.isEmpty() }
?.toTypedArray()
?.get(1) ?: ""
}
return ""
}
fun getString(key: String?, defaultValue: String): String? {
val value = preferences?.getString(key, "")
if (value?.isEmpty() == true && defaultValue.isNotEmpty()) {
edit()?.putString(key, defaultValue)?.apply()
return defaultValue
}
return value
}
fun getBoolean(key: String?, defaultValue: Boolean): Boolean {
val containsKey = preferences?.contains(key)
val value = preferences?.getBoolean(key, false)
if (value == false && defaultValue && containsKey == false) {
edit()?.putBoolean(key, true)?.apply()
return true
}
return value == true
}
companion object {
private var preferences: SharedPreferences? = null
@JvmStatic
var instance: PrefService? = null
private set
}
}

View File

@ -1,13 +0,0 @@
package net.mynero.wallet.service;
public class ServiceBase {
private final MoneroHandlerThread thread;
public ServiceBase(MoneroHandlerThread thread) {
this.thread = thread;
}
public MoneroHandlerThread getThread() {
return thread;
}
}

View File

@ -0,0 +1,3 @@
package net.mynero.wallet.service
open class ServiceBase(@JvmField val thread: MoneroHandlerThread?)

View File

@ -1,33 +0,0 @@
package net.mynero.wallet.service;
import net.mynero.wallet.model.PendingTransaction;
import java.util.ArrayList;
import java.util.List;
import kotlin.Pair;
public class TxService extends ServiceBase {
public static TxService instance = null;
public TxService(MoneroHandlerThread thread) {
super(thread);
instance = this;
}
public static TxService getInstance() {
return instance;
}
public PendingTransaction createTx(String address, String amount, boolean sendAll, PendingTransaction.Priority feePriority, ArrayList<String> selectedUtxos) throws Exception {
return this.getThread().createTx(address, amount, sendAll, feePriority, selectedUtxos);
}
public PendingTransaction createTx(List<Pair<String, String>> dests, boolean sendAll, PendingTransaction.Priority feePriority, ArrayList<String> selectedUtxos) throws Exception {
return this.getThread().createTx(dests, sendAll, feePriority, selectedUtxos);
}
public boolean sendTx(PendingTransaction pendingTransaction) {
return this.getThread().sendTx(pendingTransaction);
}
}

View File

@ -0,0 +1,39 @@
package net.mynero.wallet.service
import net.mynero.wallet.model.PendingTransaction
class TxService(thread: MoneroHandlerThread) : ServiceBase(thread) {
init {
instance = this
}
@Throws(Exception::class)
fun createTx(
address: String,
amount: String,
sendAll: Boolean,
feePriority: PendingTransaction.Priority,
selectedUtxos: ArrayList<String>
): PendingTransaction? {
return thread?.createTx(address, amount, sendAll, feePriority, selectedUtxos)
}
@Throws(Exception::class)
fun createTx(
dests: List<Pair<String, String>>,
sendAll: Boolean,
feePriority: PendingTransaction.Priority,
selectedUtxos: ArrayList<String>
): PendingTransaction? {
return thread?.createTx(dests, sendAll, feePriority, selectedUtxos)
}
fun sendTx(pendingTransaction: PendingTransaction): Boolean {
return thread?.sendTx(pendingTransaction) == true
}
companion object {
@JvmField
var instance: TxService? = null
}
}

View File

@ -1,137 +0,0 @@
package net.mynero.wallet.service;
import android.util.Pair;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import net.mynero.wallet.model.CoinsInfo;
import net.mynero.wallet.model.PendingTransaction;
import net.mynero.wallet.model.Wallet;
import net.mynero.wallet.model.WalletManager;
import net.mynero.wallet.util.Constants;
import org.json.JSONArray;
import org.json.JSONException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
public class UTXOService extends ServiceBase {
public static UTXOService instance = null;
private final MutableLiveData<List<CoinsInfo>> _utxos = new MutableLiveData<>();
public LiveData<List<CoinsInfo>> utxos = _utxos;
private List<CoinsInfo> internalCachedUtxos = new ArrayList<>();
private ArrayList<String> frozenCoins = new ArrayList<>();
public UTXOService(MoneroHandlerThread thread) {
super(thread);
instance = this;
try {
this.loadFrozenCoins();
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
public static UTXOService getInstance() {
return instance;
}
public void refreshUtxos() {
List<CoinsInfo> coinsInfos = getUtxosInternal();
_utxos.postValue(coinsInfos);
internalCachedUtxos = coinsInfos;
}
public List<CoinsInfo> getUtxos() {
return Collections.unmodifiableList(internalCachedUtxos);
}
private List<CoinsInfo> getUtxosInternal() {
return WalletManager.getInstance().getWallet().getCoins().getAll();
}
public void toggleFrozen(HashMap<String, CoinsInfo> selectedCoins) {
ArrayList<String> frozenCoinsCopy = new ArrayList<>(frozenCoins);
for (CoinsInfo coin : selectedCoins.values()) {
if (frozenCoinsCopy.contains(coin.pubKey)) {
frozenCoinsCopy.remove(coin.pubKey);
} else {
frozenCoinsCopy.add(coin.pubKey);
}
}
this.frozenCoins = frozenCoinsCopy;
this.saveFrozenCoins();
refreshUtxos();
BalanceService.getInstance().refreshBalance();
}
public boolean isCoinFrozen(CoinsInfo coinsInfo) {
return frozenCoins.contains(coinsInfo.pubKey);
}
private void loadFrozenCoins() throws JSONException {
PrefService prefService = PrefService.getInstance();
String frozenCoinsArrayString = prefService.getString(Constants.PREF_FROZEN_COINS, "[]");
JSONArray frozenCoinsArray = new JSONArray(frozenCoinsArrayString);
for (int i = 0; i < frozenCoinsArray.length(); i++) {
String pubKey = frozenCoinsArray.getString(i);
frozenCoins.add(pubKey);
}
this.refreshUtxos();
}
private void saveFrozenCoins() {
PrefService prefService = PrefService.getInstance();
JSONArray jsonArray = new JSONArray();
ArrayList<String> frozenCoinsCopy = new ArrayList<>(frozenCoins);
for (String pubKey : frozenCoinsCopy) {
jsonArray.put(pubKey);
}
prefService.edit().putString(Constants.PREF_FROZEN_COINS, jsonArray.toString()).apply();
}
public ArrayList<String> selectUtxos(long amount, boolean sendAll, PendingTransaction.Priority feePriority) throws Exception {
final long basicFeeEstimate = calculateBasicFee(amount, feePriority);
final long amountWithBasicFee = amount + basicFeeEstimate;
ArrayList<String> selectedUtxos = new ArrayList<>();
ArrayList<String> seenTxs = new ArrayList<>();
List<CoinsInfo> utxos = new ArrayList<>(getUtxos());
long amountSelected = 0;
Collections.sort(utxos);
//loop through each utxo
for (CoinsInfo coinsInfo : utxos) {
if (!coinsInfo.isSpent() && coinsInfo.isUnlocked() && !coinsInfo.isFrozen() && !frozenCoins.contains(coinsInfo.pubKey)) { //filter out spent, locked, and frozen outputs
if (sendAll) {
// if send all, add all utxos and set amount to send all
selectedUtxos.add(coinsInfo.keyImage);
amountSelected = Wallet.SWEEP_ALL;
} else {
//if amount selected is still less than amount needed, and the utxos tx hash hasn't already been seen, add utxo
if (amountSelected <= amountWithBasicFee && !seenTxs.contains(coinsInfo.hash)) {
selectedUtxos.add(coinsInfo.keyImage);
// we don't want to spend multiple utxos from the same transaction, so we prevent that from happening here.
seenTxs.add(coinsInfo.hash);
amountSelected += coinsInfo.amount;
}
}
}
}
if (amountSelected < amountWithBasicFee && !sendAll) {
throw new Exception("insufficient wallet balance");
}
return selectedUtxos;
}
private long calculateBasicFee(long amount, PendingTransaction.Priority feePriority) {
ArrayList<Pair<String, Long>> destinations = new ArrayList<>();
destinations.add(new Pair<>("87MRtZPrWUCVUgcFHdsVb5MoZUcLtqfD3FvQVGwftFb8eSdMnE39JhAJcbuSW8X2vRaRsB9RQfuCpFciybJFHaz3QYPhCLw", amount));
// destination string doesn't actually matter here, so i'm using the donation address. amount also technically doesn't matter
return WalletManager.getInstance().getWallet().estimateTransactionFee(destinations, feePriority);
}
}

View File

@ -0,0 +1,144 @@
package net.mynero.wallet.service
import android.util.Pair
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import net.mynero.wallet.model.CoinsInfo
import net.mynero.wallet.model.PendingTransaction
import net.mynero.wallet.model.Wallet
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.util.Constants
import org.json.JSONArray
import org.json.JSONException
import java.util.Collections
class UTXOService(thread: MoneroHandlerThread?) : ServiceBase(thread) {
private val _utxos = MutableLiveData<List<CoinsInfo>>()
@JvmField
var utxos: LiveData<List<CoinsInfo>> = _utxos
private var internalCachedUtxos: List<CoinsInfo> = ArrayList()
private var frozenCoins = ArrayList<String?>()
val utxosInternal: List<CoinsInfo>
get() {
return WalletManager.instance?.wallet?.coins?.all ?: emptyList()
}
init {
instance = this
try {
loadFrozenCoins()
} catch (e: JSONException) {
throw RuntimeException(e)
}
}
fun refreshUtxos() {
val coinsInfos: List<CoinsInfo> = this.utxosInternal
_utxos.postValue(coinsInfos)
internalCachedUtxos = coinsInfos
}
fun getUtxos(): List<CoinsInfo> {
return Collections.unmodifiableList(internalCachedUtxos)
}
fun toggleFrozen(selectedCoins: HashMap<String?, CoinsInfo>) {
val frozenCoinsCopy = ArrayList(frozenCoins)
for (coin in selectedCoins.values) {
if (frozenCoinsCopy.contains(coin.pubKey)) {
frozenCoinsCopy.remove(coin.pubKey)
} else {
frozenCoinsCopy.add(coin.pubKey)
}
}
frozenCoins = frozenCoinsCopy
saveFrozenCoins()
refreshUtxos()
BalanceService.instance!!.refreshBalance()
}
fun isCoinFrozen(coinsInfo: CoinsInfo): Boolean {
return frozenCoins.contains(coinsInfo.pubKey)
}
@Throws(JSONException::class)
private fun loadFrozenCoins() {
val prefService = PrefService.instance
val frozenCoinsArrayString = prefService!!.getString(Constants.PREF_FROZEN_COINS, "[]")
val frozenCoinsArray = JSONArray(frozenCoinsArrayString)
for (i in 0 until frozenCoinsArray.length()) {
val pubKey = frozenCoinsArray.getString(i)
frozenCoins.add(pubKey)
}
refreshUtxos()
}
private fun saveFrozenCoins() {
val prefService = PrefService.instance
val jsonArray = JSONArray()
val frozenCoinsCopy = ArrayList(frozenCoins)
for (pubKey in frozenCoinsCopy) {
jsonArray.put(pubKey)
}
prefService!!.edit()!!
.putString(Constants.PREF_FROZEN_COINS, jsonArray.toString()).apply()
}
@Throws(Exception::class)
fun selectUtxos(
amount: Long,
sendAll: Boolean,
feePriority: PendingTransaction.Priority
): ArrayList<String> {
val basicFeeEstimate = calculateBasicFee(amount, feePriority)
val amountWithBasicFee = amount + basicFeeEstimate
val selectedUtxos = ArrayList<String>()
val seenTxs = ArrayList<String>()
val utxos: List<CoinsInfo> = ArrayList(getUtxos())
var amountSelected: Long = 0
utxos.sorted()
//loop through each utxo
for (coinsInfo in utxos) {
if (!coinsInfo.isSpent && coinsInfo.isUnlocked && !coinsInfo.isFrozen && !frozenCoins.contains(
coinsInfo.pubKey
)
) { //filter out spent, locked, and frozen outputs
if (sendAll) {
// if send all, add all utxos and set amount to send all
coinsInfo.keyImage?.let { selectedUtxos.add(it) }
amountSelected = Wallet.SWEEP_ALL
} else {
//if amount selected is still less than amount needed, and the utxos tx hash hasn't already been seen, add utxo
if (amountSelected <= amountWithBasicFee && !seenTxs.contains(coinsInfo.hash)) {
coinsInfo.keyImage?.let { selectedUtxos.add(it) }
// we don't want to spend multiple utxos from the same transaction, so we prevent that from happening here.
coinsInfo.hash?.let { seenTxs.add(it) }
amountSelected += coinsInfo.amount
}
}
}
}
if (amountSelected < amountWithBasicFee && !sendAll) {
throw Exception("insufficient wallet balance")
}
return selectedUtxos
}
private fun calculateBasicFee(amount: Long, feePriority: PendingTransaction.Priority): Long {
val destinations = ArrayList<Pair<String, Long>>()
destinations.add(
Pair(
"87MRtZPrWUCVUgcFHdsVb5MoZUcLtqfD3FvQVGwftFb8eSdMnE39JhAJcbuSW8X2vRaRsB9RQfuCpFciybJFHaz3QYPhCLw",
amount
)
)
// destination string doesn't actually matter here, so i'm using the donation address. amount also technically doesn't matter
return WalletManager.instance!!.wallet!!.estimateTransactionFee(destinations, feePriority)
}
companion object {
@JvmStatic
var instance: UTXOService? = null
}
}