diff --git a/app/src/main/java/net/mynero/wallet/service/AddressService.java b/app/src/main/java/net/mynero/wallet/service/AddressService.java deleted file mode 100644 index 8e82491..0000000 --- a/app/src/main/java/net/mynero/wallet/service/AddressService.java +++ /dev/null @@ -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); - } -} diff --git a/app/src/main/java/net/mynero/wallet/service/AddressService.kt b/app/src/main/java/net/mynero/wallet/service/AddressService.kt new file mode 100644 index 0000000..48339d6 --- /dev/null +++ b/app/src/main/java/net/mynero/wallet/service/AddressService.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/net/mynero/wallet/service/BalanceService.java b/app/src/main/java/net/mynero/wallet/service/BalanceService.java deleted file mode 100644 index 4b4e76c..0000000 --- a/app/src/main/java/net/mynero/wallet/service/BalanceService.java +++ /dev/null @@ -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 = new MutableLiveData<>(null); - public LiveData 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(); - } -} diff --git a/app/src/main/java/net/mynero/wallet/service/BalanceService.kt b/app/src/main/java/net/mynero/wallet/service/BalanceService.kt new file mode 100644 index 0000000..9927a9b --- /dev/null +++ b/app/src/main/java/net/mynero/wallet/service/BalanceService.kt @@ -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(null) + + @JvmField + var balanceInfo: LiveData = _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 + } +} \ No newline at end of file diff --git a/app/src/main/java/net/mynero/wallet/service/BlockchainService.java b/app/src/main/java/net/mynero/wallet/service/BlockchainService.java deleted file mode 100644 index 0897cf8..0000000 --- a/app/src/main/java/net/mynero/wallet/service/BlockchainService.java +++ /dev/null @@ -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 _currentHeight = new MutableLiveData<>(0L); - private final MutableLiveData _connectionStatus = new MutableLiveData<>(Wallet.ConnectionStatus.ConnectionStatus_Disconnected); - public LiveData height = _currentHeight; - public LiveData 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); - } -} diff --git a/app/src/main/java/net/mynero/wallet/service/BlockchainService.kt b/app/src/main/java/net/mynero/wallet/service/BlockchainService.kt new file mode 100644 index 0000000..45691ca --- /dev/null +++ b/app/src/main/java/net/mynero/wallet/service/BlockchainService.kt @@ -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 = _currentHeight + + @JvmField + var connectionStatus: LiveData = _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 + } +} \ No newline at end of file diff --git a/app/src/main/java/net/mynero/wallet/service/HistoryService.java b/app/src/main/java/net/mynero/wallet/service/HistoryService.java deleted file mode 100644 index c02d940..0000000 --- a/app/src/main/java/net/mynero/wallet/service/HistoryService.java +++ /dev/null @@ -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> _history = new MutableLiveData<>(); - public LiveData> history = _history; - - public HistoryService(MoneroHandlerThread thread) { - super(thread); - instance = this; - } - - public static HistoryService getInstance() { - return instance; - } - - public void refreshHistory() { - _history.postValue(getHistory()); - } - - private List getHistory() { - return WalletManager.getInstance().getWallet().getHistory().getAll(); - } -} diff --git a/app/src/main/java/net/mynero/wallet/service/HistoryService.kt b/app/src/main/java/net/mynero/wallet/service/HistoryService.kt new file mode 100644 index 0000000..b70f602 --- /dev/null +++ b/app/src/main/java/net/mynero/wallet/service/HistoryService.kt @@ -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>() + + @JvmField + var history: LiveData> = _history + + init { + instance = this + } + + fun refreshHistory() { + _history.postValue(getHistory()) + } + + private fun getHistory(): List { + return WalletManager.instance!!.wallet!!.history!!.all + } + + companion object { + @JvmStatic + var instance: HistoryService? = null + private set + } +} \ No newline at end of file diff --git a/app/src/main/java/net/mynero/wallet/service/MoneroHandlerThread.java b/app/src/main/java/net/mynero/wallet/service/MoneroHandlerThread.java deleted file mode 100644 index 663a0bd..0000000 --- a/app/src/main/java/net/mynero/wallet/service/MoneroHandlerThread.java +++ /dev/null @@ -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 selectedUtxos) throws Exception { - ArrayList> dests = new ArrayList<>(); - dests.add(new Pair(address, amountStr)); - return createTx(dests, sendAll, feePriority, selectedUtxos); - } - - public PendingTransaction createTx(List> dests, boolean sendAll, PendingTransaction.Priority feePriority, ArrayList selectedUtxos) throws Exception { - long totalAmount = 0; - ArrayList outputs = new ArrayList<>(); - - for (Pair dest : dests) { - long amount = Wallet.getAmountFromString(dest.component2()); - totalAmount += amount; - outputs.add(new TransactionOutput(dest.component1(), amount)); - } - ArrayList 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 dest = dests.get(0); - String address = dest.component1(); - return wallet.createSweepTransaction(address, feePriority, preferredInputs); - } - - List finalOutputs = maybeAddDonationOutputs(totalAmount, outputs, preferredInputs); - return wallet.createTransactionMultDest(finalOutputs, feePriority, preferredInputs); - } - - private List maybeAddDonationOutputs(long amount, List outputs, List 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 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 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(); - } -} diff --git a/app/src/main/java/net/mynero/wallet/service/MoneroHandlerThread.kt b/app/src/main/java/net/mynero/wallet/service/MoneroHandlerThread.kt new file mode 100644 index 0000000..edf8d3f --- /dev/null +++ b/app/src/main/java/net/mynero/wallet/service/MoneroHandlerThread.kt @@ -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 + ): PendingTransaction? { + val dests = ArrayList>() + dests.add(Pair(address, amountStr)) + return createTx(dests, sendAll, feePriority, selectedUtxos) + } + + @Throws(Exception::class) + fun createTx( + dests: List>, + sendAll: Boolean, + feePriority: PendingTransaction.Priority, + selectedUtxos: ArrayList + ): PendingTransaction? { + var totalAmount: Long = 0 + val outputs = ArrayList() + for (dest in dests) { + val amount = getAmountFromString(dest.component2()) + totalAmount += amount + outputs.add(TransactionOutput(dest.component1(), amount)) + } + val preferredInputs: ArrayList + 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, + preferredInputs: List + ): List { + 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, 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() + } +} \ No newline at end of file diff --git a/app/src/main/java/net/mynero/wallet/service/PrefService.java b/app/src/main/java/net/mynero/wallet/service/PrefService.java deleted file mode 100644 index 150bd2b..0000000 --- a/app/src/main/java/net/mynero/wallet/service/PrefService.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/net/mynero/wallet/service/PrefService.kt b/app/src/main/java/net/mynero/wallet/service/PrefService.kt new file mode 100644 index 0000000..1a7ff98 --- /dev/null +++ b/app/src/main/java/net/mynero/wallet/service/PrefService.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/net/mynero/wallet/service/ServiceBase.java b/app/src/main/java/net/mynero/wallet/service/ServiceBase.java deleted file mode 100644 index 8f4d6f2..0000000 --- a/app/src/main/java/net/mynero/wallet/service/ServiceBase.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/net/mynero/wallet/service/ServiceBase.kt b/app/src/main/java/net/mynero/wallet/service/ServiceBase.kt new file mode 100644 index 0000000..17c6f7a --- /dev/null +++ b/app/src/main/java/net/mynero/wallet/service/ServiceBase.kt @@ -0,0 +1,3 @@ +package net.mynero.wallet.service + +open class ServiceBase(@JvmField val thread: MoneroHandlerThread?) \ No newline at end of file diff --git a/app/src/main/java/net/mynero/wallet/service/TxService.java b/app/src/main/java/net/mynero/wallet/service/TxService.java deleted file mode 100644 index bb8d7d0..0000000 --- a/app/src/main/java/net/mynero/wallet/service/TxService.java +++ /dev/null @@ -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 selectedUtxos) throws Exception { - return this.getThread().createTx(address, amount, sendAll, feePriority, selectedUtxos); - } - - public PendingTransaction createTx(List> dests, boolean sendAll, PendingTransaction.Priority feePriority, ArrayList selectedUtxos) throws Exception { - return this.getThread().createTx(dests, sendAll, feePriority, selectedUtxos); - } - - public boolean sendTx(PendingTransaction pendingTransaction) { - return this.getThread().sendTx(pendingTransaction); - } -} diff --git a/app/src/main/java/net/mynero/wallet/service/TxService.kt b/app/src/main/java/net/mynero/wallet/service/TxService.kt new file mode 100644 index 0000000..2d8c9d9 --- /dev/null +++ b/app/src/main/java/net/mynero/wallet/service/TxService.kt @@ -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 + ): PendingTransaction? { + return thread?.createTx(address, amount, sendAll, feePriority, selectedUtxos) + } + + @Throws(Exception::class) + fun createTx( + dests: List>, + sendAll: Boolean, + feePriority: PendingTransaction.Priority, + selectedUtxos: ArrayList + ): 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/net/mynero/wallet/service/UTXOService.java b/app/src/main/java/net/mynero/wallet/service/UTXOService.java deleted file mode 100644 index b7aa222..0000000 --- a/app/src/main/java/net/mynero/wallet/service/UTXOService.java +++ /dev/null @@ -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> _utxos = new MutableLiveData<>(); - public LiveData> utxos = _utxos; - private List internalCachedUtxos = new ArrayList<>(); - private ArrayList 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 coinsInfos = getUtxosInternal(); - _utxos.postValue(coinsInfos); - internalCachedUtxos = coinsInfos; - } - - public List getUtxos() { - return Collections.unmodifiableList(internalCachedUtxos); - } - - private List getUtxosInternal() { - return WalletManager.getInstance().getWallet().getCoins().getAll(); - } - - public void toggleFrozen(HashMap selectedCoins) { - ArrayList 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 frozenCoinsCopy = new ArrayList<>(frozenCoins); - for (String pubKey : frozenCoinsCopy) { - jsonArray.put(pubKey); - } - prefService.edit().putString(Constants.PREF_FROZEN_COINS, jsonArray.toString()).apply(); - } - - public ArrayList selectUtxos(long amount, boolean sendAll, PendingTransaction.Priority feePriority) throws Exception { - final long basicFeeEstimate = calculateBasicFee(amount, feePriority); - final long amountWithBasicFee = amount + basicFeeEstimate; - ArrayList selectedUtxos = new ArrayList<>(); - ArrayList seenTxs = new ArrayList<>(); - List 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> 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); - } -} diff --git a/app/src/main/java/net/mynero/wallet/service/UTXOService.kt b/app/src/main/java/net/mynero/wallet/service/UTXOService.kt new file mode 100644 index 0000000..ac6912d --- /dev/null +++ b/app/src/main/java/net/mynero/wallet/service/UTXOService.kt @@ -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>() + + @JvmField + var utxos: LiveData> = _utxos + private var internalCachedUtxos: List = ArrayList() + private var frozenCoins = ArrayList() + val utxosInternal: List + get() { + return WalletManager.instance?.wallet?.coins?.all ?: emptyList() + } + + init { + instance = this + try { + loadFrozenCoins() + } catch (e: JSONException) { + throw RuntimeException(e) + } + } + + fun refreshUtxos() { + val coinsInfos: List = this.utxosInternal + _utxos.postValue(coinsInfos) + internalCachedUtxos = coinsInfos + } + + fun getUtxos(): List { + return Collections.unmodifiableList(internalCachedUtxos) + } + + fun toggleFrozen(selectedCoins: HashMap) { + 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 { + val basicFeeEstimate = calculateBasicFee(amount, feePriority) + val amountWithBasicFee = amount + basicFeeEstimate + val selectedUtxos = ArrayList() + val seenTxs = ArrayList() + val utxos: List = 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>() + 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 + } +} \ No newline at end of file