From 2ebc828d3ed1d7959b8b4e747f240bf93180c54d Mon Sep 17 00:00:00 2001 From: pokkst Date: Thu, 22 Sep 2022 15:16:09 -0500 Subject: [PATCH] Add ability to select UTXOs to spend --- app/src/main/cpp/monerujo.cpp | 5 +- .../java/net/mynero/wallet/MainActivity.java | 4 + .../wallet/adapter/CoinsInfoAdapter.java | 111 ++++++++++++++++++ .../dialog/SendBottomSheetDialog.java | 24 +++- .../fragment/settings/SettingsFragment.java | 22 ++++ .../wallet/fragment/utxos/UtxosFragment.java | 96 +++++++++++++++ .../wallet/fragment/utxos/UtxosViewModel.java | 7 ++ .../net/mynero/wallet/model/CoinsInfo.java | 8 +- .../wallet/service/MoneroHandlerThread.java | 10 +- .../net/mynero/wallet/service/TxService.java | 6 +- .../mynero/wallet/service/UTXOService.java | 60 ++++++++++ app/src/main/res/layout/fragment_settings.xml | 14 ++- app/src/main/res/layout/fragment_utxos.xml | 34 ++++++ .../res/layout/send_bottom_sheet_dialog.xml | 14 ++- .../main/res/layout/utxo_selection_item.xml | 31 +++++ app/src/main/res/navigation/main_nav.xml | 12 +- app/src/main/res/values/strings.xml | 2 + 17 files changed, 447 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/net/mynero/wallet/adapter/CoinsInfoAdapter.java create mode 100644 app/src/main/java/net/mynero/wallet/fragment/utxos/UtxosFragment.java create mode 100644 app/src/main/java/net/mynero/wallet/fragment/utxos/UtxosViewModel.java create mode 100644 app/src/main/java/net/mynero/wallet/service/UTXOService.java create mode 100644 app/src/main/res/layout/fragment_utxos.xml create mode 100644 app/src/main/res/layout/utxo_selection_item.xml diff --git a/app/src/main/cpp/monerujo.cpp b/app/src/main/cpp/monerujo.cpp index 5c5e0e8..70232ec 100644 --- a/app/src/main/cpp/monerujo.cpp +++ b/app/src/main/cpp/monerujo.cpp @@ -1083,12 +1083,13 @@ Java_net_mynero_wallet_model_Wallet_getCoinsJ(JNIEnv *env, jobject instance) { jobject newCoinsInfo(JNIEnv *env, Monero::CoinsInfo *info) { jmethodID c = env->GetMethodID(class_CoinsInfo, "", - "(JZLjava/lang/String;)V"); + "(JZLjava/lang/String;J)V"); jstring _key_image = env->NewStringUTF(info->keyImage().c_str()); jobject result = env->NewObject(class_CoinsInfo, c, static_cast (info->globalOutputIndex()), info->spent(), - _key_image); + _key_image, + static_cast (info->amount())); env->DeleteLocalRef(_key_image); return result; } diff --git a/app/src/main/java/net/mynero/wallet/MainActivity.java b/app/src/main/java/net/mynero/wallet/MainActivity.java index 0c98249..5f4a8a4 100644 --- a/app/src/main/java/net/mynero/wallet/MainActivity.java +++ b/app/src/main/java/net/mynero/wallet/MainActivity.java @@ -24,6 +24,7 @@ import net.mynero.wallet.service.HistoryService; import net.mynero.wallet.service.MoneroHandlerThread; import net.mynero.wallet.service.PrefService; import net.mynero.wallet.service.TxService; +import net.mynero.wallet.service.UTXOService; import net.mynero.wallet.util.Constants; import net.mynero.wallet.util.UriData; @@ -36,6 +37,7 @@ public class MainActivity extends AppCompatActivity implements MoneroHandlerThre private AddressService addressService = null; private HistoryService historyService = null; private BlockchainService blockchainService = null; + private UTXOService utxoService = null; private boolean proceedToSend = false; private UriData uriData = null; @@ -96,6 +98,7 @@ public class MainActivity extends AppCompatActivity implements MoneroHandlerThre this.addressService = new AddressService(thread); this.historyService = new HistoryService(thread); this.blockchainService = new BlockchainService(thread); + this.utxoService = new UTXOService(thread); thread.start(); } @@ -105,6 +108,7 @@ public class MainActivity extends AppCompatActivity implements MoneroHandlerThre this.balanceService.refreshBalance(); this.blockchainService.refreshBlockchain(); this.addressService.refreshAddresses(); + this.utxoService.refreshUtxos(); } @Override diff --git a/app/src/main/java/net/mynero/wallet/adapter/CoinsInfoAdapter.java b/app/src/main/java/net/mynero/wallet/adapter/CoinsInfoAdapter.java new file mode 100644 index 0000000..8f4add4 --- /dev/null +++ b/app/src/main/java/net/mynero/wallet/adapter/CoinsInfoAdapter.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.mynero.wallet.adapter; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.recyclerview.widget.RecyclerView; + +import net.mynero.wallet.R; +import net.mynero.wallet.model.CoinsInfo; +import net.mynero.wallet.model.Wallet; + +import java.util.ArrayList; +import java.util.List; + +public class CoinsInfoAdapter extends RecyclerView.Adapter { + + private List localDataSet; + private List selectedUtxos; + private CoinsInfoAdapterListener listener = null; + + /** + * Initialize the dataset of the Adapter. + */ + public CoinsInfoAdapter(CoinsInfoAdapterListener listener) { + this.listener = listener; + this.localDataSet = new ArrayList<>(); + this.selectedUtxos = new ArrayList<>(); + } + + public void submitList(List dataSet, List selectedUtxos) { + this.localDataSet = dataSet; + this.selectedUtxos = selectedUtxos; + notifyDataSetChanged(); + } + + public void updateSelectedUtxos(List selectedUtxos) { + this.selectedUtxos = selectedUtxos; + notifyDataSetChanged(); + } + + // Create new views (invoked by the layout manager) + @Override + public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { + // Create a new view, which defines the UI of the list item + View view = LayoutInflater.from(viewGroup.getContext()) + .inflate(R.layout.utxo_selection_item, viewGroup, false); + + return new ViewHolder(listener, view); + } + + // Replace the contents of a view (invoked by the layout manager) + @Override + public void onBindViewHolder(ViewHolder viewHolder, final int position) { + CoinsInfo tx = localDataSet.get(position); + viewHolder.bind(tx, selectedUtxos); + } + + // Return the size of your dataset (invoked by the layout manager) + @Override + public int getItemCount() { + return localDataSet.size(); + } + + public interface CoinsInfoAdapterListener { + void onUtxoSelected(CoinsInfo coinsInfo); + } + + /** + * Provide a reference to the type of views that you are using + * (custom ViewHolder). + */ + public static class ViewHolder extends RecyclerView.ViewHolder { + private CoinsInfoAdapterListener listener = null; + + public ViewHolder(CoinsInfoAdapterListener listener, View view) { + super(view); + this.listener = listener; + } + + public void bind(CoinsInfo coinsInfo, List selectedUtxos) { + boolean selected = selectedUtxos.contains(coinsInfo.getKeyImage()); + TextView keyImageTextView = itemView.findViewById(R.id.utxo_key_image_textview); + TextView amountTextView = itemView.findViewById(R.id.utxo_amount_textview); + amountTextView.setText(Wallet.getDisplayAmount(coinsInfo.getAmount())); + keyImageTextView.setText(coinsInfo.getKeyImage()); + itemView.setOnLongClickListener(view -> { + listener.onUtxoSelected(coinsInfo); + return true; + }); + } + } +} + diff --git a/app/src/main/java/net/mynero/wallet/fragment/dialog/SendBottomSheetDialog.java b/app/src/main/java/net/mynero/wallet/fragment/dialog/SendBottomSheetDialog.java index 19826a9..dab0ffc 100644 --- a/app/src/main/java/net/mynero/wallet/fragment/dialog/SendBottomSheetDialog.java +++ b/app/src/main/java/net/mynero/wallet/fragment/dialog/SendBottomSheetDialog.java @@ -27,16 +27,20 @@ import com.google.zxing.client.android.Intents; import com.journeyapps.barcodescanner.ScanContract; import com.journeyapps.barcodescanner.ScanOptions; import net.mynero.wallet.R; +import net.mynero.wallet.model.CoinsInfo; import net.mynero.wallet.model.PendingTransaction; import net.mynero.wallet.model.Wallet; import net.mynero.wallet.service.BalanceService; import net.mynero.wallet.service.TxService; +import net.mynero.wallet.service.UTXOService; import net.mynero.wallet.util.Helper; import net.mynero.wallet.util.UriData; +import java.util.ArrayList; import java.util.List; public class SendBottomSheetDialog extends BottomSheetDialogFragment { + public ArrayList selectedUtxos = new ArrayList<>(); private final MutableLiveData _sendingMax = new MutableLiveData<>(false); public LiveData sendingMax = _sendingMax; private final ActivityResultLauncher cameraPermissionsLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), granted -> { @@ -61,6 +65,7 @@ public class SendBottomSheetDialog extends BottomSheetDialogFragment { private TextView addressTextView; private TextView amountTextView; private TextView feeRadioGroupLabelTextView; + private TextView selectedUtxosValueTextView; private Button createButton; private Button sendButton; private Button sendMaxButton; @@ -92,6 +97,7 @@ public class SendBottomSheetDialog extends BottomSheetDialogFragment { amountTextView = view.findViewById(R.id.amount_pending_textview); feeRadioGroup = view.findViewById(R.id.tx_fee_radiogroup); feeRadioGroupLabelTextView = view.findViewById(R.id.tx_fee_radiogroup_label_textview); + selectedUtxosValueTextView = view.findViewById(R.id.selected_utxos_value_textview); if (uriData != null) { addressEditText.setText(uriData.getAddress()); @@ -100,6 +106,22 @@ public class SendBottomSheetDialog extends BottomSheetDialogFragment { } } + if(!selectedUtxos.isEmpty()) { + long selectedValue = 0; + + for(CoinsInfo coinsInfo : UTXOService.getInstance().getUtxos()) { + if(selectedUtxos.contains(coinsInfo.getKeyImage())) { + selectedValue += coinsInfo.getAmount(); + } + } + + String valueString = Wallet.getDisplayAmount(selectedValue); + selectedUtxosValueTextView.setVisibility(View.VISIBLE); + selectedUtxosValueTextView.setText(getResources().getString(R.string.selected_utxos_value, valueString)); + } else { + selectedUtxosValueTextView.setVisibility(View.GONE); + } + bindObservers(); bindListeners(); } @@ -232,7 +254,7 @@ public class SendBottomSheetDialog extends BottomSheetDialogFragment { private void createTx(String address, String amount, boolean sendAll, PendingTransaction.Priority feePriority) { AsyncTask.execute(() -> { - PendingTransaction pendingTx = TxService.getInstance().createTx(address, amount, sendAll, feePriority); + PendingTransaction pendingTx = TxService.getInstance().createTx(address, amount, sendAll, feePriority, selectedUtxos); if (pendingTx != null && pendingTx.getStatus() == PendingTransaction.Status.Status_Ok) { _pendingTransaction.postValue(pendingTx); } else { diff --git a/app/src/main/java/net/mynero/wallet/fragment/settings/SettingsFragment.java b/app/src/main/java/net/mynero/wallet/fragment/settings/SettingsFragment.java index 13242a0..81e1732 100644 --- a/app/src/main/java/net/mynero/wallet/fragment/settings/SettingsFragment.java +++ b/app/src/main/java/net/mynero/wallet/fragment/settings/SettingsFragment.java @@ -18,7 +18,11 @@ import androidx.annotation.Nullable; import androidx.appcompat.widget.SwitchCompat; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavDirections; +import androidx.navigation.fragment.NavHostFragment; import net.mynero.wallet.R; import net.mynero.wallet.data.DefaultNodes; @@ -73,6 +77,8 @@ public class SettingsFragment extends Fragment implements PasswordBottomSheetDia super.onViewCreated(view, savedInstanceState); mViewModel = new ViewModelProvider(this).get(SettingsViewModel.class); Button displaySeedButton = view.findViewById(R.id.display_seed_button); + Button displayUtxosButton = view.findViewById(R.id.display_utxos_button); + selectNodeButton = view.findViewById(R.id.select_node_button); SwitchCompat nightModeSwitch = view.findViewById(R.id.day_night_switch); SwitchCompat torSwitch = view.findViewById(R.id.tor_switch); @@ -138,6 +144,10 @@ public class SettingsFragment extends Fragment implements PasswordBottomSheetDia } }); + displayUtxosButton.setOnClickListener(view1 -> { + navigate(R.id.nav_to_utxos); + }); + TextView statusTextView = view.findViewById(R.id.status_textview); BlockchainService.getInstance().connectionStatus.observe(getViewLifecycleOwner(), connectionStatus -> { if(connectionStatus == Wallet.ConnectionStatus.ConnectionStatus_Connected) { @@ -218,4 +228,16 @@ public class SettingsFragment extends Fragment implements PasswordBottomSheetDia dialog.listener = this; dialog.show(getActivity().getSupportFragmentManager(), "node_selection_dialog"); } + + private void navigate(int destination) { + FragmentActivity activity = getActivity(); + if (activity != null) { + FragmentManager fm = activity.getSupportFragmentManager(); + NavHostFragment navHostFragment = + (NavHostFragment) fm.findFragmentById(R.id.nav_host_fragment); + if (navHostFragment != null) { + navHostFragment.getNavController().navigate(destination); + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/net/mynero/wallet/fragment/utxos/UtxosFragment.java b/app/src/main/java/net/mynero/wallet/fragment/utxos/UtxosFragment.java new file mode 100644 index 0000000..b4c0341 --- /dev/null +++ b/app/src/main/java/net/mynero/wallet/fragment/utxos/UtxosFragment.java @@ -0,0 +1,96 @@ +package net.mynero.wallet.fragment.utxos; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import net.mynero.wallet.R; +import net.mynero.wallet.adapter.CoinsInfoAdapter; +import net.mynero.wallet.fragment.dialog.SendBottomSheetDialog; +import net.mynero.wallet.model.CoinsInfo; +import net.mynero.wallet.service.UTXOService; + +import java.util.ArrayList; + +public class UtxosFragment extends Fragment implements CoinsInfoAdapter.CoinsInfoAdapterListener { + + private UtxosViewModel mViewModel; + private ArrayList selectedUtxos = new ArrayList<>(); + private CoinsInfoAdapter adapter = new CoinsInfoAdapter(this); + private Button sendUtxosButton; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_utxos, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + mViewModel = new ViewModelProvider(this).get(UtxosViewModel.class); + bindListeners(view); + bindObservers(view); + } + + private void bindListeners(View view) { + sendUtxosButton = view.findViewById(R.id.send_utxos_button); + sendUtxosButton.setVisibility(View.GONE); + sendUtxosButton.setOnClickListener(view1 -> { + SendBottomSheetDialog sendDialog = new SendBottomSheetDialog(); + sendDialog.selectedUtxos = selectedUtxos; + sendDialog.show(getActivity().getSupportFragmentManager(), null); + }); + } + + private void bindObservers(View view) { + RecyclerView utxosRecyclerView = view.findViewById(R.id.transaction_history_recyclerview); + UTXOService utxoService = UTXOService.getInstance(); + utxosRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); + utxosRecyclerView.setAdapter(adapter); + if (utxoService != null) { + utxoService.utxos.observe(getViewLifecycleOwner(), utxos -> { + ArrayList filteredUtxos = new ArrayList<>(); + for(CoinsInfo coinsInfo : utxos) { + if(!coinsInfo.isSpent()) { + filteredUtxos.add(coinsInfo); + } + } + if (filteredUtxos.isEmpty()) { + utxosRecyclerView.setVisibility(View.GONE); + } else { + adapter.submitList(filteredUtxos, selectedUtxos); + utxosRecyclerView.setVisibility(View.VISIBLE); + } + }); + } + } + + @Override + public void onUtxoSelected(CoinsInfo coinsInfo) { + boolean selected = selectedUtxos.contains(coinsInfo.getKeyImage()); + if(selected) { + System.out.println("Deselecting: " + coinsInfo.getKeyImage()); + selectedUtxos.remove(coinsInfo.getKeyImage()); + } else { + System.out.println("Selecting: " + coinsInfo.getKeyImage()); + selectedUtxos.add(coinsInfo.getKeyImage()); + } + + if(selectedUtxos.isEmpty()) { + sendUtxosButton.setVisibility(View.GONE); + } else { + sendUtxosButton.setVisibility(View.VISIBLE); + } + + adapter.updateSelectedUtxos(selectedUtxos); + } +} \ No newline at end of file diff --git a/app/src/main/java/net/mynero/wallet/fragment/utxos/UtxosViewModel.java b/app/src/main/java/net/mynero/wallet/fragment/utxos/UtxosViewModel.java new file mode 100644 index 0000000..367ec96 --- /dev/null +++ b/app/src/main/java/net/mynero/wallet/fragment/utxos/UtxosViewModel.java @@ -0,0 +1,7 @@ +package net.mynero.wallet.fragment.utxos; + +import androidx.lifecycle.ViewModel; + +public class UtxosViewModel extends ViewModel { + +} \ No newline at end of file diff --git a/app/src/main/java/net/mynero/wallet/model/CoinsInfo.java b/app/src/main/java/net/mynero/wallet/model/CoinsInfo.java index 259ed47..8ef386b 100644 --- a/app/src/main/java/net/mynero/wallet/model/CoinsInfo.java +++ b/app/src/main/java/net/mynero/wallet/model/CoinsInfo.java @@ -32,11 +32,13 @@ public class CoinsInfo implements Parcelable { long globalOutputIndex; boolean spent; String keyImage; + long amount; - public CoinsInfo(long globalOutputIndex, boolean spent, String keyImage) { + public CoinsInfo(long globalOutputIndex, boolean spent, String keyImage, long amount) { this.globalOutputIndex = globalOutputIndex; this.spent = spent; this.keyImage = keyImage; + this.amount = amount; } protected CoinsInfo(Parcel in) { @@ -67,6 +69,10 @@ public class CoinsInfo implements Parcelable { return keyImage; } + public long getAmount() { + return amount; + } + @Override public int describeContents() { return 0; diff --git a/app/src/main/java/net/mynero/wallet/service/MoneroHandlerThread.java b/app/src/main/java/net/mynero/wallet/service/MoneroHandlerThread.java index 04bc870..6d1a29a 100644 --- a/app/src/main/java/net/mynero/wallet/service/MoneroHandlerThread.java +++ b/app/src/main/java/net/mynero/wallet/service/MoneroHandlerThread.java @@ -122,10 +122,14 @@ public class MoneroHandlerThread extends Thread implements WalletListener { listener.onRefresh(); } - public PendingTransaction createTx(String address, String amountStr, boolean sendAll, PendingTransaction.Priority feePriority) { + public PendingTransaction createTx(String address, String amountStr, boolean sendAll, PendingTransaction.Priority feePriority, ArrayList selectedUtxos) { long amount = sendAll ? SWEEP_ALL : Wallet.getAmountFromString(amountStr); - ArrayList preferredInputs = new ArrayList<>(); - preferredInputs.add(""); + ArrayList preferredInputs; + if(selectedUtxos.isEmpty()) { + preferredInputs = UTXOService.getInstance().selectUtxos(amount, sendAll); + } else { + preferredInputs = selectedUtxos; + } return wallet.createTransaction(new TxData(address, amount, 0, feePriority, preferredInputs)); } diff --git a/app/src/main/java/net/mynero/wallet/service/TxService.java b/app/src/main/java/net/mynero/wallet/service/TxService.java index a26580d..881e8f5 100644 --- a/app/src/main/java/net/mynero/wallet/service/TxService.java +++ b/app/src/main/java/net/mynero/wallet/service/TxService.java @@ -2,6 +2,8 @@ package net.mynero.wallet.service; import net.mynero.wallet.model.PendingTransaction; +import java.util.ArrayList; + public class TxService extends ServiceBase { public static TxService instance = null; @@ -14,8 +16,8 @@ public class TxService extends ServiceBase { return instance; } - public PendingTransaction createTx(String address, String amount, boolean sendAll, PendingTransaction.Priority feePriority) { - return this.getThread().createTx(address, amount, sendAll, feePriority); + public PendingTransaction createTx(String address, String amount, boolean sendAll, PendingTransaction.Priority feePriority, ArrayList selectedUtxos) { + return this.getThread().createTx(address, amount, sendAll, feePriority, selectedUtxos); } public boolean sendTx(PendingTransaction pendingTransaction) { diff --git a/app/src/main/java/net/mynero/wallet/service/UTXOService.java b/app/src/main/java/net/mynero/wallet/service/UTXOService.java new file mode 100644 index 0000000..a2585bc --- /dev/null +++ b/app/src/main/java/net/mynero/wallet/service/UTXOService.java @@ -0,0 +1,60 @@ +package net.mynero.wallet.service; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import net.mynero.wallet.model.CoinsInfo; +import net.mynero.wallet.model.TransactionInfo; +import net.mynero.wallet.model.Wallet; +import net.mynero.wallet.model.WalletManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class UTXOService extends ServiceBase { + public static UTXOService instance = null; + private final MutableLiveData> _utxos = new MutableLiveData<>(); + public LiveData> utxos = _utxos; + public UTXOService(MoneroHandlerThread thread) { + super(thread); + instance = this; + } + + public static UTXOService getInstance() { + return instance; + } + + public void refreshUtxos() { + _utxos.postValue(getUtxos()); + } + + public List getUtxos() { + return WalletManager.getInstance().getWallet().getCoins().getAll(); + } + + public ArrayList selectUtxos(long amount, boolean sendAll) { + ArrayList selectedUtxos = new ArrayList<>(); + List utxos = getUtxos(); + if(sendAll) { + for(CoinsInfo coinsInfo : utxos) { + selectedUtxos.add(coinsInfo.getKeyImage()); + } + } else { + long amountSelected = 0; + Collections.shuffle(utxos); + for (CoinsInfo coinsInfo : utxos) { + if (amount == Wallet.SWEEP_ALL) { + selectedUtxos.add(coinsInfo.getKeyImage()); + } else { + if (amountSelected <= amount) { + selectedUtxos.add(coinsInfo.getKeyImage()); + amountSelected += coinsInfo.getAmount(); + } + } + } + } + + return selectedUtxos; + } +} diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml index 4d9db64..6e18d66 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -56,6 +56,18 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/wallet_settings_textview" /> +