diff --git a/app/src/main/java/com/cobo/cold/DataRepository.java b/app/src/main/java/com/cobo/cold/DataRepository.java index e3dee73..e7abd81 100644 --- a/app/src/main/java/com/cobo/cold/DataRepository.java +++ b/app/src/main/java/com/cobo/cold/DataRepository.java @@ -130,6 +130,10 @@ public class DataRepository { return mDb.txDao().loadElectrumTxsSync(coinId); } + public List loadAllTxSync(String coinId) { + return mDb.txDao().loadTxsSync(coinId); + } + public LiveData loadTx(String txId) { return mDb.txDao().load(txId); } diff --git a/app/src/main/java/com/cobo/cold/db/dao/TxDao.java b/app/src/main/java/com/cobo/cold/db/dao/TxDao.java index d2fe3c3..4f7edca 100644 --- a/app/src/main/java/com/cobo/cold/db/dao/TxDao.java +++ b/app/src/main/java/com/cobo/cold/db/dao/TxDao.java @@ -36,6 +36,9 @@ public interface TxDao { @Query("SELECT * FROM txs where coinId = :coinId and signId = 'electrum_sign_id' ORDER BY timeStamp DESC") List loadElectrumTxsSync(String coinId); + @Query("SELECT * FROM txs where coinId = :coinId") + List loadTxsSync(String coinId); + @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(TxEntity tx); diff --git a/app/src/main/java/com/cobo/cold/ui/fragment/BaseFragment.java b/app/src/main/java/com/cobo/cold/ui/fragment/BaseFragment.java index f1a0fda..b6d307e 100644 --- a/app/src/main/java/com/cobo/cold/ui/fragment/BaseFragment.java +++ b/app/src/main/java/com/cobo/cold/ui/fragment/BaseFragment.java @@ -142,5 +142,9 @@ public abstract class BaseFragment extends Fragment { public void navigate(@IdRes int id, Bundle data) { NavHostFragment.findNavController(this).navigate(id, data); } + + public AppCompatActivity getHostActivity() { + return mActivity; + } } diff --git a/app/src/main/java/com/cobo/cold/ui/fragment/main/FeeAttackChecking.java b/app/src/main/java/com/cobo/cold/ui/fragment/main/FeeAttackChecking.java new file mode 100644 index 0000000..1b58d23 --- /dev/null +++ b/app/src/main/java/com/cobo/cold/ui/fragment/main/FeeAttackChecking.java @@ -0,0 +1,78 @@ +package com.cobo.cold.ui.fragment.main; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; + +import androidx.databinding.DataBindingUtil; +import androidx.navigation.Navigation; + +import com.cobo.cold.R; +import com.cobo.cold.databinding.CommonModalBinding; +import com.cobo.cold.db.entity.TxEntity; +import com.cobo.cold.ui.fragment.BaseFragment; +import com.cobo.cold.ui.modal.ModalDialog; + +import java.util.Objects; + +import static com.cobo.cold.ui.fragment.main.TxFragment.KEY_TX_ID; +import static com.cobo.cold.viewmodel.ElectrumViewModel.ELECTRUM_SIGN_ID; + +public class FeeAttackChecking { + + public static final String KEY_DUPLICATE_TX = "key_duplicate_tx"; + public interface FeeAttackCheckingResult { + + int NORMAL = 1; + int DUPLICATE_TX = 2; + int SAME_OUTPUTS = 3; + } + private BaseFragment fragment; + + public FeeAttackChecking(BaseFragment fragment) { + this.fragment = fragment; + } + + public void showFeeAttackWarning() { + ModalDialog modalDialog = ModalDialog.newInstance(); + CommonModalBinding binding = DataBindingUtil.inflate( + LayoutInflater.from(fragment.getHostActivity()), R.layout.common_modal, + null, false); + modalDialog.setBinding(binding); + binding.title.setText(R.string.abnormal_tx); + binding.subTitle.setText(R.string.fee_attack_warning); + binding.confirm.setText(R.string.know); + binding.confirm.setOnClickListener(v -> modalDialog.dismiss()); + modalDialog.show(fragment.getHostActivity().getSupportFragmentManager(),""); + } + + private void navigateToSignedTx(String txId, String signId) { + Bundle bundle = new Bundle(); + bundle.putString(KEY_TX_ID, txId); + bundle.putBoolean(KEY_DUPLICATE_TX,true); + if (ELECTRUM_SIGN_ID.equals(signId)) { + Navigation.findNavController(Objects.requireNonNull(fragment.getView())) + .navigate(R.id.action_to_electrumTxFragment, bundle); + } else { + Navigation.findNavController(Objects.requireNonNull(fragment.getView())) + .navigate(R.id.action_to_txFragment, bundle); + } + } + + public void showDuplicateTx(TxEntity tx) { + ModalDialog modalDialog = ModalDialog.newInstance(); + CommonModalBinding binding = DataBindingUtil.inflate( + LayoutInflater.from(fragment.getHostActivity()), R.layout.common_modal, + null, false); + modalDialog.setBinding(binding); + binding.title.setText(R.string.broadcast_tx); + binding.close.setVisibility(View.GONE); + binding.subTitle.setText(R.string.already_signed); + binding.confirm.setText(R.string.broadcast_tx); + binding.confirm.setOnClickListener(v -> { + modalDialog.dismiss(); + navigateToSignedTx(tx.getTxId(), tx.getSignId()); + }); + modalDialog.show(fragment.getHostActivity().getSupportFragmentManager(),""); + } +} diff --git a/app/src/main/java/com/cobo/cold/ui/fragment/main/TxConfirmFragment.java b/app/src/main/java/com/cobo/cold/ui/fragment/main/TxConfirmFragment.java index ecb9f45..23d89ed 100644 --- a/app/src/main/java/com/cobo/cold/ui/fragment/main/TxConfirmFragment.java +++ b/app/src/main/java/com/cobo/cold/ui/fragment/main/TxConfirmFragment.java @@ -57,6 +57,9 @@ import java.util.Objects; import static com.cobo.cold.ui.fragment.Constants.KEY_NAV_ID; import static com.cobo.cold.ui.fragment.main.BroadcastTxFragment.KEY_TXID; +import static com.cobo.cold.ui.fragment.main.FeeAttackChecking.FeeAttackCheckingResult.DUPLICATE_TX; +import static com.cobo.cold.ui.fragment.main.FeeAttackChecking.FeeAttackCheckingResult.NORMAL; +import static com.cobo.cold.ui.fragment.main.FeeAttackChecking.FeeAttackCheckingResult.SAME_OUTPUTS; public class TxConfirmFragment extends BaseFragment { @@ -72,6 +75,8 @@ public class TxConfirmFragment extends BaseFragment { private SigningDialog signingDialog; private TxEntity txEntity; private ModalDialog addingAddressDialog; + private int feeAttackCheckingState; + private FeeAttackChecking feeAttackChecking; @Override protected int setView() { @@ -93,6 +98,10 @@ public class TxConfirmFragment extends BaseFragment { } private void handleSign() { + if (feeAttackCheckingState == SAME_OUTPUTS) { + feeAttackChecking.showFeeAttackWarning(); + return; + } boolean fingerprintSignEnable = Utilities.isFingerprintSignEnable(mActivity); if (txEntity != null) { if (FeatureFlags.ENABLE_WHITE_LIST) { @@ -171,6 +180,16 @@ public class TxConfirmFragment extends BaseFragment { navigateUp(); } }); + + viewModel.feeAttackChecking().observe(this, state -> { + feeAttackCheckingState = state; + if (state != NORMAL) { + feeAttackChecking = new FeeAttackChecking(this); + } + if(state == DUPLICATE_TX) { + feeAttackChecking.showDuplicateTx(viewModel.getPreviousSignTx()); + } + }); } private void refreshMemoDisplay() { diff --git a/app/src/main/java/com/cobo/cold/ui/fragment/main/TxFragment.java b/app/src/main/java/com/cobo/cold/ui/fragment/main/TxFragment.java index 149efb9..b4b9efd 100644 --- a/app/src/main/java/com/cobo/cold/ui/fragment/main/TxFragment.java +++ b/app/src/main/java/com/cobo/cold/ui/fragment/main/TxFragment.java @@ -26,6 +26,7 @@ import android.text.style.ForegroundColorSpan; import android.view.View; import androidx.lifecycle.ViewModelProviders; +import androidx.navigation.fragment.NavHostFragment; import com.cobo.coinlib.utils.Coins; import com.cobo.cold.R; @@ -44,6 +45,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import static com.cobo.cold.ui.fragment.main.FeeAttackChecking.KEY_DUPLICATE_TX; + public class TxFragment extends BaseFragment { @@ -58,7 +61,14 @@ public class TxFragment extends BaseFragment { @Override protected void init(View view) { Bundle data = Objects.requireNonNull(getArguments()); - mBinding.toolbar.setNavigationOnClickListener(v -> navigateUp()); + mBinding.toolbar.setNavigationOnClickListener(v -> { + if (data.getBoolean(KEY_DUPLICATE_TX)) { + NavHostFragment.findNavController(this) + .popBackStack(R.id.assetListFragment, false); + } else { + navigateUp(); + } + }); CoinListViewModel viewModel = ViewModelProviders.of(mActivity).get(CoinListViewModel.class); viewModel.loadTx(data.getString(KEY_TX_ID)).observe(this, txEntity -> { mBinding.setTx(txEntity); diff --git a/app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumTxConfirmFragment.java b/app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumTxConfirmFragment.java index b71dbbc..9c817be 100644 --- a/app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumTxConfirmFragment.java +++ b/app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumTxConfirmFragment.java @@ -42,6 +42,7 @@ import com.cobo.cold.databinding.ProgressModalBinding; import com.cobo.cold.db.entity.TxEntity; import com.cobo.cold.encryptioncore.utils.ByteFormatter; import com.cobo.cold.ui.fragment.BaseFragment; +import com.cobo.cold.ui.fragment.main.FeeAttackChecking; import com.cobo.cold.ui.fragment.main.TransactionItem; import com.cobo.cold.ui.fragment.main.TransactionItemAdapter; import com.cobo.cold.ui.modal.ModalDialog; @@ -66,6 +67,9 @@ import java.util.Objects; import static com.cobo.cold.ui.fragment.Constants.KEY_NAV_ID; import static com.cobo.cold.ui.fragment.main.BroadcastTxFragment.KEY_TXID; +import static com.cobo.cold.ui.fragment.main.FeeAttackChecking.FeeAttackCheckingResult.DUPLICATE_TX; +import static com.cobo.cold.ui.fragment.main.FeeAttackChecking.FeeAttackCheckingResult.NORMAL; +import static com.cobo.cold.ui.fragment.main.FeeAttackChecking.FeeAttackCheckingResult.SAME_OUTPUTS; import static com.cobo.cold.viewmodel.ElectrumViewModel.exportSuccess; import static com.cobo.cold.viewmodel.ElectrumViewModel.hasSdcard; import static com.cobo.cold.viewmodel.ElectrumViewModel.showNoSdcardModal; @@ -86,6 +90,8 @@ public class ElectrumTxConfirmFragment extends BaseFragment changeAddress = new ArrayList<>(); + private int feeAttackCheckingState; + private FeeAttackChecking feeAttackChecking; public static void showExportTxnDialog(AppCompatActivity activity, String txId, String hex, Runnable onExportSuccess) { @@ -151,6 +157,10 @@ public class ElectrumTxConfirmFragment extends BaseFragment { + feeAttackCheckingState = state; + if (state != NORMAL) { + feeAttackChecking = new FeeAttackChecking(this); + } + if(state == DUPLICATE_TX) { + feeAttackChecking.showDuplicateTx(viewModel.getPreviousSignTx()); + } + }); } private void refreshAmount() { diff --git a/app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumTxFragment.java b/app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumTxFragment.java index e18c7d8..b564ba4 100644 --- a/app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumTxFragment.java +++ b/app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumTxFragment.java @@ -25,6 +25,7 @@ import android.text.style.ForegroundColorSpan; import android.view.View; import androidx.lifecycle.ViewModelProviders; +import androidx.navigation.fragment.NavHostFragment; import com.cobo.coinlib.utils.Base43; import com.cobo.coinlib.utils.Coins; @@ -46,13 +47,14 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import static com.cobo.cold.ui.fragment.main.FeeAttackChecking.KEY_DUPLICATE_TX; import static com.cobo.cold.ui.fragment.main.electrum.ElectrumBroadcastTxFragment.showElectrumInfo; import static com.cobo.cold.ui.fragment.main.electrum.ElectrumTxConfirmFragment.showExportTxnDialog; public class ElectrumTxFragment extends BaseFragment { - public static final String KEY_TX_ID = "txid"; + private static final String KEY_TX_ID = "txid"; private TxEntity txEntity; private List changeAddress = new ArrayList<>(); @@ -64,7 +66,14 @@ public class ElectrumTxFragment extends BaseFragment { @Override protected void init(View view) { Bundle data = Objects.requireNonNull(getArguments()); - mBinding.toolbar.setNavigationOnClickListener(v -> navigateUp()); + mBinding.toolbar.setNavigationOnClickListener(v -> { + if (data.getBoolean(KEY_DUPLICATE_TX)) { + NavHostFragment.findNavController(this) + .popBackStack(R.id.assetListFragment, false); + } else { + navigateUp(); + } + }); CoinListViewModel viewModel = ViewModelProviders.of(mActivity).get(CoinListViewModel.class); viewModel.loadTx(data.getString(KEY_TX_ID)).observe(this, txEntity -> { mBinding.setTx(txEntity); diff --git a/app/src/main/java/com/cobo/cold/viewmodel/TxConfirmViewModel.java b/app/src/main/java/com/cobo/cold/viewmodel/TxConfirmViewModel.java index 76b0287..e4d59b6 100644 --- a/app/src/main/java/com/cobo/cold/viewmodel/TxConfirmViewModel.java +++ b/app/src/main/java/com/cobo/cold/viewmodel/TxConfirmViewModel.java @@ -76,6 +76,9 @@ import java.util.concurrent.Executors; import java.util.stream.Stream; import static com.cobo.coinlib.coins.BTC.Electrum.TxUtils.isMasterPublicKeyMatch; +import static com.cobo.cold.ui.fragment.main.FeeAttackChecking.FeeAttackCheckingResult.DUPLICATE_TX; +import static com.cobo.cold.ui.fragment.main.FeeAttackChecking.FeeAttackCheckingResult.NORMAL; +import static com.cobo.cold.ui.fragment.main.FeeAttackChecking.FeeAttackCheckingResult.SAME_OUTPUTS; import static com.cobo.cold.viewmodel.ElectrumViewModel.ELECTRUM_SIGN_ID; import static com.cobo.cold.viewmodel.ElectrumViewModel.adapt; @@ -90,10 +93,13 @@ public class TxConfirmViewModel extends AndroidViewModel { private final MutableLiveData observableTx = new MutableLiveData<>(); private final MutableLiveData parseTxException = new MutableLiveData<>(); private final MutableLiveData addingAddress = new MutableLiveData<>(); + private final MutableLiveData feeAttachCheckingResult = new MutableLiveData<>(); private AbsTx transaction; private String coinCode; private final MutableLiveData signState = new MutableLiveData<>(); private AuthenticateModal.OnVerify.VerifyToken token; + private TxEntity previousSignedTx; + public TxConfirmViewModel(@NonNull Application application) { super(application); @@ -123,18 +129,49 @@ public class TxConfirmViewModel extends AndroidViewModel { if (transaction instanceof UtxoTx) { if (!checkChangeAddress(transaction)) { observableTx.postValue(null); - parseTxException.postValue(new InvalidTransactionException("invlid change address")); + parseTxException.postValue(new InvalidTransactionException("invalid change address")); return; } } TxEntity tx = generateTxEntity(object); observableTx.postValue(tx); + if (Coins.BTC.coinCode().equals(transaction.getCoinCode())) { + feeAttackChecking(tx); + } } catch (JSONException e) { e.printStackTrace(); } }); } + public TxEntity getPreviousSignTx() { + return previousSignedTx; + } + + private void feeAttackChecking(TxEntity txEntity) { + AppExecutors.getInstance().diskIO().execute(() -> { + String inputs = txEntity.getFrom(); + String outputs = txEntity.getTo(); + List txs = mRepository.loadAllTxSync(Coins.BTC.coinId()); + for (TxEntity tx : txs) { + if (inputs.equals(tx.getFrom()) && outputs.equals(tx.getTo())) { + previousSignedTx = tx; + feeAttachCheckingResult.postValue(DUPLICATE_TX); + break; + } else if (outputs.equals(tx.getTo())) { + feeAttachCheckingResult.postValue(SAME_OUTPUTS); + break; + } else { + feeAttachCheckingResult.postValue(NORMAL); + } + } + }); + } + + public LiveData feeAttackChecking() { + return feeAttachCheckingResult; + } + private TxEntity generateTxEntity(JSONObject object) throws JSONException { TxEntity tx = new TxEntity(); NumberFormat nf = NumberFormat.getInstance(); diff --git a/app/src/main/res/navigation/nav_graph_main.xml b/app/src/main/res/navigation/nav_graph_main.xml index 0bf8a26..aa21cac 100644 --- a/app/src/main/res/navigation/nav_graph_main.xml +++ b/app/src/main/res/navigation/nav_graph_main.xml @@ -307,6 +307,12 @@ app:exitAnim="@anim/slide_out_left" app:popEnterAnim="@android:anim/slide_in_left" app:popExitAnim="@android:anim/slide_out_right" /> + + + +
1. 至少投掷50次,才能生成助记词;建议您最少投掷99次,获得256位的随机熵值;Cobo 金库不限制您的最大投掷次数。
2. 建议您使用赌场级骰子(六个面向上的概率相同),操作过程中确保骰子在空中得到足够的翻转,且避免总是从骰子特定面朝上进行投掷。]]>
开始 为防偷窥,解锁时将隐藏轨迹 + 异常交易 + 检测到该笔交易有风险,不能对其签名,您可前往:https://cobo.com/hardware-wallet/XXX 了解详情,您也可邮件联系客服:support@cobo.com + 检测到您已对该笔交易签名,您可直接广播该交易 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d445418..c5494d7 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -343,4 +343,7 @@
1. Roll the dice at least 50 times. Roll 99 times to obtain 256-bit entropy (recommended). There is no limit to the number of rolls you can use.
2. We recommend you use casino dice to increase the entropy of each dice throw.]]>
Start To prevent peeping, track will be hidden when unlocked + Abnormal Transaction + Detect that the transaction is risky and cannot be signed. You can go to: https// for details, or contact us: support@cobo.com + You have signed the transaction, can broadcast the transaction directly