Satoshis Dream (#159)

xmr.to integration
merge-requests/3/head
m2049r 7 years ago committed by GitHub
parent d855328c52
commit f8e701aa07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -8,8 +8,8 @@ android {
applicationId "com.m2049r.xmrwallet"
minSdkVersion 21
targetSdkVersion 25
versionCode 51
versionName "1.2.11"
versionCode 62
versionName "1.3.2 'Satoshis Dream'"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {

@ -46,6 +46,7 @@ import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import com.m2049r.xmrwallet.data.BarcodeData;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.util.Helper;
@ -304,14 +305,14 @@ public class ReceiveFragment extends Fragment {
return;
}
StringBuffer sb = new StringBuffer();
sb.append(ScannerFragment.QR_SCHEME).append(address);
sb.append(BarcodeData.XMR_SCHEME).append(address);
boolean first = true;
if (!paymentId.isEmpty()) {
if (first) {
sb.append("?");
first = false;
}
sb.append(ScannerFragment.QR_PAYMENTID).append('=').append(paymentId);
sb.append(BarcodeData.XMR_PAYMENTID).append('=').append(paymentId);
}
if (!xmrAmount.isEmpty()) {
if (first) {
@ -319,7 +320,7 @@ public class ReceiveFragment extends Fragment {
} else {
sb.append("&");
}
sb.append(ScannerFragment.QR_AMOUNT).append('=').append(xmrAmount);
sb.append(BarcodeData.XMR_AMOUNT).append('=').append(xmrAmount);
}
String text = sb.toString();
int size = Math.min(qrCode.getHeight(), qrCode.getWidth());

@ -36,7 +36,7 @@ public class ScannerFragment extends Fragment implements ZXingScannerView.Result
private OnScannedListener onScannedListener;
public interface OnScannedListener {
boolean onScanned(String uri);
boolean onScanned(String qrCode);
}
private ZXingScannerView mScannerView;
@ -56,10 +56,6 @@ public class ScannerFragment extends Fragment implements ZXingScannerView.Result
mScannerView.startCamera();
}
static final String QR_SCHEME = "monero:";
static final String QR_PAYMENTID = "tx_payment_id";
static final String QR_AMOUNT = "tx_amount";
@Override
public void handleResult(Result rawResult) {
if ((rawResult.getBarcodeFormat() == BarcodeFormat.QR_CODE)) {

@ -20,6 +20,10 @@ import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import com.m2049r.xmrwallet.util.Helper;
import java.io.File;
import static android.view.WindowManager.LayoutParams;
public abstract class SecureActivity extends AppCompatActivity {
@ -27,8 +31,9 @@ public abstract class SecureActivity extends AppCompatActivity {
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
File f = new File(Helper.getStorageRoot(this), "screenshot");
// set FLAG_SECURE to prevent screenshots in Release Mode
if (!BuildConfig.DEBUG) {
if ((!BuildConfig.DEBUG) && (!f.exists())) {
getWindow().setFlags(LayoutParams.FLAG_SECURE, LayoutParams.FLAG_SECURE);
}
}

@ -30,10 +30,13 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import com.m2049r.xmrwallet.model.TransactionInfo;
import com.m2049r.xmrwallet.model.Transfer;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.UserNotes;
import com.m2049r.xmrwallet.widget.Toolbar;
import java.text.SimpleDateFormat;
@ -68,12 +71,23 @@ public class TxFragment extends Fragment {
private TextView etTxNotes;
private Button bTxNotes;
// XMRTO stuff
private View cvXmrTo;
private TextView tvTxXmrToKey;
private TextView tvDestinationBtc;
private TextView tvTxAmountBtc;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_tx_info, container, false);
cvXmrTo = view.findViewById(R.id.cvXmrTo);
tvTxXmrToKey = (TextView) view.findViewById(R.id.tvTxXmrToKey);
tvDestinationBtc = (TextView) view.findViewById(R.id.tvDestinationBtc);
tvTxAmountBtc = (TextView) view.findViewById(R.id.tvTxAmountBtc);
tvTxTimestamp = (TextView) view.findViewById(R.id.tvTxTimestamp);
tvTxId = (TextView) view.findViewById(R.id.tvTxId);
tvTxKey = (TextView) view.findViewById(R.id.tvTxKey);
@ -94,7 +108,16 @@ public class TxFragment extends Fragment {
info.notes = null; // force reload on next view
bTxNotes.setEnabled(false);
etTxNotes.setEnabled(false);
activityCallback.onSetNote(info.hash, etTxNotes.getText().toString());
userNotes.setNote(etTxNotes.getText().toString());
activityCallback.onSetNote(info.hash, userNotes.txNotes);
}
});
tvTxXmrToKey.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Helper.clipBoardCopy(getActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString());
Toast.makeText(getActivity(), getString(R.string.message_copy_xmrtokey), Toast.LENGTH_SHORT).show();
}
});
@ -175,12 +198,14 @@ public class TxFragment extends Fragment {
}
TransactionInfo info = null;
UserNotes userNotes = null;
void loadNotes(TransactionInfo info) {
if (info.notes == null) {
if ((userNotes == null) || (info.notes == null)) {
info.notes = activityCallback.getTxNotes(info.hash);
}
etTxNotes.setText(info.notes);
userNotes = new UserNotes(info.notes);
etTxNotes.setText(userNotes.note);
}
private void setTxColour(int clr) {
@ -266,8 +291,21 @@ public class TxFragment extends Fragment {
tvTxTransfers.setText(sb.toString());
tvDestination.setText(dstSb.toString());
this.info = info;
showBtcInfo();
}
void showBtcInfo() {
if (userNotes.xmrtoKey != null) {
cvXmrTo.setVisibility(View.VISIBLE);
tvTxXmrToKey.setText(userNotes.xmrtoKey);
tvDestinationBtc.setText(userNotes.xmrtoDestination);
tvTxAmountBtc.setText(userNotes.xmrtoAmount + " BTC");
} else {
cvXmrTo.setVisibility(View.GONE);
}
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

@ -23,7 +23,6 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.PowerManager;
@ -38,17 +37,18 @@ import com.m2049r.xmrwallet.data.BarcodeData;
import com.m2049r.xmrwallet.data.TxData;
import com.m2049r.xmrwallet.dialog.DonationFragment;
import com.m2049r.xmrwallet.dialog.HelpFragment;
import com.m2049r.xmrwallet.fragment.send.SendAddressWizardFragment;
import com.m2049r.xmrwallet.fragment.send.SendFragment;
import com.m2049r.xmrwallet.model.PendingTransaction;
import com.m2049r.xmrwallet.model.TransactionInfo;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.service.WalletService;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.UserNotes;
import com.m2049r.xmrwallet.widget.Toolbar;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import timber.log.Timber;
@ -465,7 +465,7 @@ public class WalletActivity extends SecureActivity implements WalletFragment.Lis
}
@Override
public void onTransactionCreated(final PendingTransaction pendingTransaction) {
public void onTransactionCreated(final String txTag, final PendingTransaction pendingTransaction) {
try {
final SendFragment sendFragment = (SendFragment)
getSupportFragmentManager().findFragmentById(R.id.fragment_container);
@ -477,7 +477,7 @@ public class WalletActivity extends SecureActivity implements WalletFragment.Lis
getWallet().disposePendingTransaction();
sendFragment.onCreateTransactionFailed(errorText);
} else {
sendFragment.onTransactionCreated(pendingTransaction);
sendFragment.onTransactionCreated(txTag, pendingTransaction);
}
}
});
@ -588,11 +588,11 @@ public class WalletActivity extends SecureActivity implements WalletFragment.Lis
///////////////////////////
@Override
public void onSend(String notes) {
public void onSend(UserNotes notes) {
if (mIsBound) { // no point in talking to unbound service
Intent intent = new Intent(getApplicationContext(), WalletService.class);
intent.putExtra(WalletService.REQUEST, WalletService.REQUEST_CMD_SEND);
intent.putExtra(WalletService.REQUEST_CMD_SEND_NOTES, notes);
intent.putExtra(WalletService.REQUEST_CMD_SEND_NOTES, notes.txNotes);
startService(intent);
Timber.d("SEND TX request sent");
} else {
@ -617,11 +617,12 @@ public class WalletActivity extends SecureActivity implements WalletFragment.Lis
}
@Override
public void onPrepareSend(TxData txData) {
public void onPrepareSend(final String tag, final TxData txData) {
if (mIsBound) { // no point in talking to unbound service
Intent intent = new Intent(getApplicationContext(), WalletService.class);
intent.putExtra(WalletService.REQUEST, WalletService.REQUEST_CMD_TX);
intent.putExtra(WalletService.REQUEST_CMD_TX_DATA, txData);
intent.putExtra(WalletService.REQUEST_CMD_TX_TAG, tag);
startService(intent);
Timber.d("CREATE TX request sent");
} else {
@ -694,6 +695,7 @@ public class WalletActivity extends SecureActivity implements WalletFragment.Lis
@Override
public void onDisposeRequest() {
//TODO consider doing this through the WalletService to avoid concurrency issues
getWallet().disposePendingTransaction();
}

@ -19,6 +19,8 @@ package com.m2049r.xmrwallet.data;
import android.net.Uri;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.util.BitcoinAddressValidator;
import java.util.HashMap;
import java.util.Map;
@ -34,7 +36,7 @@ public class BarcodeData {
static final String BTC_AMOUNT = "amount";
public enum Asset {
XMR
XMR, BTC
}
public Asset asset = null;
@ -67,6 +69,14 @@ public class BarcodeData {
if (bcData == null) {
bcData = parseMoneroNaked(qrCode);
}
// check for btc uri
if (bcData == null) {
bcData = parseBitcoinUri(qrCode);
}
// check for naked btc addres
if (bcData == null) {
bcData = parseBitcoinNaked(qrCode);
}
return bcData;
}
@ -134,4 +144,57 @@ public class BarcodeData {
return new BarcodeData(Asset.XMR, address);
}
// bitcoin:mpQ84J43EURZHkCnXbyQ4PpNDLLBqdsMW2?amount=0.01
static public BarcodeData parseBitcoinUri(String uri) {
Timber.d("parseBitcoinUri=%s", uri);
if (uri == null) return null;
if (!uri.startsWith(BTC_SCHEME)) return null;
String noScheme = uri.substring(BTC_SCHEME.length());
Uri bitcoin = Uri.parse(noScheme);
Map<String, String> parms = new HashMap<>();
String query = bitcoin.getQuery();
if (query != null) {
String[] args = query.split("&");
for (String arg : args) {
String[] namevalue = arg.split("=");
if (namevalue.length == 0) {
continue;
}
parms.put(Uri.decode(namevalue[0]).toLowerCase(),
namevalue.length > 1 ? Uri.decode(namevalue[1]) : "");
}
}
String address = bitcoin.getPath();
String amount = parms.get(BTC_AMOUNT);
if (amount != null) {
try {
Double.parseDouble(amount);
} catch (NumberFormatException ex) {
Timber.d(ex.getLocalizedMessage());
return null; // we have an amount but its not a number!
}
}
if (!BitcoinAddressValidator.validate(address, WalletManager.getInstance().isTestNet())) {
Timber.d("address invalid");
return null;
}
return new BarcodeData(BarcodeData.Asset.BTC, address, amount);
}
static public BarcodeData parseBitcoinNaked(String address) {
Timber.d("parseBitcoinNaked=%s", address);
if (address == null) return null;
if (!BitcoinAddressValidator.validate(address, WalletManager.getInstance().isTestNet())) {
Timber.d("address invalid");
return null;
}
return new BarcodeData(BarcodeData.Asset.BTC, address);
}
}

@ -20,36 +20,38 @@ import android.os.Parcel;
import android.os.Parcelable;
import com.m2049r.xmrwallet.model.PendingTransaction;
import com.m2049r.xmrwallet.util.UserNotes;
import timber.log.Timber;
// https://stackoverflow.com/questions/2139134/how-to-send-an-object-from-one-android-activity-to-another-using-intents
public class TxData implements Parcelable {
public TxData() {
}
public TxData(TxData txData) {
this.dst_addr = txData.dst_addr;
this.dstAddr = txData.dstAddr;
this.paymentId = txData.paymentId;
this.amount = txData.amount;
this.mixin = txData.mixin;
this.priority = txData.priority;
}
public TxData(String dst_addr,
public TxData(String dstAddr,
String paymentId,
long amount,
int mixin,
PendingTransaction.Priority priority) {
this.dst_addr = dst_addr;
this.dstAddr = dstAddr;
this.paymentId = paymentId;
this.amount = amount;
this.mixin = mixin;
this.priority = priority;
}
public long getFee() {
return 0L;
}
public String getDestinationAddress() {
return dst_addr;
return dstAddr;
}
public String getPaymentId() {
@ -68,15 +70,45 @@ public class TxData implements Parcelable {
return priority;
}
final private String dst_addr;
final private String paymentId;
final private long amount;
final private int mixin;
final private PendingTransaction.Priority priority;
public void setDestinationAddress(String dstAddr) {
this.dstAddr = dstAddr;
}
public void setPaymentId(String paymentId) {
this.paymentId = paymentId;
}
public void setAmount(long amount) {
this.amount = amount;
}
public void setMixin(int mixin) {
this.mixin = mixin;
}
public void setPriority(PendingTransaction.Priority priority) {
this.priority = priority;
}
public UserNotes getUserNotes() {
return userNotes;
}
public void setUserNotes(UserNotes userNotes) {
this.userNotes = userNotes;
}
private String dstAddr;
private String paymentId;
private long amount;
private int mixin;
private PendingTransaction.Priority priority;
private UserNotes userNotes;
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeString(dst_addr);
out.writeString(dstAddr);
out.writeString(paymentId);
out.writeLong(amount);
out.writeInt(mixin);
@ -94,8 +126,8 @@ public class TxData implements Parcelable {
}
};
private TxData(Parcel in) {
dst_addr = in.readString();
protected TxData(Parcel in) {
dstAddr = in.readString();
paymentId = in.readString();
amount = in.readLong();
mixin = in.readInt();
@ -111,8 +143,8 @@ public class TxData implements Parcelable {
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append("dst_addr:");
sb.append(dst_addr);
sb.append("dstAddr:");
sb.append(dstAddr);
sb.append(",paymentId:");
sb.append(paymentId);
sb.append(",amount:");

@ -0,0 +1,98 @@
/*
* 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 com.m2049r.xmrwallet.data;
import android.os.Parcel;
import com.m2049r.xmrwallet.model.PendingTransaction;
public class TxDataBtc extends TxData {
private String xmrtoUuid;
private String btcAddress;
private double btcAmount;
public TxDataBtc() {
super();
}
public TxDataBtc(TxDataBtc txDataBtc) {
super(txDataBtc);
}
public String getXmrtoUuid() {
return xmrtoUuid;
}
public void setXmrtoUuid(String xmrtoUuid) {
this.xmrtoUuid = xmrtoUuid;
}
public String getBtcAddress() {
return btcAddress;
}
public void setBtcAddress(String btcAddress) {
this.btcAddress = btcAddress;
}
public double getBtcAmount() {
return btcAmount;
}
public void setBtcAmount(double btcAmount) {
this.btcAmount = btcAmount;
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeString(xmrtoUuid);
out.writeString(btcAddress);
out.writeDouble(btcAmount);
}
// this is used to regenerate your object. All Parcelables must have a CREATOR that implements these two methods
public static final Creator<TxDataBtc> CREATOR = new Creator<TxDataBtc>() {
public TxDataBtc createFromParcel(Parcel in) {
return new TxDataBtc(in);
}
public TxDataBtc[] newArray(int size) {
return new TxDataBtc[size];
}
};
protected TxDataBtc(Parcel in) {
super(in);
xmrtoUuid = in.readString();
btcAddress = in.readString();
btcAmount = in.readDouble();
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append(",xmrtoUuid:");
sb.append(xmrtoUuid);
sb.append(",btcAddress:");
sb.append(btcAddress);
sb.append(",btcAmount:");
sb.append(btcAmount);
return sb.toString();
}
}

@ -14,13 +14,14 @@
* limitations under the License.
*/
package com.m2049r.xmrwallet;
package com.m2049r.xmrwallet.fragment.send;
import android.content.Context;
import android.os.Bundle;
import android.support.design.widget.TextInputLayout;
import android.support.v7.widget.CardView;
import android.text.Editable;
import android.text.Html;
import android.text.InputType;
import android.text.TextWatcher;
import android.view.KeyEvent;
@ -32,9 +33,13 @@ import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.BarcodeData;
import com.m2049r.xmrwallet.data.TxData;
import com.m2049r.xmrwallet.data.TxDataBtc;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.util.BitcoinAddressValidator;
import com.m2049r.xmrwallet.util.Helper;
import timber.log.Timber;
@ -56,12 +61,12 @@ public class SendAddressWizardFragment extends SendWizardFragment {
return this;
}
interface Listener {
void setAddress(final String address);
public interface Listener {
void setBarcodeData(BarcodeData data);
void setPaymentId(final String paymentId);
void setMode(SendFragment.Mode mode);
void setBarcodeData(BarcodeData data);
TxData getTxData();
}
private EditText etDummy;
@ -71,6 +76,8 @@ public class SendAddressWizardFragment extends SendWizardFragment {
private CardView cvScan;
private View tvPaymentIdIntegrated;
private View llPaymentId;
private TextView tvXmrTo;
private View llXmrTo;
OnScanListener onScanListener;
@ -89,6 +96,9 @@ public class SendAddressWizardFragment extends SendWizardFragment {
tvPaymentIdIntegrated = view.findViewById(R.id.tvPaymentIdIntegrated);
llPaymentId = view.findViewById(R.id.llPaymentId);
llXmrTo = view.findViewById(R.id.llXmrTo);
tvXmrTo = (TextView) view.findViewById(R.id.tvXmrTo);
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto)));
etAddress = (TextInputLayout) view.findViewById(R.id.etAddress);
etAddress.getEditText().setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
@ -113,12 +123,25 @@ public class SendAddressWizardFragment extends SendWizardFragment {
public void afterTextChanged(Editable editable) {
etAddress.setError(null);
if (isIntegratedAddress()) {
Timber.d("isIntegratedAddress");
etPaymentId.getEditText().getText().clear();
llPaymentId.setVisibility(View.GONE);
llPaymentId.setVisibility(View.INVISIBLE);
tvPaymentIdIntegrated.setVisibility(View.VISIBLE);
} else { // we don't
llXmrTo.setVisibility(View.INVISIBLE);
sendListener.setMode(SendFragment.Mode.XMR);
} else if (isBitcoinAddress()) {
Timber.d("isBitcoinAddress");
etPaymentId.getEditText().getText().clear();
llPaymentId.setVisibility(View.INVISIBLE);
tvPaymentIdIntegrated.setVisibility(View.INVISIBLE);
llXmrTo.setVisibility(View.VISIBLE);
sendListener.setMode(SendFragment.Mode.BTC);
} else {
Timber.d("isStandardAddress");
llPaymentId.setVisibility(View.VISIBLE);
tvPaymentIdIntegrated.setVisibility(View.GONE);
tvPaymentIdIntegrated.setVisibility(View.INVISIBLE);
llXmrTo.setVisibility(View.INVISIBLE);
sendListener.setMode(SendFragment.Mode.XMR);
}
}
@ -188,13 +211,14 @@ public class SendAddressWizardFragment extends SendWizardFragment {
private boolean checkAddressNoError() {
String address = etAddress.getEditText().getText().toString();
return Wallet.isAddressValid(address);
return Wallet.isAddressValid(address)
|| BitcoinAddressValidator.validate(address, WalletManager.getInstance().isTestNet());
}
private boolean checkAddress() {
boolean ok = checkAddressNoError();
if (!ok) {
etAddress.setError(getString(R.string.send_qr_address_invalid));
etAddress.setError(getString(R.string.send_address_invalid));
} else {
etAddress.setError(null);
}
@ -203,7 +227,16 @@ public class SendAddressWizardFragment extends SendWizardFragment {
private boolean isIntegratedAddress() {
String address = etAddress.getEditText().getText().toString();
return (address.length() == INTEGRATED_ADDRESS_LENGTH) && Wallet.isAddressValid(address);
return (address.length() == INTEGRATED_ADDRESS_LENGTH)
&& Wallet.isAddressValid(address, WalletManager.getInstance().isTestNet());
}
private boolean isBitcoinAddress() {
String address = etAddress.getEditText().getText().toString();
if ((address.length() >= 27) && (address.length() <= 34))
return BitcoinAddressValidator.validate(address, WalletManager.getInstance().isTestNet());
else
return false;
}
private boolean checkPaymentId() {
@ -235,8 +268,15 @@ public class SendAddressWizardFragment extends SendWizardFragment {
}
if (!ok) return false;
if (sendListener != null) {
sendListener.setAddress(etAddress.getEditText().getText().toString());
sendListener.setPaymentId(etPaymentId.getEditText().getText().toString());
TxData txData = sendListener.getTxData();
if (isBitcoinAddress()) {
((TxDataBtc) txData).setBtcAddress(etAddress.getEditText().getText().toString());
txData.setDestinationAddress(null);
txData.setPaymentId("");
} else {
txData.setDestinationAddress(etAddress.getEditText().getText().toString());
txData.setPaymentId(etPaymentId.getEditText().getText().toString());
}
}
return true;
}

@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.m2049r.xmrwallet;
package com.m2049r.xmrwallet.fragment.send;
import android.os.Bundle;
import android.view.LayoutInflater;
@ -22,10 +22,12 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.BarcodeData;
import com.m2049r.xmrwallet.widget.ExchangeTextView;
import com.m2049r.xmrwallet.data.TxData;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.widget.ExchangeTextView;
import com.m2049r.xmrwallet.widget.NumberPadView;
import timber.log.Timber;
@ -48,7 +50,7 @@ public class SendAmountWizardFragment extends SendWizardFragment {
interface Listener {
SendFragment.Listener getActivityCallback();
void setAmount(final long amount);
TxData getTxData();
BarcodeData popBarcodeData();
}
@ -99,9 +101,9 @@ public class SendAmountWizardFragment extends SendWizardFragment {
if (sendListener != null) {
String xmr = evAmount.getAmount();
if (xmr != null) {
sendListener.setAmount(Wallet.getAmountFromString(xmr));
sendListener.getTxData().setAmount(Wallet.getAmountFromString(xmr));
} else {
sendListener.setAmount(0L);
sendListener.getTxData().setAmount(0L);
}
}
return true;

@ -0,0 +1,261 @@
/*
* 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 com.m2049r.xmrwallet.fragment.send;
import android.os.Bundle;
import android.text.Html;
import android.text.Spanned;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.BarcodeData;
import com.m2049r.xmrwallet.data.TxDataBtc;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.OkHttpClientSingleton;
import com.m2049r.xmrwallet.widget.ExchangeBtcTextView;
import com.m2049r.xmrwallet.widget.NumberPadView;
import com.m2049r.xmrwallet.widget.SendProgressView;
import com.m2049r.xmrwallet.xmrto.XmrToError;
import com.m2049r.xmrwallet.xmrto.XmrToException;
import com.m2049r.xmrwallet.xmrto.api.QueryOrderParameters;
import com.m2049r.xmrwallet.xmrto.api.XmrToApi;
import com.m2049r.xmrwallet.xmrto.api.XmrToCallback;
import com.m2049r.xmrwallet.xmrto.network.XmrToApiImpl;
import java.text.NumberFormat;
import java.util.Locale;
import okhttp3.HttpUrl;
import timber.log.Timber;
public class SendBtcAmountWizardFragment extends SendWizardFragment {
public static SendBtcAmountWizardFragment newInstance(SendAmountWizardFragment.Listener listener) {
SendBtcAmountWizardFragment instance = new SendBtcAmountWizardFragment();
instance.setSendListener(listener);
return instance;
}
SendAmountWizardFragment.Listener sendListener;
public SendBtcAmountWizardFragment setSendListener(SendAmountWizardFragment.Listener listener) {
this.sendListener = listener;
return this;
}
private TextView tvFunds;
private ExchangeBtcTextView evAmount;
private NumberPadView numberPad;
private TextView tvXmrToParms;
private SendProgressView evParams;
private View llXmrToParms;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState)));
sendListener = (SendAmountWizardFragment.Listener) getParentFragment();
View view = inflater.inflate(R.layout.fragment_send_btc_amount, container, false);
tvFunds = (TextView) view.findViewById(R.id.tvFunds);
evParams = (SendProgressView) view.findViewById(R.id.evXmrToParms);
llXmrToParms = view.findViewById(R.id.llXmrToParms);
tvXmrToParms = (TextView) view.findViewById(R.id.tvXmrToParms);
evAmount = (ExchangeBtcTextView) view.findViewById(R.id.evAmount);
numberPad = (NumberPadView) view.findViewById(R.id.numberPad);
numberPad.setListener(evAmount);
Helper.hideKeyboard(getActivity());
return view;
}
@Override
public boolean onValidateFields() {
if (!evAmount.validate(maxBtc, minBtc)) {
return false;
}
if (sendListener != null) {
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
String btcString = evAmount.getAmount();
if (btcString != null) {
try {
double btc = Double.parseDouble(btcString);
Timber.d("setAmount %f", btc);
txDataBtc.setBtcAmount(btc);
} catch (NumberFormatException ex) {
Timber.d(ex.getLocalizedMessage());
txDataBtc.setBtcAmount(0);
}
} else {
txDataBtc.setBtcAmount(0);
}
}
return true;
}
double maxBtc = 0;
double minBtc = 0;
@Override
public void onPauseFragment() {
llXmrToParms.setVisibility(View.INVISIBLE);
}
@Override
public void onResumeFragment() {
super.onResumeFragment();
Timber.d("onResumeFragment()");
Helper.hideKeyboard(getActivity());
final long funds = getTotalFunds();
tvFunds.setText(getString(R.string.send_available,
Wallet.getDisplayAmount(funds)));
if ((evAmount.getAmount() == null) || evAmount.getAmount().isEmpty()) {
final BarcodeData data = sendListener.popBarcodeData();
if ((data != null) && (data.amount != null)) {
evAmount.setAmount(data.amount);
}
}
callXmrTo();
}
long getTotalFunds() {
return sendListener.getActivityCallback().getTotalFunds();
}
private QueryOrderParameters orderParameters = null;
private void processOrderParms(final QueryOrderParameters orderParameters) {
this.orderParameters = orderParameters;
getView().post(new Runnable() {
@Override
public void run() {
evAmount.setRate(1.0d / orderParameters.getPrice());
NumberFormat df = NumberFormat.getInstance(Locale.US);
df.setMaximumFractionDigits(6);
String min = df.format(orderParameters.getLowerLimit());
String max = df.format(orderParameters.getUpperLimit());
String rate = df.format(orderParameters.getPrice());
Spanned xmrParmText = Html.fromHtml(getString(R.string.info_send_xmrto_parms, min, max, rate));
if (orderParameters.isZeroConfEnabled()) {
String zeroConf = df.format(orderParameters.getZeroConfMaxAmount());
Spanned zeroConfText = Html.fromHtml(getString(R.string.info_send_xmrto_zeroconf, zeroConf));
xmrParmText = (Spanned) TextUtils.concat(xmrParmText, " ", zeroConfText);
}
tvXmrToParms.setText(xmrParmText);
maxBtc = orderParameters.getUpperLimit();
minBtc = orderParameters.getLowerLimit();
Timber.d("minBtc=%f / maxBtc=%f", minBtc, maxBtc);
final long funds = getTotalFunds();
double availableXmr = 1.0 * funds / 1000000000000L;
maxBtc = Math.min(maxBtc, availableXmr * orderParameters.getPrice());
String availBtcString = df.format(availableXmr * orderParameters.getPrice());
String availXmrString = df.format(availableXmr);
tvFunds.setText(getString(R.string.send_available_btc,
availXmrString,
availBtcString));
llXmrToParms.setVisibility(View.VISIBLE);
evParams.hideProgress();
}
});
}
private void processOrderParmsError(final Exception ex) {
evAmount.setRate(0);
orderParameters = null;
maxBtc = 0;
minBtc = 0;
Timber.e(ex);
getView().post(new Runnable() {
@Override
public void run() {
if (ex instanceof XmrToException) {
XmrToException xmrEx = (XmrToException) ex;
XmrToError xmrErr = xmrEx.getError();
if (xmrErr != null) {
if (xmrErr.isRetryable()) {
evParams.showMessage(xmrErr.getErrorId().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_retry));
evParams.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
evParams.setOnClickListener(null);
callXmrTo();
}
});
} else {
evParams.showMessage(xmrErr.getErrorId().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_noretry));
}
} else {
evParams.showMessage(getString(R.string.label_generic_xmrto_error),
getString(R.string.text_generic_xmrto_error, xmrEx.getCode()),
getString(R.string.text_noretry));
}
} else {
evParams.showMessage(getString(R.string.label_generic_xmrto_error),
ex.getLocalizedMessage(),
getString(R.string.text_noretry));
}
}
});
}
private void callXmrTo() {
evParams.showProgress(getString(R.string.label_send_progress_queryparms));
getXmrToApi().queryOrderParameters(new XmrToCallback<QueryOrderParameters>() {
@Override
public void onSuccess(final QueryOrderParameters orderParameters) {
processOrderParms(orderParameters);
}
@Override
public void onError(final Exception e) {
processOrderParmsError(e);
}
});
}
private XmrToApi xmrToApi = null;
private final XmrToApi getXmrToApi() {
if (xmrToApi == null) {
synchronized (this) {
if (xmrToApi == null) {
xmrToApi = new XmrToApiImpl(OkHttpClientSingleton.getOkHttpClient(),
Helper.getXmrToBaseUrl());
}
}
}
return xmrToApi;
}
}

@ -0,0 +1,680 @@
/*
* 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 com.m2049r.xmrwallet.fragment.send;
import android.app.Activity;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Handler;
import android.support.design.widget.TextInputLayout;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.TxData;
import com.m2049r.xmrwallet.data.TxDataBtc;
import com.m2049r.xmrwallet.model.PendingTransaction;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.OkHttpClientSingleton;
import com.m2049r.xmrwallet.widget.SendProgressView;
import com.m2049r.xmrwallet.xmrto.XmrToError;
import com.m2049r.xmrwallet.xmrto.XmrToException;
import com.m2049r.xmrwallet.xmrto.api.CreateOrder;
import com.m2049r.xmrwallet.xmrto.api.QueryOrderStatus;
import com.m2049r.xmrwallet.xmrto.api.XmrToApi;
import com.m2049r.xmrwallet.xmrto.api.XmrToCallback;
import com.m2049r.xmrwallet.xmrto.network.XmrToApiImpl;
import java.text.NumberFormat;
import java.util.Locale;
import okhttp3.HttpUrl;
import timber.log.Timber;
public class SendBtcConfirmWizardFragment extends SendWizardFragment implements SendConfirm {
private final int QUERY_INTERVAL = 500;//ms
public static SendBtcConfirmWizardFragment newInstance(SendConfirmWizardFragment.Listener listener) {
SendBtcConfirmWizardFragment instance = new SendBtcConfirmWizardFragment();
instance.setSendListener(listener);
return instance;
}
SendConfirmWizardFragment.Listener sendListener;
public SendBtcConfirmWizardFragment setSendListener(SendConfirmWizardFragment.Listener listener) {
this.sendListener = listener;
return this;
}
private View llStageA;
private SendProgressView evStageA;
private View llStageB;
private SendProgressView evStageB;
private View llStageC;
private SendProgressView evStageC;
private TextView tvTxBtcAmount;
private TextView tvTxBtcRate;
private TextView tvTxBtcAddress;
private TextView tvTxXmrToKey;
private TextView tvTxFee;
private TextView tvTxTotal;
private View llConfirmSend;
private Button bSend;
private View pbProgressSend;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Timber.d("onCreateView(%s)", (String.valueOf(savedInstanceState)));
View view = inflater.inflate(
R.layout.fragment_send_btc_confirm, container, false);
tvTxBtcAddress = (TextView) view.findViewById(R.id.tvTxBtcAddress);
tvTxBtcAmount = ((TextView) view.findViewById(R.id.tvTxBtcAmount));
tvTxBtcRate = (TextView) view.findViewById(R.id.tvTxBtcRate);
tvTxXmrToKey = (TextView) view.findViewById(R.id.tvTxXmrToKey);
tvTxFee = (TextView) view.findViewById(R.id.tvTxFee);
tvTxTotal = (TextView) view.findViewById(R.id.tvTxTotal);
llStageA = view.findViewById(R.id.llStageA);
evStageA = (SendProgressView) view.findViewById(R.id.evStageA);
llStageB = view.findViewById(R.id.llStageB);
evStageB = (SendProgressView) view.findViewById(R.id.evStageB);
llStageC = view.findViewById(R.id.llStageC);
evStageC = (SendProgressView) view.findViewById(R.id.evStageC);
tvTxXmrToKey.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Helper.clipBoardCopy(getActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString());
Toast.makeText(getActivity(), getString(R.string.message_copy_xmrtokey), Toast.LENGTH_SHORT).show();
}
});
llConfirmSend = view.findViewById(R.id.llConfirmSend);
pbProgressSend = view.findViewById(R.id.pbProgressSend);
bSend = (Button) view.findViewById(R.id.bSend);
bSend.setEnabled(false);
bSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Timber.d("bSend.setOnClickListener");
bSend.setEnabled(false);
preSend();
}
});
return view;
}
int inProgress = 0;
final static int STAGE_X = 0;
final static int STAGE_A = 1;
final static int STAGE_B = 2;
final static int STAGE_C = 3;
private void showProgress(int stage, String progressText) {
Timber.d("showProgress(%d)", stage);
inProgress = stage;
switch (stage) {
case STAGE_A:
evStageA.showProgress(progressText);
break;
case STAGE_B:
evStageB.showProgress(progressText);
break;
case STAGE_C:
evStageC.showProgress(progressText);
break;
default:
throw new IllegalStateException("unknown stage " + stage);
}
}
public void hideProgress() {
Timber.d("hideProgress(%d)", inProgress);
switch (inProgress) {
case STAGE_A:
evStageA.hideProgress();
llStageA.setVisibility(View.VISIBLE);
break;
case STAGE_B:
evStageB.hideProgress();
llStageB.setVisibility(View.VISIBLE);
break;
case STAGE_C:
evStageC.hideProgress();
llStageC.setVisibility(View.VISIBLE);
break;
default:
throw new IllegalStateException("unknown stage " + inProgress);
}
inProgress = STAGE_X;
}
public void showStageError(String code, String message, String solution) {
switch (inProgress) {
case STAGE_A:
evStageA.showMessage(code, message, solution);
break;
case STAGE_B:
evStageB.showMessage(code, message, solution);
break;
case STAGE_C:
evStageC.showMessage(code, message, solution);
break;
default:
throw new IllegalStateException("unknown stage");
}
inProgress = STAGE_X;
}
PendingTransaction pendingTransaction = null;
void send() {
Timber.d("SEND @%d", sendCountdown);
if (sendCountdown <= 0) {
Timber.i("User waited too long in password dialog.");
Toast.makeText(getContext(), getString(R.string.send_xmrto_timeout), Toast.LENGTH_SHORT).show();
return;
}
sendListener.getTxData().getUserNotes().setXmrtoStatus(xmrtoStatus);
((TxDataBtc) sendListener.getTxData()).setXmrtoUuid(xmrtoStatus.getUuid());
// TODO make method in TxDataBtc to set both of the above in one go
sendListener.commitTransaction();
pbProgressSend.setVisibility(View.VISIBLE);
}
@Override
public void sendFailed() {
Timber.e("SEND FAILED");
pbProgressSend.setVisibility(View.INVISIBLE);
}
@Override
// callback from wallet when PendingTransaction created (started by prepareSend() here
public void transactionCreated(final String txTag, final PendingTransaction pendingTransaction) {
if (isResumed
&& (inProgress == STAGE_C)
&& (xmrtoStatus != null)
&& (xmrtoStatus.isCreated()
&& (xmrtoStatus.getUuid().equals(txTag)))) {
this.pendingTransaction = pendingTransaction;
getView().post(new Runnable() {
@Override
public void run() {
hideProgress();
tvTxFee.setText(Wallet.getDisplayAmount(pendingTransaction.getFee()));
tvTxTotal.setText(Wallet.getDisplayAmount(
pendingTransaction.getFee() + pendingTransaction.getAmount()));
updateSendButton();
}
});
} else {
this.pendingTransaction = null;
sendListener.disposeTransaction();
}
}
@Override
public void createTransactionFailed(String errorText) {
Timber.e("CREATE TX FAILED");
if (pendingTransaction != null) {
throw new IllegalStateException("pendingTransaction is not null");
}
showStageError(getString(R.string.send_create_tx_error_title),
errorText,
getString(R.string.text_noretry_monero));
}
@Override
public boolean onValidateFields() {
return true;
}
private boolean isResumed = false;
@Override
public void onPauseFragment() {
isResumed = false;
stopSendTimer();
sendListener.disposeTransaction();
pendingTransaction = null;
inProgress = STAGE_X;
updateSendButton();
super.onPauseFragment();
}
@Override
public void onResumeFragment() {
super.onResumeFragment();
Timber.d("onResumeFragment()");
if (sendListener.getMode() != SendFragment.Mode.BTC) {
throw new IllegalStateException("Mode is not BTC!");
}
Helper.hideKeyboard(getActivity());
llStageA.setVisibility(View.INVISIBLE);
evStageA.hideProgress();
llStageB.setVisibility(View.INVISIBLE);
evStageB.hideProgress();
llStageC.setVisibility(View.INVISIBLE);
evStageC.hideProgress();
isResumed = true;
if ((pendingTransaction == null) && (inProgress == STAGE_X)) {
createOrder();
} // otherwise just sit there blank
// TODO: don't sit there blank - can this happen? should we just die?
}
private int sendCountdown = 0;
private static final int XMRTO_COUNTDOWN = 10 * 60; // 10 minutes
private static final int XMRTO_COUNTDOWN_STEP = 1; // 1 second
Runnable updateRunnable = null;
void startSendTimer() {
Timber.d("startSendTimer()");
sendCountdown = XMRTO_COUNTDOWN;
updateRunnable = new Runnable() {
@Override
public void run() {
if (!isAdded())
return;
Timber.d("updateTimer()");
if (sendCountdown <= 0) {
bSend.setEnabled(false);
sendCountdown = 0;
Toast.makeText(getContext(), getString(R.string.send_xmrto_timeout), Toast.LENGTH_SHORT).show();
}
int minutes = sendCountdown / 60;
int seconds = sendCountdown % 60;
String t = String.format("%d:%02d", minutes, seconds);
bSend.setText(getString(R.string.send_send_timed_label, t));
if (sendCountdown > 0) {
sendCountdown -= XMRTO_COUNTDOWN_STEP;
getView().postDelayed(this, XMRTO_COUNTDOWN_STEP * 1000);
}
}
};
getView().post(updateRunnable);
}
void stopSendTimer() {
getView().removeCallbacks(updateRunnable);
}
void updateSendButton() {
Timber.d("updateSendButton()");
if (pendingTransaction != null) {
llConfirmSend.setVisibility(View.VISIBLE);
bSend.setEnabled(sendCountdown > 0);
} else {
llConfirmSend.setVisibility(View.GONE);
bSend.setEnabled(false);
}
}
public void preSend() {
final Activity activity = getActivity();
View promptsView = getLayoutInflater().inflate(R.layout.prompt_password, null);
android.app.AlertDialog.Builder alertDialogBuilder = new android.app.AlertDialog.Builder(activity);
alertDialogBuilder.setView(promptsView);
final TextInputLayout etPassword = (TextInputLayout) promptsView.findViewById(R.id.etPassword);
etPassword.setHint(getString(R.string.prompt_send_password));
etPassword.getEditText().addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
if (etPassword.getError() != null) {
etPassword.setError(null);
}
}
@Override
public void beforeTextChanged(CharSequence s, int start,
int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start,
int before, int count) {
}
});
alertDialogBuilder
.setCancelable(false)
.setPositiveButton("OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
String pass = etPassword.getEditText().getText().toString();
if (getActivityCallback().verifyWalletPassword(pass)) {
dialog.dismiss();
Helper.hideKeyboardAlways(activity);
send();
} else {
etPassword.setError(getString(R.string.bad_password));
}
}
})
.setNegativeButton("Cancel",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
Helper.hideKeyboardAlways(activity);
dialog.cancel();
bSend.setEnabled(sendCountdown > 0); // allow to try again
}
});
final android.app.AlertDialog passwordDialog = alertDialogBuilder.create();
passwordDialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(DialogInterface dialog) {
Button button = ((android.app.AlertDialog) dialog).getButton(android.app.AlertDialog.BUTTON_POSITIVE);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String pass = etPassword.getEditText().getText().toString();
if (getActivityCallback().verifyWalletPassword(pass)) {
Helper.hideKeyboardAlways(activity);
passwordDialog.dismiss();
send();
} else {
etPassword.setError(getString(R.string.bad_password));
}
}
});
}
});
Helper.showKeyboard(passwordDialog);
// accept keyboard "ok"
etPassword.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() {
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) {
String pass = etPassword.getEditText().getText().toString();
if (getActivityCallback().verifyWalletPassword(pass)) {
Helper.hideKeyboardAlways(activity);
passwordDialog.dismiss();
send();
} else {
etPassword.setError(getString(R.string.bad_password));
}
return true;
}
return false;
}
});
passwordDialog.show();
}
// creates a pending transaction and calls us back with transactionCreated()
// or createTransactionFailed()
void prepareSend() {
if (!isResumed) return;
if ((xmrtoStatus == null)) {
throw new IllegalStateException("xmrtoStatus is null");
}
if ((!xmrtoStatus.isCreated())) {
throw new IllegalStateException("order is not created");
}
showProgress(3, getString(R.string.label_send_progress_create_tx));
TxData txData = sendListener.getTxData();
txData.setDestinationAddress(xmrtoStatus.getXmrReceivingAddress());
txData.setPaymentId(xmrtoStatus.getXmrRequiredPaymentIdShort());
txData.setAmount(Wallet.getAmountFromDouble(xmrtoStatus.getXmrAmountTotal()));
getActivityCallback().onPrepareSend(xmrtoStatus.getUuid(), txData);
}
SendFragment.Listener getActivityCallback() {
return sendListener.getActivityCallback();
}
private CreateOrder xmrtoOrder = null;
private void processCreateOrder(final CreateOrder createOrder) {
Timber.d("processCreateOrder %s", createOrder.getUuid());
xmrtoOrder = createOrder;
if (QueryOrderStatus.State.TO_BE_CREATED.toString().equals(createOrder.getState())) {
getView().post(new Runnable() {
@Override
public void run() {
tvTxXmrToKey.setText(createOrder.getUuid());
tvTxBtcAddress.setText(createOrder.getBtcDestAddress());
hideProgress();
}
});
queryOrder(createOrder.getUuid());
} else {
throw new IllegalStateException("Create Order is not TO_BE_CREATED");
}
}
private void processCreateOrderError(final Exception ex) {
Timber.e("processCreateOrderError %s", ex.getLocalizedMessage());
getView().post(new Runnable() {
@Override
public void run() {
if (ex instanceof XmrToException) {
XmrToException xmrEx = (XmrToException) ex;
XmrToError xmrErr = xmrEx.getError();
if (xmrErr != null) {
if (xmrErr.isRetryable()) {
showStageError(xmrErr.getErrorId().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_retry));
evStageA.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
evStageA.setOnClickListener(null);
createOrder();
}
});
} else {
showStageError(xmrErr.getErrorId().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_noretry));
}
} else {
showStageError(getString(R.string.label_generic_xmrto_error),
getString(R.string.text_generic_xmrto_error, xmrEx.getCode()),
getString(R.string.text_noretry));
}
} else {
evStageA.showMessage(getString(R.string.label_generic_xmrto_error),
ex.getLocalizedMessage(),
getString(R.string.text_noretry));
}
}
});
}
private void createOrder() {
if (!isResumed) return;
Timber.d("createOrder");
xmrtoOrder = null;
xmrtoStatus = null;
showProgress(1, getString(R.string.label_send_progress_xmrto_create));
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
double btcAmount = txDataBtc.getBtcAmount();
getXmrToApi().createOrder(btcAmount, txDataBtc.getBtcAddress(), new XmrToCallback<CreateOrder>() {
@Override
public void onSuccess(CreateOrder createOrder) {
if (!isResumed) return;
if (xmrtoOrder != null) {
Timber.w("another ongoing create order request");
return;
}
processCreateOrder(createOrder);
}
@Override
public void onError(Exception ex) {
if (!isResumed) return;
if (xmrtoOrder != null) {
Timber.w("another ongoing create order request");
return;
}
processCreateOrderError(ex);
}
});
}
private QueryOrderStatus xmrtoStatus = null;
private void processQueryOrder(final QueryOrderStatus status) {
Timber.d("processQueryOrder %s for %s", status.getState().toString(), status.getUuid());
xmrtoStatus = status;
if (status.isCreated()) {
getView().post(new Runnable() {
@Override
public void run() {
NumberFormat df = NumberFormat.getInstance(Locale.US);
df.setMaximumFractionDigits(12);
String btcAmount = df.format(status.getBtcAmount());
String xmrAmountTotal = df.format(status.getXmrAmountTotal());
tvTxBtcAmount.setText(getString(R.string.text_send_btc_amount, btcAmount, xmrAmountTotal));
String xmrPriceBtc = df.format(status.getXmrPriceBtc());
tvTxBtcRate.setText(getString(R.string.text_send_btc_rate, xmrPriceBtc));
double calcRate = status.getBtcAmount() / status.getXmrPriceBtc();
Timber.i("Rates: %f / %f", calcRate, status.getXmrPriceBtc());
tvTxBtcAddress.setText(status.getBtcDestAddress()); // TODO test if this is different?
Timber.i("Expires @ %s, in %s seconds", status.getExpiresAt().toString(), status.getSecondsTillTimeout());
Timber.i("Status = %s", status.getState().toString());
tvTxXmrToKey.setText(status.getUuid());
Timber.d("AmountRemaining=%f, XmrAmountTotal=%f", status.getXmrAmountRemaining(), status.getXmrAmountTotal());
hideProgress();
startSendTimer();
prepareSend();
}
});
} else {
Timber.d("try again!");
handler.postDelayed(new Runnable() {
@Override
public void run() {
queryOrder(status.getUuid());
}
}, QUERY_INTERVAL);
}
}
Handler handler = new Handler();
private void processQueryOrderError(final Exception ex) {
Timber.e("processQueryOrderError %s", ex.getLocalizedMessage());
getView().post(new Runnable() {
@Override
public void run() {
if (ex instanceof XmrToException) {
XmrToException xmrEx = (XmrToException) ex;
XmrToError xmrErr = xmrEx.getError();
if (xmrErr != null) {
if (xmrErr.isRetryable()) {
showStageError(xmrErr.getErrorId().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_retry));
evStageB.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
evStageB.setOnClickListener(null);
queryOrder(xmrtoOrder.getUuid());
}
});
} else {
showStageError(xmrErr.getErrorId().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_noretry));
}
} else {
showStageError(getString(R.string.label_generic_xmrto_error),
getString(R.string.text_generic_xmrto_error, xmrEx.getCode()),
getString(R.string.text_noretry));
}
} else {
evStageB.showMessage(getString(R.string.label_generic_xmrto_error),
ex.getLocalizedMessage(),
getString(R.string.text_noretry));
}
}
});
}
private void queryOrder(final String uuid) {
Timber.d("queryOrder(%s)", uuid);
if (!isResumed) return;
getView().post(new Runnable() {
@Override
public void run() {
xmrtoStatus = null;
showProgress(2, getString(R.string.label_send_progress_xmrto_query));
getXmrToApi().queryOrderStatus(uuid, new XmrToCallback<QueryOrderStatus>() {
@Override
public void onSuccess(QueryOrderStatus status) {
if (!isResumed) return;
if (xmrtoOrder == null) return;
if (!status.getUuid().equals(xmrtoOrder.getUuid())) {
Timber.d("Query UUID does not match");
// ignore (we got a response to a stale request)
return;
}
if (xmrtoStatus != null)
throw new IllegalStateException("xmrtoStatus must be null here!");
processQueryOrder(status);
}
@Override
public void onError(Exception ex) {
if (!isResumed) return;
processQueryOrderError(ex);
}
});
}
});
}
private XmrToApi xmrToApi = null;
private final XmrToApi getXmrToApi() {
if (xmrToApi == null) {
synchronized (this) {
if (xmrToApi == null) {
xmrToApi = new XmrToApiImpl(OkHttpClientSingleton.getOkHttpClient(),
Helper.getXmrToBaseUrl());
}
}
}
return xmrToApi;
}
}

@ -0,0 +1,267 @@
/*
* 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 com.m2049r.xmrwallet.fragment.send;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.PendingTx;
import com.m2049r.xmrwallet.data.TxDataBtc;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.OkHttpClientSingleton;
import com.m2049r.xmrwallet.xmrto.XmrToException;
import com.m2049r.xmrwallet.xmrto.api.QueryOrderStatus;
import com.m2049r.xmrwallet.xmrto.api.XmrToApi;
import com.m2049r.xmrwallet.xmrto.api.XmrToCallback;
import com.m2049r.xmrwallet.xmrto.network.XmrToApiImpl;
import java.text.NumberFormat;
import java.util.Locale;
import okhttp3.HttpUrl;
import timber.log.Timber;
public class SendBtcSuccessWizardFragment extends SendWizardFragment {
public static SendBtcSuccessWizardFragment newInstance(SendSuccessWizardFragment.Listener listener) {
SendBtcSuccessWizardFragment instance = new SendBtcSuccessWizardFragment();
instance.setSendListener(listener);
return instance;
}
SendSuccessWizardFragment.Listener sendListener;
public SendBtcSuccessWizardFragment setSendListener(SendSuccessWizardFragment.Listener listener) {
this.sendListener = listener;
return this;
}
ImageButton bCopyTxId;
private TextView tvTxId;
private TextView tvTxAddress;
private TextView tvTxPaymentId;
private TextView tvTxAmount;
private TextView tvTxFee;
private TextView tvXmrToAmount;
private TextView tvXmrToStatus;
private ImageView ivXmrToStatus;
private ImageView ivXmrToStatusBig;
private ProgressBar pbXmrto;
private TextView tvTxXmrToKey;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState)));
View view = inflater.inflate(
R.layout.fragment_send_btc_success, container, false);
bCopyTxId = (ImageButton) view.findViewById(R.id.bCopyTxId);
bCopyTxId.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Helper.clipBoardCopy(getActivity(), getString(R.string.label_send_txid), tvTxId.getText().toString());
Toast.makeText(getActivity(), getString(R.string.message_copy_txid), Toast.LENGTH_SHORT).show();
}
});
tvXmrToAmount = (TextView) view.findViewById(R.id.tvXmrToAmount);
tvXmrToStatus = (TextView) view.findViewById(R.id.tvXmrToStatus);
ivXmrToStatus = (ImageView) view.findViewById(R.id.ivXmrToStatus);
ivXmrToStatusBig = (ImageView) view.findViewById(R.id.ivXmrToStatusBig);
tvTxId = (TextView) view.findViewById(R.id.tvTxId);
tvTxAddress = (TextView) view.findViewById(R.id.tvTxAddress);
tvTxPaymentId = (TextView) view.findViewById(R.id.tvTxPaymentId);
tvTxAmount = ((TextView) view.findViewById(R.id.tvTxAmount));
tvTxFee = (TextView) view.findViewById(R.id.tvTxFee);
pbXmrto = (ProgressBar) view.findViewById(R.id.pbXmrto);
pbXmrto.getIndeterminateDrawable().setColorFilter(0x61000000, android.graphics.PorterDuff.Mode.MULTIPLY);
tvTxXmrToKey = (TextView) view.findViewById(R.id.tvTxXmrToKey);
tvTxXmrToKey.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Helper.clipBoardCopy(getActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString());
Toast.makeText(getActivity(), getString(R.string.message_copy_xmrtokey), Toast.LENGTH_SHORT).show();
;
}
});
return view;
}
@Override
public boolean onValidateFields() {
return true;
}
private boolean isResumed = false;
@Override
public void onPauseFragment() {
isResumed = false;
super.onPauseFragment();
}
TxDataBtc btcData = null;
@Override
public void onResumeFragment() {
super.onResumeFragment();
Timber.d("onResumeFragment()");
Helper.hideKeyboard(getActivity());
isResumed = true;
btcData = (TxDataBtc) sendListener.getTxData();
tvTxAddress.setText(btcData.getDestinationAddress());
String paymentId = btcData.getPaymentId();
if ((paymentId != null) && (!paymentId.isEmpty())) {
tvTxPaymentId.setText(btcData.getPaymentId());
} else {
tvTxPaymentId.setText("-");
}
final PendingTx committedTx = sendListener.getCommittedTx();
if (committedTx != null) {
tvTxId.setText(committedTx.txId);
bCopyTxId.setEnabled(true);
bCopyTxId.setImageResource(R.drawable.ic_content_copy_black_24dp);
tvTxAmount.setText(getString(R.string.send_amount, Helper.getDisplayAmount(committedTx.amount)));
tvTxFee.setText(getString(R.string.send_fee, Helper.getDisplayAmount(committedTx.fee)));
if (btcData != null) {
NumberFormat df = NumberFormat.getInstance(Locale.US);
df.setMaximumFractionDigits(12);
String btcAmount = df.format(btcData.getBtcAmount());
tvXmrToAmount.setText(getString(R.string.info_send_xmrto_success_btc, btcAmount));
//TODO btcData.getBtcAddress();
tvTxXmrToKey.setText(btcData.getXmrtoUuid());
queryOrder();
} else {
throw new IllegalStateException("btcData is null");
}
}
sendListener.enableDone();
}
private final int QUERY_INTERVAL = 1000; // ms
private void processQueryOrder(final QueryOrderStatus status) {
Timber.d("processQueryOrder %s for %s", status.getState().toString(), status.getUuid());
if (!btcData.getXmrtoUuid().equals(status.getUuid()))
throw new IllegalStateException("UUIDs do not match!");
if (isResumed && (getView() != null))
getView().post(new Runnable() {
@Override
public void run() {
showXmrToStatus(status);
if (!status.isTerminal()) {
getView().postDelayed(new Runnable() {
@Override
public void run() {
queryOrder();
}
}, QUERY_INTERVAL);
}
}
});
}
private void queryOrder() {
Timber.d("queryOrder(%s)", btcData.getXmrtoUuid());
if (!isResumed) return;
getXmrToApi().queryOrderStatus(btcData.getXmrtoUuid(), new XmrToCallback<QueryOrderStatus>() {
@Override
public void onSuccess(QueryOrderStatus status) {
if (!isAdded()) return;
processQueryOrder(status);
}
@Override
public void onError(final Exception ex) {
if (!isResumed) return;
Timber.e(ex);
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (ex instanceof XmrToException) {
Toast.makeText(getActivity(), ((XmrToException) ex).getError().getErrorMsg(), Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getActivity(), ex.getLocalizedMessage(), Toast.LENGTH_LONG).show();
}
}
});
}
});
}
private int statusResource = 0;
void showXmrToStatus(final QueryOrderStatus status) {
if (status.isError()) {
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_error, status.toString()));
statusResource = R.drawable.ic_error_red_24dp;
pbXmrto.getIndeterminateDrawable().setColorFilter(0xff8b0000, android.graphics.PorterDuff.Mode.MULTIPLY);
} else if (status.isSent()) {
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_sent));
statusResource = R.drawable.ic_success_green_24dp;
pbXmrto.getIndeterminateDrawable().setColorFilter(0xFF417505, android.graphics.PorterDuff.Mode.MULTIPLY);
} else if (status.isPending()) {
if (status.isPaid()) {
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_paid));
} else {
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_unpaid));
}
statusResource = R.drawable.ic_pending_orange_24dp;
pbXmrto.getIndeterminateDrawable().setColorFilter(0xFFFF6105, android.graphics.PorterDuff.Mode.MULTIPLY);
} else {
throw new IllegalStateException("status is broken: " + status.toString());
}
ivXmrToStatus.setImageResource(statusResource);
if (status.isTerminal()) {
pbXmrto.setVisibility(View.INVISIBLE);
ivXmrToStatus.setVisibility(View.GONE);
ivXmrToStatusBig.setImageResource(statusResource);
ivXmrToStatusBig.setVisibility(View.VISIBLE);
}
}
private XmrToApi xmrToApi = null;
private final XmrToApi getXmrToApi() {
if (xmrToApi == null) {
synchronized (this) {
if (xmrToApi == null) {
xmrToApi = new XmrToApiImpl(OkHttpClientSingleton.getOkHttpClient(),
Helper.getXmrToBaseUrl());
}
}
}
return xmrToApi;
}
}

@ -0,0 +1,27 @@
/*
* 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 com.m2049r.xmrwallet.fragment.send;
import com.m2049r.xmrwallet.model.PendingTransaction;
interface SendConfirm {
void sendFailed();
void createTransactionFailed(String errorText);
void transactionCreated(String txTag, PendingTransaction pendingTransaction);
}

@ -14,12 +14,13 @@
* limitations under the License.
*/
package com.m2049r.xmrwallet;
package com.m2049r.xmrwallet.fragment.send;
import android.app.Activity;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.design.widget.TextInputLayout;
import android.support.v7.app.AlertDialog;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
@ -30,14 +31,16 @@ import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.TextView;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.TxData;
import com.m2049r.xmrwallet.model.PendingTransaction;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.UserNotes;
import timber.log.Timber;
public class SendConfirmWizardFragment extends SendWizardFragment {
public class SendConfirmWizardFragment extends SendWizardFragment implements SendConfirm {
public static SendConfirmWizardFragment newInstance(Listener listener) {
SendConfirmWizardFragment instance = new SendConfirmWizardFragment();
@ -57,11 +60,11 @@ public class SendConfirmWizardFragment extends SendWizardFragment {
TxData getTxData();
String getNotes();
void commitTransaction();
void disposeTransaction();
SendFragment.Mode getMode();
}
private TextView tvTxAddress;
@ -122,8 +125,11 @@ public class SendConfirmWizardFragment extends SendWizardFragment {
PendingTransaction pendingTransaction = null;
@Override
// callback from wallet when PendingTransaction created
void transactionCreated(PendingTransaction pendingTransaction) {
public void transactionCreated(String txTag, PendingTransaction pendingTransaction) {
// ignore txTag - the app flow ensures this is the correct tx
// TODO: use the txTag
hideProgress();
if (isResumed) {
this.pendingTransaction = pendingTransaction;
@ -138,10 +144,22 @@ public class SendConfirmWizardFragment extends SendWizardFragment {
pbProgressSend.setVisibility(View.VISIBLE);
}
void sendFailed() {
@Override
public void sendFailed() {
pbProgressSend.setVisibility(View.INVISIBLE);
}
@Override
public void createTransactionFailed(String errorText) {
hideProgress();
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setCancelable(true).
setTitle(getString(R.string.send_create_tx_error_title)).
setMessage(errorText).
create().
show();
}
@Override
public boolean onValidateFields() {
return true;
@ -173,9 +191,9 @@ public class SendConfirmWizardFragment extends SendWizardFragment {
} else {
tvTxPaymentId.setText("-");
}
String notes = sendListener.getNotes();
if ((notes != null) && (!notes.isEmpty())) {
tvTxNotes.setText(sendListener.getNotes());
UserNotes notes = sendListener.getTxData().getUserNotes();
if ((notes != null) && (!notes.note.isEmpty())) {
tvTxNotes.setText(notes.note);
} else {
tvTxNotes.setText("-");
}
@ -296,8 +314,10 @@ public class SendConfirmWizardFragment extends SendWizardFragment {
passwordDialog.show();
}
// creates a pending transaction and calls us back with transactionCreated()
// or createTransactionFailed()
void prepareSend(TxData txData) {
getActivityCallback().onPrepareSend(txData);
getActivityCallback().onPrepareSend(null, txData);
}
SendFragment.Listener getActivityCallback() {

@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.m2049r.xmrwallet;
package com.m2049r.xmrwallet.fragment.send;
import android.content.Context;
import android.graphics.drawable.Drawable;
@ -22,9 +22,8 @@ import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AlertDialog;
import android.text.InputType;
import android.util.SparseArray;
import android.view.LayoutInflater;
@ -36,12 +35,16 @@ import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import com.m2049r.xmrwallet.OnBackPressedListener;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.BarcodeData;
import com.m2049r.xmrwallet.data.PendingTx;
import com.m2049r.xmrwallet.data.TxData;
import com.m2049r.xmrwallet.data.TxDataBtc;
import com.m2049r.xmrwallet.layout.SpendViewPager;
import com.m2049r.xmrwallet.model.PendingTransaction;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.UserNotes;
import com.m2049r.xmrwallet.widget.DotBar;
import com.m2049r.xmrwallet.widget.Toolbar;
@ -62,11 +65,11 @@ public class SendFragment extends Fragment
public interface Listener {
long getTotalFunds();
void onPrepareSend(TxData data);
void onPrepareSend(String tag, TxData data);
boolean verifyWalletPassword(String password);
void onSend(String notes);
void onSend(UserNotes notes);
void onDisposeRequest();
@ -231,7 +234,42 @@ public class SendFragment extends Fragment
}
}
public class SpendPagerAdapter extends FragmentPagerAdapter {
enum Mode {
XMR, BTC
}
Mode mode = Mode.XMR;
@Override
public void setMode(Mode aMode) {
if (mode != aMode) {
mode = aMode;
switch (aMode) {
case XMR:
txData = new TxData();
break;
case BTC:
txData = new TxDataBtc();
break;
default:
throw new IllegalArgumentException("Mode " + String.valueOf(aMode) + " unknown!");
}
getView().post(new Runnable() {
@Override
public void run() {
pagerAdapter.notifyDataSetChanged();
}
});
Timber.d("New Mode = " + mode.toString());
}
}
@Override
public Mode getMode() {
return mode;
}
public class SpendPagerAdapter extends FragmentStatePagerAdapter {
private static final int POS_ADDRESS = 0;
private static final int POS_AMOUNT = 1;
private static final int POS_SETTINGS = 2;
@ -257,6 +295,7 @@ public class SendFragment extends Fragment
@Override
public Object instantiateItem(ViewGroup container, int position) {
Timber.d("instantiateItem %d", position);
SendWizardFragment fragment = (SendWizardFragment) super.instantiateItem(container, position);
myFragments.put(position, new WeakReference<>(fragment));
return fragment;
@ -264,6 +303,7 @@ public class SendFragment extends Fragment
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
Timber.d("destroyItem %d", position);
myFragments.remove(position);
super.destroyItem(container, position, object);
}
@ -279,19 +319,39 @@ public class SendFragment extends Fragment
@Override
public SendWizardFragment getItem(int position) {
Timber.d("getItem(%d) CREATE", position);
switch (position) {
case POS_ADDRESS:
return SendAddressWizardFragment.newInstance(SendFragment.this);
case POS_AMOUNT:
return SendAmountWizardFragment.newInstance(SendFragment.this);
case POS_SETTINGS:
return SendSettingsWizardFragment.newInstance(SendFragment.this);
case POS_CONFIRM:
return SendConfirmWizardFragment.newInstance(SendFragment.this);
case POS_SUCCESS:
return SendSuccessWizardFragment.newInstance(SendFragment.this);
default:
throw new IllegalArgumentException("no such send position(" + position + ")");
Timber.d("Mode=" + mode.toString());
if (mode == Mode.XMR) {
switch (position) {
case POS_ADDRESS:
return SendAddressWizardFragment.newInstance(SendFragment.this);
case POS_AMOUNT:
return SendAmountWizardFragment.newInstance(SendFragment.this);
case POS_SETTINGS:
return SendSettingsWizardFragment.newInstance(SendFragment.this);
case POS_CONFIRM:
return SendConfirmWizardFragment.newInstance(SendFragment.this);
case POS_SUCCESS:
return SendSuccessWizardFragment.newInstance(SendFragment.this);
default:
throw new IllegalArgumentException("no such send position(" + position + ")");
}
} else if (mode == Mode.BTC) {
switch (position) {
case POS_ADDRESS:
return SendAddressWizardFragment.newInstance(SendFragment.this);
case POS_AMOUNT:
return SendBtcAmountWizardFragment.newInstance(SendFragment.this);
case POS_SETTINGS:
return SendSettingsWizardFragment.newInstance(SendFragment.this);
case POS_CONFIRM:
return SendBtcConfirmWizardFragment.newInstance(SendFragment.this);
case POS_SUCCESS:
return SendBtcSuccessWizardFragment.newInstance(SendFragment.this);
default:
throw new IllegalArgumentException("no such send position(" + position + ")");
}
} else {
throw new IllegalStateException("Unknown mode!");
}
}
@ -314,24 +374,25 @@ public class SendFragment extends Fragment
return null;
}
}
@Override
public int getItemPosition(Object object) {
Timber.d("getItemPosition %s", String.valueOf(object));
if ((object instanceof SendAddressWizardFragment) || (object instanceof SendSettingsWizardFragment)) {
return POSITION_UNCHANGED;
} else {
return POSITION_NONE;
}
}
}
@Override
public TxData getTxData() {
return new TxData(sendAddress, sendPaymentId, sendAmount, sendMixin, sendPriority);
return txData;
}
@Override
public String getNotes() {
return sendNotes;
}
private TxData txData = new TxData();
private String sendAddress;
private String sendPaymentId;
private long sendAmount;
private PendingTransaction.Priority sendPriority;
private int sendMixin;
private String sendNotes;
private BarcodeData barcodeData;
// Listeners
@ -347,36 +408,6 @@ public class SendFragment extends Fragment
return data;
}
@Override
public void setAddress(final String address) {
sendAddress = address;
}
@Override
public void setPaymentId(final String paymentId) {
sendPaymentId = paymentId;
}
@Override
public void setAmount(final long amount) {
sendAmount = amount;
}
@Override
public void setPriority(final PendingTransaction.Priority priority) {
sendPriority = priority;
}
@Override
public void setMixin(final int mixin) {
sendMixin = mixin;
}
@Override
public void setNotes(final String notes) {
sendNotes = notes;
}
boolean isComitted() {
return committedTx != null;
}
@ -391,9 +422,9 @@ public class SendFragment extends Fragment
@Override
public void commitTransaction() {
Timber.d("REALLY SEND A %s", getNotes());
Timber.d("REALLY SEND");
disableNavigation(); // committed - disable all navigation
activityCallback.onSend(getNotes());
activityCallback.onSend(txData.getUserNotes());
committedTx = pendingTx;
}
@ -418,12 +449,11 @@ public class SendFragment extends Fragment
// callbacks from send service
public void onTransactionCreated(PendingTransaction pendingTransaction) {
//public void onTransactionCreated(TestTransaction pendingTransaction) {
final SendConfirmWizardFragment confirmFragment = getConfirmFragment();
if (confirmFragment != null) {
public void onTransactionCreated(final String txTag, final PendingTransaction pendingTransaction) {
final SendConfirm confirm = getSendConfirm();
if (confirm != null) {
pendingTx = new PendingTx(pendingTransaction);
confirmFragment.transactionCreated(pendingTransaction);
confirm.transactionCreated(txTag, pendingTransaction);
} else {
// not in confirm fragment => dispose & move on
disposeTransaction();
@ -443,23 +473,16 @@ public class SendFragment extends Fragment
}
public void onCreateTransactionFailed(String errorText) {
final SendConfirmWizardFragment confirmFragment = getConfirmFragment();
if (confirmFragment != null) {
confirmFragment.hideProgress();
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setCancelable(true).
setTitle(getString(R.string.send_create_tx_error_title)).
setMessage(errorText).
create().
show();
final SendConfirm confirm = getSendConfirm();
if (confirm != null) {
confirm.createTransactionFailed(errorText);
}
}
SendConfirmWizardFragment getConfirmFragment() {
SendConfirm getSendConfirm() {
final SendWizardFragment fragment = pagerAdapter.getFragment(SpendPagerAdapter.POS_CONFIRM);
if (fragment instanceof SendConfirmWizardFragment) {
return (SendConfirmWizardFragment) fragment;
if (fragment instanceof SendConfirm) {
return (SendConfirm) fragment;
} else {
return null;
}
@ -469,8 +492,8 @@ public class SendFragment extends Fragment
Timber.d("txid=%s", txId);
pagerAdapter.addSuccess();
Timber.d("numPages=%d", spendViewPager.getAdapter().getCount());
spendViewPager.setCurrentItem(SpendPagerAdapter.POS_SUCCESS);
activityCallback.setToolbarButton(Toolbar.BUTTON_NONE);
spendViewPager.setCurrentItem(SpendPagerAdapter.POS_SUCCESS);
}
public void onSendTransactionFailed(final String error) {
@ -478,7 +501,7 @@ public class SendFragment extends Fragment
committedTx = null;
Toast.makeText(getContext(), getString(R.string.status_transaction_failed, error), Toast.LENGTH_SHORT).show();
enableNavigation();
final SendConfirmWizardFragment fragment = getConfirmFragment();
final SendConfirm fragment = getSendConfirm();
if (fragment != null) {
fragment.sendFailed();
}

@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.m2049r.xmrwallet;
package com.m2049r.xmrwallet.fragment.send;
import android.os.Bundle;
import android.text.InputType;
@ -27,8 +27,11 @@ import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.TxData;
import com.m2049r.xmrwallet.model.PendingTransaction;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.UserNotes;
import timber.log.Timber;
@ -48,12 +51,7 @@ public class SendSettingsWizardFragment extends SendWizardFragment {
}
interface Listener {
void setPriority(final PendingTransaction.Priority priority);
void setMixin(final int mixin);
void setNotes(final String notes);
TxData getTxData();
}
final static int Mixins[] = {4, 7, 12, 25}; // must match the layout XML
@ -103,13 +101,10 @@ public class SendSettingsWizardFragment extends SendWizardFragment {
@Override
public boolean onValidateFields() {
if (sendListener != null) {
int mixin = Mixins[sMixin.getSelectedItemPosition()];
int priorityIndex = sPriority.getSelectedItemPosition();
PendingTransaction.Priority priority = Priorities[priorityIndex];
sendListener.setPriority(priority);
sendListener.setMixin(mixin);
String notes = etNotes.getText().toString();
sendListener.setNotes(notes);
TxData txData = sendListener.getTxData();
txData.setPriority(Priorities[sPriority.getSelectedItemPosition()]);
txData.setMixin(Mixins[sMixin.getSelectedItemPosition()]);
txData.setUserNotes(new UserNotes(etNotes.getText().toString()));
}
return true;
}

@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.m2049r.xmrwallet;
package com.m2049r.xmrwallet.fragment.send;
import android.os.Bundle;
import android.view.LayoutInflater;
@ -24,10 +24,10 @@ import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.PendingTx;
import com.m2049r.xmrwallet.data.TxData;
import com.m2049r.xmrwallet.util.Helper;
import timber.log.Timber;
@ -47,23 +47,21 @@ public class SendSuccessWizardFragment extends SendWizardFragment {
}
interface Listener {
String getNotes();
TxData getTxData();
PendingTx getCommittedTx();
void enableDone();
SendFragment.Mode getMode();
}
ImageButton bCopyAddress;
ImageButton bCopyTxId;
private TextView tvTxId;
private TextView tvTxAddress;
private TextView tvTxPaymentId;
private TextView tvTxNotes;
private TextView tvTxAmount;
private TextView tvTxFee;
private TextView tvTxTotal;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
@ -74,21 +72,20 @@ public class SendSuccessWizardFragment extends SendWizardFragment {
View view = inflater.inflate(
R.layout.fragment_send_success, container, false);
bCopyAddress = (ImageButton) view.findViewById(R.id.bCopyAddress);
bCopyAddress.setOnClickListener(new View.OnClickListener() {
bCopyTxId = (ImageButton) view.findViewById(R.id.bCopyTxId);
bCopyTxId.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
copyAddress();
Helper.clipBoardCopy(getActivity(), getString(R.string.label_send_txid), tvTxId.getText().toString());
Toast.makeText(getActivity(), getString(R.string.message_copy_address), Toast.LENGTH_SHORT).show();
}
});
tvTxId = (TextView) view.findViewById(R.id.tvTxId);
tvTxAddress = (TextView) view.findViewById(R.id.tvTxAddress);
tvTxPaymentId = (TextView) view.findViewById(R.id.tvTxPaymentId);
tvTxNotes = (TextView) view.findViewById(R.id.tvTxNotes);
tvTxAmount = ((TextView) view.findViewById(R.id.tvTxAmount));
tvTxFee = (TextView) view.findViewById(R.id.tvTxFee);
tvTxTotal = (TextView) view.findViewById(R.id.tvTxTotal);
return view;
}
@ -117,30 +114,15 @@ public class SendSuccessWizardFragment extends SendWizardFragment {
} else {
tvTxPaymentId.setText("-");
}
String notes = sendListener.getNotes();
if ((notes != null) && (!notes.isEmpty())) {
tvTxNotes.setText(sendListener.getNotes());
} else {
tvTxNotes.setText("-");
}
final PendingTx committedTx = sendListener.getCommittedTx();
if (committedTx != null) {
tvTxId.setText(committedTx.txId);
bCopyAddress.setEnabled(true);
bCopyAddress.setImageResource(R.drawable.ic_content_copy_black_24dp);
tvTxAmount.setText(Wallet.getDisplayAmount(committedTx.amount));
tvTxFee.setText(Wallet.getDisplayAmount(committedTx.fee));
//tvTxDust.setText(Wallet.getDisplayAmount(pendingTransaction.getDust()));
tvTxTotal.setText(Wallet.getDisplayAmount(
committedTx.fee + committedTx.amount));
bCopyTxId.setEnabled(true);
bCopyTxId.setImageResource(R.drawable.ic_content_copy_black_24dp);
tvTxAmount.setText(getString(R.string.send_amount, Helper.getDisplayAmount(committedTx.amount)));
tvTxFee.setText(getString(R.string.send_fee, Helper.getDisplayAmount(committedTx.fee)));
}
sendListener.enableDone();
}
void copyAddress() {
Helper.clipBoardCopy(getActivity(), getString(R.string.label_send_txid), tvTxId.getText().toString());
Toast.makeText(getActivity(), getString(R.string.message_copy_address), Toast.LENGTH_SHORT).show();
}
}

@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.m2049r.xmrwallet;
package com.m2049r.xmrwallet.fragment.send;
import android.support.v4.app.Fragment;

@ -21,7 +21,7 @@ import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
import com.m2049r.xmrwallet.SendFragment;
import com.m2049r.xmrwallet.fragment.send.SendFragment;
public class SpendViewPager extends ViewPager {

@ -22,11 +22,13 @@ import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.model.TransactionInfo;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.UserNotes;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@ -101,6 +103,7 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
}
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
final ImageView ivTxType;
final TextView tvAmount;
final TextView tvFee;
final TextView tvPaymentId;
@ -109,10 +112,11 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
ViewHolder(View itemView) {
super(itemView);
this.tvAmount = (TextView) itemView.findViewById(R.id.tx_amount);
this.tvFee = (TextView) itemView.findViewById(R.id.tx_fee);
this.tvPaymentId = (TextView) itemView.findViewById(R.id.tx_paymentid);
this.tvDateTime = (TextView) itemView.findViewById(R.id.tx_datetime);
ivTxType = (ImageView) itemView.findViewById(R.id.ivTxType);
tvAmount = (TextView) itemView.findViewById(R.id.tx_amount);
tvFee = (TextView) itemView.findViewById(R.id.tx_fee);
tvPaymentId = (TextView) itemView.findViewById(R.id.tx_paymentid);
tvDateTime = (TextView) itemView.findViewById(R.id.tx_datetime);
}
private String getDateTime(long time) {
@ -121,7 +125,6 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
private void setTxColour(int clr) {
tvAmount.setTextColor(clr);
tvFee.setTextColor(clr);
}
void bind(int position) {
@ -134,20 +137,23 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
String displayAmount = Helper.getDisplayAmount(realAmount, Helper.DISPLAY_DIGITS_INFO);
if (infoItem.direction == TransactionInfo.Direction.Direction_Out) {
this.tvAmount.setText(context.getString(R.string.tx_list_amount_negative, displayAmount));
tvAmount.setText(context.getString(R.string.tx_list_amount_negative, displayAmount));
} else {
this.tvAmount.setText(context.getString(R.string.tx_list_amount_positive, displayAmount));
tvAmount.setText(context.getString(R.string.tx_list_amount_positive, displayAmount));
}
if ((infoItem.fee > 0)) {
String fee = Helper.getDisplayAmount(infoItem.fee, 5);
this.tvFee.setText(context.getString(R.string.tx_list_fee, fee));
tvFee.setText(context.getString(R.string.tx_list_fee, fee));
tvFee.setVisibility(View.VISIBLE);
} else {
this.tvFee.setText("");
tvFee.setText("");
tvFee.setVisibility(View.GONE);
}
if (infoItem.isFailed) {
this.tvAmount.setText(context.getString(R.string.tx_list_amount_failed, displayAmount));
this.tvFee.setText(context.getString(R.string.tx_list_failed_text));
tvFee.setVisibility(View.VISIBLE);
setTxColour(failedColour);
} else if (infoItem.isPending) {
setTxColour(pendingColour);
@ -156,11 +162,20 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
} else {
setTxColour(outboundColour);
}
if ((infoItem.notes == null) || (infoItem.notes.isEmpty())) {
UserNotes userNotes = new UserNotes(infoItem.notes);
if (userNotes.xmrtoKey != null) {
ivTxType.setVisibility(View.VISIBLE);
} else {
ivTxType.setVisibility(View.INVISIBLE);
}
if ((userNotes.note.isEmpty())) {
this.tvPaymentId.setText(infoItem.paymentId.equals("0000000000000000") ? "" : infoItem.paymentId);
} else {
this.tvPaymentId.setText(infoItem.notes);
this.tvPaymentId.setText(userNotes.note);
}
this.tvDateTime.setText(getDateTime(infoItem.timestamp));
itemView.setOnClickListener(this);

@ -30,12 +30,12 @@ import android.os.Process;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.WalletActivity;
import com.m2049r.xmrwallet.data.TxData;
import com.m2049r.xmrwallet.model.PendingTransaction;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.model.WalletListener;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.data.TxData;
import timber.log.Timber;
@ -54,6 +54,7 @@ public class WalletService extends Service {
public static final String REQUEST_CMD_TX = "createTX";
public static final String REQUEST_CMD_TX_DATA = "data";
public static final String REQUEST_CMD_TX_TAG = "tag";
public static final String REQUEST_CMD_SWEEP = "sweepTX";
@ -209,7 +210,7 @@ public class WalletService extends Service {
void onWalletStored(boolean success);
void onTransactionCreated(PendingTransaction pendingTransaction);
void onTransactionCreated(String tag, PendingTransaction pendingTransaction);
void onTransactionSent(String txid);
@ -301,7 +302,9 @@ public class WalletService extends Service {
} else if (cmd.equals(REQUEST_CMD_TX)) {
Wallet myWallet = getWallet();
Timber.d("CREATE TX for wallet: %s", myWallet.getName());
myWallet.disposePendingTransaction(); // remove any old pending tx
TxData txData = extras.getParcelable(REQUEST_CMD_TX_DATA);
String txTag = extras.getString(REQUEST_CMD_TX_TAG);
PendingTransaction pendingTransaction = myWallet.createTransaction(txData);
PendingTransaction.Status status = pendingTransaction.getStatus();
Timber.d("transaction status %s", status);
@ -309,13 +312,15 @@ public class WalletService extends Service {
Timber.w("Create Transaction failed: %s", pendingTransaction.getErrorString());
}
if (observer != null) {
observer.onTransactionCreated(pendingTransaction);
observer.onTransactionCreated(txTag, pendingTransaction);
} else {
myWallet.disposePendingTransaction();
}
} else if (cmd.equals(REQUEST_CMD_SWEEP)) {
Wallet myWallet = getWallet();
Timber.d("SWEEP TX for wallet: %s", myWallet.getName());
myWallet.disposePendingTransaction(); // remove any old pending tx
String txTag = extras.getString(REQUEST_CMD_TX_TAG);
PendingTransaction pendingTransaction = myWallet.createSweepUnmixableTransaction();
PendingTransaction.Status status = pendingTransaction.getStatus();
Timber.d("transaction status %s", status);
@ -323,7 +328,7 @@ public class WalletService extends Service {
Timber.w("Create Transaction failed: %s", pendingTransaction.getErrorString());
}
if (observer != null) {
observer.onTransactionCreated(pendingTransaction);
observer.onTransactionCreated(txTag, pendingTransaction);
} else {
myWallet.disposePendingTransaction();
}

@ -0,0 +1,73 @@
/*
* Copyright (c) 2017 m2049r er al.
*
* 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 com.m2049r.xmrwallet.util;
// based on https://rosettacode.org/wiki/Bitcoin/address_validation#Java
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class BitcoinAddressValidator {
private static final String ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
public static boolean validate(String addrress, boolean testnet) {
if (addrress.length() < 26 || addrress.length() > 35)
return false;
byte[] decoded = decodeBase58To25Bytes(addrress);
if (decoded == null)
return false;
if (!testnet) {
if ((decoded[0] != 0x00) && (decoded[0] != 0x05)) return false;
} else {
if ((decoded[0] != 0x6f) && (decoded[0] != 0xc4)) return false;
}
byte[] hash1 = sha256(Arrays.copyOfRange(decoded, 0, 21));
byte[] hash2 = sha256(hash1);
return Arrays.equals(Arrays.copyOfRange(hash2, 0, 4), Arrays.copyOfRange(decoded, 21, 25));
}
private static byte[] decodeBase58To25Bytes(String input) {
BigInteger num = BigInteger.ZERO;
for (char t : input.toCharArray()) {
int p = ALPHABET.indexOf(t);
if (p == -1)
return null;
num = num.multiply(BigInteger.valueOf(58)).add(BigInteger.valueOf(p));
}
byte[] result = new byte[25];
byte[] numBytes = num.toByteArray();
System.arraycopy(numBytes, 0, result, result.length - numBytes.length, numBytes.length);
return result;
}
private static byte[] sha256(byte[] data) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(data);
return md.digest();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
}

@ -38,6 +38,7 @@ import android.view.inputmethod.InputMethodManager;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.model.WalletManager;
import java.io.File;
import java.io.IOException;
@ -49,6 +50,7 @@ import java.util.Locale;
import javax.net.ssl.HttpsURLConnection;
import okhttp3.HttpUrl;
import timber.log.Timber;
public class Helper {
@ -160,7 +162,7 @@ public class Helper {
}
static public String getDisplayAmount(long amount) {
return getDisplayAmount(amount, 20);
return getDisplayAmount(amount, 12);
}
static public String getDisplayAmount(long amount, int maxDecimals) {
@ -271,4 +273,11 @@ public class Helper {
return ShakeAnimation;
}
static public HttpUrl getXmrToBaseUrl() {
if ((WalletManager.getInstance() == null) || WalletManager.getInstance().isTestNet()) {
return HttpUrl.parse("https://test.xmr.to/api/v2/xmr2btc/");
} else {
return HttpUrl.parse("https://xmr.to/api/v2/xmr2btc/");
}
}
}

@ -0,0 +1,90 @@
/*
* 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 com.m2049r.xmrwallet.util;
import com.m2049r.xmrwallet.xmrto.api.QueryOrderStatus;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import timber.log.Timber;
public class UserNotes {
public String txNotes = "";
public String note = "";
public String xmrtoKey = null;
public String xmrtoAmount = null; // could be a double - but we are not doing any calculations
public String xmrtoDestination = null;
public UserNotes(final String txNotes) {
if (txNotes == null) {
return;
}
this.txNotes = txNotes;
Pattern p = Pattern.compile("^\\{(xmrto-\\w{6}),([0-9.]*)BTC,(\\w*)\\} ?(.*)");
Matcher m = p.matcher(txNotes);
if (m.find()) {
xmrtoKey = m.group(1);
xmrtoAmount = m.group(2);
xmrtoDestination = m.group(3);
note = m.group(4);
} else {
note = txNotes;
}
}
public void setNote(String newNote) {
if (newNote != null) {
note = newNote;
} else {
note = "";
}
txNotes = buildTxNote();
}
public void setXmrtoStatus(QueryOrderStatus xmrtoStatus) {
if (xmrtoStatus != null) {
xmrtoKey = xmrtoStatus.getUuid();
xmrtoAmount = String.valueOf(xmrtoStatus.getBtcAmount());
xmrtoDestination = xmrtoStatus.getBtcDestAddress();
} else {
xmrtoKey = null;
xmrtoAmount = null;
xmrtoDestination = null;
}
txNotes = buildTxNote();
}
private String buildTxNote() {
StringBuffer sb = new StringBuffer();
if (xmrtoKey != null) {
if ((xmrtoAmount == null) || (xmrtoDestination == null))
throw new IllegalArgumentException("Broken notes");
sb.append("{");
sb.append(xmrtoKey);
sb.append(",");
sb.append(xmrtoAmount);
sb.append("BTC,");
sb.append(xmrtoDestination);
sb.append("}");
if ((note != null) && (!note.isEmpty()))
sb.append(" ");
}
sb.append(note);
return sb.toString();
}
}

@ -0,0 +1,198 @@
/*
* 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.
*/
// based on https://code.tutsplus.com/tutorials/creating-compound-views-on-android--cms-22889
package com.m2049r.xmrwallet.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.util.Helper;
import timber.log.Timber;
public class ExchangeBtcTextView extends LinearLayout
implements NumberPadView.NumberPadListener {
String btcAmount = null;
String xmrAmount = null;
private boolean validate(String amount, double max, double min) {
boolean ok = true;
if (amount != null) {
try {
double x = Double.parseDouble(amount);
if ((x < min) || (x > max)) {
ok = false;
}
} catch (NumberFormatException ex) {
Timber.e(ex.getLocalizedMessage());
ok = false;
}
} else {
ok = false;
}
return ok;
}
public boolean validate(double maxBtc, double minBtc) {
Timber.d("validate(maxBtc=%f,minBtc=%f)", maxBtc, minBtc);
boolean ok = true;
if (!validate(btcAmount, maxBtc, minBtc)) {
Timber.d("btcAmount invalid %s", btcAmount);
shakeAmountField();
return false;
}
return true;
}
void shakeAmountField() {
tvAmountA.startAnimation(Helper.getShakeAnimation(getContext()));
}
void shakeExchangeField() {
tvAmountB.startAnimation(Helper.getShakeAnimation(getContext()));
}
public void setRate(double xmrBtcRate) {
this.xmrBtcRate = xmrBtcRate;
post(new Runnable() {
@Override
public void run() {
exchange();
}
});
}
public void setAmount(String btcAmount) {
this.btcAmount = btcAmount;
tvAmountA.setText(btcAmount);
xmrAmount = null;
exchange();
}
public String getAmount() {
return btcAmount;
}
TextView tvAmountA;
TextView tvAmountB;
Spinner sCurrencyA;
Spinner sCurrencyB;
public ExchangeBtcTextView(Context context) {
super(context);
initializeViews(context);
}
public ExchangeBtcTextView(Context context, AttributeSet attrs) {
super(context, attrs);
initializeViews(context);
}
public ExchangeBtcTextView(Context context,
AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
initializeViews(context);
}
/**
* Inflates the views in the layout.
*
* @param context the current context for the view.
*/
private void initializeViews(Context context) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.view_exchange_btc_text, this);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
tvAmountA = (TextView) findViewById(R.id.tvAmountA);
tvAmountB = (TextView) findViewById(R.id.tvAmountB);
sCurrencyA = (Spinner) findViewById(R.id.sCurrencyA);
sCurrencyB = (Spinner) findViewById(R.id.sCurrencyB);
ArrayAdapter<String> btcAdapter = new ArrayAdapter<String>(getContext(),
android.R.layout.simple_spinner_item,
new String[]{"BTC"});
sCurrencyA.setAdapter(btcAdapter);
sCurrencyA.setEnabled(false);
ArrayAdapter<String> xmrAdapter = new ArrayAdapter<String>(getContext(),
android.R.layout.simple_spinner_item,
new String[]{"XMR"});
sCurrencyB.setAdapter(xmrAdapter);
sCurrencyB.setEnabled(false);
}
double xmrBtcRate = 0;
public void exchange() {
btcAmount = tvAmountA.getText().toString();
if (!btcAmount.isEmpty() && (xmrBtcRate > 0)) {
double xmr = xmrBtcRate * Double.parseDouble(btcAmount);
xmrAmount = Helper.getFormattedAmount(xmr, true);
} else {
xmrAmount = "";
}
tvAmountB.setText(getResources().getString(R.string.send_amount_btc_xmr, xmrAmount));
Timber.d("%s BTC =%f> %s XMR", btcAmount, xmrBtcRate, xmrAmount);
}
// deal with attached numpad
@Override
public void onDigitPressed(final int digit) {
tvAmountA.append(String.valueOf(digit));
exchange();
}
@Override
public void onPointPressed() {
//TODO locale?
if (tvAmountA.getText().toString().indexOf('.') == -1) {
if (tvAmountA.getText().toString().isEmpty()) {
tvAmountA.append("0");
}
tvAmountA.append(".");
}
}
@Override
public void onBackSpacePressed() {
String entry = tvAmountA.getText().toString();
int length = entry.length();
if (length > 0) {
tvAmountA.setText(entry.substring(0, entry.length() - 1));
exchange();
}
}
@Override
public void onClearAll() {
tvAmountA.setText(null);
exchange();
}
}

@ -436,6 +436,9 @@ public class ExchangeTextView extends LinearLayout
public void onPointPressed() {
//TODO locale?
if (tvAmountA.getText().toString().indexOf('.') == -1) {
if (tvAmountA.getText().toString().isEmpty()) {
tvAmountA.append("0");
}
tvAmountA.append(".");
}
}

@ -0,0 +1,87 @@
/*
* 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 com.m2049r.xmrwallet.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.m2049r.xmrwallet.R;
public class SendProgressView extends LinearLayout {
public SendProgressView(Context context, AttributeSet attrs) {
super(context, attrs);
initializeViews(context);
}
public SendProgressView(Context context,
AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
initializeViews(context);
}
private void initializeViews(Context context) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.view_send_progress, this);
}
View pbProgress;
View llMessage;
TextView tvCode;
TextView tvMessage;
TextView tvSolution;
@Override
protected void onFinishInflate() {
super.onFinishInflate();
pbProgress = findViewById(R.id.pbProgress);
llMessage = findViewById(R.id.llMessage);
tvCode = (TextView) findViewById(R.id.tvCode);
tvMessage = (TextView) findViewById(R.id.tvMessage);
tvSolution = (TextView) findViewById(R.id.tvSolution);
}
public void showProgress(String progressText) {
pbProgress.setVisibility(VISIBLE);
tvCode.setVisibility(INVISIBLE);
tvMessage.setText(progressText);
llMessage.setVisibility(VISIBLE);
tvSolution.setVisibility(INVISIBLE);
}
public void hideProgress() {
pbProgress.setVisibility(INVISIBLE);
llMessage.setVisibility(INVISIBLE);
}
public void showMessage(String code, String message, String solution) {
tvCode.setText(code);
tvMessage.setText(message);
tvSolution.setText(solution);
tvCode.setVisibility(VISIBLE);
llMessage.setVisibility(VISIBLE);
tvSolution.setVisibility(VISIBLE);
pbProgress.setVisibility(INVISIBLE);
}
}

@ -0,0 +1,107 @@
/*
* Copyright (c) 2017 m2049r et al.
*
* 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 com.m2049r.xmrwallet.xmrto;
import org.json.JSONException;
import org.json.JSONObject;
public class XmrToError {
private final Error errorId;
private final String errorMsg;
public enum Error {
XMRTO_ERROR_UNDEF,
XMRTO_ERROR_001, // (503) internal services not available try again later
XMRTO_ERROR_002, // (400) malformed bitcoin address check address validity
XMRTO_ERROR_003, // (400) invalid bitcoin amount check amount data type
XMRTO_ERROR_004, // (400) bitcoin amount out of bounds check min and max amount
XMRTO_ERROR_005, // (400) unexpected validation error contact support
XMRTO_ERROR_006, // (404) requested order not found check order UUID
XMRTO_ERROR_007, // (503) third party service not available try again later
XMRTO_ERROR_008, // (503) insufficient funds available try again later
XMRTO_ERROR_009 // (400) invalid request check request parameters
}
public boolean isRetryable() {
return (errorId == Error.XMRTO_ERROR_001)
|| (errorId == Error.XMRTO_ERROR_007)
|| (errorId == Error.XMRTO_ERROR_008);
}
public static String getErrorIdString(Error anError) {
return anError.toString().replace('_', '-');
}
// mostly for testing
public XmrToError(String errorId, String message) {
Error error;
try {
error = Error.valueOf(errorId.replace('-', '_'));
} catch (IllegalArgumentException ex) {
error = Error.XMRTO_ERROR_UNDEF;
}
this.errorId = error;
this.errorMsg = message;
}
public XmrToError(final JSONObject jsonObject) throws JSONException {
final String errorId = jsonObject.getString("error");
Error error;
try {
error = Error.valueOf(errorId.replace('-', '_'));
} catch (IllegalArgumentException ex) {
error = Error.XMRTO_ERROR_UNDEF;
}
this.errorId = error;
this.errorMsg = jsonObject.getString("error_msg");
}
public Error getErrorId() {
return errorId;
}
public String getErrorMsg() {
return errorMsg;
}
@Override
public String toString() {
return getErrorIdString(getErrorId()) + ": " + getErrorMsg();
}
/* Errors from Query Parameters
HTTP code XMR.TO error code Error message/reason Solution
503 XMRTO-ERROR-001 internal services not available try again later
503 XMRTO-ERROR-007 third party service not available try again later
503 XMRTO-ERROR-008 insufficient funds available try again later
*/
/* Errors from Create Order
HTTP code XMR.TO error code Error message/reason Solution
503 XMRTO-ERROR-001 internal services not available try again later
400 XMRTO-ERROR-002 malformed bitcoin address check address validity
400 XMRTO-ERROR-003 invalid bitcoin amount check amount data type
503 XMRTO-ERROR-004 bitcoin amount out of bounds check min and max amount
400 XMRTO-ERROR-005 unexpected validation error contact support
*/
/* Errors from Query Order
HTTP code XMR.TO error code Error message/reason Solution
400 XMRTO-ERROR-009 invalid request check request parameters
404 XMRTO-ERROR-006 requested order not found check order UUID
*/
}

@ -0,0 +1,42 @@
/*
* Copyright (c) 2017 m2049r et al.
*
* 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 com.m2049r.xmrwallet.xmrto;
public class XmrToException extends Exception {
private int code;
private XmrToError error;
public XmrToException(final int code) {
super();
this.code = code;
this.error = null;
}
public XmrToException(final int code, final XmrToError error) {
super();
this.code = code;
this.error = error;
}
public int getCode() {
return code;
}
public XmrToError getError() {
return error;
}
}

@ -0,0 +1,28 @@
/*
* Copyright (c) 2017 m2049r et al.
*
* 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 com.m2049r.xmrwallet.xmrto.api;
public interface CreateOrder {
Double getBtcAmount();
String getBtcDestAddress();
String getState();
String getUuid();
}

@ -0,0 +1,31 @@
/*
* Copyright (c) 2017 m2049r et al.
*
* 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 com.m2049r.xmrwallet.xmrto.api;
public interface QueryOrderParameters {
double getLowerLimit(); // "lower_limit": <lower_order_limit_in_btc_as_float>,
double getPrice(); // "price": <price_of_1_btc_in_xmr_as_offered_by_service_as_float>,
double getUpperLimit(); // "upper_limit": <upper_order_limit_in_btc_as_float>,
boolean isZeroConfEnabled(); // "zero_conf_enabled": <true_if_zero_conf_is_enabled_as_boolean>,
double getZeroConfMaxAmount(); // "zero_conf_max_amount": <up_to_this_amount_zero_conf_is_possible_as_float>
}

@ -0,0 +1,87 @@
/*
* Copyright (c) 2017 m2049r et al.
*
* 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 com.m2049r.xmrwallet.xmrto.api;
import java.util.Date;
public interface QueryOrderStatus {
enum State {
UNDEF,
TO_BE_CREATED, // order creation pending
UNPAID, // waiting for Monero payment by user
UNDERPAID, // order partially paid
PAID_UNCONFIRMED, // order paid, waiting for enough confirmations
PAID, // order paid and sufficiently confirmed
BTC_SENT, // bitcoin payment sent
TIMED_OUT, // order timed out before payment was complete
NOT_FOUND // order wasnt found in system (never existed or was purged)
}
boolean isCreated();
boolean isTerminal();
boolean isPending();
boolean isPaid();
boolean isSent();
boolean isError();
State getState(); // "state": "<order_state_as_string>",
double getBtcAmount(); // "btc_amount": <requested_amount_in_btc_as_float>,
String getBtcDestAddress(); // "btc_dest_address": "<requested_destination_address_as_string>",
String getUuid(); // "uuid": "<unique_order_identifier_as_12_character_string>"
int getBtcNumConfirmations(); // "btc_num_confirmations": <btc_num_confirmations_as_integer>,
int getBtcNumConfirmationsBeforePurge(); // "btc_num_confirmations_before_purge": <btc_num_confirmations_before_purge_as_integer>,
String getBtcTransactionId(); // "btc_transaction_id": "<btc_transaction_id_as_string>",
Date getCreatedAt(); // "created_at": "<timestamp_as_string>",
Date getExpiresAt(); // "expires_at": "<timestamp_as_string>",
int getSecondsTillTimeout(); // "seconds_till_timeout": <seconds_till_timeout_as_integer>,
double getXmrAmountTotal(); // "xmr_amount_total": <amount_in_xmr_for_this_order_as_float>,
double getXmrAmountRemaining(); // "xmr_amount_remaining": <amount_in_xmr_that_the_user_must_still_send_as_float>,
int getXmrNumConfirmationsRemaining(); // "xmr_num_confirmations_remaining": <num_xmr_confirmations_remaining_before_bitcoins_will_be_sent_as_integer>,
double getXmrPriceBtc(); // "xmr_price_btc": <price_of_1_btc_in_xmr_as_offered_by_service_as_float>,
String getXmrReceivingAddress(); // "xmr_receiving_address": "xmr_old_style_address_user_can_send_funds_to_as_string",
String getXmrReceivingIntegratedAddress(); // "xmr_receiving_integrated_address": "xmr_integrated_address_user_needs_to_send_funds_to_as_string",
int getXmrRecommendedMixin(); // "xmr_recommended_mixin": <xmr_recommended_mixin_as_integer>,
@Deprecated
double getXmrRequiredAmount(); // "xmr_required_amount": <xmr_amount_user_needs_to_send_as_float>,
String getXmrRequiredPaymentIdLong(); // "xmr_required_payment_id_long": "xmr_payment_id_user_needs_to_include_when_using_old_stlye_address_as_string"
String getXmrRequiredPaymentIdShort(); // "xmr_required_payment_id_short": "xmr_payment_id_included_in_integrated_address_as_string"
}

@ -0,0 +1,46 @@
/*
* Copyright (c) 2017 m2049r et al.
*
* 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 com.m2049r.xmrwallet.xmrto.api;
import android.support.annotation.NonNull;
public interface XmrToApi {
/**
* Queries the order parameter.
*
* @param callback the callback with the OrderParameter object
*/
void queryOrderParameters(@NonNull final XmrToCallback<QueryOrderParameters> callback);
/**
* Creates an order
*
* @param amount the desired amount
* @param address the target bitcoin address
*/
void createOrder(final double amount, @NonNull final String address, @NonNull final XmrToCallback<CreateOrder> callback);
/**
* Queries the order status for given current order
*
* @param uuid the order uuid
* @param callback the callback with the OrderStatus object
*/
void queryOrderStatus(@NonNull final String uuid, @NonNull final XmrToCallback<QueryOrderStatus> callback);
}

@ -0,0 +1,24 @@
/*
* Copyright (c) 2017 m2049r et al.
*
* 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 com.m2049r.xmrwallet.xmrto.api;
public interface XmrToCallback<T> {
void onSuccess(T t);
void onError(Exception ex);
}

@ -0,0 +1,89 @@
/*
* Copyright (c) 2017 m2049r et al.
*
* 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 com.m2049r.xmrwallet.xmrto.network;
import android.support.annotation.NonNull;
import com.m2049r.xmrwallet.xmrto.api.XmrToCallback;
import com.m2049r.xmrwallet.xmrto.api.CreateOrder;
import org.json.JSONException;
import org.json.JSONObject;
class CreateOrderImpl implements CreateOrder {
private final String state;
private final double btcAmount;
private final String btcDestAddress;
private final String uuid;
public Double getBtcAmount() {
return btcAmount;
}
public String getBtcDestAddress() {
return btcDestAddress;
}
public String getUuid() {
return uuid;
}
public String getState() {
return state;
}
CreateOrderImpl(final JSONObject jsonObject) throws JSONException {
this.state = jsonObject.getString("state");
this.btcAmount = jsonObject.getDouble("btc_amount");
this.btcDestAddress = jsonObject.getString("btc_dest_address");
this.uuid = jsonObject.getString("uuid");
}
public static void call(@NonNull final XmrToApiCall api, final double amount, @NonNull final String address,
@NonNull final XmrToCallback<CreateOrder> callback) {
try {
final JSONObject request = createRequest(amount, address);
api.call("order_create", request, new NetworkCallback() {
@Override
public void onSuccess(JSONObject jsonObject) {
try {
callback.onSuccess(new CreateOrderImpl(jsonObject));
} catch (JSONException ex) {
callback.onError(ex);
}
}
@Override
public void onError(Exception ex) {
callback.onError(ex);
}
});
} catch (JSONException ex) {
callback.onError(ex);
}
}
static JSONObject createRequest(final double amount, final String address) throws JSONException {
final JSONObject jsonObject = new JSONObject();
jsonObject.put("btc_amount", amount);
jsonObject.put("btc_dest_address", address);
return jsonObject;
}
}

@ -0,0 +1,27 @@
/*
* Copyright (c) 2017 m2049r et al.
*
* 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 com.m2049r.xmrwallet.xmrto.network;
import org.json.JSONObject;
interface NetworkCallback {
void onSuccess(JSONObject jsonObject);
void onError(Exception ex);
}

@ -0,0 +1,82 @@
/*
* Copyright (c) 2017 m2049r et al.
*
* 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 com.m2049r.xmrwallet.xmrto.network;
import android.support.annotation.NonNull;
import com.m2049r.xmrwallet.xmrto.api.XmrToCallback;
import com.m2049r.xmrwallet.xmrto.api.QueryOrderParameters;
import org.json.JSONException;
import org.json.JSONObject;
class QueryOrderParametersImpl implements QueryOrderParameters {
private double lowerLimit; // "lower_limit": <lower_order_limit_in_btc_as_float>,
private double price; // "price": <price_of_1_btc_in_xmr_as_offered_by_service_as_float>,
private double upperLimit; // "upper_limit": <upper_order_limit_in_btc_as_float>,
private boolean isZeroConfEnabled; // "zero_conf_enabled": <true_if_zero_conf_is_enabled_as_boolean>,
private double zeroConfMaxAmount; // "zero_conf_max_amount": <up_to_this_amount_zero_conf_is_possible_as_float>
public double getLowerLimit() {
return lowerLimit;
}
public double getPrice() {
return price;
}
public double getUpperLimit() {
return upperLimit;
}
public boolean isZeroConfEnabled() {
return isZeroConfEnabled;
}
public double getZeroConfMaxAmount() {
return zeroConfMaxAmount;
}
QueryOrderParametersImpl(final JSONObject jsonObject) throws JSONException {
lowerLimit = jsonObject.getDouble("lower_limit");
price = jsonObject.getDouble("price");
upperLimit = jsonObject.getDouble("upper_limit");
isZeroConfEnabled = jsonObject.getBoolean("zero_conf_enabled");
zeroConfMaxAmount = jsonObject.getDouble("zero_conf_max_amount");
}
public static void call(@NonNull final XmrToApiCall api,
@NonNull final XmrToCallback<QueryOrderParameters> callback) {
api.call("order_parameter_query", new NetworkCallback() {
@Override
public void onSuccess(JSONObject jsonObject) {
try {
callback.onSuccess(new QueryOrderParametersImpl(jsonObject));
} catch (JSONException ex) {
callback.onError(ex);
}
}
@Override
public void onError(Exception ex) {
callback.onError(ex);
}
});
}
//{"zero_conf_enabled":true,"price":0.015537,"upper_limit":20.0,"lower_limit":0.001,"zero_conf_max_amount":0.1}
}

@ -0,0 +1,255 @@
/*
* Copyright (c) 2017 m2049r et al.
*
* 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 com.m2049r.xmrwallet.xmrto.network;
import android.support.annotation.NonNull;
import com.m2049r.xmrwallet.xmrto.api.XmrToCallback;
import com.m2049r.xmrwallet.xmrto.api.QueryOrderStatus;
import org.json.JSONException;
import org.json.JSONObject;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import timber.log.Timber;
class QueryOrderStatusImpl implements QueryOrderStatus {
public static final SimpleDateFormat DATETIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
public static Date parseDate(String dateString) throws ParseException {
return DATETIME_FORMATTER.parse(dateString.replaceAll("Z$", "+0000"));
}
private QueryOrderStatus.State state; // "state": "<order_state_as_string>",
private double btcAmount; // "btc_amount": <requested_amount_in_btc_as_float>,
private String btcDestAddress; // "btc_dest_address": "<requested_destination_address_as_string>",
private String uuid; // "uuid": "<unique_order_identifier_as_12_character_string>"
// the following are only returned if the state is "after" TO_BE_CREATED
private int btcNumConfirmations; // "btc_num_confirmations": <btc_num_confirmations_as_integer>,
private int btcNumConfirmationsBeforePurge; // "btc_num_confirmations_before_purge": <btc_num_confirmations_before_purge_as_integer>,
private String btcTransactionId; // "btc_transaction_id": "<btc_transaction_id_as_string>",
private Date createdAt; // "created_at": "<timestamp_as_string>",
private Date expiresAt; // "expires_at": "<timestamp_as_string>",
private int secondsTillTimeout; // "seconds_till_timeout": <seconds_till_timeout_as_integer>,
private double xmrAmountTotal; // "xmr_amount_total": <amount_in_xmr_for_this_order_as_float>,
private double xmrAmountRemaining; // "xmr_amount_remaining": <amount_in_xmr_that_the_user_must_still_send_as_float>,
private int xmrNumConfirmationsRemaining; // "xmr_num_confirmations_remaining": <num_xmr_confirmations_remaining_before_bitcoins_will_be_sent_as_integer>,
private double xmrPriceBtc; // "xmr_price_btc": <price_of_1_btc_in_xmr_as_offered_by_service_as_float>,
private String xmrReceivingAddress; // "xmr_receiving_address": "xmr_old_style_address_user_can_send_funds_to_as_string",
private String xmrReceivingIntegratedAddress; // "xmr_receiving_integrated_address": "xmr_integrated_address_user_needs_to_send_funds_to_as_string",
private int xmrRecommendedMixin; // "xmr_recommended_mixin": <xmr_recommended_mixin_as_integer>,
private double xmrRequiredAmount; // "xmr_required_amount": <xmr_amount_user_needs_to_send_as_float>,
private String xmrRequiredPaymentIdLong; // "xmr_required_payment_id_long": "xmr_payment_id_user_needs_to_include_when_using_old_stlye_address_as_string"
private String xmrRequiredPaymentIdShort; // "xmr_required_payment_id_short": "xmr_payment_id_included_in_integrated_address_as_string"
public QueryOrderStatus.State getState() {
return state;
}
public double getBtcAmount() {
return btcAmount;
}
public String getBtcDestAddress() {
return btcDestAddress;
}
public String getUuid() {
return uuid;
}
public int getBtcNumConfirmations() {
return btcNumConfirmations;
}
public int getBtcNumConfirmationsBeforePurge() {
return btcNumConfirmationsBeforePurge;
}
public String getBtcTransactionId() {
return btcTransactionId;
}
public Date getCreatedAt() {
return createdAt;
}
public Date getExpiresAt() {
return expiresAt;
}
public int getSecondsTillTimeout() {
return secondsTillTimeout;
}
public double getXmrAmountTotal() {
return xmrAmountTotal;
}
public double getXmrAmountRemaining() {
return xmrAmountRemaining;
}
public int getXmrNumConfirmationsRemaining() {
return xmrNumConfirmationsRemaining;
}
public double getXmrPriceBtc() {
return xmrPriceBtc;
}
public String getXmrReceivingAddress() {
return xmrReceivingAddress;
}
public String getXmrReceivingIntegratedAddress() {
return xmrReceivingIntegratedAddress;
}
public int getXmrRecommendedMixin() {
return xmrRecommendedMixin;
}
public double getXmrRequiredAmount() {
return xmrRequiredAmount;
}
public String getXmrRequiredPaymentIdLong() {
return xmrRequiredPaymentIdLong;
}
public String getXmrRequiredPaymentIdShort() {
return xmrRequiredPaymentIdShort;
}
public boolean isCreated() {
return (state.equals(State.UNPAID) ||
state.equals(State.BTC_SENT) ||
state.equals(State.PAID) ||
state.equals(State.PAID_UNCONFIRMED) ||
state.equals(State.UNDERPAID));
}
public boolean isTerminal() {
return (state.equals(State.BTC_SENT) || isError());
}
public boolean isError() {
return (state.equals(State.UNDEF) ||
state.equals(State.NOT_FOUND) ||
state.equals(State.TIMED_OUT));
}
public boolean isPending() {
return (state.equals(State.TO_BE_CREATED) ||
state.equals(State.UNPAID) ||
state.equals(State.UNDERPAID) ||
state.equals(State.PAID_UNCONFIRMED) ||
state.equals(State.PAID));
}
public boolean isPaid() {
return (state.equals(State.PAID_UNCONFIRMED) ||
state.equals(State.PAID));
}
public boolean isSent() {
return state.equals(State.BTC_SENT);
}
QueryOrderStatusImpl(final JSONObject jsonObject) throws JSONException {
Timber.d(jsonObject.toString(4));
String stateName = jsonObject.getString("state"); // "state": "<order_state_as_string>",
State state;
try {
state = State.valueOf(stateName);
} catch (IllegalArgumentException ex) {
state = State.UNDEF; //TODO: throws IllegalArgumentException?
}
this.state = state;
btcAmount = jsonObject.getDouble("btc_amount"); // "btc_amount": <requested_amount_in_btc_as_float>,
btcDestAddress = jsonObject.getString("btc_dest_address"); // "btc_dest_address": "<requested_destination_address_as_string>",
uuid = jsonObject.getString("uuid"); // "uuid": "<unique_order_identifier_as_12_character_string>"
if (isCreated()) {
btcNumConfirmations = jsonObject.getInt("btc_num_confirmations"); // "btc_num_confirmations": <btc_num_confirmations_as_integer>,
btcNumConfirmationsBeforePurge = jsonObject.getInt("btc_num_confirmations_before_purge"); // "btc_num_confirmations_before_purge": <btc_num_confirmations_before_purge_as_integer>,
btcTransactionId = jsonObject.getString("btc_transaction_id"); // "btc_transaction_id": "<btc_transaction_id_as_string>",
try {
String created = jsonObject.getString("created_at"); // "created_at": "<timestamp_as_string>",
createdAt = parseDate(created);
String expires = jsonObject.getString("expires_at"); // "expires_at": "<timestamp_as_string>",
expiresAt = parseDate(expires);
} catch (ParseException ex) {
throw new JSONException(ex.getLocalizedMessage());
}
secondsTillTimeout = jsonObject.getInt("seconds_till_timeout"); // "seconds_till_timeout": <seconds_till_timeout_as_integer>,
xmrAmountTotal = jsonObject.getDouble("xmr_amount_total"); // "xmr_amount_total": <amount_in_xmr_for_this_order_as_float>,
xmrAmountRemaining = jsonObject.getDouble("xmr_amount_remaining"); // "xmr_amount_remaining": <amount_in_xmr_that_the_user_must_still_send_as_float>,
xmrNumConfirmationsRemaining = jsonObject.getInt("xmr_num_confirmations_remaining"); // "xmr_num_confirmations_remaining": <num_xmr_confirmations_remaining_before_bitcoins_will_be_sent_as_integer>,
xmrPriceBtc = jsonObject.getDouble("xmr_price_btc"); // "xmr_price_btc": <price_of_1_btc_in_xmr_as_offered_by_service_as_float>,
xmrReceivingAddress = jsonObject.getString("xmr_receiving_address"); // "xmr_receiving_address": "xmr_old_style_address_user_can_send_funds_to_as_string",
xmrReceivingIntegratedAddress = jsonObject.getString("xmr_receiving_integrated_address"); // "xmr_receiving_integrated_address": "xmr_integrated_address_user_needs_to_send_funds_to_as_string",
xmrRecommendedMixin = jsonObject.getInt("xmr_recommended_mixin"); // "xmr_recommended_mixin": <xmr_recommended_mixin_as_integer>,
xmrRequiredAmount = jsonObject.getDouble("xmr_required_amount"); // "xmr_required_amount": <xmr_amount_user_needs_to_send_as_float>,
xmrRequiredPaymentIdLong = jsonObject.getString("xmr_required_payment_id_long"); // "xmr_required_payment_id_long": "xmr_payment_id_user_needs_to_include_when_using_old_stlye_address_as_string"
xmrRequiredPaymentIdShort = jsonObject.getString("xmr_required_payment_id_short"); // "xmr_required_payment_id_short": "xmr_payment_id_included_in_integrated_address_as_string"
}
}
public static void call(@NonNull final XmrToApiCall api, @NonNull final String uuid,
@NonNull final XmrToCallback<QueryOrderStatus> callback) {
try {
final JSONObject request = createRequest(uuid);
api.call("order_status_query", request, new NetworkCallback() {
@Override
public void onSuccess(JSONObject jsonObject) {
try {
callback.onSuccess(new QueryOrderStatusImpl(jsonObject));
} catch (JSONException ex) {
callback.onError(ex);
}
}
@Override
public void onError(Exception ex) {
callback.onError(ex);
}
});
} catch (JSONException ex) {
callback.onError(ex);
}
}
/**
* Create JSON request object
*
* @param uuid unique_order_identifier_as_12_character_string
*/
static JSONObject createRequest(final String uuid) throws JSONException {
final JSONObject jsonObject = new JSONObject();
jsonObject.put("uuid", uuid);
return jsonObject;
}
}

@ -0,0 +1,28 @@
/*
* Copyright (c) 2017 m2049r et al.
*
* 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 com.m2049r.xmrwallet.xmrto.network;
import android.support.annotation.NonNull;
import org.json.JSONObject;
interface XmrToApiCall {
void call(@NonNull final String path, @NonNull final NetworkCallback callback);
void call(@NonNull final String path, final JSONObject request, @NonNull final NetworkCallback callback);
}

@ -0,0 +1,143 @@
/*
* Copyright (c) 2017 m2049r et al.
*
* 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 com.m2049r.xmrwallet.xmrto.network;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import com.m2049r.xmrwallet.xmrto.api.XmrToCallback;
import com.m2049r.xmrwallet.xmrto.XmrToError;
import com.m2049r.xmrwallet.xmrto.XmrToException;
import com.m2049r.xmrwallet.xmrto.api.CreateOrder;
import com.m2049r.xmrwallet.xmrto.api.QueryOrderParameters;
import com.m2049r.xmrwallet.xmrto.api.QueryOrderStatus;
import com.m2049r.xmrwallet.xmrto.api.XmrToApi;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import okhttp3.Call;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import timber.log.Timber;
public class XmrToApiImpl implements XmrToApi, XmrToApiCall {
@NonNull
private final OkHttpClient okHttpClient;
private final HttpUrl baseUrl;
public XmrToApiImpl(@NonNull final OkHttpClient okHttpClient, final HttpUrl baseUrl) {
this.okHttpClient = okHttpClient;
this.baseUrl = baseUrl;
}
public XmrToApiImpl(@NonNull final OkHttpClient okHttpClient) {
this(okHttpClient, HttpUrl.parse("https://xmr.to/api/v2/xmr2btc/"));
}
@Override
public void createOrder(final double amount, @NonNull final String address,
@NonNull final XmrToCallback<CreateOrder> callback) {
CreateOrderImpl.call(this, amount, address, callback);
}
@Override
public void queryOrderStatus(@NonNull final String uuid,
@NonNull final XmrToCallback<QueryOrderStatus> callback) {
QueryOrderStatusImpl.call(this, uuid, callback);
}
@Override
public void queryOrderParameters(@NonNull final XmrToCallback<QueryOrderParameters> callback) {
QueryOrderParametersImpl.call(this, callback);
}
@Override
public void call(@NonNull final String path, @NonNull final NetworkCallback callback) {
call(path, null, callback);
}
@Override
public void call(@NonNull final String path, final JSONObject request, @NonNull final NetworkCallback callback) {
final HttpUrl url = baseUrl.newBuilder()
.addPathSegment(path)
.addPathSegment("") // xmr.to needs a trailing slash!
.build();
Timber.d(url.toString());
final Request httpRequest = createHttpRequest(request, url);
Timber.d(httpRequest.toString());
Timber.d(request == null ? "null request" : request.toString());
okHttpClient.newCall(httpRequest).enqueue(new okhttp3.Callback() {
@Override
public void onFailure(final Call call, final IOException ex) {
Timber.d("A");
callback.onError(ex);
}
@Override
public void onResponse(final Call call, final Response response) throws IOException {
Timber.d("onResponse code=%d", response.code());
if (response.isSuccessful()) {
try {
final JSONObject json = new JSONObject(response.body().string());
callback.onSuccess(json);
} catch (JSONException ex) {
callback.onError(ex);
}
} else {
try {
final JSONObject json = new JSONObject(response.body().string());
final XmrToError error = new XmrToError(json);
Timber.e("xmr.to says %d/%s", response.code(), error.toString());
callback.onError(new XmrToException(response.code(), error));
} catch (JSONException ex) {
callback.onError(new XmrToException(response.code()));
}
}
}
});
}
private Request createHttpRequest(final JSONObject request, final HttpUrl url) {
if (request != null) {
final RequestBody body = RequestBody.create(
MediaType.parse("application/json"), request.toString());
return new Request.Builder()
.url(url)
.post(body)
.build();
} else {
return new Request.Builder()
.url(url)
.get()
.build();
}
}
}

@ -0,0 +1,46 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="24dp"
android:height="24dp"
android:viewportHeight="58.0"
android:viewportWidth="58.0">
<path
android:fillType="evenOdd"
android:pathData="M17.11,23.49L13.05,27.55L26.1,40.6L55.1,11.6L51.04,7.54L26.1,32.48L17.11,23.49L17.11,23.49ZM52.2,29C52.2,41.76 41.76,52.2 29,52.2C16.24,52.2 5.8,41.76 5.8,29C5.8,16.24 16.24,5.8 29,5.8C31.32,5.8 33.35,6.09 35.38,6.67L40.02,2.03C36.54,0.87 32.77,0 29,0C13.05,0 0,13.05 0,29C0,44.95 13.05,58 29,58C44.95,58 58,44.95 58,29L52.2,29L52.2,29Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="58.0"
android:endY="6.439293516E-15"
android:startX="2.1746516868E-31"
android:startY="58.0"
android:type="linear">
<item
android:color="#FFF0006B"
android:offset="0.0" />
<item
android:color="#FFFF6600"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFE"
android:fillType="evenOdd"
android:pathData="M20,13C20,18.52 15.52,23 10,23C4.48,23 -0,18.52 -0,13C-0,7.48 4.48,3.01 10,3.01C15.52,3.01 20,7.48 20,13"
android:strokeColor="#00000000"
android:strokeWidth="1" />
<path
android:fillColor="#FF6105"
android:fillType="evenOdd"
android:pathData="M10,3.01C4.48,3.01 -0.01,7.49 0,13C0,14.11 0.18,15.17 0.51,16.16L3.5,16.16L3.5,7.75L10,14.25L16.49,7.75L16.49,16.16L19.49,16.16C19.82,15.17 19.99,14.11 19.99,13C20,7.48 15.52,3.01 10,3.01L10,3.01Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
<path
android:fillColor="#464543"
android:fillType="evenOdd"
android:pathData="M8.5,15.74L5.67,12.91L5.67,18.2L4.58,18.2L3.5,18.2L1.45,18.2C3.21,21.08 6.38,23 10,23C13.62,23 16.79,21.08 18.54,18.2L16.49,18.2L14.56,18.2L14.33,18.2L14.33,12.91L11.49,15.74L10,17.23L8.5,15.74L8.5,15.74Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="@color/gradientOrange"
android:pathData="M8.59,16.34l4.58,-4.59 -4.58,-4.59L10,5.75l6,6 -6,6z" />
</vector>

@ -0,0 +1,26 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="58.0"
android:viewportWidth="58.0">
<path
android:fillColor="@color/gradientPink"
android:pathData="M17.11,23.49L13.05,27.55L26.1,40.6L55.1,11.6L51.04,7.54L26.1,32.48L17.11,23.49L17.11,23.49ZM52.2,29C52.2,41.76 41.76,52.2 29,52.2C16.24,52.2 5.8,41.76 5.8,29C5.8,16.24 16.24,5.8 29,5.8C31.32,5.8 33.35,6.09 35.38,6.67L40.02,2.03C36.54,0.87 32.77,0 29,0C13.05,0 0,13.05 0,29C0,44.95 13.05,58 29,58C44.95,58 58,44.95 58,29L52.2,29L52.2,29Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
<path
android:fillColor="#FFFFFE"
android:pathData="M20,13C20,18.52 15.52,23 10,23C4.48,23 -0,18.52 -0,13C-0,7.48 4.48,3.01 10,3.01C15.52,3.01 20,7.48 20,13"
android:strokeColor="#00000000"
android:strokeWidth="1" />
<path
android:fillColor="#FF6105"
android:pathData="M10,3.01C4.48,3.01 -0.01,7.49 0,13C0,14.11 0.18,15.17 0.51,16.16L3.5,16.16L3.5,7.75L10,14.25L16.49,7.75L16.49,16.16L19.49,16.16C19.82,15.17 19.99,14.11 19.99,13C20,7.48 15.52,3.01 10,3.01L10,3.01Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
<path
android:fillColor="#464543"
android:pathData="M8.5,15.74L5.67,12.91L5.67,18.2L4.58,18.2L3.5,18.2L1.45,18.2C3.21,21.08 6.38,23 10,23C13.62,23 16.79,21.08 18.54,18.2L16.49,18.2L14.56,18.2L14.33,18.2L14.33,12.91L11.49,15.74L10,17.23L8.5,15.74L8.5,15.74Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="@color/moneroBlack"
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
android:fillColor="#FF000000"
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z" />
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFffffff"
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#ff8b0000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13L7,13v-2h10v2z" />
</vector>

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M8.59,16.34l4.58,-4.59 -4.58,-4.59L10,5.75l6,6 -6,6z"/>
</vector>

@ -24,9 +24,6 @@
<path
android:fillColor="#FFffffff"
android:pathData="M251.43,76.85a4.48,4.48 0,0 1,-1 3.24,4.61 4.61,0 0,1 -3.33,1h-2.43v4.67h3.56c5.93,0 8.85,-3 8.85,-8.88V35.07h-5.69Z" />
<path
android:fillColor="#FFffffff"
android:pathData="M59.82,29.57a3.9,3.9 0,0 0,2.76 -6.64A3.85,3.85 0,0 0,56 25.69a3.8,3.8 0,0 0,1.1 2.75A3.64,3.64 0,0 0,59.82 29.57Z" />
<path
android:fillColor="#FFffffff"
android:pathData="M59,38.31a13.12,13.12 0,0 0,-9.64 -3.79,13.81 13.81,0 0,0 -7.35,2A14,14 0,0 0,36.92 42a12,12 0,0 0,-4.82 -5.54A13.65,13.65 0,0 0,25 34.52q-7.7,0 -11.21,6.14V35.07H8V69.46h5.75V50.05q0,-5.2 2.54,-7.89a9.09,9.09 0,0 1,7 -2.69,9 9,0 0,1 6.91,2.69q2.5,2.69 2.5,7.89v19.4h5.64V50.05q0,-5.2 2.54,-7.89a9.06,9.06 0,0 1,6.92 -2.69,9 9,0 0,1 6.91,2.69c1.69,1.8 2.52,4.43 2.52,7.89V69.46l0,7.39a4.48,4.48 0,0 1,-0.95 3.24,4.59 4.59,0 0,1 -3.33,1H50.41v4.67H54c5.93,0 8.82,-3 8.82,-8.88V49.11C62.82,44.44 61.56,40.84 59,38.31Z" />

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="@color/give"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM6.5,9L10,5.5 13.5,9L11,9v4L9,13L9,9L6.5,9zM17.5,15L14,18.5 10.5,15L13,15v-4h2v4h2.5z" />
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="@color/take"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -17,6 +17,13 @@
android:layout_height="48dp"
android:src="@mipmap/ic_launcher" />
<TextView
style="@style/MoneroLabel.Caps"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/about_whoami" />
<TextView
android:id="@+id/tvVersion"
android:layout_width="match_parent"
@ -29,8 +36,8 @@
style="@style/MoneroText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/header_top"
android:layout_marginBottom="@dimen/header_top"
android:layout_marginTop="@dimen/header_top"
android:gravity="start"
android:textSize="10sp"
tools:text="@string/menu_help" />

@ -277,7 +277,7 @@
android:layout_marginTop="4dp"
android:enabled="false"
android:minHeight="36dp"
android:text="@string/send_send_hint" />
android:text="@string/send_send_label" />
<Button
android:id="@+id/bReallySend"

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
@ -28,8 +29,8 @@
<FrameLayout
android:layout_width="match_parent"
android:layout_height="96dp"
android:layout_marginBottom="4dp">
android:layout_height="wrap_content"
android:layout_marginBottom="48dp">
<TextView
android:id="@+id/tvPaymentIdIntegrated"
@ -37,17 +38,48 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:layout_margin="8dp"
android:drawablePadding="8dp"
android:drawableStart="@drawable/ic_check_gray_24dp"
android:gravity="center"
android:text="@string/info_paymentid_intergrated"
android:textSize="18sp"
android:visibility="gone" />
android:visibility="invisible" />
<LinearLayout
android:id="@+id/llXmrTo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="horizontal"
android:visibility="invisible">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:paddingTop="8dp"
android:src="@drawable/gunther_24dp" />
<TextView
android:id="@+id/tvXmrTo"
style="@style/MoneroText.Info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:gravity="start|top"
android:singleLine="false"
android:textSize="18sp"
tools:text="@string/info_xmrto" />
</LinearLayout>
<LinearLayout
android:id="@+id/llPaymentId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="4dp"
android:orientation="horizontal"
android:visibility="visible"

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tvFunds"
style="@style/MoneroText.Funds"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
tools:text="@string/send_available_btc" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.m2049r.xmrwallet.widget.SendProgressView
android:id="@+id/evXmrToParms"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<LinearLayout
android:id="@+id/llXmrToParms"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal"
android:visibility="invisible">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:paddingTop="8dp"
android:src="@drawable/ic_xmrto_32dp" />
<TextView
android:id="@+id/tvXmrToParms"
style="@style/MoneroText.Info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|top"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:singleLine="false"
tools:text="@string/info_send_xmrto_parms" />
</LinearLayout>
</FrameLayout>
<com.m2049r.xmrwallet.widget.ExchangeBtcTextView
android:id="@+id/evAmount"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginTop="16dp"
android:orientation="vertical" />
<com.m2049r.xmrwallet.widget.NumberPadView
android:id="@+id/numberPad"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:gravity="center" />
</LinearLayout>

@ -0,0 +1,252 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:foreground="?android:attr/selectableItemBackground"
card_view:cardCornerRadius="2dp"
card_view:cardElevation="8dp"
card_view:contentPadding="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.m2049r.xmrwallet.widget.SendProgressView
android:id="@+id/evStageA"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<LinearLayout
android:id="@+id/llStageA"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
android:visibility="visible">
<TextView
style="@style/MoneroText.Confirm.Label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:drawablePadding="8dp"
android:drawableStart="@drawable/ic_xmrto_32dp"
android:gravity="center_vertical"
android:text="@string/label_send_btc_xmrto_key" />
<TextView
android:id="@+id/tvTxXmrToKey"
style="@style/MoneroText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/dotGray"
android:drawableEnd="@drawable/ic_content_copy_white_24dp"
android:drawablePadding="16dp"
android:paddingBottom="8dp"
android:paddingEnd="8dp"
android:paddingStart="24dp"
android:paddingTop="8dp"
android:textColor="@color/white"
tools:text="XMR.TO-d2KQ" />
<TextView
style="@style/MoneroText.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:text="@string/label_send_btc_xmrto_info" />
<TextView
style="@style/MoneroText.Confirm.Label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginTop="16dp"
android:text="@string/label_send_btc_address"
android:textAlignment="textStart" />
<TextView
android:id="@+id/tvTxBtcAddress"
style="@style/MoneroText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAlignment="textStart"
tools:text="mpQ84J43EURZHkCnXbyQ4PpNDLLBqdsMW2" />
</LinearLayout>
</FrameLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="8dp"
android:layout_marginTop="8dp"
android:background="@android:color/darker_gray" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.m2049r.xmrwallet.widget.SendProgressView
android:id="@+id/evStageB"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<LinearLayout
android:id="@+id/llStageB"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
android:visibility="invisible">
<TextView
style="@style/MoneroText.Confirm.Label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/label_send_btc_amount"
android:textAlignment="textStart" />
<TextView
android:id="@+id/tvTxBtcAmount"
style="@style/MoneroText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAlignment="textStart"
tools:text="1.75 BTC = 84.118438761777 XMR" />
<TextView
android:id="@+id/tvTxBtcRate"
style="@style/MoneroLabel.Gray"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAlignment="textStart"
android:textSize="16sp"
tools:text="(Rate: 0.020804 BTC/XMR)" />
</LinearLayout>
</FrameLayout>
</LinearLayout>
</android.support.v7.widget.CardView>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp">
<com.m2049r.xmrwallet.widget.SendProgressView
android:id="@+id/evStageC"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<LinearLayout
android:id="@+id/llStageC"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
android:visibility="invisible">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:orientation="horizontal"
android:weightSum="3">
<TextView
android:id="@+id/tvTxFeeLabel"
style="@style/MoneroLabel.Gray"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/send_fee_btc_label"
android:textAlignment="textStart"
android:textSize="16sp" />
<TextView
android:id="@+id/tvTxFee"
style="@style/MoneroText.Gray"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:textAlignment="textEnd"
android:textSize="16sp"
tools:text="0.006817000000" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:orientation="horizontal"
android:weightSum="3">
<TextView
android:id="@+id/tvTxTotalLabel"
style="@style/MoneroLabel.Caps.Black"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/send_total_btc_label"
android:textAlignment="textStart"
android:textSize="16sp" />
<TextView
android:id="@+id/tvTxTotal"
style="@style/MoneroText.Black"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:textAlignment="textEnd"
android:textSize="16sp"
tools:text="143.014817000000" />
</LinearLayout>
</LinearLayout>
</FrameLayout>
<LinearLayout
android:id="@+id/llConfirmSend"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="vertical"
android:visibility="invisible">
<Button
android:id="@+id/bSend"
style="@style/MoneroButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:enabled="true"
android:padding="8dp"
android:text="@string/send_send_label" />
<ProgressBar
android:id="@+id/pbProgressSend"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:indeterminate="true"
android:visibility="invisible" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,260 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<ImageView
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_gravity="top"
android:layout_marginEnd="24dp"
android:padding="6dp"
android:src="@drawable/ic_check_circle_xmr" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
style="@style/MoneroText.Success"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/label_send_success" />
<TextView
android:id="@+id/tvTxAmount"
style="@style/MoneroText.Balance.Orange"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="2"
android:textAlignment="textEnd"
tools:text="143.008000 XMR" />
<TextView
android:id="@+id/tvTxFee"
style="@style/MoneroText.Gray.SuccessFee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="2"
android:textAlignment="textEnd"
tools:text="+0.006817 Fee" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<TextView
style="@style/MoneroText.Confirm.Label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_send_txid"
android:textAlignment="textStart" />
<ImageButton
android:id="@+id/bCopyTxId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:background="?android:selectableItemBackground"
android:enabled="false"
android:src="@drawable/ic_content_nocopy_black_24dp" />
</LinearLayout>
<TextView
android:id="@+id/tvTxId"
style="@style/MoneroText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textAlignment="textStart"
tools:text="fcb12cbe9f43d4e8b9ee54f48d450a89a6937946db856506820df0539571801d" />
<TextView
style="@style/MoneroText.Confirm.Label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/label_send_address"
android:textAlignment="textStart" />
<TextView
android:id="@+id/tvTxAddress"
style="@style/MoneroText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textAlignment="textStart"
tools:text="4AdkPJoxn7JCvAby9szgnt93MSEwdnxdhaASxbTBm6x5dCwmsDep2UYN4FhStDn5i11nsJbpU7oj59ahg8gXb1Mg3viqCuk" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<TextView
android:id="@+id/labelPaymentId"
style="@style/MoneroText.Confirm.Label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_send_payment_id" />
<TextView
android:id="@+id/tvTxPaymentId"
style="@style/MoneroText.Confirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
tools:text="d666a38d4a28fb38" />
</LinearLayout>
<android.support.v7.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:id="@+id/cvXmrTo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginTop="8dp"
card_view:cardCornerRadius="2dp"
card_view:cardElevation="8dp"
card_view:contentPadding="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="@+id/flXmrtoProgress"
android:layout_width="72dp"
android:layout_height="wrap_content"
android:layout_centerInParent="true">
<ProgressBar
android:id="@+id/pbXmrto"
android:layout_width="match_parent"
android:layout_height="72dp"
android:layout_gravity="center"
android:backgroundTintMode="src_in"
android:indeterminate="true"
android:indeterminateTint="@color/moneroFab" />
<ImageView
android:id="@+id/ivXmrToStatus"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:src="@drawable/ic_pending_orange_24dp"
android:visibility="visible" />
<ImageView
android:id="@+id/ivXmrToStatusBig"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_gravity="center"
android:visibility="invisible" />
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="top|start"
android:layout_marginStart="10dp"
android:layout_marginTop="0dp"
android:src="@drawable/ic_xmrto_whitestroke_24px" />
</FrameLayout>
<TextView
android:id="@+id/tvXmrToStatus"
style="@style/MoneroText"
android:layout_width="72dp"
android:layout_height="wrap_content"
android:layout_below="@+id/flXmrtoProgress"
android:gravity="center"
android:text="@string/info_send_xmrto_query"
android:textSize="10sp"
android:textStyle="bold"
tools:text="Confirmation Pending" />
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:orientation="vertical">
<TextView
style="@style/MoneroText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/info_send_xmrto_success_order_label"
android:textAllCaps="true"
android:textStyle="bold" />
<TextView
android:id="@+id/tvXmrToAmount"
style="@style/MoneroText.Balance.Orange"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:gravity="center"
tools:text="0.001 BTC" />
<TextView
android:id="@+id/tvTxXmrToKey"
style="@style/MoneroText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/dotGray"
android:drawableEnd="@drawable/ic_content_copy_white_24dp"
android:drawablePadding="8dp"
android:paddingBottom="8dp"
android:paddingEnd="8dp"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:textColor="@color/white"
tools:text="XMR.TO-d2KQ" />
<TextView
style="@style/MoneroText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginTop="8dp"
android:drawablePadding="4dp"
android:drawableStart="@drawable/ic_info_outline_gray_24dp"
android:gravity="center"
android:text="@string/label_send_btc_xmrto_info"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
</android.support.v7.widget.CardView>
</LinearLayout>
</ScrollView>

@ -183,7 +183,7 @@
android:layout_marginTop="48dp"
android:enabled="true"
android:padding="8dp"
android:text="@string/send_send_hint" />
android:text="@string/send_send_label" />
<ProgressBar
android:id="@+id/pbProgressSend"

@ -13,30 +13,55 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginTop="4dp"
android:gravity="center"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<ImageView
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_gravity="center"
android:layout_margin="16dp"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_gravity="top"
android:layout_marginEnd="24dp"
android:padding="6dp"
android:src="@drawable/ic_check_circle" />
<TextView
style="@style/MoneroText.Sucess"
android:layout_width="wrap_content"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:text="@string/label_send_success" />
android:orientation="vertical">
<TextView
style="@style/MoneroText.Success"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/label_send_success" />
<TextView
android:id="@+id/tvTxAmount"
style="@style/MoneroText.Balance.Orange"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="2"
android:textAlignment="textEnd"
tools:text="143.008000 XMR" />
<TextView
android:id="@+id/tvTxFee"
style="@style/MoneroText.Gray.SuccessFee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="2"
android:textAlignment="textEnd"
tools:text="+0.006817 Fee" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<TextView
@ -47,7 +72,7 @@
android:textAlignment="textStart" />
<ImageButton
android:id="@+id/bCopyAddress"
android:id="@+id/bCopyTxId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
@ -86,15 +111,16 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginTop="4dp"
android:orientation="horizontal">
<TextView
android:id="@+id/labelPaymentId"
style="@style/MoneroText.Confirm.Label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_send_payment_id"
android:textAlignment="textStart" />
android:text="@string/label_send_payment_id" />
<TextView
android:id="@+id/tvTxPaymentId"
@ -102,114 +128,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textAlignment="textStart"
tools:text="d666a38d4a28fb38" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:orientation="horizontal">
<TextView
style="@style/MoneroText.Confirm.Label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_send_notes"
android:textAlignment="textStart" />
<TextView
android:id="@+id/tvTxNotes"
style="@style/MoneroText.Confirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textAlignment="textStart"
tools:text="gunegugumobil" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="@android:color/darker_gray" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvTxAmountLabel"
style="@style/MoneroLabel.Gray"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/send_amount_label"
android:textAlignment="textStart" />
<TextView
android:id="@+id/tvTxAmount"
style="@style/MoneroText.Gray"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:textAlignment="textEnd"
tools:text="143.008000000000" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:orientation="horizontal"
android:weightSum="3">
<TextView
android:id="@+id/tvTxFeeLabel"
style="@style/MoneroLabel.Gray"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/send_fee_label"
android:textAlignment="textStart" />
<TextView
android:id="@+id/tvTxFee"
style="@style/MoneroText.Gray"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:textAlignment="textEnd"
tools:text="0.006817000000" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:orientation="horizontal"
android:weightSum="3">
<TextView
android:id="@+id/tvTxTotalLabel"
style="@style/MoneroLabel.Caps.Black"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/send_total_label"
android:textAlignment="textStart" />
<TextView
android:id="@+id/tvTxTotal"
style="@style/MoneroText.Black"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:textAlignment="textEnd"
tools:text="143.014817000000" />
</LinearLayout>
</LinearLayout>
</ScrollView>

@ -58,6 +58,114 @@
tools:text="2017-10-09 12:44:13 +0200" />
</LinearLayout>
<android.support.v7.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:id="@+id/cvXmrTo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:foreground="?android:attr/selectableItemBackground"
card_view:cardCornerRadius="2dp"
card_view:cardElevation="8dp"
card_view:contentPadding="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
style="@style/MoneroText.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawablePadding="8dp"
android:drawableStart="@drawable/ic_xmrto_32dp"
android:gravity="center"
android:text="@string/label_send_btc_xmrto_info" />
<TableLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:shrinkColumns="1">
<TableRow>
<TextView
style="@style/MoneroLabel.Small"
android:layout_gravity="center_vertical"
android:gravity="end"
android:padding="8dp"
android:text="@string/tx_amount_btc" />
<TextView
android:id="@+id/tvTxAmountBtc"
style="@style/MoneroText"
android:gravity="start"
android:padding="8dp"
android:selectAllOnFocus="true"
android:textColor="@color/gradientOrange"
android:textIsSelectable="true"
tools:text="1.008 BTC" />
</TableRow>
<TableRow>
<TextView
android:id="@+id/labelDestinationBtc"
style="@style/MoneroLabel.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="end"
android:padding="8dp"
android:text="@string/tx_destination_btc" />
<TextView
android:id="@+id/tvDestinationBtc"
style="@style/MoneroText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start"
android:padding="8dp"
android:selectAllOnFocus="true"
android:text="mjn127C5wRQCULksMYMFHLp9UTdQuCfbZ9"
android:textIsSelectable="true" />
</TableRow>
<TableRow>
<TextView
android:id="@+id/labelTxXmrToKey"
style="@style/MoneroLabel.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="end"
android:padding="8dp"
android:text="@string/label_send_btc_xmrto_key_lb" />
<LinearLayout>
<TextView
android:id="@+id/tvTxXmrToKey"
style="@style/MoneroText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/dotGray"
android:drawableEnd="@drawable/ic_content_copy_white_24dp"
android:drawablePadding="16dp"
android:paddingBottom="8dp"
android:paddingEnd="8dp"
android:paddingStart="24dp"
android:paddingTop="8dp"
android:textColor="@color/white"
tools:text="XMR.TO-d2KQ" />
</LinearLayout>
</TableRow>
</TableLayout>
</LinearLayout>
</android.support.v7.widget.CardView>
<TableLayout
android:layout_width="match_parent"

@ -16,11 +16,19 @@
android:orientation="horizontal"
android:padding="8dp">
<ImageView
android:id="@+id/ivTxType"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_gravity="center"
android:src="@drawable/ic_xmrto_32dp"
android:visibility="visible"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="11"
android:layout_weight="9"
android:orientation="vertical">
<TextView
@ -29,7 +37,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
tools:text="999999.99999" />
tools:text="999.999999" />
<TextView
android:id="@+id/tx_fee"

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:showIn="LinearLayout">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:orientation="horizontal">
<Spinner
android:id="@+id/sCurrencyA"
android:layout_width="56dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:textAlignment="center" />
<TextView
android:id="@+id/tvAmountA"
style="@style/MoneroText.Balance.Orange"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center|start"
android:layout_marginStart="16dp"
android:layout_weight="3"
android:hint="@string/send_amount_hint"
android:padding="4dp"
android:singleLine="true"
tools:text="87.00000" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<Spinner
android:id="@+id/sCurrencyB"
android:layout_width="56sp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:textAlignment="center" />
<TextView
android:id="@+id/tvAmountB"
style="@style/MoneroText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center|start"
android:layout_marginStart="16dp"
android:layout_weight="3"
android:hint="@string/send_amount_hint"
android:padding="4dp"
android:singleLine="true"
tools:text="87.00000" />
</LinearLayout>
</merge>

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:showIn="LinearLayout">
<ProgressBar
android:id="@+id/pbProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="8dp"
android:indeterminate="true"
android:visibility="invisible" />
<LinearLayout
android:id="@+id/llMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="visible">
<TextView
android:id="@+id/tvCode"
style="@style/MoneroText.Confirm.Label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
tools:text="XMRTO-ERROR-001" />
<TextView
android:id="@+id/tvMessage"
style="@style/MoneroText.Confirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
tools:text="internal services not available" />
<TextView
android:id="@+id/tvSolution"
style="@style/MoneroText.Confirm.Label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
tools:text="Touch to retry" />
</LinearLayout>
</merge>

@ -2,7 +2,7 @@
<resources>
<string name="about_title">Acerca De</string>
<string name="about_close">Cerrar</string>
<string name="about_version">Soy monerujo %1$s (%2$d)</string>
<string name="about_version">Versión %1$s (%2$d)</string>
<string name="donation_text">
\"¡Donar, bastardos ingratos!\"

@ -182,7 +182,6 @@
<string name="send_qr_hint">Escanear</string>
<string name="send_prepare_hint">Preparar</string>
<string name="send_dispose_hint">Disponer (Deshacer)</string>
<string name="send_send_hint">Gastar mis dulces Moneroj</string>
<string name="send_really_send_hint">Último Paso - ¡Confirma!</string>
<string name="send_qr_invalid">No es un Código QR de monero</string>
<string name="send_qr_address_invalid">Dirección de Monero inválida</string>
@ -215,10 +214,10 @@
<string name="tx_timestamp">Marca de tiempo</string>
<string name="tx_id">ID de Transacción</string>
<string name="tx_key">Clave de Transacción</string>
<string name="tx_destination">Destinatario</string>
<string name="tx_destination">Destino</string>
<string name="tx_paymentId">ID de Pago</string>
<string name="tx_blockheight">Bloque</string>
<string name="tx_amount">Cantidad</string>
<string name="tx_amount">Monto</string>
<string name="tx_fee">Comisión</string>
<string name="tx_transfers">Transferencias</string>
<string name="tx_notes">Notas</string>
@ -271,4 +270,67 @@
<string name="fab_restore_viewonly">Restaurar cartera de sólo vista</string>
<string name="fab_restore_key">Restaurar cartera con claves privadas</string>
<string name="fab_restore_seed">Resturar cartera con semilla de 25 palabras</string>
<string name="info_xmrto"><![CDATA[
<b>Ingresaste una dirección Bitcoin</b><br/>
<i>Vas a enviar XMR y el destinatario recibirá BTC usando el servicio <b>XMR.TO</b>.</i>
]]></string>
<string name="info_send_xmrto_success_sending">Enviando</string>
<string name="info_send_xmrto_success_status_label">Estado:</string>
<string name="info_send_xmrto_success_status">%1$s es %1$s (#%1$d)</string>
<string name="info_send_xmrto_success_btc">%1$s BTC</string>
<string name="info_txdetails_xmrto_key">Clave:</string>
<string name="info_txdetails_xmrto_address">Dirección:</string>
<string name="info_txdetails_xmrto_status">Estado:</string>
<string name="info_txdetails_xmrto_amount">%1$s BTC</string>
<string name="info_send_xmrto_paid">Confirmación pendiente</string>
<string name="info_send_xmrto_unpaid">Pago pendiente</string>
<string name="info_send_xmrto_error">Error de XMR.TO (%1$s)</string>
<string name="info_send_xmrto_sent">BTC Enviados!</string>
<string name="info_send_xmrto_query">Consultando &#8230;</string>
<string name="info_send_xmrto_parms"><![CDATA[
<b>Puedes enviar %1$s &#8212; %2$s BTC</b>.<br/>
<i><b>XMR.TO</b> está ofreciendo una tasa de cambio de <b>%3$s BTC</b> <u>en este momento</u></i>.
]]></string>
<string name="info_send_xmrto_zeroconf"><![CDATA[
<i>Montos hasta <b>%1$s BTC</b> serán enviados <u>en el momento</u>!</i>
]]></string>
<string name="send_available_btc">Saldo: %2$s BTC (%1$s XMR)</string>
<string name="label_send_progress_xmrto_create">Creando orden XMR.TO</string>
<string name="label_send_progress_xmrto_query">Consultando orden XMR.TO</string>
<string name="label_send_progress_create_tx">Preparando transacción Monero</string>
<string name="label_send_progress_queryparms">Consultando parámetros XMR.TO</string>
<string name="label_generic_xmrto_error">ERROR XMR.TO</string>
<string name="text_generic_xmrto_error">Código: %1$d</string>
<string name="text_retry">Toca para reintentar</string>
<string name="text_noretry_monero">Parece que estamos atascados!</string>
<string name="text_noretry">Oh-oh, parece que XMR.TO no está disponible ahora!</string>
<string name="text_send_btc_amount">%1$s BTC = %2$s XMR</string>
<string name="text_send_btc_rate">(Cambio: %1$s BTC/XMR)</string>
<string name="label_send_btc_details">Click para más detalles</string>
<string name="label_send_btc_xmrto_info">Visita https://xmr.to para soporte y rastreo</string>
<string name="label_send_txinfo">Mira en los datos de la TX para más detalles</string>
<string name="label_send_btc_xmrto_key_lb">Clave secreta\nXMR.TO</string>
<string name="label_send_btc_xmrto_key">Clave secreta XMR.TO</string>
<string name="label_send_btc_address">Dirección BTC destino</string>
<string name="label_send_btc_amount">Cantidad</string>
<string name="label_send_btc_create_order">Creando orden XMR.TO</string>
<string name="label_send_btc_retry">Toca para reintentar</string>
<string name="send_xmrto_timeout">Oye, esperaste demasiado!</string>
<string name="label_copy_xmrtokey">Clave XMR.TO</string>
<string name="message_copy_xmrtokey">¡Clave XMR.TO copiada al portapapeles!</string>
<string name="send_amount_btc_xmr">%1$s (indicativo)</string>
<string name="send_send_label">Gastar mis preciados moneroj</string>
<string name="send_send_timed_label">Gastar mis preciados moneroj (%1$s)</string>
<string name="send_address_invalid">No es una dirección válida</string>
<string name="send_fee_btc_label">Comisión (XMR)</string>
<string name="send_total_btc_label">Total (XMR)</string>
<string name="send_amount">%1$s XMR</string>
<string name="send_fee">+%1$s Comisión</string>
<string name="tx_status_btc">Estado</string>
<string name="tx_id_btc">TX ID\n(BTC)</string>
<string name="tx_destination_btc">Destino\n(BTC)</string>
<string name="tx_amount_btc">Monto\n(BTC)</string>
<string name="receive_bitcoin_paymentid_invalid">Debe quedar vacío con una dirección Bitcoin</string>
<string name="about_whoami">Soy monerujo</string>
<string name="info_send_xmrto_success_order_label">Orden XMR.TO</string>
</resources>

@ -2,14 +2,15 @@
<resources>
<string name="about_title">About</string>
<string name="about_close">Close</string>
<string name="about_version">I am monerujo %1$s (%2$d)</string>
<string name="about_whoami">I am monerujo</string>
<string name="about_version">Version %1$s (%2$d)</string>
<string name="donation_text">
\"Donate you ungrateful bastards!\"
</string>
<string name="donation_address_label">Donations Address</string>
<string name="donation_address">4AdkPJoxn7JCvAby9szgnt93MSEwdnxdhaASxbTBm6x5dCwmsDep2UYN4FhStDn5i11nsJbpU7oj59ahg8gXb1Mg3viqCuk</string>
<string name="donation_address" translatable="false">4AdkPJoxn7JCvAby9szgnt93MSEwdnxdhaASxbTBm6x5dCwmsDep2UYN4FhStDn5i11nsJbpU7oj59ahg8gXb1Mg3viqCuk</string>
<string name="donation_credits"><![CDATA[
<b>Credits</b>
@ -64,7 +65,7 @@
</p>
]]></string>
<string name="about_licenses"><![CDATA[
<string name="about_licenses" translatable="false"><![CDATA[
<h1>Open Source Licenses</h1>
<h2>Licensed under the Apache License, Version 2.0</h2>
<h3>monerujo (https://github.com/m2049r/xmrwallet)</h3>

@ -8,6 +8,8 @@
<color name="gradientOrange">#FFFF6105</color>
<color name="gradientPink">#FFF0006B</color>
<color name="xmrtoYellow">#FFFFD700</color>
<color name="trafficGray">#9B9B9B</color>
<color name="take">#FF417505</color>

@ -1,6 +1,6 @@
<resources>
<string name="app_name">monerujo</string>
<string name="login_activity_name">monerujo</string>
<string name="app_name" translatable="false">monerujo</string>
<string name="login_activity_name" translatable="false">monerujo</string>
<string name="wallet_activity_name">Wallet</string>
<string name="menu_testnet">Testnet</string>
@ -21,16 +21,75 @@
<string name="label_cancel">Cancel</string>
<string name="label_wallet_advanced_details">Touch for detailed information</string>
<string name="label_send_success">Success!</string>
<string name="label_send_success">Successfully sent</string>
<string name="label_send_done">Done</string>
<string name="label_receive_info_gen_qr_code">Touch for QR Code</string>
<string name="info_send_prio_fees">Higher Priority = Higher Fees</string>
<string name="info_xmrto"><![CDATA[
<b>You entered a Bitcoin address.</b><br/>
<i>You&apos;ll send XMR and the receiver will get BTC using the <b>XMR.TO</b> service.</i>
]]></string>
<string name="info_send_xmrto_success_order_label">XMR.TO Order</string>
<string name="info_send_xmrto_success_sending">Sending</string>
<string name="info_send_xmrto_success_status_label">Status:</string>
<string name="info_send_xmrto_success_btc">%1$s BTC</string>
<string name="info_send_xmrto_success_status">%1$s is %1$s (#%1$d)</string>
<string name="info_txdetails_xmrto_key">Key:</string>
<string name="info_txdetails_xmrto_address">Address:</string>
<string name="info_txdetails_xmrto_status">Status:</string>
<string name="info_txdetails_xmrto_amount">%1$s BTC</string>
<string name="info_send_xmrto_paid">Confirmation Pending</string>
<string name="info_send_xmrto_unpaid">Payment Pending</string>
<string name="info_send_xmrto_error">XMR.TO Error (%1$s)</string>
<string name="info_send_xmrto_sent">BTC Sent!</string>
<string name="info_send_xmrto_query">Querying &#8230;</string>
<string name="info_send_xmrto_parms"><![CDATA[
<b>You can send %1$s &#8212; %2$s BTC</b>.<br/>
<i><b>XMR.TO</b> is giving you an exchange rate of <b>%3$s BTC</b> <u>right now</u></i>.
]]></string>
<string name="info_send_xmrto_zeroconf"><![CDATA[
<i>Amounts up to <b>%1$s BTC</b> will be sent <u>instantly</u>!</i>
]]></string>
<string name="send_available_btc">Balance: %2$s BTC (%1$s XMR)</string>
<string name="info_paymentid_intergrated">Payment ID integrated</string>
<string name="info_prepare_tx">Preparing your transaction</string>
<string name="message_copy_txid">Transaction ID copied to clipboard!</string>
<string name="label_send_progress_xmrto_create">Creating XMR.TO Order</string>
<string name="label_send_progress_xmrto_query">Querying XMR.TO Order</string>
<string name="label_send_progress_create_tx">Preparing Monero Transaction</string>
<string name="label_send_progress_queryparms">Querying xmr.to parameters</string>
<string name="label_generic_xmrto_error">XMR.TO ERROR</string>
<string name="text_generic_xmrto_error">Code: %1$d</string>
<string name="text_retry">Touch to retry</string>
<string name="text_noretry_monero">Now we\'re stuck here!</string>
<string name="text_noretry">Uh-oh, XMR.TO does not seem be available right now!</string>
<string name="text_send_btc_amount">%1$s BTC = %2$s XMR</string>
<string name="text_send_btc_rate">(Rate: %1$s BTC/XMR)</string>
<string name="label_send_btc_details">Click for more details</string>
<string name="label_send_btc_xmrto_info">Visit xmr.to for support &amp; tracking</string>
<string name="label_send_txinfo">See TX details for more details</string>
<string name="label_send_btc_xmrto_key_lb">Secret Key\nXMR.TO</string>
<string name="label_send_btc_xmrto_key">XMR.TO Secret Key</string>
<string name="label_send_btc_address">Destination BTC Address</string>
<string name="label_send_btc_amount">Amount</string>
<string name="label_send_btc_create_order">Creating XMR.TO Order</string>
<string name="label_send_btc_retry">Touch to retry</string>
<string name="label_send_txid">Transaction ID</string>
<string name="label_send_address">Destination Address</string>
@ -74,6 +133,8 @@
<string name="status_transaction_failed">Transaction failed: %1$s</string>
<string name="status_transaction_prepare_failed">Could not create transaction!</string>
<string name="send_xmrto_timeout">Dude, you waited too long!</string>
<string name="service_busy">I am still busy with your last wallet &#8230;</string>
<string name="prompt_rename">Rename %1$s</string>
@ -96,8 +157,7 @@
<string name="title_amount">Amount</string>
<string name="title_date">Date</string>
<string name="xmr_unconfirmed_amount">+ %1$s XMR unconfirmed</string>
<!--string name="xmr_balance">%1$s XMR</string-->
<string name="xmr">XMR</string>
<string name="xmr" translatable="false">XMR</string>
<string name="label_transactions">Transactions</string>
<string name="text_daemonConnected">Daemon connected!</string>
@ -119,8 +179,11 @@
<string name="label_copy_viewkey">View Key</string>
<string name="label_copy_address">Public Address</string>
<string name="label_copy_xmrtokey">XMR.TO Key</string>
<string name="message_copy_viewkey">View Key copied to clipboard!</string>
<string name="message_copy_xmrtokey">XMR.TO Key copied to clipboard!</string>
<string name="message_copy_address">Wallet Address copied to clipboard!</string>
<string name="message_copy_txid">Transaction ID copied to clipboard!</string>
<string name="message_noselect_seed">Selecting seed disabled for security reasons!</string>
<string name="message_noselect_key">Selecting spend key disabled for security reasons!</string>
<string name="message_nocopy">Copy disabled for security reasons!</string>
@ -171,11 +234,11 @@
<string name="generate_mnemonic_label">Mnemonic Seed</string>
<string name="generate_restoreheight_label">Restore Height:</string>
<!--string name="generate_check_keys">Check your keys!</string-->
<string name="generate_check_key">Enter valid Key</string>
<string name="generate_check_address">Enter valid Address</string>
<string name="generate_check_mnemonic">Enter your 25 word seed</string>
<!--string name="generate_check_something">Check your entry!</string-->
<string name="send_amount_btc_xmr">%1$s (indicative)</string>
<string name="send_address_hint">Receiver\'s Address</string>
<string name="send_paymentid_hint">Payment ID (optional)</string>
@ -188,13 +251,15 @@
<string name="send_qr_hint">Scan</string>
<string name="send_prepare_hint">Prepare</string>
<string name="send_dispose_hint">Dispose (Undo)</string>
<string name="send_send_hint">Spend my sweet Moneroj</string>
<string name="send_send_label">Spend my sweet Moneroj</string>
<string name="send_send_timed_label">Spend my sweet Moneroj (%1$s)</string>
<string name="send_really_send_hint">Last Step - Confirm!</string>
<string name="send_qr_invalid">Not a monero QR Code</string>
<string name="send_qr_address_invalid">Invalid Monero address</string>
<string name="send_qr_invalid">Not a QR Code</string>
<string name="send_qr_address_invalid">Not a valid payment QR code</string>
<string name="send_address_invalid">Not a valid address</string>
<string name="send_preparing_progress">Preparing transaction</string>
<string name="send_title">Send</string>
<string name="send_available">Available funds: %1$s XMR</string>
<string name="send_available">Balance: %1$s XMR</string>
<string name="send_address_title">Address</string>
<string name="send_amount_title">Amount</string>
<string name="send_settings_title">Settings</string>
@ -204,10 +269,15 @@
<string name="send_amount_too_large">Amount > Funds</string>
<string name="send_amount_label">Amount</string>
<string name="send_fee_btc_label">Fee (XMR)</string>
<string name="send_fee_label">Fee</string>
<string name="send_dust_label">Dust</string>
<string name="send_total_btc_label">Total (XMR)</string>
<string name="send_total_label">Total</string>
<string name="send_amount">%1$s XMR</string>
<string name="send_fee">+%1$s Fee</string>
<string name="send_create_tx_error_title">Create Transaction Error</string>
<string name="tx_list_fee_pending">incl. %1$s fee</string>
@ -217,14 +287,18 @@
<string name="tx_list_amount_negative">- %1$s</string>
<string name="tx_list_amount_positive">+ %1$s</string>
<string name="tx_status_btc">Status</string>
<string name="tx_address">Address</string>
<string name="tx_timestamp">Timestamp</string>
<string name="tx_id">TX ID</string>
<string name="tx_id_btc">TX ID\n(BTC)</string>
<string name="tx_key">TX Key</string>
<string name="tx_destination">Destination</string>
<string name="tx_destination_btc">Destination\n(BTC)</string>
<string name="tx_paymentId">Payment ID</string>
<string name="tx_blockheight">Block</string>
<string name="tx_amount">Amount</string>
<string name="tx_amount_btc">Amount\n(BTC)</string>
<string name="tx_fee">Fee</string>
<string name="tx_transfers">Transfers</string>
<string name="tx_notes">Notes</string>
@ -248,6 +322,7 @@
<string name="receive_cannot_open">Could not open wallet!</string>
<string name="receive_paymentid_invalid">16 or 64 Hex characters (0-9,a-f)</string>
<string name="receive_integrated_paymentid_invalid">Must be empty with integrated address</string>
<string name="receive_bitcoin_paymentid_invalid">Must be empty with bitcoin address</string>
<string name="receive_amount_invalid">Must be > 0</string>
<string name="receive_amount_empty">Enter value</string>
@ -266,9 +341,9 @@
<string name="archive_alert_yes">Yes, do that!</string>
<string name="archive_alert_no">No thanks!</string>
<string name="big_amount">999999.999999999999</string>
<string name="big_amount" translatable="false">999999.999999999999</string>
<string-array name="mixin">
<string-array name="mixin" translatable="false">
<item>Ringsize 5</item>
<item>Ringsize 8</item>
<item>Ringsize 13</item>
@ -282,7 +357,7 @@
<item>Priority High</item>
</string-array>
<string-array name="currency">
<string-array name="currency" translatable="false">
<item>XMR</item>
<item>EUR</item>
<item>USD</item>

@ -82,9 +82,7 @@
</style>
<style name="MoneroText.Balance.Orange">
<item name="android:textSize">32sp</item>
<item name="android:textColor">@color/gradientOrange</item>
<item name="android:textStyle">bold</item>
</style>
<style name="MoneroText.Unconfirmed">
@ -119,7 +117,7 @@
<!-- textSize is in dp here to match the layout of the numpad-->
<style name="MoneroLabel.NumPad">
<item name="android:textSize">36dp</item>
<item name="android:textSize">36sp</item>
<item name="android:textColor">@color/moneroBlack</item>
</style>
@ -175,15 +173,15 @@
</style>
<style name="MoneroText.Label">
<item name="android:textSize">18dp</item>
<item name="android:textSize">18sp</item>
</style>
<style name="MoneroText.Confirm">
<item name="android:textSize">16dp</item>
<item name="android:textSize">16sp</item>
</style>
<style name="MoneroText.Confirm.Label">
<item name="android:textSize">16dp</item>
<item name="android:textSize">16sp</item>
<item name="android:textStyle">bold</item>
</style>
@ -193,15 +191,15 @@
</style>
<style name="MoneroText.Label.Medium">
<item name="android:textSize">14dp</item>
<item name="android:textSize">14sp</item>
</style>
<style name="MoneroText.Small">
<item name="android:textSize">10dp</item>
<item name="android:textSize">10sp</item>
</style>
<style name="MoneroText.Medium">
<item name="android:textSize">12dp</item>
<item name="android:textSize">12sp</item>
</style>
<style name="MoneroText.Medium.Bold">
@ -209,13 +207,19 @@
</style>
<style name="MoneroText.Large">
<item name="android:textSize">24dp</item>
<item name="android:textSize">24sp</item>
</style>
<style name="MoneroText.Sucess">
<item name="android:textSize">40dp</item>
<item name="android:textColor">@color/gradientOrange</item>
<style name="MoneroText.Success">
<item name="android:textSize">18sp</item>
<item name="android:textColor">@color/dotGray</item>
<item name="android:textAllCaps">true</item>
<item name="android:textStyle">bold</item>
</style>
<style name="MoneroText.Gray.SuccessFee">
<item name="android:textSize">18sp</item>
</style>
<style name="MoneroText.Button" parent="@android:style/TextAppearance.DeviceDefault.Medium">

@ -0,0 +1,55 @@
/*
* 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 com.m2049r.xmrwallet.util;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
public class BitcoinAddressValidatorTest {
@Test
public void validateBTC_shouldValidate() {
assertTrue(BitcoinAddressValidator.validate("1AGNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62i", false));
assertTrue(BitcoinAddressValidator.validate("1Q1pE5vPGEEMqRcVRMbtBK842Y6Pzo6nK9", false));
assertTrue(BitcoinAddressValidator.validate("3R2MPpTNQLCNs13qnHz89Rm82jQ27bAwft", false));
assertTrue(BitcoinAddressValidator.validate("34QjytsE8GVRbUBvYNheftqJ5CHfDHvQRD", false));
assertTrue(BitcoinAddressValidator.validate("3GsAUrD4dnCqtaTTUzzsQWoymHNkEFrgGF", false));
assertTrue(BitcoinAddressValidator.validate("3NagLCvw8fLwtoUrK7s2mJPy9k6hoyWvTU", false));
assertTrue(BitcoinAddressValidator.validate("mjn127C5wRQCULksMYMFHLp9UTdQuCfbZ9", true));
}
@Test
public void validateBTC_shouldNotValidate() {
assertTrue(BitcoinAddressValidator.validate("3NagLCvw8fLwtoUrK7s2mJPy9k6hoyWvTU", false));
assertTrue(BitcoinAddressValidator.validate("mjn127C5wRQCULksMYMFHLp9UTdQuCfbZ9", true));
assertTrue(!BitcoinAddressValidator.validate("1AGNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62j", true));
assertTrue(!BitcoinAddressValidator.validate("1AGNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62X", true));
assertTrue(!BitcoinAddressValidator.validate("1ANNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62i", true));
assertTrue(!BitcoinAddressValidator.validate("1A Na15ZQXAZUgFiqJ2i7Z2DPU2J6hW62i", true));
assertTrue(!BitcoinAddressValidator.validate("BZbvjr", true));
assertTrue(!BitcoinAddressValidator.validate("i55j", true));
assertTrue(!BitcoinAddressValidator.validate("1AGNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62!", true));
assertTrue(!BitcoinAddressValidator.validate("1AGNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62iz", false));
assertTrue(!BitcoinAddressValidator.validate("1AGNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62izz", false));
assertTrue(!BitcoinAddressValidator.validate("1Q1pE5vPGEEMqRcVRMbtBK842Y6Pzo6nJ9", false));
assertTrue(!BitcoinAddressValidator.validate("1AGNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62I", false));
assertTrue(!BitcoinAddressValidator.validate("3NagLCvw8fLwtoUrK7s2mJPy9k6hoyWvTU ", false));
assertTrue(!BitcoinAddressValidator.validate(" 3NagLCvw8fLwtoUrK7s2mJPy9k6hoyWvTU ", false));
}
}

@ -0,0 +1,103 @@
/*
* 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 com.m2049r.xmrwallet.util;
import org.junit.Test;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class UserNoteTest {
@Test
public void createFromTxNote_noNote() {
UserNotes userNotes = new UserNotes("{xmrto-iyrpxU,0.009BTC,mjn127C5wRQCULksMYMFHLp9UTdQuCfbZ9}");
assertTrue("xmrto-iyrpxU".equals(userNotes.xmrtoKey));
assertTrue("0.009".equals(userNotes.xmrtoAmount));
assertTrue("mjn127C5wRQCULksMYMFHLp9UTdQuCfbZ9".equals(userNotes.xmrtoDestination));
assertTrue(userNotes.note.isEmpty());
}
@Test
public void createFromTxNote_withNote() {
UserNotes userNotes = new UserNotes("{xmrto-iyrpxU,0.009BTC,mjn127C5wRQCULksMYMFHLp9UTdQuCfbZ9} aNote");
assertTrue("xmrto-iyrpxU".equals(userNotes.xmrtoKey));
assertTrue("0.009".equals(userNotes.xmrtoAmount));
assertTrue("mjn127C5wRQCULksMYMFHLp9UTdQuCfbZ9".equals(userNotes.xmrtoDestination));
assertTrue("aNote".equals(userNotes.note));
}
@Test
public void createFromTxNote_withNoteNoSpace() {
UserNotes userNotes = new UserNotes("{xmrto-iyrpxU,0.009BTC,mjn127C5wRQCULksMYMFHLp9UTdQuCfbZ9}aNote");
assertTrue("xmrto-iyrpxU".equals(userNotes.xmrtoKey));
assertTrue("0.009".equals(userNotes.xmrtoAmount));
assertTrue("mjn127C5wRQCULksMYMFHLp9UTdQuCfbZ9".equals(userNotes.xmrtoDestination));
assertTrue("aNote".equals(userNotes.note));
}
@Test
public void createFromTxNote_brokenA() {
String brokenNote = "{mrto-iyrpxU,0.009BTC,mjn127C5wRQCULksMYMFHLp9UTdQuCfbZ9}";
UserNotes userNotes = new UserNotes(brokenNote);
assertNull(userNotes.xmrtoKey);
assertNull(userNotes.xmrtoAmount);
assertNull(userNotes.xmrtoDestination);
assertTrue(brokenNote.equals(userNotes.note));
}
@Test
public void createFromTxNote_brokenB() {
String brokenNote = "{xmrto-iyrpxU,0.009BTC,mjn127C5wRQCULksMYMFHLp9UTdQuCfbZ9";
UserNotes userNotes = new UserNotes(brokenNote);
assertNull(userNotes.xmrtoKey);
assertNull(userNotes.xmrtoAmount);
assertNull(userNotes.xmrtoDestination);
assertTrue(brokenNote.equals(userNotes.note));
}
@Test
public void createFromTxNote_normal() {
String aNote = "aNote";
UserNotes userNotes = new UserNotes(aNote);
assertNull(userNotes.xmrtoKey);
assertNull(userNotes.xmrtoAmount);
assertNull(userNotes.xmrtoDestination);
assertTrue(aNote.equals(userNotes.note));
}
@Test
public void createFromTxNote_empty() {
String aNote = "";
UserNotes userNotes = new UserNotes(aNote);
assertNull(userNotes.xmrtoKey);
assertNull(userNotes.xmrtoAmount);
assertNull(userNotes.xmrtoDestination);
assertTrue(aNote.equals(userNotes.note));
}
@Test
public void createFromTxNote_null() {
UserNotes userNotes = new UserNotes(null);
assertNull(userNotes.xmrtoKey);
assertNull(userNotes.xmrtoAmount);
assertNull(userNotes.xmrtoDestination);
assertNotNull(userNotes.note);
assertTrue(userNotes.note.isEmpty());
}
}

@ -0,0 +1,194 @@
/*
* Copyright (c) 2017 m2049r et al.
*
* 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 com.m2049r.xmrwallet.xmrto.network;
import com.m2049r.xmrwallet.xmrto.api.XmrToCallback;
import com.m2049r.xmrwallet.xmrto.XmrToError;
import com.m2049r.xmrwallet.xmrto.XmrToException;
import com.m2049r.xmrwallet.xmrto.api.CreateOrder;
import com.m2049r.xmrwallet.xmrto.api.XmrToApi;
import net.jodah.concurrentunit.Waiter;
import org.json.JSONException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.concurrent.TimeoutException;
import okhttp3.OkHttpClient;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import static org.junit.Assert.assertEquals;
public class XmrToApiCreateOrderTest {
private MockWebServer mockWebServer;
private XmrToApi xmrToApi;
private OkHttpClient okHttpClient = new OkHttpClient();
private Waiter waiter;
@Mock
XmrToCallback<CreateOrder> mockOrderXmrToCallback;
@Before
public void setUp() throws Exception {
mockWebServer = new MockWebServer();
mockWebServer.start();
waiter = new Waiter();
MockitoAnnotations.initMocks(this);
xmrToApi = new XmrToApiImpl(okHttpClient, mockWebServer.url("/"));
}
@After
public void tearDown() throws Exception {
mockWebServer.shutdown();
}
@Test
public void createOrder_shouldBePostMethod()
throws InterruptedException, TimeoutException {
xmrToApi.createOrder(0.5, "btcsomething", mockOrderXmrToCallback);
RecordedRequest request = mockWebServer.takeRequest();
assertEquals("POST", request.getMethod());
}
@Test
public void createOrder_shouldBeContentTypeJson()
throws InterruptedException, TimeoutException {
xmrToApi.createOrder(0.5, "19y91nJyzXsLEuR7Nj9pc3o5SeHNc8A9RW", mockOrderXmrToCallback);
RecordedRequest request = mockWebServer.takeRequest();
assertEquals("application/json; charset=utf-8", request.getHeader("Content-Type"));
}
@Test
public void createOrder_shouldContainValidBody()
throws InterruptedException, TimeoutException {
final String validBody = "{\"btc_amount\":0.1,\"btc_dest_address\":\"19y91nJyzXsLEuR7Nj9pc3o5SeHNc8A9RW\"}";
xmrToApi.createOrder(0.1, "19y91nJyzXsLEuR7Nj9pc3o5SeHNc8A9RW", mockOrderXmrToCallback);
RecordedRequest request = mockWebServer.takeRequest();
String body = request.getBody().readUtf8();
assertEquals(validBody, body);
}
@Test
public void createOrder_wasSuccessfulShouldRespondWithOrder()
throws InterruptedException, JSONException, TimeoutException {
final double amount = 1.23456789;
final String address = "19y91nJyzXsLEuR7Nj9pc3o5SeHNc8A9RW";
final String uuid = "xmrto-abcdef";
final String state = "TO_BE_CREATED";
MockResponse jsonMockResponse = new MockResponse().setBody(
createMockCreateOrderResponse(amount, address, uuid, state));
mockWebServer.enqueue(jsonMockResponse);
xmrToApi.createOrder(amount, address, new XmrToCallback<CreateOrder>() {
@Override
public void onSuccess(final CreateOrder order) {
waiter.assertEquals(order.getBtcAmount(), amount);
waiter.assertEquals(order.getBtcDestAddress(), address);
waiter.assertEquals(order.getState(), state);
waiter.assertEquals(order.getUuid(), uuid);
waiter.resume();
}
@Override
public void onError(final Exception e) {
waiter.fail(e);
waiter.resume();
}
});
waiter.await();
}
@Test
public void createOrder_wasNotSuccessfulShouldCallOnError()
throws InterruptedException, JSONException, TimeoutException {
mockWebServer.enqueue(new MockResponse().setResponseCode(500));
xmrToApi.createOrder(0.5, "19y91nJyzXsLEuR7Nj9pc3o5SeHNc8A9RW", new XmrToCallback<CreateOrder>() {
@Override
public void onSuccess(final CreateOrder order) {
waiter.fail();
waiter.resume();
}
@Override
public void onError(final Exception e) {
waiter.assertTrue(e instanceof XmrToException);
waiter.assertTrue(((XmrToException) e).getCode() == 500);
waiter.resume();
}
});
waiter.await();
}
@Test
public void createOrder_malformedAddressShouldCallOnError()
throws InterruptedException, JSONException, TimeoutException {
mockWebServer.enqueue(new MockResponse().
setResponseCode(400).
setBody("{\"error_msg\":\"malformed bitcoin address\",\"error\":\"XMRTO-ERROR-002\"}"));
xmrToApi.createOrder(0.5, "xxx", new XmrToCallback<CreateOrder>() {
@Override
public void onSuccess(final CreateOrder order) {
waiter.fail();
waiter.resume();
}
@Override
public void onError(final Exception e) {
waiter.assertTrue(e instanceof XmrToException);
XmrToException xmrEx = (XmrToException) e;
waiter.assertTrue(xmrEx.getCode() == 400);
waiter.assertNotNull(xmrEx.getError());
waiter.assertEquals(xmrEx.getError().getErrorId(), XmrToError.Error.XMRTO_ERROR_002);
waiter.assertEquals(xmrEx.getError().getErrorMsg(), "malformed bitcoin address");
waiter.resume();
}
});
waiter.await();
}
private String createMockCreateOrderResponse(final double amount, final String address,
final String uuid, final String state) {
return "{\n"
+ " \"state\": \"" + state + "\",\n"
+ " \"btc_amount\": \"" + amount + "\",\n"
+ " \"btc_dest_address\": \"" + address + "\",\n"
+ " \"uuid\": \"" + uuid + "\"\n"
+ "}";
}
}

@ -0,0 +1,179 @@
/*
* Copyright (c) 2017 m2049r et al.
*
* 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 com.m2049r.xmrwallet.xmrto.network;
import com.m2049r.xmrwallet.xmrto.api.XmrToCallback;
import com.m2049r.xmrwallet.xmrto.XmrToError;
import com.m2049r.xmrwallet.xmrto.XmrToException;
import com.m2049r.xmrwallet.xmrto.api.QueryOrderParameters;
import com.m2049r.xmrwallet.xmrto.api.XmrToApi;
import net.jodah.concurrentunit.Waiter;
import org.json.JSONException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.concurrent.TimeoutException;
import okhttp3.OkHttpClient;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import static org.junit.Assert.assertEquals;
public class XmrToApiOrderParameterTest {
private MockWebServer mockWebServer;
private XmrToApi xmrToApi;
private OkHttpClient okHttpClient = new OkHttpClient();
private Waiter waiter;
@Mock
XmrToCallback<QueryOrderParameters> mockParametersXmrToCallback;
@Before
public void setUp() throws Exception {
mockWebServer = new MockWebServer();
mockWebServer.start();
waiter = new Waiter();
MockitoAnnotations.initMocks(this);
xmrToApi = new XmrToApiImpl(okHttpClient, mockWebServer.url("/"));
}
@After
public void tearDown() throws Exception {
mockWebServer.shutdown();
}
@Test
public void orderParameter_shouldBeGetMethod()
throws InterruptedException, TimeoutException {
xmrToApi.queryOrderParameters(mockParametersXmrToCallback);
RecordedRequest request = mockWebServer.takeRequest();
assertEquals("GET", request.getMethod());
}
@Test
public void orderParameter_wasSuccessfulShouldRespondWithParameters()
throws InterruptedException, JSONException, TimeoutException {
final boolean isZeroConfEnabled = true;
final double price = 0.015537;
final double upperLimit = 20.0;
final double lowerLimit = 0.001;
final double zeroConfMaxAmount = 0.1;
MockResponse jsonMockResponse = new MockResponse().setBody(
createMockOrderParameterResponse(isZeroConfEnabled, price, upperLimit, lowerLimit, zeroConfMaxAmount));
mockWebServer.enqueue(jsonMockResponse);
xmrToApi.queryOrderParameters(new XmrToCallback<QueryOrderParameters>() {
@Override
public void onSuccess(final QueryOrderParameters orderParameter) {
waiter.assertEquals(orderParameter.getLowerLimit(), lowerLimit);
waiter.assertEquals(orderParameter.getUpperLimit(), upperLimit);
waiter.assertEquals(orderParameter.getPrice(), price);
waiter.assertEquals(orderParameter.getZeroConfMaxAmount(), zeroConfMaxAmount);
waiter.assertEquals(orderParameter.isZeroConfEnabled(), isZeroConfEnabled);
waiter.resume();
}
@Override
public void onError(final Exception e) {
waiter.fail(e);
waiter.resume();
}
});
waiter.await();
}
@Test
public void orderParameter_wasNotSuccessfulShouldCallOnError()
throws InterruptedException, JSONException, TimeoutException {
mockWebServer.enqueue(new MockResponse().setResponseCode(500));
xmrToApi.queryOrderParameters(new XmrToCallback<QueryOrderParameters>() {
@Override
public void onSuccess(final QueryOrderParameters orderParameter) {
waiter.fail();
waiter.resume();
}
@Override
public void onError(final Exception e) {
waiter.assertTrue(e instanceof XmrToException);
waiter.assertTrue(((XmrToException) e).getCode() == 500);
waiter.resume();
}
});
waiter.await();
}
@Test
public void orderParameter_thirdPartyServiceNotAvailableShouldCallOnError()
throws InterruptedException, JSONException, TimeoutException {
mockWebServer.enqueue(new MockResponse().
setResponseCode(503).
setBody("{\"error_msg\":\"third party service not available\",\"error\":\"XMRTO-ERROR-007\"}"));
xmrToApi.queryOrderParameters(new XmrToCallback<QueryOrderParameters>() {
@Override
public void onSuccess(final QueryOrderParameters orderParameter) {
waiter.fail();
waiter.resume();
}
@Override
public void onError(final Exception e) {
waiter.assertTrue(e instanceof XmrToException);
XmrToException xmrEx = (XmrToException) e;
waiter.assertTrue(xmrEx.getCode() == 503);
waiter.assertNotNull(xmrEx.getError());
waiter.assertEquals(xmrEx.getError().getErrorId(), XmrToError.Error.XMRTO_ERROR_007);
waiter.assertEquals(xmrEx.getError().getErrorMsg(), "third party service not available");
waiter.resume();
}
});
waiter.await();
}
private String createMockOrderParameterResponse(
final boolean isZeroConfEnabled,
final double price,
final double upperLimit,
final double lowerLimit,
final double zeroConfMaxAmount) {
return "{\n"
+ " \"zero_conf_enabled\": \"" + isZeroConfEnabled + "\",\n"
+ " \"price\": \"" + price + "\",\n"
+ " \"upper_limit\": \"" + upperLimit + "\",\n"
+ " \"lower_limit\": \"" + lowerLimit + "\",\n"
+ " \"zero_conf_max_amount\": \"" + zeroConfMaxAmount + "\"\n"
+ "}";
}
}

@ -0,0 +1,300 @@
/*
* Copyright (c) 2017 m2049r et al.
*
* 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 com.m2049r.xmrwallet.xmrto.network;
import com.m2049r.xmrwallet.xmrto.api.XmrToCallback;
import com.m2049r.xmrwallet.xmrto.XmrToError;
import com.m2049r.xmrwallet.xmrto.XmrToException;
import com.m2049r.xmrwallet.xmrto.api.QueryOrderStatus;
import com.m2049r.xmrwallet.xmrto.api.XmrToApi;
import net.jodah.concurrentunit.Waiter;
import org.json.JSONException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeoutException;
import okhttp3.OkHttpClient;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import static org.junit.Assert.assertEquals;
public class XmrToApiQueryOrderTest {
static final SimpleDateFormat DATETIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
static Date ParseDate(String dateString) throws ParseException {
return DATETIME_FORMATTER.parse(dateString.replaceAll("Z$", "+0000"));
}
private MockWebServer mockWebServer;
private XmrToApi xmrToApi;
private OkHttpClient okHttpClient = new OkHttpClient();
private Waiter waiter;
@Mock
XmrToCallback<QueryOrderStatus> mockQueryXmrToCallback;
@Before
public void setUp() throws Exception {
mockWebServer = new MockWebServer();
mockWebServer.start();
waiter = new Waiter();
MockitoAnnotations.initMocks(this);
xmrToApi = new XmrToApiImpl(okHttpClient, mockWebServer.url("/"));
}
@After
public void tearDown() throws Exception {
mockWebServer.shutdown();
}
@Test
public void orderStatus_shouldBePostMethod()
throws InterruptedException, TimeoutException {
xmrToApi.queryOrderStatus("xmrto - efMsiU", mockQueryXmrToCallback);
RecordedRequest request = mockWebServer.takeRequest();
assertEquals("POST", request.getMethod());
}
@Test
public void orderStatus_shouldBeContentTypeJson()
throws InterruptedException, TimeoutException {
xmrToApi.queryOrderStatus("xmrto - efMsiU", mockQueryXmrToCallback);
RecordedRequest request = mockWebServer.takeRequest();
assertEquals("application/json; charset=utf-8", request.getHeader("Content-Type"));
}
@Test
public void orderStatus_shouldContainValidBody()
throws InterruptedException, TimeoutException {
final String validBody = "{\"uuid\":\"xmrto - efMsiU\"}";
xmrToApi.queryOrderStatus("xmrto - efMsiU", mockQueryXmrToCallback);
RecordedRequest request = mockWebServer.takeRequest();
String body = request.getBody().readUtf8();
assertEquals(validBody, body);
}
@Test
public void orderStatus_wasSuccessfulShouldRespondWithOrder()
throws InterruptedException, JSONException, TimeoutException {
//TODO: state enum
// TODO dates are dates
final String state = "UNPAID";
final double btcAmount = 0.1;
final String btcDestAddress = "1FhnVJi2V1k4MqXm2nHoEbY5LV7FPai7bb";
final String uuid = "xmrto - efMsiU";
final int btcNumConfirmations = 0;
final int btcNumConfirmationsBeforePurge = 144;
final String btcTransactionId = "";
final String createdAt = "2017-11-17T12:20:02Z";
final String expiresAt = "2017-11-17T12:35:02Z";
final int secondsTillTimeout = 882;
final double xmrAmountTotal = 6.464;
final double xmrAmountRemaining = 6.464;
final int xmrNumConfirmationsRemaining = -1;
final double xmrPriceBtc = 0.0154703;
final String xmrReceivingAddress = "44TVPcCSHebEQp4LnapPkhb2pondb2Ed7GJJLc6TkKwtSyumUnQ6QzkCCkojZycH2MRfLcujCM7QR1gdnRULRraV4UpB5n4";
final String xmrReceivingIntegratedAddress = "4EAAQR1vtv7EQp4LnapPkhb2pondb2Ed7GJJLc6TkKwtSyumUnQ6QzkCCkojZycH2MRfLcujCM7QR1gdnRULRraV6B5rRtHLeXGQSECXy9";
final int xmrRecommendedMixin = 5;
final double xmrRequiredAmount = 6.464;
final String xmrRequiredPaymentIdLong = "56beabc3ca6d52a78c9a44cefebeb870054d8b367cc7065bff1bdb553caca85c";
final String xmrRequiredPaymentIdShort = "eeb6086436b267cf";
MockResponse jsonMockResponse = new MockResponse().setBody(
createMockQueryOrderResponse(
state,
btcAmount,
btcDestAddress,
uuid,
btcNumConfirmations,
btcNumConfirmationsBeforePurge,
btcTransactionId,
createdAt,
expiresAt,
secondsTillTimeout,
xmrAmountTotal,
xmrAmountRemaining,
xmrNumConfirmationsRemaining,
xmrPriceBtc,
xmrReceivingAddress,
xmrReceivingIntegratedAddress,
xmrRecommendedMixin,
xmrRequiredAmount,
xmrRequiredPaymentIdLong,
xmrRequiredPaymentIdShort));
mockWebServer.enqueue(jsonMockResponse);
xmrToApi.queryOrderStatus(uuid, new XmrToCallback<QueryOrderStatus>() {
@Override
public void onSuccess(final QueryOrderStatus orderStatus) {
waiter.assertEquals(orderStatus.getState().toString(), state);
waiter.assertEquals(orderStatus.getBtcAmount(), btcAmount);
waiter.assertEquals(orderStatus.getBtcDestAddress(), btcDestAddress);
waiter.assertEquals(orderStatus.getUuid(), uuid);
waiter.assertEquals(orderStatus.getBtcNumConfirmations(), btcNumConfirmations);
waiter.assertEquals(orderStatus.getBtcNumConfirmationsBeforePurge(), btcNumConfirmationsBeforePurge);
waiter.assertEquals(orderStatus.getBtcTransactionId(), btcTransactionId);
try {
waiter.assertEquals(orderStatus.getCreatedAt(), ParseDate(createdAt));
waiter.assertEquals(orderStatus.getExpiresAt(), ParseDate(expiresAt));
} catch (ParseException ex) {
waiter.fail(ex);
}
waiter.assertEquals(orderStatus.getSecondsTillTimeout(), secondsTillTimeout);
waiter.assertEquals(orderStatus.getXmrAmountTotal(), xmrAmountTotal);
waiter.assertEquals(orderStatus.getXmrAmountRemaining(), xmrAmountRemaining);
waiter.assertEquals(orderStatus.getXmrNumConfirmationsRemaining(), xmrNumConfirmationsRemaining);
waiter.assertEquals(orderStatus.getXmrPriceBtc(), xmrPriceBtc);
waiter.assertEquals(orderStatus.getXmrReceivingAddress(), xmrReceivingAddress);
waiter.assertEquals(orderStatus.getXmrReceivingIntegratedAddress(), xmrReceivingIntegratedAddress);
waiter.assertEquals(orderStatus.getXmrRecommendedMixin(), xmrRecommendedMixin);
waiter.assertEquals(orderStatus.getXmrRequiredAmount(), xmrRequiredAmount);
waiter.assertEquals(orderStatus.getXmrRequiredPaymentIdLong(), xmrRequiredPaymentIdLong);
waiter.assertEquals(orderStatus.getXmrRequiredPaymentIdShort(), xmrRequiredPaymentIdShort);
waiter.resume();
}
@Override
public void onError(final Exception e) {
waiter.fail(e);
waiter.resume();
}
});
waiter.await();
}
@Test
public void orderStatus_wasNotSuccessfulShouldCallOnError()
throws InterruptedException, JSONException, TimeoutException {
mockWebServer.enqueue(new MockResponse().setResponseCode(500));
xmrToApi.queryOrderStatus("xmrto - efMsiU", new XmrToCallback<QueryOrderStatus>() {
@Override
public void onSuccess(final QueryOrderStatus orderStatus) {
waiter.fail();
waiter.resume();
}
@Override
public void onError(final Exception e) {
waiter.assertTrue(e instanceof XmrToException);
waiter.assertTrue(((XmrToException) e).getCode() == 500);
waiter.resume();
}
});
waiter.await();
}
@Test
public void orderStatus_orderNotFoundShouldCallOnError()
throws InterruptedException, JSONException, TimeoutException {
mockWebServer.enqueue(new MockResponse().
setResponseCode(404).
setBody("{\"error_msg\":\"requested order not found\",\"error\":\"XMRTO-ERROR-006\"}"));
xmrToApi.queryOrderStatus("xmrto - efMsiU", new XmrToCallback<QueryOrderStatus>() {
@Override
public void onSuccess(final QueryOrderStatus orderStatus) {
waiter.fail();
waiter.resume();
}
@Override
public void onError(final Exception e) {
waiter.assertTrue(e instanceof XmrToException);
XmrToException xmrEx = (XmrToException) e;
waiter.assertTrue(xmrEx.getCode() == 404);
waiter.assertNotNull(xmrEx.getError());
waiter.assertEquals(xmrEx.getError().getErrorId(), XmrToError.Error.XMRTO_ERROR_006);
waiter.assertEquals(xmrEx.getError().getErrorMsg(), "requested order not found");
waiter.resume();
}
});
waiter.await();
}
private String createMockQueryOrderResponse(
final String state,
final double btcAmount,
final String btcDestAddress,
final String uuid,
final int btcNumConfirmations,
final int btcNumConfirmationsBeforePurge,
final String btcTransactionId,
final String createdAt,
final String expiresAt,
final int secondsTillTimeout,
final double xmrAmountTotal,
final double xmrAmountRemaining,
final int xmrNumConfirmationsRemaining,
final double xmrPriceBtc,
final String xmrReceivingAddress,
final String xmrReceivingIntegratedAddress,
final int xmrRecommendedMixin,
final double xmrRequiredAmount,
final String xmrRequiredPaymentIdLong,
final String xmrRequiredPaymentIdShort
) {
return "{\n" +
" \"xmr_price_btc\": \"" + xmrPriceBtc + "\",\n" +
" \"uuid\":\"" + uuid + "\",\n" +
" \"state\":\"" + state + "\",\n" +
" \"btc_amount\":\"" + btcAmount + "\",\n" +
" \"btc_dest_address\":\"" + btcDestAddress + "\",\n" +
" \"xmr_required_amount\":\"" + xmrRequiredAmount + "\",\n" +
" \"xmr_receiving_address\":\"" + xmrReceivingAddress + "\",\n" +
" \"xmr_receiving_integrated_address\":\"" + xmrReceivingIntegratedAddress + "\",\n" +
" \"xmr_required_payment_id_long\":\"" + xmrRequiredPaymentIdLong + "\",\n" +
" \"xmr_required_payment_id_short\":\"" + xmrRequiredPaymentIdShort + "\",\n" +
" \"created_at\":\"" + createdAt + "\",\n" +
" \"expires_at\":\"" + expiresAt + "\",\n" +
" \"seconds_till_timeout\":\"" + secondsTillTimeout + "\",\n" +
" \"xmr_amount_total\":\"" + xmrAmountTotal + "\",\n" +
" \"xmr_amount_remaining\":\"" + xmrAmountRemaining + "\",\n" +
" \"xmr_num_confirmations_remaining\":\"" + xmrNumConfirmationsRemaining + "\",\n" +
" \"xmr_recommended_mixin\":\"" + xmrRecommendedMixin + "\",\n" +
" \"btc_num_confirmations_before_purge\":\"" + btcNumConfirmationsBeforePurge + "\",\n" +
" \"btc_num_confirmations\":\"" + btcNumConfirmations + "\",\n" +
" \"btc_transaction_id\":\"" + btcTransactionId + "\""
+ "}";
}
}
Loading…
Cancel
Save