diff --git a/app/build.gradle b/app/build.gradle index f03c323f..bb70949d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "com.m2049r.xmrwallet" minSdkVersion 21 targetSdkVersion 31 - versionCode 3200 - versionName "3.2.0 'Decoy Selection'" + versionCode 3307 + versionName "3.3.7 'Pocket Change'" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" externalNativeBuild { cmake { diff --git a/app/src/main/cpp/monerujo.cpp b/app/src/main/cpp/monerujo.cpp index a91c58b1..773c8d5a 100644 --- a/app/src/main/cpp/monerujo.cpp +++ b/app/src/main/cpp/monerujo.cpp @@ -34,8 +34,10 @@ extern "C" #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR , LOG_TAG,__VA_ARGS__) static JavaVM *cachedJVM; +static jclass class_String; static jclass class_ArrayList; static jclass class_WalletListener; +static jclass class_CoinsInfo; static jclass class_TransactionInfo; static jclass class_Transfer; static jclass class_Ledger; @@ -43,6 +45,14 @@ static jclass class_WalletStatus; std::mutex _listenerMutex; +//void jstringToString(JNIEnv *env, std::string &str, jstring jstr) { +// if (!jstr) return; +// const int len = env->GetStringUTFLength(jstr); +// const char *chars = env->GetStringUTFChars(jstr, nullptr); +// str.assign(chars, len); +// env->ReleaseStringUTFChars(jstr, chars); +//} + JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) { cachedJVM = jvm; LOGI("JNI_OnLoad"); @@ -52,8 +62,12 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) { } //LOGI("JNI_OnLoad ok"); + class_String = static_cast(jenv->NewGlobalRef( + jenv->FindClass("java/lang/String"))); class_ArrayList = static_cast(jenv->NewGlobalRef( jenv->FindClass("java/util/ArrayList"))); + class_CoinsInfo = static_cast(jenv->NewGlobalRef( + jenv->FindClass("com/m2049r/xmrwallet/model/CoinsInfo"))); class_TransactionInfo = static_cast(jenv->NewGlobalRef( jenv->FindClass("com/m2049r/xmrwallet/model/TransactionInfo"))); class_Transfer = static_cast(jenv->NewGlobalRef( @@ -510,8 +524,8 @@ Java_com_m2049r_xmrwallet_model_WalletManager_startMining(JNIEnv *env, jobject i const char *_address = env->GetStringUTFChars(address, nullptr); bool success = Monero::WalletManagerFactory::getWalletManager()->startMining(std::string(_address), - background_mining, - ignore_battery); + background_mining, + ignore_battery); env->ReleaseStringUTFChars(address, _address); return static_cast(success); } @@ -553,7 +567,7 @@ Java_com_m2049r_xmrwallet_model_WalletManager_closeJ(JNIEnv *env, jobject instan jobject walletInstance) { Monero::Wallet *wallet = getHandle(env, walletInstance); bool closeSuccess = Monero::WalletManagerFactory::getWalletManager()->closeWallet(wallet, - false); + false); if (closeSuccess) { MyWalletListener *walletListener = getHandle(env, walletInstance, "listenerHandle"); @@ -951,6 +965,58 @@ Java_com_m2049r_xmrwallet_model_Wallet_rescanBlockchainAsyncJ(JNIEnv *env, jobje //TODO virtual void setAutoRefreshInterval(int millis) = 0; //TODO virtual int autoRefreshInterval() const = 0; +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_createTransactionMultDest(JNIEnv *env, jobject instance, + jobjectArray destinations, + jstring payment_id, + jlongArray amounts, + jint mixin_count, + jint priority, + jint accountIndex, + jintArray subaddresses) { + std::vector dst_addr; + std::vector amount; + + int destSize = env->GetArrayLength(destinations); + assert(destSize == env->GetArrayLength(amounts)); + jlong *_amounts = env->GetLongArrayElements(amounts, nullptr); + for (int i = 0; i < destSize; i++) { + jstring dest = (jstring) env->GetObjectArrayElement(destinations, i); + const char *_dest = env->GetStringUTFChars(dest, nullptr); + dst_addr.emplace_back(_dest); + env->ReleaseStringUTFChars(dest, _dest); + amount.emplace_back((uint64_t) _amounts[i]); + } + env->ReleaseLongArrayElements(amounts, _amounts, 0); + + std::set subaddr_indices; + if (subaddresses != nullptr) { + int subaddrSize = env->GetArrayLength(subaddresses); + jint *_subaddresses = env->GetIntArrayElements(subaddresses, nullptr); + for (int i = 0; i < subaddrSize; i++) { + subaddr_indices.insert((uint32_t) _subaddresses[i]); + } + env->ReleaseIntArrayElements(subaddresses, _subaddresses, 0); + } + + const char *_payment_id = env->GetStringUTFChars(payment_id, nullptr); + + Monero::PendingTransaction::Priority _priority = + static_cast(priority); + + Monero::Wallet *wallet = getHandle(env, instance); + + Monero::PendingTransaction *tx = + wallet->createTransactionMultDest(dst_addr, _payment_id, + amount, (uint32_t) mixin_count, + _priority, + (uint32_t) accountIndex, + subaddr_indices); + + env->ReleaseStringUTFChars(payment_id, _payment_id); + return reinterpret_cast(tx); +} + JNIEXPORT jlong JNICALL Java_com_m2049r_xmrwallet_model_Wallet_createTransactionJ(JNIEnv *env, jobject instance, jstring dst_addr, jstring payment_id, @@ -965,9 +1031,9 @@ Java_com_m2049r_xmrwallet_model_Wallet_createTransactionJ(JNIEnv *env, jobject i Monero::Wallet *wallet = getHandle(env, instance); Monero::PendingTransaction *tx = wallet->createTransaction(_dst_addr, _payment_id, - amount, (uint32_t) mixin_count, - _priority, - (uint32_t) accountIndex); + amount, (uint32_t) mixin_count, + _priority, + (uint32_t) accountIndex); env->ReleaseStringUTFChars(dst_addr, _dst_addr); env->ReleaseStringUTFChars(payment_id, _payment_id); @@ -990,9 +1056,9 @@ Java_com_m2049r_xmrwallet_model_Wallet_createSweepTransaction(JNIEnv *env, jobje Monero::optional empty; Monero::PendingTransaction *tx = wallet->createTransaction(_dst_addr, _payment_id, - empty, (uint32_t) mixin_count, - _priority, - (uint32_t) accountIndex); + empty, (uint32_t) mixin_count, + _priority, + (uint32_t) accountIndex); env->ReleaseStringUTFChars(dst_addr, _dst_addr); env->ReleaseStringUTFChars(payment_id, _payment_id); @@ -1019,6 +1085,36 @@ Java_com_m2049r_xmrwallet_model_Wallet_disposeTransaction(JNIEnv *env, jobject i wallet->disposeTransaction(_pendingTransaction); } +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_estimateTransactionFee(JNIEnv *env, jobject instance, + jobjectArray addresses, + jlongArray amounts, + jint priority) { + + std::vector> destinations; + + int destSize = env->GetArrayLength(addresses); + assert(destSize == env->GetArrayLength(amounts)); + jlong *_amounts = env->GetLongArrayElements(amounts, nullptr); + for (int i = 0; i < destSize; i++) { + std::pair pair; + jstring dest = (jstring) env->GetObjectArrayElement(addresses, i); + const char *_dest = env->GetStringUTFChars(dest, nullptr); + pair.first = _dest; + env->ReleaseStringUTFChars(dest, _dest); + pair.second = ((uint64_t) _amounts[i]); + destinations.emplace_back(pair); + } + env->ReleaseLongArrayElements(amounts, _amounts, 0); + + Monero::PendingTransaction::Priority _priority = + static_cast(priority); + + Monero::Wallet *wallet = getHandle(env, instance); + + return static_cast(wallet->estimateTransactionFee(destinations, _priority)); +} + //virtual bool exportKeyImages(const std::string &filename) = 0; //virtual bool importKeyImages(const std::string &filename) = 0; @@ -1032,6 +1128,12 @@ Java_com_m2049r_xmrwallet_model_Wallet_getHistoryJ(JNIEnv *env, jobject instance //virtual AddressBook * addressBook() const = 0; +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_getCoinsJ(JNIEnv *env, jobject instance) { + Monero::Wallet *wallet = getHandle(env, instance); + return reinterpret_cast(wallet->coins()); +} + JNIEXPORT jlong JNICALL Java_com_m2049r_xmrwallet_model_Wallet_setListenerJ(JNIEnv *env, jobject instance, jobject javaListener) { @@ -1202,7 +1304,7 @@ Java_com_m2049r_xmrwallet_model_Wallet_getLastSubaddress(JNIEnv *env, jobject in JNIEXPORT jint JNICALL Java_com_m2049r_xmrwallet_model_TransactionHistory_getCount(JNIEnv *env, jobject instance) { Monero::TransactionHistory *history = getHandle(env, - instance); + instance); return history->count(); } @@ -1270,7 +1372,62 @@ jobject newTransactionInfo(JNIEnv *env, Monero::TransactionInfo *info) { #include #include -jobject cpp2java(JNIEnv *env, const std::vector &vector) { +// Coins + +jobject newCoinsInfo(JNIEnv *env, Monero::CoinsInfo *info) { + jstring _hash = env->NewStringUTF(info->hash().c_str()); + + jmethodID c = env->GetMethodID(class_CoinsInfo, "", "(IIJJLjava/lang/String;ZZJZ)V"); + jobject result = env->NewObject(class_CoinsInfo, c, + static_cast (info->subaddrAccount()), + static_cast (info->subaddrIndex()), + static_cast (info->amount()), + static_cast (info->blockHeight()), + _hash, + info->spent(), + info->frozen(), + static_cast (info->unlockTime()), + info->unlocked()); + env->DeleteLocalRef(_hash); + return result; +} + +jobject coinsInfoArrayList(JNIEnv *env, const std::vector &vector, + uint32_t accountIndex, bool unspentOnly) { + + jmethodID java_util_ArrayList_ = env->GetMethodID(class_ArrayList, "", "(I)V"); + jmethodID java_util_ArrayList_add = env->GetMethodID(class_ArrayList, "add", + "(Ljava/lang/Object;)Z"); + + jobject arrayList = env->NewObject(class_ArrayList, java_util_ArrayList_, + static_cast (vector.size())); + for (Monero::CoinsInfo *s: vector) { + if (s->subaddrAccount() != accountIndex) continue; + if (s->spent() && unspentOnly) continue; + jobject info = newCoinsInfo(env, s); + env->CallBooleanMethod(arrayList, java_util_ArrayList_add, info); + env->DeleteLocalRef(info); + } + return arrayList; +} + +JNIEXPORT jint JNICALL +Java_com_m2049r_xmrwallet_model_Coins_getCount(JNIEnv *env, jobject instance) { + Monero::Coins *coins = getHandle(env, instance); + return coins->count(); +} + +JNIEXPORT jobject JNICALL +Java_com_m2049r_xmrwallet_model_Coins_refresh(JNIEnv *env, jobject instance, jint accountIndex, + jboolean unspentOnly) { + Monero::Coins *coins = getHandle(env, instance); + coins->refresh(); + return coinsInfoArrayList(env, coins->getAll(), (uint32_t) accountIndex, unspentOnly); +} + +jobject +transactionInfoArrayList(JNIEnv *env, const std::vector &vector, + uint32_t accountIndex) { jmethodID java_util_ArrayList_ = env->GetMethodID(class_ArrayList, "", "(I)V"); jmethodID java_util_ArrayList_add = env->GetMethodID(class_ArrayList, "add", @@ -1279,6 +1436,7 @@ jobject cpp2java(JNIEnv *env, const std::vector &vect jobject arrayList = env->NewObject(class_ArrayList, java_util_ArrayList_, static_cast (vector.size())); for (Monero::TransactionInfo *s: vector) { + if (s->subaddrAccount() != accountIndex) continue; jobject info = newTransactionInfo(env, s); env->CallBooleanMethod(arrayList, java_util_ArrayList_add, info); env->DeleteLocalRef(info); @@ -1287,11 +1445,12 @@ jobject cpp2java(JNIEnv *env, const std::vector &vect } JNIEXPORT jobject JNICALL -Java_com_m2049r_xmrwallet_model_TransactionHistory_refreshJ(JNIEnv *env, jobject instance) { +Java_com_m2049r_xmrwallet_model_TransactionHistory_refreshJ(JNIEnv *env, jobject instance, + jint accountIndex) { Monero::TransactionHistory *history = getHandle(env, - instance); + instance); history->refresh(); - return cpp2java(env, history->getAll()); + return transactionInfoArrayList(env, history->getAll(), (uint32_t) accountIndex); } // TransactionInfo is implemented in Java - no need here @@ -1326,19 +1485,19 @@ Java_com_m2049r_xmrwallet_model_PendingTransaction_commit(JNIEnv *env, jobject i JNIEXPORT jlong JNICALL Java_com_m2049r_xmrwallet_model_PendingTransaction_getAmount(JNIEnv *env, jobject instance) { Monero::PendingTransaction *tx = getHandle(env, instance); - return tx->amount(); + return static_cast(tx->amount()); } JNIEXPORT jlong JNICALL Java_com_m2049r_xmrwallet_model_PendingTransaction_getDust(JNIEnv *env, jobject instance) { Monero::PendingTransaction *tx = getHandle(env, instance); - return tx->dust(); + return static_cast(tx->dust()); } JNIEXPORT jlong JNICALL Java_com_m2049r_xmrwallet_model_PendingTransaction_getFee(JNIEnv *env, jobject instance) { Monero::PendingTransaction *tx = getHandle(env, instance); - return tx->fee(); + return static_cast(tx->fee()); } // TODO this returns a vector of strings - deal with this later - for now return first one @@ -1355,7 +1514,7 @@ Java_com_m2049r_xmrwallet_model_PendingTransaction_getFirstTxIdJ(JNIEnv *env, jo JNIEXPORT jlong JNICALL Java_com_m2049r_xmrwallet_model_PendingTransaction_getTxCount(JNIEnv *env, jobject instance) { Monero::PendingTransaction *tx = getHandle(env, instance); - return tx->txCount(); + return static_cast(tx->txCount()); } diff --git a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java index 589eb4cc..6cdc60b2 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java @@ -658,11 +658,11 @@ public class LoginActivity extends BaseActivity break; case NetworkType_Testnet: toolbar.setSubtitle(getString(R.string.connect_testnet)); - toolbar.setBackgroundResource(ThemeHelper.getThemedResourceId(this, R.attr.colorPrimaryDark)); + toolbar.setBackgroundResource(ThemeHelper.getThemedResourceId(this, androidx.appcompat.R.attr.colorPrimaryDark)); break; case NetworkType_Stagenet: toolbar.setSubtitle(getString(R.string.connect_stagenet)); - toolbar.setBackgroundResource(ThemeHelper.getThemedResourceId(this, R.attr.colorPrimaryDark)); + toolbar.setBackgroundResource(ThemeHelper.getThemedResourceId(this, androidx.appcompat.R.attr.colorPrimaryDark)); break; default: throw new IllegalStateException("NetworkType unknown: " + net); diff --git a/app/src/main/java/com/m2049r/xmrwallet/TxFragment.java b/app/src/main/java/com/m2049r/xmrwallet/TxFragment.java index 73556965..e0ec08bf 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/TxFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/TxFragment.java @@ -79,6 +79,7 @@ public class TxFragment extends Fragment { private TextView tvTxPaymentId; private TextView tvTxBlockheight; private TextView tvTxAmount; + private TextView tvTxPocketChangeAmount; private TextView tvTxFee; private TextView tvTxTransfers; private TextView etTxNotes; @@ -116,6 +117,7 @@ public class TxFragment extends Fragment { tvTxPaymentId = view.findViewById(R.id.tvTxPaymentId); tvTxBlockheight = view.findViewById(R.id.tvTxBlockheight); tvTxAmount = view.findViewById(R.id.tvTxAmount); + tvTxPocketChangeAmount = view.findViewById(R.id.tvTxPocketChangeAmount); tvTxFee = view.findViewById(R.id.tvTxFee); tvTxTransfers = view.findViewById(R.id.tvTxTransfers); etTxNotes = view.findViewById(R.id.etTxNotes); @@ -249,8 +251,10 @@ public class TxFragment extends Fragment { } String sign = (info.direction == TransactionInfo.Direction.Direction_In ? "+" : "-"); - long realAmount = info.amount; - tvTxAmount.setText(sign + Wallet.getDisplayAmount(realAmount)); + tvTxAmount.setText(sign + Wallet.getDisplayAmount(info.amount)); + final long pcAmount = info.getPocketChangeAmount(); + tvTxPocketChangeAmount.setVisibility(pcAmount > 0 ? View.VISIBLE : View.GONE); + tvTxPocketChangeAmount.setText(getString(R.string.pocketchange_tx_detail, Wallet.getDisplayAmount(pcAmount))); if ((info.fee > 0)) { String fee = Wallet.getDisplayAmount(info.fee); diff --git a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java index cb65fbac..3112dd94 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java @@ -52,8 +52,8 @@ import com.m2049r.xmrwallet.data.BarcodeData; import com.m2049r.xmrwallet.data.Subaddress; import com.m2049r.xmrwallet.data.TxData; import com.m2049r.xmrwallet.data.UserNotes; -import com.m2049r.xmrwallet.dialog.CreditsFragment; import com.m2049r.xmrwallet.dialog.HelpFragment; +import com.m2049r.xmrwallet.dialog.PocketChangeFragment; import com.m2049r.xmrwallet.fragment.send.SendAddressWizardFragment; import com.m2049r.xmrwallet.fragment.send.SendFragment; import com.m2049r.xmrwallet.ledger.LedgerProgressDialog; @@ -82,7 +82,8 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste WalletFragment.DrawerLocker, NavigationView.OnNavigationItemSelectedListener, SubaddressFragment.Listener, - SubaddressInfoFragment.Listener { + SubaddressInfoFragment.Listener, + PocketChangeFragment.Listener { public static final String REQUEST_ID = "id"; public static final String REQUEST_PW = "pw"; @@ -285,8 +286,6 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste onWalletRescan(); } else if (itemId == R.id.action_info) { onWalletDetails(); - } else if (itemId == R.id.action_credits) { - CreditsFragment.display(getSupportFragmentManager()); } else if (itemId == R.id.action_share) { onShareTxInfo(); } else if (itemId == R.id.action_help_tx_info) { @@ -301,6 +300,8 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste HelpFragment.display(getSupportFragmentManager(), R.string.help_send); } else if (itemId == R.id.action_rename) { onAccountRename(); + } else if (itemId == R.id.action_pocketchange) { + PocketChangeFragment.display(getSupportFragmentManager(), getWallet().getPocketChangeSetting()); } else if (itemId == R.id.action_subaddresses) { showSubaddresses(true); } else if (itemId == R.id.action_streetmode) { @@ -422,7 +423,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste break; case NetworkType_Stagenet: case NetworkType_Testnet: - toolbar.setBackgroundResource(ThemeHelper.getThemedResourceId(this, R.attr.colorPrimaryDark)); + toolbar.setBackgroundResource(ThemeHelper.getThemedResourceId(this, androidx.appcompat.R.attr.colorPrimaryDark)); break; default: throw new IllegalStateException("Unsupported Network: " + WalletManager.getInstance().getNetworkType()); @@ -628,6 +629,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste @Override public void onWalletStarted(final Wallet.Status walletStatus) { + loadPocketChangeSettings(); runOnUiThread(() -> { dismissProgressDialog(); if (walletStatus == null) { @@ -1104,6 +1106,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste public void setAccountIndex(int accountIndex) { getWallet().setAccountIndex(accountIndex); + loadPocketChangeSettings(); selectedSubaddressIndex = 0; } @@ -1214,4 +1217,19 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste b.putInt("subaddressIndex", subaddressIndex); replaceFragmentWithTransition(view, new SubaddressInfoFragment(), null, b); } + + @Override + public void setPocketChange(Wallet.PocketChangeSetting setting) { + SharedPreferences.Editor editor = getPrefs().edit(); + editor.putString(getWallet().getAddress() + "_PC", setting.toPrefString()); + editor.apply(); + getWallet().setPocketChangeSetting(setting); + } + + + public void loadPocketChangeSettings() { + final String settings = getPrefs().getString(getWallet().getAddress() + "_PC", "0"); + getWallet().setPocketChangeSetting(Wallet.PocketChangeSetting.from(settings)); + } + } diff --git a/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java b/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java index dc43045e..a26cb9a2 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java @@ -112,7 +112,7 @@ public class WalletFragment extends Fragment flExchange = view.findViewById(R.id.flExchange); ((ProgressBar) view.findViewById(R.id.pbExchange)).getIndeterminateDrawable(). setColorFilter( - ThemeHelper.getThemedColor(getContext(), R.attr.colorPrimaryVariant), + ThemeHelper.getThemedColor(getContext(), com.google.android.material.R.attr.colorPrimaryVariant), android.graphics.PorterDuff.Mode.MULTIPLY); tvProgress = view.findViewById(R.id.tvProgress); diff --git a/app/src/main/java/com/m2049r/xmrwallet/data/NodeInfo.java b/app/src/main/java/com/m2049r/xmrwallet/data/NodeInfo.java index c77d3267..f9026cb5 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/data/NodeInfo.java +++ b/app/src/main/java/com/m2049r/xmrwallet/data/NodeInfo.java @@ -270,7 +270,7 @@ public class NodeInfo extends Node { (hostAddress.isOnion() ? " .onion  " : ""), " " + info)); view.setText(text); if (isError) - view.setTextColor(ThemeHelper.getThemedColor(ctx, R.attr.colorError)); + view.setTextColor(ThemeHelper.getThemedColor(ctx, androidx.appcompat.R.attr.colorError)); else view.setTextColor(ThemeHelper.getThemedColor(ctx, android.R.attr.textColorSecondary)); } diff --git a/app/src/main/java/com/m2049r/xmrwallet/data/TxData.java b/app/src/main/java/com/m2049r/xmrwallet/data/TxData.java index 144948c9..e7997445 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/data/TxData.java +++ b/app/src/main/java/com/m2049r/xmrwallet/data/TxData.java @@ -19,92 +19,88 @@ package com.m2049r.xmrwallet.data; import android.os.Parcel; import android.os.Parcelable; +import com.m2049r.xmrwallet.model.CoinsInfo; import com.m2049r.xmrwallet.model.PendingTransaction; import com.m2049r.xmrwallet.model.Wallet; -import com.m2049r.xmrwallet.util.Helper; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import timber.log.Timber; // https://stackoverflow.com/questions/2139134/how-to-send-an-object-from-one-android-activity-to-another-using-intents +@ToString public class TxData implements Parcelable { + @Getter + private String[] destinations = new String[1]; + @Getter + private long[] amounts = new long[1]; + @Getter + @Setter + private int mixin; + @Getter + @Setter + private PendingTransaction.Priority priority; + @Getter + private int[] subaddresses; + + @Getter + @Setter + private UserNotes userNotes; public TxData() { } - public TxData(TxData txData) { - this.dstAddr = txData.dstAddr; - this.amount = txData.amount; - this.mixin = txData.mixin; - this.priority = txData.priority; - } - - public TxData(String dstAddr, - long amount, - int mixin, - PendingTransaction.Priority priority) { - this.dstAddr = dstAddr; - this.amount = amount; - this.mixin = mixin; - this.priority = priority; - } - - public String getDestinationAddress() { - return dstAddr; + public String getDestination() { + return destinations[0]; } public long getAmount() { - return amount; + return amounts[0]; } - public double getAmountAsDouble() { - return 1.0 * amount / Helper.ONE_XMR; + public long getPocketChangeAmount() { + long change = 0; + for (int i = 1; i < amounts.length; i++) { + change += amounts[i]; + } + return change; } - public int getMixin() { - return mixin; - } - - public PendingTransaction.Priority getPriority() { - return priority; - } - - public void setDestinationAddress(String dstAddr) { - this.dstAddr = dstAddr; + public void setDestination(String destination) { + destinations[0] = destination; } public void setAmount(long amount) { - this.amount = amount; + amounts[0] = amount; } public void setAmount(double amount) { - this.amount = Wallet.getAmountFromDouble(amount); + setAmount(Wallet.getAmountFromDouble(amount)); } - public void setMixin(int mixin) { - this.mixin = mixin; + private void resetPocketChange() { + if (destinations.length > 1) { + final String destination = getDestination(); + destinations = new String[1]; + destinations[0] = destination; + final long amount = getAmount(); + amounts = new long[1]; + amounts[0] = amount; + } } - 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 long amount; - private int mixin; - private PendingTransaction.Priority priority; - - private UserNotes userNotes; - @Override public void writeToParcel(Parcel out, int flags) { - out.writeString(dstAddr); - out.writeLong(amount); + out.writeInt(destinations.length); + out.writeStringArray(destinations); + out.writeLongArray(amounts); out.writeInt(mixin); out.writeInt(priority.getValue()); } @@ -121,11 +117,13 @@ public class TxData implements Parcelable { }; protected TxData(Parcel in) { - dstAddr = in.readString(); - amount = in.readLong(); + int len = in.readInt(); + destinations = new String[len]; + in.readStringArray(destinations); + amounts = new long[len]; + in.readLongArray(amounts); mixin = in.readInt(); priority = PendingTransaction.Priority.fromInteger(in.readInt()); - } @Override @@ -133,17 +131,117 @@ public class TxData implements Parcelable { return 0; } - @Override - public String toString() { - StringBuffer sb = new StringBuffer(); - sb.append("dstAddr:"); - sb.append(dstAddr); - sb.append(",amount:"); - sb.append(amount); - sb.append(",mixin:"); - sb.append(mixin); - sb.append(",priority:"); - sb.append(priority); - return sb.toString(); + ////////////////////////// + /// PocketChange Stuff /// + ////////////////////////// + + final static public int POCKETCHANGE_IDX = 1; // subaddress index of first pocketchange slot + final static public int POCKETCHANGE_SLOTS = 10; // number of pocketchange slots + final static public int POCKETCHANGE_IDX_MAX = POCKETCHANGE_IDX + POCKETCHANGE_SLOTS - 1; + + @Data + static private class PocketChangeSlot { + private long amount; + private long spendableAmount; + + public void add(CoinsInfo coin) { + amount += coin.getAmount(); + if (coin.isSpendable()) spendableAmount += coin.getAmount(); + } + } + + // returns null if it can't create a PocketChange Transaction + // it assumes there is enough reserve to deal with fees - otherwise we get an error on + // creating the actual transaction + // String destination, long amount are already set! + public void createPocketChange(Wallet wallet) { + Wallet.PocketChangeSetting setting = wallet.getPocketChangeSetting(); + if (!setting.isEnabled()) { + resetPocketChange(); + return; + } + if ((destinations.length != 1) || (destinations[0] == null)) + throw new IllegalStateException("invalid destinations"); + if ((amounts.length != 1)) + throw new IllegalStateException("invalid amount"); + + final long amount = getAmount(); + // find spendable slot, and all non-slot outputs (spendableSubaddressIdx) + int usableSubaddressIdx = -1; + List coins = wallet.getCoinsInfos(true); + Set spendableSubaddressIdx = new HashSet<>(); + PocketChangeSlot reserves = new PocketChangeSlot(); // everything not in a slot spendable + PocketChangeSlot[] slots = new PocketChangeSlot[POCKETCHANGE_SLOTS]; + for (int i = 0; i < POCKETCHANGE_SLOTS; i++) { + slots[i] = new PocketChangeSlot(); + } + for (CoinsInfo coin : coins) { + int subaddressIdx = coin.getAddressIndex(); + if ((subaddressIdx < POCKETCHANGE_IDX) || (subaddressIdx > POCKETCHANGE_IDX_MAX)) { // spendableSubaddressIdx + reserves.add(coin); + spendableSubaddressIdx.add(subaddressIdx); + } else { // PocketChange slot + final int slotIdx = subaddressIdx - POCKETCHANGE_IDX; + slots[slotIdx].add(coin); + if (slots[slotIdx].getSpendableAmount() >= amount) { + usableSubaddressIdx = subaddressIdx; + } + } + } + long spendableAmount = reserves.getSpendableAmount(); + final long pocketChangeAmount = setting.getAmount(); + if (spendableAmount < pocketChangeAmount) + return; // do conventional transaction + Timber.d("usableSubaddressIdx=%d", usableSubaddressIdx); + if (usableSubaddressIdx >= 0) { + spendableSubaddressIdx.add(usableSubaddressIdx); + spendableAmount += slots[usableSubaddressIdx - POCKETCHANGE_IDX].getAmount(); + } else { + // use everything + spendableSubaddressIdx.clear(); + } + spendableAmount -= amount; // reserve the amount we need + // now we have the and all spendableSubaddressIdx subaddresses to use and how much spendableSubaddressIdx we have + // find any slots to fill if possible: + List slotsToFill = new ArrayList<>(); + List slotToFillAmounts = new ArrayList<>(); + for (int i = 0; i < POCKETCHANGE_SLOTS; i++) { + if (slots[i].getAmount() < pocketChangeAmount) { + final long topupAmount = pocketChangeAmount - slots[i].getAmount(); + if (topupAmount <= spendableAmount) { + slotsToFill.add(i); + slotToFillAmounts.add(topupAmount); + spendableAmount -= topupAmount; + Timber.d("FILL %d with %d", i, topupAmount); + } + } + } + + String[] destinations; + long[] amounts; + while (true) { + destinations = new String[slotsToFill.size() + 1]; + destinations[0] = getDestination(); + amounts = new long[slotsToFill.size() + 1]; + amounts[0] = getAmount(); + if (slotsToFill.size() == 0) break; + for (int i = 0; i < slotsToFill.size(); i++) { + destinations[i + 1] = wallet.getSubaddress(slotsToFill.get(i) + POCKETCHANGE_IDX); + amounts[i + 1] = slotToFillAmounts.get(i); + } + final long fees = wallet.estimateTransactionFee(this) * 10; // pessimistic + if (fees < spendableAmount) break; + spendableAmount += slotToFillAmounts.get(0); + slotsToFill.remove(0); + slotToFillAmounts.remove(0); + } + + this.destinations = destinations; + this.amounts = amounts; + subaddresses = new int[spendableSubaddressIdx.size()]; + int i = 0; + for (int subaddressIdx : spendableSubaddressIdx) { + subaddresses[i++] = subaddressIdx; + } } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/data/TxDataBtc.java b/app/src/main/java/com/m2049r/xmrwallet/data/TxDataBtc.java index 55ec71a0..8f41839d 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/data/TxDataBtc.java +++ b/app/src/main/java/com/m2049r/xmrwallet/data/TxDataBtc.java @@ -41,10 +41,6 @@ public class TxDataBtc extends TxData { super(); } - public TxDataBtc(TxDataBtc txDataBtc) { - super(txDataBtc); - } - @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); diff --git a/app/src/main/java/com/m2049r/xmrwallet/dialog/PocketChangeFragment.java b/app/src/main/java/com/m2049r/xmrwallet/dialog/PocketChangeFragment.java new file mode 100644 index 00000000..2d4c55c2 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/dialog/PocketChangeFragment.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2023 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.dialog; + +import android.app.Dialog; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.slider.Slider; +import com.google.android.material.switchmaterial.SwitchMaterial; +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.util.Helper; + +public class PocketChangeFragment extends DialogFragment implements Slider.OnChangeListener { + static final String TAG = "PocketChangeFragment"; + static final String ENABLED = "enabled"; + static final String TICK = "tick"; + + public static PocketChangeFragment newInstance(boolean enabled, int tick) { + PocketChangeFragment fragment = new PocketChangeFragment(); + Bundle bundle = new Bundle(); + bundle.putInt(ENABLED, enabled ? 1 : 0); + bundle.putInt(TICK, tick); + fragment.setArguments(bundle); + return fragment; + } + + public static void display(FragmentManager fm, @NonNull Wallet.PocketChangeSetting setting) { + FragmentTransaction ft = fm.beginTransaction(); + Fragment prev = fm.findFragmentByTag(TAG); + if (prev != null) { + ft.remove(prev); + } + PocketChangeFragment.newInstance(setting.isEnabled(), getTick(setting.getAmount())).show(ft, TAG); + } + + SwitchMaterial switchPocketChange; + Slider slider; + TextView tvProgressLabel; + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_pocketchange_setting, null); + boolean enabled = false; + int progress = 0; + Bundle arguments = getArguments(); + if (arguments != null) { + enabled = arguments.getInt(ENABLED) > 0; + progress = arguments.getInt(TICK); + } + + final View llAmount = view.findViewById(R.id.llAmount); + switchPocketChange = view.findViewById(R.id.switchPocketChange); + switchPocketChange.setOnCheckedChangeListener((buttonView, isChecked) -> llAmount.setVisibility(isChecked ? View.VISIBLE : View.INVISIBLE)); + slider = view.findViewById(R.id.seekbar); + slider.addOnChangeListener(this); + switchPocketChange.setChecked(enabled); + tvProgressLabel = view.findViewById(R.id.seekbar_value); + slider.setValue(progress); + llAmount.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE); + onValueChange(slider, slider.getValue(), false); + + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity()) + .setView(view) + .setPositiveButton(R.string.label_apply, + (dialog, whichButton) -> { + final FragmentActivity activity = getActivity(); + if (activity instanceof Listener) { + ((Listener) activity).setPocketChange(Wallet.PocketChangeSetting.of(switchPocketChange.isChecked(), getAmount())); + } + } + ); + return builder.create(); + } + + private long getAmount() { + return Wallet.getAmountFromDouble(getAmount((int) slider.getValue())); + } + + private static final double[] AMOUNTS = {0.1, 0.2, 0.3, 0.5, 0.8, 1.3}; + + private static double getAmount(int i) { + return AMOUNTS[i]; + } + + // find the closest amount we have + private static int getTick(long amount) { + int enabled = amount > 0 ? 1 : -1; + amount = Math.abs(amount); + double lastDiff = Double.MAX_VALUE; + for (int i = 0; i < AMOUNTS.length; i++) { + final double diff = Math.abs(Helper.ONE_XMR * AMOUNTS[i] - amount); + if (lastDiff < diff) return i - 1; + lastDiff = diff; + } + return enabled * (AMOUNTS.length - 1); + } + + @Override + public void onValueChange(@NonNull Slider slider, float value, boolean fromUser) { + tvProgressLabel.setText(getString(R.string.pocketchange_amount, getAmount((int) value))); + } + + public interface Listener { + void setPocketChange(Wallet.PocketChangeSetting setting); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAddressWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAddressWizardFragment.java index 2a4a2e12..2bfaf289 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAddressWizardFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAddressWizardFragment.java @@ -411,10 +411,10 @@ public class SendAddressWizardFragment extends SendWizardFragment { if (txData instanceof TxDataBtc) { ((TxDataBtc) txData).setBtcAddress(etAddress.getEditText().getText().toString()); ((TxDataBtc) txData).setBtcSymbol(selectedCrypto.getSymbol()); - txData.setDestinationAddress(null); + txData.setDestination(null); ServiceHelper.ASSET = selectedCrypto.getSymbol().toLowerCase(); } else { - txData.setDestinationAddress(etAddress.getEditText().getText().toString()); + txData.setDestination(etAddress.getEditText().getText().toString()); ServiceHelper.ASSET = null; } txData.setUserNotes(new UserNotes(etNotes.getEditText().getText().toString())); diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAmountWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAmountWizardFragment.java index 12edaf67..2038d3ed 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAmountWizardFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAmountWizardFragment.java @@ -23,6 +23,8 @@ import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.TextView; +import com.google.android.material.progressindicator.LinearProgressIndicator; +import com.google.android.material.switchmaterial.SwitchMaterial; import com.m2049r.xmrwallet.R; import com.m2049r.xmrwallet.data.BarcodeData; import com.m2049r.xmrwallet.data.TxData; diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcConfirmWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcConfirmWizardFragment.java index 66a66c2f..cac4786e 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcConfirmWizardFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcConfirmWizardFragment.java @@ -348,7 +348,7 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements } showProgress(3, getString(R.string.label_send_progress_create_tx)); final TxData txData = sendListener.getTxData(); - txData.setDestinationAddress(xmrtoOrder.getXmrAddress()); + txData.setDestination(xmrtoOrder.getXmrAddress()); txData.setAmount(xmrtoOrder.getXmrAmount()); getActivityCallback().onPrepareSend(xmrtoOrder.getOrderId(), txData); } diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcSuccessWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcSuccessWizardFragment.java index 41c13db8..5c5b4a7b 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcSuccessWizardFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcSuccessWizardFragment.java @@ -140,7 +140,7 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment { isResumed = true; btcData = (TxDataBtc) sendListener.getTxData(); - tvTxAddress.setText(btcData.getDestinationAddress()); + tvTxAddress.setText(btcData.getDestination()); final PendingTx committedTx = sendListener.getCommittedTx(); if (committedTx != null) { diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendConfirmWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendConfirmWizardFragment.java index 99acd244..6fefec7b 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendConfirmWizardFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendConfirmWizardFragment.java @@ -64,12 +64,14 @@ public class SendConfirmWizardFragment extends SendWizardFragment implements Sen private TextView tvTxAddress; private TextView tvTxNotes; private TextView tvTxAmount; + private TextView tvTxChange; private TextView tvTxFee; private TextView tvTxTotal; private View llProgress; private View bSend; private View llConfirmSend; private View pbProgressSend; + private View llPocketChange; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @@ -83,12 +85,14 @@ public class SendConfirmWizardFragment extends SendWizardFragment implements Sen tvTxAddress = view.findViewById(R.id.tvTxAddress); tvTxNotes = view.findViewById(R.id.tvTxNotes); tvTxAmount = view.findViewById(R.id.tvTxAmount); + tvTxChange = view.findViewById(R.id.tvTxChange); tvTxFee = view.findViewById(R.id.tvTxFee); tvTxTotal = view.findViewById(R.id.tvTxTotal); llProgress = view.findViewById(R.id.llProgress); pbProgressSend = view.findViewById(R.id.pbProgressSend); llConfirmSend = view.findViewById(R.id.llConfirmSend); + llPocketChange = view.findViewById(R.id.llPocketChange); bSend = view.findViewById(R.id.bSend); bSend.setEnabled(false); @@ -181,7 +185,7 @@ public class SendConfirmWizardFragment extends SendWizardFragment implements Sen isResumed = true; final TxData txData = sendListener.getTxData(); - tvTxAddress.setText(txData.getDestinationAddress()); + tvTxAddress.setText(txData.getDestination()); UserNotes notes = sendListener.getTxData().getUserNotes(); if ((notes != null) && (!notes.note.isEmpty())) { tvTxNotes.setText(notes.note); @@ -206,7 +210,14 @@ public class SendConfirmWizardFragment extends SendWizardFragment implements Sen tvTxAmount.setText(getString(R.string.street_sweep_amount)); tvTxTotal.setText(getString(R.string.street_sweep_amount)); } else { - tvTxAmount.setText(Wallet.getDisplayAmount(pendingTransaction.getAmount())); + tvTxAmount.setText(Wallet.getDisplayAmount(pendingTransaction.getNetAmount())); + final long change = pendingTransaction.getPocketChange(); + if (change > 0) { + llPocketChange.setVisibility(View.VISIBLE); + tvTxChange.setText(Wallet.getDisplayAmount(change)); + } else { + llPocketChange.setVisibility(View.GONE); + } tvTxTotal.setText(Wallet.getDisplayAmount( pendingTransaction.getFee() + pendingTransaction.getAmount())); } diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendSuccessWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendSuccessWizardFragment.java index 34f33397..1d3c73f1 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendSuccessWizardFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendSuccessWizardFragment.java @@ -108,7 +108,7 @@ public class SendSuccessWizardFragment extends SendWizardFragment { Helper.hideKeyboard(getActivity()); final TxData txData = sendListener.getTxData(); - tvTxAddress.setText(txData.getDestinationAddress()); + tvTxAddress.setText(txData.getDestination()); final PendingTx committedTx = sendListener.getCommittedTx(); if (committedTx != null) { diff --git a/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java b/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java index 215d41b0..599ad91a 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java +++ b/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java @@ -189,7 +189,6 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter getAll(int accountIndex, boolean unspentOnly) { + return refresh(accountIndex, unspentOnly); + } + + private native List refresh(int accountIndex, boolean unspentOnly); +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/model/CoinsInfo.java b/app/src/main/java/com/m2049r/xmrwallet/model/CoinsInfo.java new file mode 100644 index 00000000..c213260d --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/model/CoinsInfo.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 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.model; + +import lombok.Value; + +// this is not the CoinsInfo from the API as that is owned by the Coins object +// this is a POJO +@Value +public class CoinsInfo { + int accountIndex; + int addressIndex; + long amount; + long blockheight; + String txHash; + boolean spent; + boolean frozen; + long unlockTime; + boolean unlocked; + + public boolean isSpendable() { + return !spent && unlocked; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/model/PendingTransaction.java b/app/src/main/java/com/m2049r/xmrwallet/model/PendingTransaction.java index 6ad620a4..37939635 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/model/PendingTransaction.java +++ b/app/src/main/java/com/m2049r/xmrwallet/model/PendingTransaction.java @@ -16,6 +16,9 @@ package com.m2049r.xmrwallet.model; +import lombok.Getter; +import lombok.Setter; + public class PendingTransaction { static { System.loadLibrary("monerujo"); @@ -63,8 +66,6 @@ public class PendingTransaction { Priority(int value) { this.value = value; } - - } public Status getStatus() { @@ -95,4 +96,11 @@ public class PendingTransaction { public native long getTxCount(); + @Getter + @Setter + private long pocketChange; + + public long getNetAmount() { + return getAmount() - pocketChange; + } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/model/TransactionHistory.java b/app/src/main/java/com/m2049r/xmrwallet/model/TransactionHistory.java index 08245f65..4cfbddbc 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/model/TransactionHistory.java +++ b/app/src/main/java/com/m2049r/xmrwallet/model/TransactionHistory.java @@ -17,8 +17,10 @@ package com.m2049r.xmrwallet.model; import java.util.ArrayList; +import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; import timber.log.Timber; @@ -61,22 +63,14 @@ public class TransactionHistory { private List transactions = new ArrayList<>(); - void refreshWithNotes(Wallet wallet) { + public void refreshWithNotes(Wallet wallet) { refresh(); loadNotes(wallet); } private void refresh() { - List transactionInfos = refreshJ(); - Timber.d("refresh size=%d", transactionInfos.size()); - for (Iterator iterator = transactionInfos.iterator(); iterator.hasNext(); ) { - TransactionInfo info = iterator.next(); - if (info.accountIndex != accountIndex) { - iterator.remove(); - } - } - transactions = transactionInfos; + transactions = refreshJ(accountIndex); } - private native List refreshJ(); + private native List refreshJ(int accountIndex); } diff --git a/app/src/main/java/com/m2049r/xmrwallet/model/TransactionInfo.java b/app/src/main/java/com/m2049r/xmrwallet/model/TransactionInfo.java index 611b7505..a32d675a 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/model/TransactionInfo.java +++ b/app/src/main/java/com/m2049r/xmrwallet/model/TransactionInfo.java @@ -101,6 +101,23 @@ public class TransactionInfo implements Parcelable, Comparable this.unlockTime = unlockTime; this.subaddressLabel = subaddressLabel; this.transfers = transfers; + calcNetAmount(); + } + + @Getter + private long netAmount; + + public long getPocketChangeAmount() { + return amount - netAmount; + } + + private void calcNetAmount() { + netAmount = amount; + if ((direction == TransactionInfo.Direction.Direction_Out) && (transfers != null)) { + for (int i = 1; i < transfers.size(); i++) { + netAmount -= transfers.get(i).amount; + } + } } public boolean isConfirmed() { @@ -138,6 +155,7 @@ public class TransactionInfo implements Parcelable, Comparable out.writeString(txKey); out.writeString(notes); out.writeString(address); + out.writeLong(netAmount); } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @@ -169,6 +187,7 @@ public class TransactionInfo implements Parcelable, Comparable txKey = in.readString(); notes = in.readString(); address = in.readString(); + netAmount = in.readLong(); } @Override diff --git a/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java b/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java index e85d0f85..d97456b9 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java +++ b/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java @@ -25,10 +25,13 @@ import com.m2049r.xmrwallet.data.TxData; import java.io.File; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.List; import java.util.Locale; import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.Value; import timber.log.Timber; public class Wallet { @@ -67,9 +70,7 @@ public class Wallet { } public boolean isOk() { - return (getStatus() == StatusEnum.Status_Ok) - && ((getConnectionStatus() == null) || - (getConnectionStatus() == ConnectionStatus.ConnectionStatus_Connected)); + return (getStatus() == StatusEnum.Status_Ok) && ((getConnectionStatus() == null) || (getConnectionStatus() == ConnectionStatus.ConnectionStatus_Connected)); } @Override @@ -110,23 +111,17 @@ public class Wallet { @RequiredArgsConstructor @Getter public enum Device { - Device_Undefined(0, 0), - Device_Software(50, 200), - Device_Ledger(5, 20); + Device_Undefined(0, 0), Device_Software(50, 200), Device_Ledger(5, 20); private final int accountLookahead; private final int subaddressLookahead; } public enum StatusEnum { - Status_Ok, - Status_Error, - Status_Critical + Status_Ok, Status_Error, Status_Critical } public enum ConnectionStatus { - ConnectionStatus_Disconnected, - ConnectionStatus_Connected, - ConnectionStatus_WrongVersion + ConnectionStatus_Disconnected, ConnectionStatus_Connected, ConnectionStatus_WrongVersion } public native String getSeed(String offset); @@ -168,16 +163,14 @@ public class Wallet { private native String getAddressJ(int accountIndex, int addressIndex); public Subaddress getSubaddressObject(int accountIndex, int subAddressIndex) { - return new Subaddress(accountIndex, subAddressIndex, - getSubaddress(subAddressIndex), getSubaddressLabel(subAddressIndex)); + return new Subaddress(accountIndex, subAddressIndex, getSubaddress(subAddressIndex), getSubaddressLabel(subAddressIndex)); } public Subaddress getSubaddressObject(int subAddressIndex) { Subaddress subaddress = getSubaddressObject(accountIndex, subAddressIndex); long amount = 0; for (TransactionInfo info : getHistory().getAll()) { - if ((info.addressIndex == subAddressIndex) - && (info.direction == TransactionInfo.Direction.Direction_In)) { + if ((info.addressIndex == subAddressIndex) && (info.direction == TransactionInfo.Direction.Direction_In)) { amount += info.amount; } } @@ -217,13 +210,10 @@ public class Wallet { // virtual std::string keysFilename() const = 0; public boolean init(long upper_transaction_size_limit) { - return initJ(WalletManager.getInstance().getDaemonAddress(), upper_transaction_size_limit, - WalletManager.getInstance().getDaemonUsername(), - WalletManager.getInstance().getDaemonPassword()); + return initJ(WalletManager.getInstance().getDaemonAddress(), upper_transaction_size_limit, WalletManager.getInstance().getDaemonUsername(), WalletManager.getInstance().getDaemonPassword()); } - private native boolean initJ(String daemon_address, long upper_transaction_size_limit, - String daemon_username, String daemon_password); + private native boolean initJ(String daemon_address, long upper_transaction_size_limit, String daemon_username, String daemon_password); // virtual bool createWatchOnly(const std::string &path, const std::string &password, const std::string &language) const = 0; // virtual void setRefreshFromBlockHeight(uint64_t refresh_from_block_height) = 0; @@ -335,36 +325,23 @@ public class Wallet { } } - public PendingTransaction createTransaction(TxData txData) { - return createTransaction( - txData.getDestinationAddress(), - txData.getAmount(), - txData.getMixin(), - txData.getPriority()); - } + private native long createTransactionMultDest(String[] destinations, String payment_id, long[] amounts, int mixin_count, int priority, int accountIndex, int[] subaddresses); - public PendingTransaction createTransaction(String dst_addr, - long amount, int mixin_count, - PendingTransaction.Priority priority) { + public PendingTransaction createTransaction(TxData txData) { disposePendingTransaction(); - int _priority = priority.getValue(); - long txHandle = - (amount == SWEEP_ALL ? - createSweepTransaction(dst_addr, "", mixin_count, _priority, - accountIndex) : - createTransactionJ(dst_addr, "", amount, mixin_count, _priority, - accountIndex)); + int _priority = txData.getPriority().getValue(); + final boolean sweepAll = txData.getAmount() == SWEEP_ALL; + Timber.d("TxData: %s", txData); + long txHandle = (sweepAll ? createSweepTransaction(txData.getDestination(), "", txData.getMixin(), _priority, accountIndex) : + createTransactionMultDest(txData.getDestinations(), "", txData.getAmounts(), txData.getMixin(), _priority, accountIndex, txData.getSubaddresses())); pendingTransaction = new PendingTransaction(txHandle); + pendingTransaction.setPocketChange(txData.getPocketChangeAmount()); return pendingTransaction; } - private native long createTransactionJ(String dst_addr, String payment_id, - long amount, int mixin_count, - int priority, int accountIndex); + private native long createTransactionJ(String dst_addr, String payment_id, long amount, int mixin_count, int priority, int accountIndex); - private native long createSweepTransaction(String dst_addr, String payment_id, - int mixin_count, - int priority, int accountIndex); + private native long createSweepTransaction(String dst_addr, String payment_id, int mixin_count, int priority, int accountIndex); public PendingTransaction createSweepUnmixableTransaction() { @@ -381,7 +358,13 @@ public class Wallet { public native void disposeTransaction(PendingTransaction pendingTransaction); -//virtual bool exportKeyImages(const std::string &filename) = 0; + public long estimateTransactionFee(TxData txData) { + return estimateTransactionFee(txData.getDestinations(), txData.getAmounts(), txData.getPriority().getValue()); + } + + private native long estimateTransactionFee(String[] destinations, long[] amounts, int priority); + + //virtual bool exportKeyImages(const std::string &filename) = 0; //virtual bool importKeyImages(const std::string &filename) = 0; @@ -403,6 +386,22 @@ public class Wallet { } //virtual AddressBook * addressBook() const = 0; + + public List getCoinsInfos(boolean unspentOnly) { + return getCoins().getAll(accountIndex, unspentOnly); + } + + private Coins coins = null; + + private Coins getCoins() { + if (coins == null) { + coins = new Coins(getCoinsJ()); + } + return coins; + } + + private native long getCoinsJ(); + //virtual void setListener(WalletListener *) = 0; private native long setListenerJ(WalletListener listener); @@ -444,8 +443,7 @@ public class Wallet { if (label.equals(NEW_ACCOUNT_NAME)) { String address = getAddress(accountIndex); int len = address.length(); - label = address.substring(0, 6) + - "\u2026" + address.substring(len - 6, len); + label = address.substring(0, 6) + "\u2026" + address.substring(len - 6, len); } return label; } @@ -504,4 +502,22 @@ public class Wallet { private native int getDeviceTypeJ(); + @Getter + @Setter + PocketChangeSetting pocketChangeSetting = PocketChangeSetting.of(false, 0); + + @Value(staticConstructor = "of") + static public class PocketChangeSetting { + boolean enabled; + long amount; + + public String toPrefString() { + return Long.toString((enabled ? 1 : -1) * amount); + } + + static public PocketChangeSetting from(String prefString) { + long value = Long.parseLong(prefString); + return of(value > 0, Math.abs(value)); + } + } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/WalletService.java b/app/src/main/java/com/m2049r/xmrwallet/service/WalletService.java index ece003ea..89ce4617 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/service/WalletService.java +++ b/app/src/main/java/com/m2049r/xmrwallet/service/WalletService.java @@ -318,9 +318,10 @@ public class WalletService extends Service { TxData txData = extras.getParcelable(REQUEST_CMD_TX_DATA); String txTag = extras.getString(REQUEST_CMD_TX_TAG); + assert txData != null; + txData.createPocketChange(myWallet); PendingTransaction pendingTransaction = myWallet.createTransaction(txData); PendingTransaction.Status status = pendingTransaction.getStatus(); - Timber.d("transaction status %s", status); if (status != PendingTransaction.Status.Status_Ok) { Timber.w("Create Transaction failed: %s", pendingTransaction.getErrorString()); } diff --git a/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeEditText.java b/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeEditText.java index 00b1b19b..f274cae4 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeEditText.java +++ b/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeEditText.java @@ -225,7 +225,7 @@ public class ExchangeEditText extends LinearLayout { // make progress circle gray pbExchange.getIndeterminateDrawable(). - setColorFilter(ThemeHelper.getThemedColor(getContext(), R.attr.colorPrimaryVariant), + setColorFilter(ThemeHelper.getThemedColor(getContext(), com.google.android.material.R.attr.colorPrimaryVariant), android.graphics.PorterDuff.Mode.MULTIPLY); sCurrencyA.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { diff --git a/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeView.java b/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeView.java index 96a4923e..6e647d1b 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeView.java +++ b/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeView.java @@ -177,7 +177,7 @@ public class ExchangeView extends LinearLayout { // make progress circle gray pbExchange.getIndeterminateDrawable(). - setColorFilter(ThemeHelper.getThemedColor(getContext(), R.attr.colorPrimaryVariant), + setColorFilter(ThemeHelper.getThemedColor(getContext(), com.google.android.material.R.attr.colorPrimaryVariant), android.graphics.PorterDuff.Mode.MULTIPLY); sCurrencyA.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { diff --git a/app/src/main/java/com/m2049r/xmrwallet/widget/PasswordEntryView.java b/app/src/main/java/com/m2049r/xmrwallet/widget/PasswordEntryView.java index 6d5fee1e..32eb8855 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/widget/PasswordEntryView.java +++ b/app/src/main/java/com/m2049r/xmrwallet/widget/PasswordEntryView.java @@ -23,7 +23,7 @@ public class PasswordEntryView extends TextInputLayout implements TextWatcher { } public PasswordEntryView(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs, R.attr.textInputStyle); + super(context, attrs, com.google.android.material.R.attr.textInputStyle); } public PasswordEntryView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { diff --git a/app/src/main/res/drawable/ic_toll.xml b/app/src/main/res/drawable/ic_toll.xml new file mode 100644 index 00000000..2a3b9ca3 --- /dev/null +++ b/app/src/main/res/drawable/ic_toll.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/layout/fragment_pocketchange_setting.xml b/app/src/main/res/layout/fragment_pocketchange_setting.xml new file mode 100644 index 00000000..dee37684 --- /dev/null +++ b/app/src/main/res/layout/fragment_pocketchange_setting.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_send_amount.xml b/app/src/main/res/layout/fragment_send_amount.xml index c891e4f3..fa08ca2d 100644 --- a/app/src/main/res/layout/fragment_send_amount.xml +++ b/app/src/main/res/layout/fragment_send_amount.xml @@ -38,7 +38,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" - android:layout_marginBottom="16dp" android:orientation="vertical" /> @@ -103,6 +103,31 @@ tools:text="143.008000000000" /> + + + + + + + + + - - - - + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-cat/strings.xml b/app/src/main/res/values-cat/strings.xml index 129efd0e..b1bbabc0 100644 --- a/app/src/main/res/values-cat/strings.xml +++ b/app/src/main/res/values-cat/strings.xml @@ -442,4 +442,9 @@ Transaction amount locked until block %1$d (in %2$d blocks ≈ %3$,.2f days) Street Mode enabled\nOnly new transactions will be shown + + To reduce waiting time on repeated spending, Monerujo can create spare change at the expense of higher fees. It\'ll try to create and maintain 10 coins of the selected amount. + Create Change + + APPLY diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index c48f0d1f..9a86b01a 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -443,4 +443,9 @@ Transaction amount locked until block %1$d (in %2$d blocks ≈ %3$,.2f days) Street Mode enabled\nOnly new transactions will be shown + + To reduce waiting time on repeated spending, Monerujo can create spare change at the expense of higher fees. It\'ll try to create and maintain 10 coins of the selected amount. + Create Change + + APPLY diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 7b74c42c..26ef78b7 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -444,4 +444,9 @@ Transaction amount locked until block %1$d (in %2$d blocks ≈ %3$,.2f days) Street Mode enabled\nOnly new transactions will be shown + + To reduce waiting time on repeated spending, Monerujo can create spare change at the expense of higher fees. It\'ll try to create and maintain 10 coins of the selected amount. + Create Change + + APPLY diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index e5aa30c6..f5712a7f 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -442,4 +442,9 @@ Transaction amount locked until block %1$d (in %2$d blocks ≈ %3$,.2f days) Street Mode enabled\nOnly new transactions will be shown + + To reduce waiting time on repeated spending, Monerujo can create spare change at the expense of higher fees. It\'ll try to create and maintain 10 coins of the selected amount. + Create Change + + APPLY diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 040c0be2..d485947d 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -2,13 +2,13 @@ Monedero - Acerca De + Legal Política de Privacidad Compartir Ayuda Recibir - Renombrar + Cambiar nombre Copia de seguridad Cambiar contraseña @@ -23,7 +23,7 @@ Aceptar Cancelar Cerrar - Información más detallada + Detalle de claves ¡Éxito! Hecho @@ -39,12 +39,12 @@ Dirección de Destino Notas - Copia de seguridad en progreso - Archivado en progreso - Cambio de nombre en progreso - Cambiando contraseña en progreso + Copia de seguridad en progreso… + Archivado en progreso… + Cambio de nombre en progreso… + Cambiando contraseña en progreso… - Guardando todo\n¡Puede llevar un tiempo! + Cerrando el monedero…\n¡Puede tardar un tiempo! Copia de seguridad exitosa ¡Copia de seguridad fallida! @@ -55,19 +55,19 @@ Nodo Cargando monedero… Monedero guardado - ¡Guardado de monedero fallido! + ¡Fallo al guardar el monedero! Conectando… ¡Conexión con el nodo fallida!\nComprueba el usuario/contraseña ¡Nodo inválido!\nInténtalo con otro. - ¡No se puede alcanzar el nodo!\nInténtalo de nuevo o prueba otro. + ¡No se puede conectar con el nodo!\nInténtalo de nuevo o prueba otro. Desconectado Transacción fallida: %1$s - Todavía estoy ocupado con tu último monedero … + Todavía estoy ocupado con el último monedero … - Renombrar %1$s + Nuevo nombre para %1$s Nueva contraseña para %1$s Repetir contraseña para %1$s @@ -88,21 +88,21 @@ + %1$s %2$s sin confirmar - Servicio de Monerujo + Monedero abierto Sincronizado: bloques restantes Escaneando: - ¡No se puede escribir en el almacenamiento externo! ¡Pánico! + ¡Oh no! ¡No se puede escribir en el almacenamiento externo! ¡De verdad necesitamos ese permiso para el almacenamiento externo! - Sin cámara = ¡Sin escaneo de QR! + ¡No se puede escanear un QR sin acceso a la cámara! Clave de Vista Dirección Pública ¡Clave de vista copiada al portapapeles! ¡Dirección del monedero copiada al portapapeles! - ¡Copia desactivada por motivos de seguridad! + ¡Copia no permitida para tu seguridad! ¡No se ha podido obtener la tasa de cambio!\nUsa XMR/XMR o inténtalo de nuevo @@ -112,25 +112,24 @@ Permitir abrir usando huella dactilar Autenticación por huella -

Con la autenticación por huella dactilar activada, puedes acceder al balance y recibir fondos +

Con la autenticación por huella dactilar activada, puedes abrir tu monedero y recibir fondos sin la necesidad de ingresar tu contraseña.

-

Sin embargo, por seguridad extra, Monerujo va a requerir tu contraseña para ver la información - sensible de tu monedero o enviar fondos.

+

Sin embargo, para tu seguridad, Monerujo va a requerir tu contraseña para ver los secretos de tu monedero o enviar fondos.

Advertencia de seguridad

Si bien es cómodo, recuerda que cualquier persona que tenga acceso a tu huella dactilar va a ser capaz de mirar el balance de tu monedero.

-

Por ejemplo, un actor malicioso cercano podría abrir tu monedero con tu dedo mientras duermes.

+

Por ejemplo, alguien podría abrir tu monedero con tu dedo mientras duermes.

¿Estás seguro de activar esta función? ]]>
Contraseñas no coinciden - Contraseña no puede estar vacía + La contraseña no puede estar vacía ¡Házme ya un monedero! ¡Ya anote todo! ¡Dame un nombre! ¡El monedero ya existe! No puede empezar con . - Creando monedero + Creando monedero… Monedero creada Introduce un número o una fecha (AAAA-MM-DD) @@ -140,17 +139,17 @@ Semilla Ver - Dirección Pública - Clave de Vista - Clave de Gasto - Semilla Mnemotécnica de 25 Palabras - Altura o Fecha (YYYY-MM-DD) de Restauración + Dirección pública + Clave de vista + Clave de gasto + Semilla mnemotécnica + Altura o fecha (YYYY-MM-DD) de restauración - Dirección Pública - Clave de Vista - Clave de Gasto - Semilla Mnemotécnica - Contraseña de restauración para el archivo del monedero + Dirección pública + Clave de vista + Clave de gasto + Semilla mnemotécnica + Clave loca para restaurar el monedero Introduce una clave válida Introduce una dirección válida @@ -181,19 +180,19 @@ Marca de tiempo ID de Transacción - Clave de Transacción + Clave de transacción Destino - ID de Pago + ID de pago Bloque Monto Comisión Transferencias Notas (opcional) - Detalles de la Transacción + Detalles de la transacción PENDIENTE - FALLIDO + FALLIDA Monto ¡No se ha podido abrir el monedero! @@ -202,19 +201,19 @@ Min. 0 XMR no es un número - Se va a mostrar información delicada.\n¡Mira por encima del hombro! + Se va a mostrar información delicada. Cualquiera con acceso a ella tendría control sobre tus fondos.\n¡Mira detrás tuyo! Estoy seguro - ¡Llévame de vuelta! - Detalles + No muestres nada + Secretos del monedero - Este monedero será borrado. Tus fondos se irán para siempre a menos que tengas tu semilla o una copia de seguridad funcional para recuperarlo. - ¡Sí, hazlo! - ¡No, gracias! + Este monedero será borrado.\nTus fondos se perderán para siempre sino tienes tu semilla mnemotéctica o una copia de seguridad y su correspondiente clave loca para recuperarla.\nPuedes encontrar ambas entre los secretos de tu monedero. + Sí, hazlo + No lo hagas Crear nuevo monedero Restaurar monedero de sólo vista - Restaurar monedero con claves privadas - Restaurar monedero con semilla de 25 palabras + Restaurar monedero con claves + Restaurar monedero con semilla Ingresaste una dirección %1$s
Vas a enviar XMR y el destinatario recibirá %1$s usando el servicio SideShift.ai. @@ -223,22 +222,22 @@ Confirmación pendiente Pago pendiente Error de SideShift.ai (%1$s) - %1$s Enviados! + %1$s enviados! Consultando … Puedes enviar %1$s — %2$s %4$s.
SideShift.ai está ofreciendo una tasa de cambio de %3$s %4$s/XMR en este momento. ]]>
Saldo: %2$s %3$s (%1$s XMR) - Creando orden SideShift.ai - Consultando orden SideShift.ai - Preparando transacción Monero - Consultando parámetros SideShift.ai - ERROR SideShift.ai + Creando orden en SideShift.ai + Consultando orden en SideShift.ai + Preparando transacción de Monero + Consultando parámetros en SideShift.ai + Error de SideShift.ai Código: %1$d Toca para reintentar - Parece que estamos atascados! - Oh-oh, parece que SideShift.ai no está disponible ahora! + ¡Parece que está atascada! + Oh no, parece que SideShift.ai no está disponible ahora. %1$s %3$s = %2$s XMR (Cambio: %1$s %2$s/XMR) Visita https://sideshift.ai para soporte y rastreo @@ -246,42 +245,42 @@ Clave secreta SideShift.ai Dirección %1$s destino Monto - Oye, tardaste demasiado! + ¡Oye, tardaste demasiado! Clave ¡Clave copiada al portapapeles! Enviar mis preciados moneroj - Gastar mis preciados moneroj (%1$s) + Enviar mis preciados moneroj (%1$s) No es una dirección válida Comisión (XMR) Total (XMR) %1$s XMR +%1$s Comisión - Soy Monerujo - Orden SideShift.ai + Monerujo + Orden de SideShift.ai Pago en BTC activado, toca para más info. Ledger activado, toca para más info. - Crear Cuenta + Crear cuenta Nueva cuenta agregada #%1$d # de cuenta - ¡Enviar todos los fondos confirmados en esta cuenta! + ¿Quieres enviar todos los fondos de esta cuenta? Subdirección - Subdirecciones Públicas #%1$d: %2$s + Subdirecciones públicas #%1$d: %2$s - Lenguaje - Usar Idioma del Sistema + Idioma + Usar el del sistema Restaurar desde Ledger Nano Comunicándose con Ledger - ¡Confirmación en Ledger requerida! + Esperando confirmación en Ledger Recuperando subdirecciones Verificando claves - Realizando cálculos alocados - Cosas de hash - Por favor (re)conecta el dispositivo Ledger + Realizando cálculos locos… + Hasheando… + Por favor reconecta el dispositivo Ledger Creando cuenta @@ -291,45 +290,45 @@ Descripción (opcional) Dirección OpenAlias no disponible - OpenAlias asegurado ✔ + OpenAlias seguro ✔ Resolviendo OpenAlias… - OpenAlias sin DNSSEC - la drección puede ser falsificada + OpenAlias sin DNSSEC - la dirección podría ser falsificada Versión de nodo incompatible - ¡por favor actualiza! - Detalles + Ver secretos - Modo Público + Modo calle - Nodo-o-matiC habilitado, toque para más información. + Nodo-matiC habilitado, toque para más información. Último bloque actualizado: %1$s Nodos - Nombre del Nodo (Opcional) - Nombre del Host + Nombre (Opcional) + Dirección del nodo Puerto Usuario (Opcional) Contraseña (Opcional) - No se puede resolver el host - ¡Necesitamos esto! - Debe ser numérico + No se puede encontrar al nodo + ¡Necesitas esto! + Debe ser un número Debe ser 1–65535 Agregar Nodo ¡Toca para refrescar! - ERROR DE CONECCIÓN %1$d - ERROR DE CONECCIÓN + ERROR AL CONECTAR %1$d + No se pudo conectar AUTENTIFICACIÓN FALLIDA - Resultados de la prueba: - Altura: %1$s (v%2$d), Ping: %3$.0fms, IP: %4$s - Probando el IP: %1$s … + Resultados: + Altura: %1$s (v%2$d)\nPing: %3$.0fms\nIP: %4$s + Probando al IP: %1$s… Por favor espera a que termine el escaneo - Toca para seleccionar o agregar nodos - Agrega nodos manualmente o tira para buscar - Escaneando la red… + Toca para elegir un nodo + Tira para refrescar + Buscando nodos… Mejores %1$d nodos marcados automáticamente Probar - Receptor + Dirección del receptor TODO! @@ -339,26 +338,26 @@ ¡Semilla de Ledger inválida! ¡Ingresar tu semilla de Ledger aquí implica un riesgo importante! - Altura de Restauración + Altura de restauración Arrancar aplicación de Monero en %1$s - ¡Re-escanear! + Re-escanear ¡Entendido! Siguiente ¡Estoy listo! ¡Bienvenido a Monerujo! - Esta aplicación te permite crear y usar monederos de Monero. Puedes guardar tus dulces moneroj en ellos. - Mantén segura tu semilla - La semilla otorga acceso total a quien la posee. Si la pierdes, nadie puede ayudarte a recuperarla y perderás tus preciados moneroj. - Enviar Criptos - Monerujo tiene SideShift.ai incorporado. Simplemente pega o escanea una dirección de BTC, LTC, ETH, DASH o DOGE y podrás enviar esas monedas usando tu XMR. + Esta aplicación te permite crear y usar monederos. Puedes recibir y enviar dulces moneros con ella. + Escribe tu semilla mnemotécnica + Esas palabras otorgan acceso total a tus fondos a quien las tengan. Si la pierdes, nadie puede ayudarte y perderás tus preciados moneros. + Cambia Monero + Monerujo tiene SideShift.ai incorporado. Simplemente ingresa una dirección de BTC, LTC, ETH, DASH o DOGE y podrás enviar esas monedas usando XMR. Nodos, a tu manera - Los nodos son tu conexión con la red de Monero. Elige entre usar nodos públicos, o ir totalmente ciberpunk conectándote al tuyo propio. - Enviar con tu huella - Ahora puedes enviar XMR con sólo tu huella dactilar si lo deseas. Para forzar el pedido de contraseña, simplemente desactiva la opción de huella. + Los nodos son tu conexión con la red de Monero. Elige entre nodos públicos, o totalmente ciberpunk conectándote al tuyo propio. + Envia con un dedo + Ahora puedes enviar XMR con sólo tu huella dactilar si lo deseas. Para extra seguridad, simplemente desactiva la opción y usa tu contraseña. Tema @@ -368,7 +367,7 @@ No hay nada aquí.\nPor favor crea o restaura un monedero. - Restaurar los nodos por defecto + Agregar nodos públicos Restauración ya en proceso… Último bloque hace %1$d segundos @@ -393,46 +392,51 @@ Por favor ingresa o escanea una dirección de Monero. ]]>
- Subdirecciones + Subdirecciones de la cuenta Nombre de la subdirección Demasiadas direcciones sin usar. ¡Usa alguna antes de crear más! Demasiadas cuentas sin usar. ¡Usa alguna antes de crear más! Transacciones recibidas en esta subdirección: Aún no hay transacciones recibidas en esta subdirección. Elige una subdirección - Presiona largo para ver detalles + Mantén presionado para ver los detalles - Delete - Delete failed! + Eliminar + ¡Falló la eliminación! - Import wallet - Import failed! + Importar monedero + ¡Falló la importación! - Reset wallet! - This wallet will be reset, losing all off-chain data (like notes, account & subaddress names, private transaction keys, …)! Use this ONLY if this wallet is corrupt and does not load! + Restaurar + Este monedero será restaurado, y perderá toda la información personalizada (notas, nombres de cuentas y subdirecciones, claves particulares de transacciones…) Tus fondos no se verán comprometidos, pero usa esto SÓLO si el monedero está corrupto y no se puede abrir. - Tor required - \u00A0WAITING FOR NODE\u00A0 - "Allow Background Starts" in Orbot Settings to use Tor! - SideShift.ai doesn\'t support Tor.\nDisable Tor to swap XMR. + Requiere Tor + \u00A0ESPERANDO AL NODO\u00A0 + Selecciona "Permitir inicios en segundo plano" en los ajustes de Orbot para usar Tor + SideShift.ai no soporta Tor.\nDesactiva Tor para cambiar XMR. - Seed encryption (EXPERIMENTAL) - Seed Offset Phrase (optional) + Semilla encriptada (EXPERIMENTAL) + Palabra clave adicional - Settings - Interface - Information - Day / Night + Ajustes + Interfaz + Información + Modo - Style + Tema Classic Baldaŭ - Failed to create QR for sharing! + ¡Error al crear QR! - Transaction amount locked until block %1$d (in %2$d blocks ≈ %3$,.2f days) + Monto trabado hasta el bloque %1$d (faltan %2$d bloques ≈ %3$,.2f días) - Street Mode enabled\nOnly new transactions will be shown + Modo calle activado\nSólo se ºmostrarán transacciones nuevas + + To reduce waiting time on repeated spending, Monerujo can create spare change at the expense of higher fees. It\'ll try to create and maintain 10 coins of the selected amount. + Create Change + + APPLY
diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index eadd219b..39b5266b 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -442,4 +442,9 @@ Transaction amount locked until block %1$d (in %2$d blocks ≈ %3$,.2f days) Street Mode enabled\nOnly new transactions will be shown + + To reduce waiting time on repeated spending, Monerujo can create spare change at the expense of higher fees. It\'ll try to create and maintain 10 coins of the selected amount. + Create Change + + APPLY diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 0b474335..f9507696 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -682,4 +682,9 @@ Transaction amount locked until block %1$d (in %2$d blocks ≈ %3$,.2f days) Street Mode enabled\nOnly new transactions will be shown + + To reduce waiting time on repeated spending, Monerujo can create spare change at the expense of higher fees. It\'ll try to create and maintain 10 coins of the selected amount. + Create Change + + APPLY diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 3a28c5cf..a833e533 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -448,4 +448,9 @@ Transaction amount locked until block %1$d (in %2$d blocks ≈ %3$,.2f days) Street Mode enabled\nOnly new transactions will be shown + + To reduce waiting time on repeated spending, Monerujo can create spare change at the expense of higher fees. It\'ll try to create and maintain 10 coins of the selected amount. + Create Change + + APPLY diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 6e89261e..b8d054ba 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -446,4 +446,9 @@ Transaction amount locked until block %1$d (in %2$d blocks ≈ %3$,.2f days) Street Mode enabled\nOnly new transactions will be shown + + To reduce waiting time on repeated spending, Monerujo can create spare change at the expense of higher fees. It\'ll try to create and maintain 10 coins of the selected amount. + Create Change + + APPLY diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 750e6726..d1fdfb36 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -447,4 +447,9 @@ Transaction amount locked until block %1$d (in %2$d blocks ≈ %3$,.2f days) Street Mode enabled\nOnly new transactions will be shown + + To reduce waiting time on repeated spending, Monerujo can create spare change at the expense of higher fees. It\'ll try to create and maintain 10 coins of the selected amount. + Create Change + + APPLY diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index cba4ef86..a4cb112a 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -447,4 +447,9 @@ Transaction amount locked until block %1$d (in %2$d blocks ≈ %3$,.2f days) Street Mode enabled\nOnly new transactions will be shown + + To reduce waiting time on repeated spending, Monerujo can create spare change at the expense of higher fees. It\'ll try to create and maintain 10 coins of the selected amount. + Create Change + + APPLY diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index c1f9c6b6..bf1a34cf 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -444,4 +444,9 @@ Transaction amount locked until block %1$d (in %2$d blocks ≈ %3$,.2f days) Street Mode enabled\nOnly new transactions will be shown + + To reduce waiting time on repeated spending, Monerujo can create spare change at the expense of higher fees. It\'ll try to create and maintain 10 coins of the selected amount. + Create Change + + APPLY diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index b2f6dec1..9cff464d 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -444,4 +444,9 @@ Transaction amount locked until block %1$d (in %2$d blocks ≈ %3$,.2f days) Street Mode enabled\nOnly new transactions will be shown + + To reduce waiting time on repeated spending, Monerujo can create spare change at the expense of higher fees. It\'ll try to create and maintain 10 coins of the selected amount. + Create Change + + APPLY diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 974049aa..5d5dad03 100755 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -437,4 +437,9 @@ aqui. Transaction amount locked until block %1$d (in %2$d blocks ≈ %3$,.2f days) Street Mode enabled\nOnly new transactions will be shown + + To reduce waiting time on repeated spending, Monerujo can create spare change at the expense of higher fees. It\'ll try to create and maintain 10 coins of the selected amount. + Create Change + + APPLY diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 841c0ea0..b9f3d2e0 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -448,4 +448,9 @@ Transaction amount locked until block %1$d (in %2$d blocks ≈ %3$,.2f days) Street Mode enabled\nOnly new transactions will be shown + + 10 × %1$3.1f XMR + To reduce waiting time on repeated spending, Monerujo can create spare change at the expense of higher fees. It\'ll try to create and maintain 10 coins of the selected amount. + + APPLY diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 7ea40515..de4df508 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -444,4 +444,9 @@ Transaction amount locked until block %1$d (in %2$d blocks ≈ %3$,.2f days) Street Mode enabled\nOnly new transactions will be shown + + To reduce waiting time on repeated spending, Monerujo can create spare change at the expense of higher fees. It\'ll try to create and maintain 10 coins of the selected amount. + Create Change + + APPLY diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index dac96ec3..60d57abe 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -448,4 +448,9 @@ Transaction amount locked until block %1$d (in %2$d blocks ≈ %3$,.2f days) Street Mode enabled\nOnly new transactions will be shown + + To reduce waiting time on repeated spending, Monerujo can create spare change at the expense of higher fees. It\'ll try to create and maintain 10 coins of the selected amount. + Create Change + + APPLY diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 46a73d05..43325dd1 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -445,4 +445,9 @@ Transaction amount locked until block %1$d (in %2$d blocks ≈ %3$,.2f days) Street Mode enabled\nOnly new transactions will be shown + + To reduce waiting time on repeated spending, Monerujo can create spare change at the expense of higher fees. It\'ll try to create and maintain 10 coins of the selected amount. + Create Change + + APPLY diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 6ad0e9ed..7c77758c 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -443,4 +443,9 @@ Transaction amount locked until block %1$d (in %2$d blocks ≈ %3$,.2f days) Street Mode enabled\nOnly new transactions will be shown + + To reduce waiting time on repeated spending, Monerujo can create spare change at the expense of higher fees. It\'ll try to create and maintain 10 coins of the selected amount. + Create Change + + APPLY diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index e379be5d..4bbba34f 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -436,4 +436,9 @@ Transaction amount locked until block %1$d (in %2$d blocks ≈ %3$,.2f days) Street Mode enabled\nOnly new transactions will be shown + + To reduce waiting time on repeated spending, Monerujo can create spare change at the expense of higher fees. It\'ll try to create and maintain 10 coins of the selected amount. + Create Change + + APPLY diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 692fd202..9faee4d3 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -442,4 +442,9 @@ Transaction amount locked until block %1$d (in %2$d blocks ≈ %3$,.2f days) Street Mode enabled\nOnly new transactions will be shown + + To reduce waiting time on repeated spending, Monerujo can create spare change at the expense of higher fees. It\'ll try to create and maintain 10 coins of the selected amount. + Create Change + + APPLY diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 74fb8c66..9bd8ff70 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -448,4 +448,9 @@ Transaction amount locked until block %1$d (in %2$d blocks ≈ %3$,.2f days) Street Mode enabled\nOnly new transactions will be shown + + To reduce waiting time on repeated spending, Monerujo can create spare change at the expense of higher fees. It\'ll try to create and maintain 10 coins of the selected amount. + Create Change + + APPLY diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 39538288..a274c89e 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -369,4 +369,9 @@ Transaction amount locked until block %1$d (in %2$d blocks ≈ %3$,.2f days) Street Mode enabled\nOnly new transactions will be shown + + To reduce waiting time on repeated spending, Monerujo can create spare change at the expense of higher fees. It\'ll try to create and maintain 10 coins of the selected amount. + Create Change + + APPLY diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index d88897f4..2e34e613 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -443,4 +443,9 @@ Transaction amount locked until block %1$d (in %2$d blocks ≈ %3$,.2f days) Street Mode enabled\nOnly new transactions will be shown + + To reduce waiting time on repeated spending, Monerujo can create spare change at the expense of higher fees. It\'ll try to create and maintain 10 coins of the selected amount. + Create Change + + APPLY diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 4d0e9b43..fd98293a 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -22,4 +22,5 @@ + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0ea93487..1cf4aef6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -520,4 +520,13 @@ Transaction amount locked until block %1$d (in %2$d blocks ≈ %3$,.2f days) Street Mode enabled\nOnly new transactions will be shown + + PocketChange + 10 × %1$3.1f XMR + PocketChange +%1$s + + To reduce waiting time on repeated spending, Monerujo can create spare change at the expense of higher fees. It\'ll try to create and maintain 10 coins of the selected amount. + Create Change + + APPLY diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 11639113..4501c4dd 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -158,6 +158,12 @@ normal + +