mirror of
https://codeberg.org/anoncontributorxmr/mysu.git
synced 2024-12-22 05:00:24 +00:00
Forgot to actually push the converted services
This commit is contained in:
parent
08b989eaab
commit
97d39a3cb4
18 changed files with 733 additions and 719 deletions
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
118
app/src/main/java/net/mynero/wallet/service/PrefService.kt
Normal file
118
app/src/main/java/net/mynero/wallet/service/PrefService.kt
Normal 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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package net.mynero.wallet.service
|
||||
|
||||
open class ServiceBase(@JvmField val thread: MoneroHandlerThread?)
|
|
@ -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);
|
||||
}
|
||||
}
|
39
app/src/main/java/net/mynero/wallet/service/TxService.kt
Normal file
39
app/src/main/java/net/mynero/wallet/service/TxService.kt
Normal 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
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
144
app/src/main/java/net/mynero/wallet/service/UTXOService.kt
Normal file
144
app/src/main/java/net/mynero/wallet/service/UTXOService.kt
Normal 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
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue