Merge branch 'bugfix/pay-to-many-paymentids'

# Conflicts:
#	app/src/main/res/values/strings.xml
This commit is contained in:
pokkst 2023-04-26 10:09:03 -05:00
commit 2ff408e622
No known key found for this signature in database
GPG key ID: 90C2ED85E67A50FF
4 changed files with 155 additions and 56 deletions

View file

@ -4,6 +4,8 @@ import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -137,45 +139,19 @@ public class SendFragment extends Fragment {
} }
}); });
sendMaxButton.setOnClickListener(view1 -> { sendMaxButton.setOnClickListener(view1 -> {
addOutputImageView.setVisibility(View.INVISIBLE); mViewModel.setSendingMax(!isSendAll());
boolean currentValue = mViewModel.sendingMax.getValue() != null ? mViewModel.sendingMax.getValue() : false;
mViewModel.setSendingMax(!currentValue);
}); });
createButton.setOnClickListener(view1 -> { createButton.setOnClickListener(view1 -> {
boolean sendAll = mViewModel.sendingMax.getValue() != null ? mViewModel.sendingMax.getValue() : false; boolean outputsValid = checkDestsValidity(isSendAll());
ArrayList<Pair<String, String>> dests = new ArrayList<>();
for(int i = 0; i < getDestCount(); i++) {
ConstraintLayout entryView = getDestView(i);
EditText amountField = entryView.findViewById(R.id.amount_edittext);
EditText addressField = entryView.findViewById(R.id.address_edittext);
String amount = amountField.getText().toString().trim();
if(!sendAll) {
if(amount.isEmpty()) {
Toast.makeText(getActivity(), getString(R.string.send_amount_empty), Toast.LENGTH_SHORT).show();
return;
}
long amountRaw = Wallet.getAmountFromString(amount); if(outputsValid) {
long balance = BalanceService.getInstance().getUnlockedBalanceRaw(); Toast.makeText(getActivity(), getString(R.string.creating_tx), Toast.LENGTH_SHORT).show();
if (amountRaw >= balance || amountRaw <= 0) { createButton.setEnabled(false);
Toast.makeText(getActivity(), getString(R.string.send_amount_invalid), Toast.LENGTH_SHORT).show(); sendMaxButton.setEnabled(false);
return; createTx(getRawDests(), isSendAll(), priority);
} } else {
} Toast.makeText(getActivity(), getString(R.string.creating_tx_failed_invalid_outputs), Toast.LENGTH_SHORT).show();
String address = addressField.getText().toString().trim();
boolean isValidAddress = Wallet.isAddressValid(address);
if(!isValidAddress) {
Toast.makeText(getActivity(), getString(R.string.send_address_invalid), Toast.LENGTH_SHORT).show();
return;
}
dests.add(new Pair<>(address, amount));
} }
Toast.makeText(getActivity(), getString(R.string.creating_tx), Toast.LENGTH_SHORT).show();
createButton.setEnabled(false);
sendMaxButton.setEnabled(false);
createTx(dests, sendAll, priority);
}); });
sendButton.setOnClickListener(view1 -> { sendButton.setOnClickListener(view1 -> {
@ -188,21 +164,72 @@ public class SendFragment extends Fragment {
}); });
} }
private boolean checkDestsValidity(boolean sendAll) {
List<Pair<String, String>> dests = getRawDests();
for(Pair<String, String> dest : dests) {
String address = dest.component1();
String amount = dest.component2();
if(!sendAll) {
if(amount.isEmpty()) {
Toast.makeText(getActivity(), getString(R.string.send_amount_empty), Toast.LENGTH_SHORT).show();
return false;
}
long amountRaw = Wallet.getAmountFromString(amount);
long balance = BalanceService.getInstance().getUnlockedBalanceRaw();
if (amountRaw >= balance || amountRaw <= 0) {
Toast.makeText(getActivity(), getString(R.string.send_amount_invalid), Toast.LENGTH_SHORT).show();
return false;
}
} else if(dests.size() > 1) {
Toast.makeText(getActivity(), getString(R.string.send_amount_invalid_sendall_paytomany), Toast.LENGTH_SHORT).show();
return false;
}
UriData uriData = UriData.parse(address);
boolean isValidAddress = uriData != null;
if(!isValidAddress) {
Toast.makeText(getActivity(), getString(R.string.send_address_invalid), Toast.LENGTH_SHORT).show();
return false;
}
if(dests.size() > 1 && uriData.hasPaymentId()) {
Toast.makeText(getActivity(), getString(R.string.paymentid_paytomany), Toast.LENGTH_SHORT).show();
return false;
}
}
return true;
}
private boolean destsHasPaymentId() {
List<Pair<String, String>> dests = getRawDests();
for(Pair<String, String> dest : dests) {
String address = dest.component1();
UriData uriData = UriData.parse(address);
if(uriData == null) return false;
if(uriData.hasPaymentId()) return true;
}
return false;
}
private void bindObservers() { private void bindObservers() {
mViewModel.sendingMax.observe(getViewLifecycleOwner(), sendingMax -> { mViewModel.sendingMax.observe(getViewLifecycleOwner(), sendingMax -> {
if (mViewModel.pendingTransaction.getValue() == null) { if (mViewModel.pendingTransaction.getValue() == null) {
if (sendingMax) { if (sendingMax) {
addOutputImageView.setVisibility(View.GONE);
prepareOutputsForMaxSend(); prepareOutputsForMaxSend();
sendMaxButton.setText(getText(R.string.undo)); sendMaxButton.setText(getText(R.string.undo));
} else { } else {
addOutputImageView.setVisibility(View.VISIBLE);
unprepareMaxSend(); unprepareMaxSend();
sendMaxButton.setText(getText(R.string.send_max)); sendMaxButton.setText(getText(R.string.send_max));
} }
} }
}); });
mViewModel.showAddOutputButton.observe(getViewLifecycleOwner(), show -> {
setAddOutputButtonVisibility((show && !destsHasPaymentId()) ? View.VISIBLE : View.INVISIBLE);
});
mViewModel.pendingTransaction.observe(getViewLifecycleOwner(), pendingTx -> { mViewModel.pendingTransaction.observe(getViewLifecycleOwner(), pendingTx -> {
showConfirmationLayout(pendingTx != null); showConfirmationLayout(pendingTx != null);
@ -220,7 +247,31 @@ public class SendFragment extends Fragment {
int index = getDestCount(); int index = getDestCount();
ConstraintLayout entryView = (ConstraintLayout)inflater.inflate(R.layout.transaction_output_item, null); ConstraintLayout entryView = (ConstraintLayout)inflater.inflate(R.layout.transaction_output_item, null);
ImageButton removeOutputImageButton = entryView.findViewById(R.id.remove_output_imagebutton); ImageButton removeOutputImageButton = entryView.findViewById(R.id.remove_output_imagebutton);
EditText addressField = entryView.findViewById(R.id.address_edittext);
addressField.addTextChangedListener(new TextWatcher() {
@Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
@Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
@Override
public void afterTextChanged(Editable editable) {
int currentOutputs = getDestCount();
UriData uriData = UriData.parse(editable.toString());
if(uriData != null) {
// we have valid address
boolean hasPaymentId = uriData.hasPaymentId();
if(currentOutputs > 1 && hasPaymentId) {
// multiple outputs when pasting/editing in integrated address. this is not allowed
Toast.makeText(getActivity(), getString(R.string.paymentid_paytomany), Toast.LENGTH_SHORT).show();
addressField.setText(null);
} else if(currentOutputs == 1 && hasPaymentId) {
// show add output button: we are sending to integrated address
mViewModel.setShowAddOutputButton(false);
}
} else if(currentOutputs == 1 && !isSendAll()) {
// when send-all is false and this is our only dest and address is invalid, then show add output button
mViewModel.setShowAddOutputButton(true);
}
}
});
entryView.findViewById(R.id.paste_amount_imagebutton).setOnClickListener(view1 -> { entryView.findViewById(R.id.paste_amount_imagebutton).setOnClickListener(view1 -> {
Context ctx = getContext(); Context ctx = getContext();
if (ctx != null) { if (ctx != null) {
@ -261,6 +312,24 @@ public class SendFragment extends Fragment {
return destList.getChildCount(); return destList.getChildCount();
} }
private List<Pair<String, String>> getRawDests() {
ArrayList<Pair<String, String>> dests = new ArrayList<>();
for(int i = 0; i < getDestCount(); i++) {
ConstraintLayout entryView = getDestView(i);
EditText amountField = entryView.findViewById(R.id.amount_edittext);
EditText addressField = entryView.findViewById(R.id.address_edittext);
String amount = amountField.getText().toString().trim();
String address = addressField.getText().toString().trim();
dests.add(new Pair<>(address, amount));
}
return dests;
}
private boolean isSendAll() {
return mViewModel.sendingMax.getValue() != null ? mViewModel.sendingMax.getValue() : false;
}
private ConstraintLayout getDestView(int pos) { private ConstraintLayout getDestView(int pos) {
return (ConstraintLayout) destList.getChildAt(pos); return (ConstraintLayout) destList.getChildAt(pos);
} }
@ -284,7 +353,7 @@ public class SendFragment extends Fragment {
private void showConfirmationLayout(boolean show) { private void showConfirmationLayout(boolean show) {
if (show) { if (show) {
destList.setVisibility(View.GONE); destList.setVisibility(View.GONE);
addOutputImageView.setVisibility(View.GONE); setAddOutputButtonVisibility(View.GONE);
sendMaxButton.setVisibility(View.GONE); sendMaxButton.setVisibility(View.GONE);
createButton.setVisibility(View.GONE); createButton.setVisibility(View.GONE);
feeRadioGroup.setVisibility(View.GONE); feeRadioGroup.setVisibility(View.GONE);
@ -296,7 +365,7 @@ public class SendFragment extends Fragment {
amountTextView.setVisibility(View.VISIBLE); amountTextView.setVisibility(View.VISIBLE);
} else { } else {
destList.setVisibility(View.VISIBLE); destList.setVisibility(View.VISIBLE);
addOutputImageView.setVisibility(View.VISIBLE); setAddOutputButtonVisibility(View.VISIBLE);
sendMaxButton.setVisibility(View.VISIBLE); sendMaxButton.setVisibility(View.VISIBLE);
createButton.setVisibility(View.VISIBLE); createButton.setVisibility(View.VISIBLE);
feeRadioGroup.setVisibility(View.VISIBLE); feeRadioGroup.setVisibility(View.VISIBLE);
@ -322,31 +391,41 @@ public class SendFragment extends Fragment {
} }
private void pasteAddress(ConstraintLayout entryView, String clipboard, boolean pastingAmount) { private void pasteAddress(ConstraintLayout entryView, String clipboard, boolean pastingAmount) {
if(pastingAmount) {
try {
Double.parseDouble(clipboard);
setAmount(entryView, clipboard);
} catch (Exception e) {
Toast.makeText(getActivity(), getString(R.string.send_amount_invalid), Toast.LENGTH_SHORT).show();
return;
}
}
UriData uriData = UriData.parse(clipboard); UriData uriData = UriData.parse(clipboard);
if (uriData != null) { if (uriData != null) {
int currentOutputs = getDestCount();
if(currentOutputs > 1 && uriData.hasPaymentId()) {
Toast.makeText(getActivity(), getString(R.string.paymentid_paytomany), Toast.LENGTH_SHORT).show();
return;
} else if(currentOutputs == 1 && uriData.hasPaymentId()) {
mViewModel.setShowAddOutputButton(false);
}
EditText addressField = entryView.findViewById(R.id.address_edittext); EditText addressField = entryView.findViewById(R.id.address_edittext);
addressField.setText(uriData.getAddress()); addressField.setText(uriData.getAddress());
if (uriData.hasAmount()) { if (uriData.hasAmount()) {
sendMaxButton.setEnabled(false); setAmount(entryView, uriData.getAmount());
EditText amountField = entryView.findViewById(R.id.amount_edittext);
amountField.setText(uriData.getAmount());
} }
} else { } else {
if(pastingAmount) { Toast.makeText(getActivity(), getString(R.string.send_address_invalid), Toast.LENGTH_SHORT).show();
try {
Double.parseDouble(clipboard);
sendMaxButton.setEnabled(false);
EditText amountField = entryView.findViewById(R.id.amount_edittext);
amountField.setText(clipboard);
} catch (Exception e) {
Toast.makeText(getActivity(), getString(R.string.send_amount_invalid), Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(getActivity(), getString(R.string.send_address_invalid), Toast.LENGTH_SHORT).show();
}
} }
} }
private void setAmount(ConstraintLayout entryView, String amount) {
sendMaxButton.setEnabled(false);
EditText amountField = entryView.findViewById(R.id.amount_edittext);
amountField.setText(amount);
}
private void createTx(List<Pair<String, String>> dests, boolean sendAll, PendingTransaction.Priority feePriority) { private void createTx(List<Pair<String, String>> dests, boolean sendAll, PendingTransaction.Priority feePriority) {
((MoneroApplication)getActivity().getApplication()).getExecutor().execute(() -> { ((MoneroApplication)getActivity().getApplication()).getExecutor().execute(() -> {
try { try {
@ -393,4 +472,8 @@ public class SendFragment extends Fragment {
} }
}); });
} }
private void setAddOutputButtonVisibility(int visibility) {
addOutputImageView.setVisibility(visibility);
}
} }

View file

@ -9,11 +9,18 @@ import net.mynero.wallet.model.PendingTransaction;
public class SendViewModel extends ViewModel { public class SendViewModel extends ViewModel {
private final MutableLiveData<Boolean> _sendingMax = new MutableLiveData<>(false); private final MutableLiveData<Boolean> _sendingMax = new MutableLiveData<>(false);
public LiveData<Boolean> sendingMax = _sendingMax; public LiveData<Boolean> sendingMax = _sendingMax;
private final MutableLiveData<Boolean> _showAddOutputButton = new MutableLiveData<>(true);
public LiveData<Boolean> showAddOutputButton = _showAddOutputButton;
private final MutableLiveData<PendingTransaction> _pendingTransaction = new MutableLiveData<>(null); private final MutableLiveData<PendingTransaction> _pendingTransaction = new MutableLiveData<>(null);
public LiveData<PendingTransaction> pendingTransaction = _pendingTransaction; public LiveData<PendingTransaction> pendingTransaction = _pendingTransaction;
public void setSendingMax(boolean value) { public void setSendingMax(boolean value) {
_sendingMax.setValue(value); _sendingMax.setValue(value);
setShowAddOutputButton(!value);
}
public void setShowAddOutputButton(boolean value) {
_showAddOutputButton.setValue(value);
} }
public void setPendingTransaction(PendingTransaction pendingTx) { public void setPendingTransaction(PendingTransaction pendingTx) {

View file

@ -1,6 +1,7 @@
package net.mynero.wallet.util; package net.mynero.wallet.util;
import net.mynero.wallet.model.Wallet; import net.mynero.wallet.model.Wallet;
import net.mynero.wallet.model.WalletManager;
import java.util.HashMap; import java.util.HashMap;
@ -46,6 +47,10 @@ public class UriData {
return address; return address;
} }
public boolean hasPaymentId() {
return !Wallet.getPaymentIdFromAddress(this.address, WalletManager.getInstance().getWallet().nettype()).isEmpty();
}
public String getAmount() { public String getAmount() {
String txAmount = params.get(Constants.URI_ARG_AMOUNT); String txAmount = params.get(Constants.URI_ARG_AMOUNT);
if (txAmount == null) { if (txAmount == null) {

View file

@ -5,6 +5,8 @@
<string name="bad_password">Incorrect password!</string> <string name="bad_password">Incorrect password!</string>
<string name="send_address_invalid">Not a valid address</string> <string name="send_address_invalid">Not a valid address</string>
<string name="send_amount_invalid">Not a valid amount</string>
<string name="send_amount_invalid_sendall_paytomany">Cannot send-all in pay-to-many</string>
<string name="tx_list_failed_text">failed</string> <string name="tx_list_failed_text">failed</string>
<string name="tx_list_amount_negative">- %1$s</string> <string name="tx_list_amount_negative">- %1$s</string>
@ -24,7 +26,6 @@
<string name="wallet_locked_balance_text">+ %1$s confirming</string> <string name="wallet_locked_balance_text">+ %1$s confirming</string>
<string name="send_amount_empty">Please enter an amount</string> <string name="send_amount_empty">Please enter an amount</string>
<string name="send_amount_invalid">Please enter a valid amount</string>
<string name="send_max">Max</string> <string name="send_max">Max</string>
<string name="undo">Undo</string> <string name="undo">Undo</string>
<string name="error_sending_tx">Error sending transaction</string> <string name="error_sending_tx">Error sending transaction</string>
@ -61,6 +62,7 @@
<string name="tx_fee_text">Fee: %1$s XMR</string> <string name="tx_fee_text">Fee: %1$s XMR</string>
<string name="receive">Receive</string> <string name="receive">Receive</string>
<string name="creating_tx">Creating transaction…</string> <string name="creating_tx">Creating transaction…</string>
<string name="creating_tx_failed_invalid_outputs">Invalid destination combination</string>
<string name="sending_tx">Sending transaction…</string> <string name="sending_tx">Sending transaction…</string>
<string name="sent_tx">Sent transaction!</string> <string name="sent_tx">Sent transaction!</string>
<string name="no_camera_permission">No camera permission</string> <string name="no_camera_permission">No camera permission</string>
@ -124,4 +126,6 @@
<string name="auth">[ auth ]</string> <string name="auth">[ auth ]</string>
<string name="to">To</string> <string name="to">To</string>
<string name="max_outputs_allowed">Maximum allowed outputs</string> <string name="max_outputs_allowed">Maximum allowed outputs</string>
<string name="paymentid_paytomany">Cannot send to integrated addresses in a pay-to-many transaction</string>
</resources> </resources>