Browse Source

support electrum (#1)

* support electrum

* fix setPassword UI issue

* resolve review issue
bug_fix
JunZhang 5 years ago
committed by GitHub
parent
commit
900abc2e8d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      app/build.gradle
  2. 2
      app/src/main/assets/bundleMap.json
  3. 2
      app/src/main/assets/script/BTC.bundle.js
  4. 2
      app/src/main/assets/script/BTC.bundle_94e3c1790748ac7fc876.js
  5. 16
      app/src/main/java/com/cobo/cold/ui/MainActivity.java
  6. 54
      app/src/main/java/com/cobo/cold/ui/fragment/main/AssetFragment.java
  7. 22
      app/src/main/java/com/cobo/cold/ui/fragment/main/QRCodeScanFragment.java
  8. 105
      app/src/main/java/com/cobo/cold/ui/fragment/main/TxConfirmFragment.java
  9. 28
      app/src/main/java/com/cobo/cold/ui/fragment/main/TxFragment.java
  10. 39
      app/src/main/java/com/cobo/cold/ui/fragment/main/TxListFragment.java
  11. 5
      app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/Callback.java
  12. 97
      app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumBroadcastTxFragment.java
  13. 107
      app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumExportFragment.java
  14. 44
      app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumGuideFragment.java
  15. 342
      app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumTxConfirmFragment.java
  16. 140
      app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumTxFragment.java
  17. 143
      app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumTxnListFragment.java
  18. 59
      app/src/main/java/com/cobo/cold/ui/modal/ExportToSdcardDialog.java
  19. 34
      app/src/main/java/com/cobo/cold/ui/views/DrawerAdapter.java
  20. 6
      app/src/main/java/com/cobo/cold/ui/views/qrcode/DynamicQrCodeView.java
  21. 30
      app/src/main/java/com/cobo/cold/ui/views/qrcode/QrCodeView.java
  22. 10
      app/src/main/java/com/cobo/cold/update/utils/FileUtils.java
  23. 5
      app/src/main/java/com/cobo/cold/update/utils/Storage.java
  24. 56
      app/src/main/java/com/cobo/cold/viewmodel/AddAddressViewModel.java
  25. 227
      app/src/main/java/com/cobo/cold/viewmodel/ElectrumViewModel.java
  26. 7
      app/src/main/java/com/cobo/cold/viewmodel/QrScanViewModel.java
  27. 135
      app/src/main/java/com/cobo/cold/viewmodel/TxConfirmViewModel.java
  28. BIN
      app/src/main/res/drawable-xhdpi/drawer_sdcard.png
  29. BIN
      app/src/main/res/drawable-xhdpi/info.png
  30. BIN
      app/src/main/res/drawable-xhdpi/more.png
  31. BIN
      app/src/main/res/drawable-xhdpi/sdcard_icon.png
  32. 136
      app/src/main/res/layout/broadcast_electrum_tx_fragment.xml
  33. 4
      app/src/main/res/layout/create_vault_modal.xml
  34. 86
      app/src/main/res/layout/dialog_bottom_sheet.xml
  35. 4
      app/src/main/res/layout/divider.xml
  36. 54
      app/src/main/res/layout/dynamic_qrcode_modal.xml
  37. 139
      app/src/main/res/layout/electrum_export.xml
  38. 86
      app/src/main/res/layout/electrum_export_guide.xml
  39. 69
      app/src/main/res/layout/electrum_tx.xml
  40. 89
      app/src/main/res/layout/electrum_tx_confirm_fragment.xml
  41. 312
      app/src/main/res/layout/electrum_tx_detail.xml
  42. 46
      app/src/main/res/layout/electrum_txn.xml
  43. 117
      app/src/main/res/layout/export_sdcard_modal.xml
  44. 60
      app/src/main/res/layout/export_success.xml
  45. 58
      app/src/main/res/layout/qrcode.xml
  46. 2
      app/src/main/res/layout/qrcode_modal.xml
  47. 8
      app/src/main/res/layout/receive_item.xml
  48. 18
      app/src/main/res/layout/tx_detail.xml
  49. 1
      app/src/main/res/layout/tx_list_item.xml
  50. 95
      app/src/main/res/layout/txn_list.xml
  51. 37
      app/src/main/res/menu/asset_hasmore.xml
  52. 1
      app/src/main/res/menu/drawer.xml
  53. 109
      app/src/main/res/navigation/nav_graph_main.xml
  54. 35
      app/src/main/res/values-zh-rCN/strings.xml
  55. 35
      app/src/main/res/values/strings.xml
  56. 14
      coinlib/src/main/java/com/cobo/coinlib/Util.java
  57. 3
      coinlib/src/main/java/com/cobo/coinlib/coins/AbsTx.java
  58. 19
      coinlib/src/main/java/com/cobo/coinlib/coins/BTC/Btc.java
  59. 10
      coinlib/src/main/java/com/cobo/coinlib/coins/BTC/Electrum/ElectrumTx.java
  60. 2
      coinlib/src/main/java/com/cobo/coinlib/coins/BTC/Electrum/TxUtils.java
  61. 5
      coinlib/src/main/java/com/cobo/coinlib/coins/BTC/UtxoTx.java
  62. 5
      coinlib/src/test/java/com/cobo/coinlib/UtilTest.java
  63. 11
      coinlib/src/test/java/com/cobo/coinlib/coin/ElectrumTxTest.java

1
app/build.gradle

@ -168,6 +168,7 @@ dependencies {
implementation 'com.allenliu.badgeview:library:1.1.1'
implementation 'net.lingala.zip4j:zip4j:1.3.2@jar'
implementation 'com.wei.android.lib:fingerprintidentify:1.2.6'
implementation 'com.github.donkingliang:ConsecutiveScroller:2.5.0'
annotationProcessor 'androidx.room:room-compiler:2.1.0'
implementation project(':encryption-core')

2
app/src/main/assets/bundleMap.json

@ -1,5 +1,5 @@
{
"BTC": "BTC.bundle.js",
"BTC": "BTC.bundle_94e3c1790748ac7fc876.js",
"BCH": "BCH.bundle.js",
"ETH": "ETH.bundle_b0d7ccb0dc5138935dc7.js",
"ETC": "ETC.bundle_b0d7ccb0dc5138935dc7.js",

2
app/src/main/assets/script/BTC.bundle.js

File diff suppressed because one or more lines are too long

2
app/src/main/assets/script/BTC.bundle_94e3c1790748ac7fc876.js

File diff suppressed because one or more lines are too long

16
app/src/main/java/com/cobo/cold/ui/MainActivity.java

@ -47,6 +47,7 @@ import com.cobo.cold.ui.fragment.AboutFragment;
import com.cobo.cold.ui.fragment.SyncFragment;
import com.cobo.cold.ui.fragment.main.AssetListFragment;
import com.cobo.cold.ui.fragment.main.ManageCoinFragment;
import com.cobo.cold.ui.fragment.main.electrum.ElectrumTxnListFragment;
import com.cobo.cold.ui.fragment.setting.SettingFragment;
import com.cobo.cold.ui.views.DrawerAdapter;
import com.cobo.cold.ui.views.FullScreenDrawer;
@ -92,11 +93,10 @@ public class MainActivity extends FullScreenActivity {
private void initNavController() {
mNavController = Navigation.findNavController(this, R.id.nav_host_fragment);
mNavController.addOnDestinationChangedListener((controller, destination, arguments) -> {
String label = Objects.requireNonNull(destination.getLabel()).toString();
int index = getKey(mMainFragments,label);
int index = getFragmentIndexByLabel(label);
if (index != -1) {
mBinding.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
currentFragmentIndex = index;
@ -110,10 +110,10 @@ public class MainActivity extends FullScreenActivity {
});
}
private int getKey(Map<Integer, String> map, String value) {
Set<Map.Entry<Integer, String>> set = map.entrySet();
private int getFragmentIndexByLabel(String label) {
Set<Map.Entry<Integer, String>> set = mMainFragments.entrySet();
for (Map.Entry<Integer, String> entry : set) {
if (entry.getValue().equals(value)) {
if (entry.getValue().equals(label)) {
return entry.getKey();
}
}
@ -159,7 +159,6 @@ public class MainActivity extends FullScreenActivity {
if (!oldBelongTo.equals(belongTo) || !oldVaultId.equals(vaultId)) {
recreate();
}
}
public class DrawerClickListener implements DrawerAdapter.OnItemClickListener {
@ -183,6 +182,10 @@ public class MainActivity extends FullScreenActivity {
mNavController.navigateUp();
mNavController.navigate(R.id.action_to_syncFragment);
break;
case R.id.drawer_sdcard:
mNavController.navigateUp();
mNavController.navigate(R.id.action_to_txnListFragment);
break;
case R.id.drawer_settings:
mNavController.navigateUp();
mNavController.navigate(R.id.action_to_settingFragment);
@ -288,6 +291,7 @@ public class MainActivity extends FullScreenActivity {
mMainFragments.put(R.id.drawer_wallet, AssetListFragment.TAG);
mMainFragments.put(R.id.drawer_manage, ManageCoinFragment.TAG);
mMainFragments.put(R.id.drawer_sync, SyncFragment.TAG);
mMainFragments.put(R.id.drawer_sdcard, ElectrumTxnListFragment.TAG);
mMainFragments.put(R.id.drawer_settings, SettingFragment.TAG);
mMainFragments.put(R.id.drawer_about, AboutFragment.TAG);
}

54
app/src/main/java/com/cobo/cold/ui/fragment/main/AssetFragment.java

@ -22,12 +22,14 @@ import android.os.Bundle;
import android.os.Handler;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.databinding.DataBindingUtil;
import androidx.databinding.Observable;
import androidx.databinding.ObservableField;
import androidx.fragment.app.Fragment;
@ -38,12 +40,14 @@ import com.cobo.coinlib.utils.Coins;
import com.cobo.cold.AppExecutors;
import com.cobo.cold.R;
import com.cobo.cold.databinding.AssetFragmentBinding;
import com.cobo.cold.databinding.DialogBottomSheetBinding;
import com.cobo.cold.db.entity.CoinEntity;
import com.cobo.cold.ui.fragment.BaseFragment;
import com.cobo.cold.ui.modal.ProgressModalDialog;
import com.cobo.cold.viewmodel.AddAddressViewModel;
import com.cobo.cold.viewmodel.CoinViewModel;
import com.cobo.cold.viewmodel.PublicKeyViewModel;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import java.util.ArrayList;
import java.util.List;
@ -80,13 +84,20 @@ public class AssetFragment extends BaseFragment<AssetFragmentBinding>
coinCode = data.getString(KEY_COIN_CODE);
id = data.getLong(KEY_ID);
showPublicKey = Coins.showPublicKey(coinCode);
mBinding.toolbar.inflateMenu(showPublicKey ? R.menu.asset_without_add : R.menu.asset);
mBinding.toolbar.inflateMenu(getMenuResId());
mBinding.toolbar.setOnMenuItemClickListener(this);
mBinding.toolbar.setNavigationOnClickListener(v -> navigateUp());
initSearchView();
initTabs();
}
private int getMenuResId() {
if (Coins.BTC.coinCode().equals(coinCode)) {
return R.menu.asset_hasmore;
}
return showPublicKey ? R.menu.asset_without_add : R.menu.asset;
}
private void initTabs() {
if (!showPublicKey) {
initViewPager();
@ -193,14 +204,10 @@ public class AssetFragment extends BaseFragment<AssetFragmentBinding>
enterSearch();
break;
case R.id.action_add:
if (fragments[0] instanceof AddressFragment) {
((AddressFragment) fragments[0]).exitEditAddressName();
}
if (mAddressNumberPicker == null) {
mAddressNumberPicker = new AddressNumberPicker();
mAddressNumberPicker.setCallback(this);
}
mAddressNumberPicker.show(mActivity.getSupportFragmentManager(), "");
handleAddAddress();
break;
case R.id.action_more:
showBottomSheetMenu();
break;
default:
break;
@ -208,6 +215,35 @@ public class AssetFragment extends BaseFragment<AssetFragmentBinding>
return true;
}
private void handleAddAddress() {
if (fragments[0] instanceof AddressFragment) {
((AddressFragment) fragments[0]).exitEditAddressName();
}
if (mAddressNumberPicker == null) {
mAddressNumberPicker = new AddressNumberPicker();
mAddressNumberPicker.setCallback(this);
}
mAddressNumberPicker.show(mActivity.getSupportFragmentManager(), "");
}
private void showBottomSheetMenu() {
BottomSheetDialog dialog = new BottomSheetDialog(mActivity);
DialogBottomSheetBinding binding = DataBindingUtil.inflate(LayoutInflater.from(mActivity),
R.layout.dialog_bottom_sheet,null,false);
binding.addAddress.setOnClickListener(v-> {
handleAddAddress();
dialog.dismiss();
});
binding.exportXpubToElectrum.setOnClickListener(v-> {
navigate(R.id.action_to_electrum_guide);
dialog.dismiss();
});
dialog.setContentView(binding.getRoot());
dialog.show();
}
private void enterSearch() {
isInSearch = true;
if (fragments[0] != null && fragments[0] instanceof AddressFragment) {

22
app/src/main/java/com/cobo/cold/ui/fragment/main/QRCodeScanFragment.java

@ -28,8 +28,10 @@ import androidx.annotation.NonNull;
import androidx.databinding.DataBindingUtil;
import androidx.lifecycle.ViewModelProviders;
import com.cobo.coinlib.coins.BTC.Electrum.ElectrumTx;
import com.cobo.coinlib.exception.CoinNotFindException;
import com.cobo.coinlib.exception.InvalidTransactionException;
import com.cobo.coinlib.utils.Base43;
import com.cobo.cold.R;
import com.cobo.cold.databinding.CommonModalBinding;
import com.cobo.cold.databinding.QrcodeScanFragmentBinding;
@ -49,6 +51,7 @@ import com.cobo.cold.viewmodel.UnknowQrCodeException;
import com.cobo.cold.viewmodel.UuidNotMatchException;
import org.json.JSONException;
import org.spongycastle.util.encoders.Hex;
import java.io.IOException;
@ -172,11 +175,30 @@ public class QRCodeScanFragment extends BaseFragment<QrcodeScanFragmentBinding>
alert(getString(R.string.invalid_webauth_qrcode_hint));
} else if ("address".equals(purpose)) {
navigateUp();
} else if (tryParseElecturmTx(res) != null) {
handleElectrumTx(res);
} else {
alert(getString(R.string.unsupported_qrcode));
}
}
private void handleElectrumTx(String res) {
String data = Hex.toHexString(Base43.decode(res));
Bundle bundle = new Bundle();
bundle.putString("txn", data);
navigate(R.id.action_to_ElectrumTxConfirmFragment, bundle);
}
private ElectrumTx tryParseElecturmTx(String res) {
try {
byte[] data = Base43.decode(res);
return ElectrumTx.parse(data);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
public void handleDecode(ScannedData[] res) {
try {

105
app/src/main/java/com/cobo/cold/ui/fragment/main/TxConfirmFragment.java

@ -53,6 +53,7 @@ import org.json.JSONArray;
import org.json.JSONException;
import java.nio.charset.StandardCharsets;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@ -63,12 +64,15 @@ import static com.cobo.cold.ui.fragment.main.BroadcastTxFragment.KEY_TXID;
public class TxConfirmFragment extends BaseFragment<TxConfirmFragmentBinding> {
public static final String KEY_TX_DATA = "tx_data";
private final Runnable forgetPassword = () -> {
Bundle data = new Bundle();
data.putInt(KEY_NAV_ID, R.id.action_to_setPasswordFragment1);
Navigation.findNavController(Objects.requireNonNull(getView()))
.navigate(R.id.action_to_verifyMnemonic, data);
};
private String data;
private TxConfirmViewModel viewModel;
private SigningDialog signingDialog;
private TxEntity txEntity;
private ModalDialog addingAddressDialog;
@ -91,13 +95,6 @@ public class TxConfirmFragment extends BaseFragment<TxConfirmFragmentBinding> {
}
private final Runnable forgetPassword = () -> {
Bundle data = new Bundle();
data.putInt(KEY_NAV_ID, R.id.action_to_setPasswordFragment1);
Navigation.findNavController(Objects.requireNonNull(getView()))
.navigate(R.id.action_to_verifyMnemonic, data);
};
private void handleSign() {
boolean fingerprintSignEnable = Utilities.isFingerprintSignEnable(mActivity);
if (txEntity != null) {
@ -142,6 +139,7 @@ public class TxConfirmFragment extends BaseFragment<TxConfirmFragmentBinding> {
this.txEntity = txEntity;
mBinding.setTx(txEntity);
refreshAmount();
refreshFromList();
refreshReceiveList();
refreshTokenUI();
refreshFeeDisplay();
@ -220,25 +218,44 @@ public class TxConfirmFragment extends BaseFragment<TxConfirmFragmentBinding> {
private void refreshReceiveList() {
String to = txEntity.getTo();
List<TxConfirmFragment.ReceiveItem> items = new ArrayList<>();
List<TransactionItem> items = new ArrayList<>();
try {
JSONArray outputs = new JSONArray(to);
for (int i = 0; i < outputs.length(); i++) {
items.add(new TxConfirmFragment.ReceiveItem(i,
outputs.getJSONObject(i).getString("value"),
items.add(new TransactionItem(i,
outputs.getJSONObject(i).getLong("value"),
outputs.getJSONObject(i).getString("address")
));
}
} catch (JSONException e) {
return;
}
TxConfirmFragment.ReceiveAdapter adapter = new TxConfirmFragment.ReceiveAdapter(mActivity);
TransactionItemAdapter adapter = new TransactionItemAdapter(mActivity,
TransactionItem.ItemType.TO);
adapter.setItems(items);
mBinding.txDetail.toList.setVisibility(View.VISIBLE);
mBinding.txDetail.toInfo.setVisibility(View.GONE);
mBinding.txDetail.toList.setAdapter(adapter);
}
private void refreshFromList() {
String from = txEntity.getFrom();
mBinding.txDetail.from.setText(from);
List<TransactionItem> items = new ArrayList<>();
try {
JSONArray inputs = new JSONArray(from);
for (int i = 0; i < inputs.length(); i++) {
items.add(new TransactionItem(i,
inputs.getJSONObject(i).getLong("value"),
inputs.getJSONObject(i).getString("address")
));
}
String fromAddress = inputs.getJSONObject(0).getString("address");
mBinding.txDetail.from.setText(fromAddress);
} catch (JSONException ignore) {
}
}
private void subscribeSignState() {
viewModel.getSignState().observe(this, s -> {
if (TxConfirmViewModel.STATE_SIGNING.equals(s)) {
@ -292,17 +309,24 @@ public class TxConfirmFragment extends BaseFragment<TxConfirmFragmentBinding> {
}
public static class ReceiveItem {
public static class TransactionItem {
final int id;
final String amount;
final String address;
public ReceiveItem(int id, String amount, String address) {
public TransactionItem(int id, long amount, String address) {
this.id = id;
this.amount = amount;
this.amount = formatSatoshi(amount);
this.address = address;
}
static String formatSatoshi(long satoshi) {
double value = satoshi / Math.pow(10, 8);
NumberFormat nf = NumberFormat.getInstance();
nf.setMaximumFractionDigits(20);
return nf.format(value) + " " + Coins.BTC.coinCode();
}
public int getId() {
return id;
}
@ -314,12 +338,22 @@ public class TxConfirmFragment extends BaseFragment<TxConfirmFragmentBinding> {
public String getAddress() {
return address;
}
public enum ItemType {
INPUT,
OUTPUT,
FROM,
TO,
}
}
public static class ReceiveAdapter extends BaseBindingAdapter<ReceiveItem, ReceiveItemBinding> {
public static class TransactionItemAdapter extends BaseBindingAdapter<TransactionItem, ReceiveItemBinding> {
public ReceiveAdapter(Context context) {
private TransactionItem.ItemType type;
public TransactionItemAdapter(Context context, TransactionItem.ItemType type) {
super(context);
this.type = type;
}
@Override
@ -328,8 +362,37 @@ public class TxConfirmFragment extends BaseFragment<TxConfirmFragmentBinding> {
}
@Override
protected void onBindItem(ReceiveItemBinding binding, ReceiveItem item) {
binding.setItem(item);
protected void onBindItem(ReceiveItemBinding binding, TransactionItem item) {
if (getItemCount() == 1 && type == TransactionItem.ItemType.TO) {
binding.info.setText(item.address);
binding.label.setText(context.getString(R.string.tx_to));
} else {
binding.info.setText(item.amount + "\n" + item.address);
binding.label.setText(getLabel(item.id));
}
}
private String getLabel(int index) {
int resId;
switch (type) {
case TO:
resId = R.string.receive_address;
break;
case FROM:
resId = R.string.send_address;
break;
case INPUT:
resId = R.string.input;
break;
case OUTPUT:
resId = R.string.output;
break;
default:
return "";
}
return context.getString(resId, index + 1);
}
}
}

28
app/src/main/java/com/cobo/cold/ui/fragment/main/TxFragment.java

@ -64,6 +64,7 @@ public class TxFragment extends BaseFragment<TxBinding> {
this.txEntity = txEntity;
new Handler().postDelayed(() -> mBinding.qrcodeLayout.qrcode.setData(getSignTxJson(txEntity)), 500);
refreshAmount();
refreshFromList();
refreshReceiveList();
refreshTokenUI();
refreshFeeDisplay();
@ -114,25 +115,44 @@ public class TxFragment extends BaseFragment<TxBinding> {
private void refreshReceiveList() {
String to = txEntity.getTo();
List<TxConfirmFragment.ReceiveItem> items = new ArrayList<>();
List<TxConfirmFragment.TransactionItem> items = new ArrayList<>();
try {
JSONArray outputs = new JSONArray(to);
for (int i = 0; i < outputs.length(); i++) {
items.add(new TxConfirmFragment.ReceiveItem(i,
outputs.getJSONObject(i).getString("value"),
items.add(new TxConfirmFragment.TransactionItem(i,
outputs.getJSONObject(i).getLong("value"),
outputs.getJSONObject(i).getString("address")
));
}
} catch (JSONException e) {
return;
}
TxConfirmFragment.ReceiveAdapter adapter = new TxConfirmFragment.ReceiveAdapter(mActivity);
TxConfirmFragment.TransactionItemAdapter adapter =
new TxConfirmFragment.TransactionItemAdapter(mActivity,
TxConfirmFragment.TransactionItem.ItemType.TO);
adapter.setItems(items);
mBinding.txDetail.toList.setVisibility(View.VISIBLE);
mBinding.txDetail.toInfo.setVisibility(View.GONE);
mBinding.txDetail.toList.setAdapter(adapter);
}
private void refreshFromList() {
String from = txEntity.getFrom();
mBinding.txDetail.from.setText(from);
List<TxConfirmFragment.TransactionItem> items = new ArrayList<>();
try {
JSONArray inputs = new JSONArray(from);
for (int i = 0; i < inputs.length(); i++) {
items.add(new TxConfirmFragment.TransactionItem(i,
inputs.getJSONObject(i).getLong("value"),
inputs.getJSONObject(i).getString("address")
));
}
String fromAddress = inputs.getJSONObject(0).getString("address");
mBinding.txDetail.from.setText(fromAddress);
} catch (JSONException ignore) {}
}
@Override
protected void initData(Bundle savedInstanceState) {

39
app/src/main/java/com/cobo/cold/ui/fragment/main/TxListFragment.java

@ -46,6 +46,7 @@ import java.util.stream.Collectors;
import static com.cobo.cold.ui.fragment.Constants.KEY_COIN_CODE;
import static com.cobo.cold.ui.fragment.Constants.KEY_COIN_ID;
import static com.cobo.cold.ui.fragment.main.TxFragment.KEY_TX_ID;
import static com.cobo.cold.viewmodel.ElectrumViewModel.ELECTRUM_SIGN_ID;
public class TxListFragment extends BaseFragment<TxListBinding> {
@ -78,8 +79,13 @@ public class TxListFragment extends BaseFragment<TxListBinding> {
txCallback = tx -> {
Bundle bundle = new Bundle();
bundle.putString(KEY_TX_ID, tx.getTxId());
Navigation.findNavController(Objects.requireNonNull(getView()))
.navigate(R.id.action_to_txFragment, bundle);
if (ELECTRUM_SIGN_ID.equals(tx.getSignId())) {
Navigation.findNavController(Objects.requireNonNull(getView()))
.navigate(R.id.action_to_electrumTxFragment, bundle);
} else {
Navigation.findNavController(Objects.requireNonNull(getView()))
.navigate(R.id.action_to_txFragment, bundle);
}
};
viewModel.loadTxs(data.getString(KEY_COIN_ID))
@ -134,17 +140,32 @@ public class TxListFragment extends BaseFragment<TxListBinding> {
protected void onBindItem(TxListItemBinding binding, TxEntity item) {
binding.setTx(item);
binding.setTxCallback(txCallback);
try {
updateTo(binding, item);
} catch (JSONException ignore) {
}
updateFrom(binding, item);
updateTo(binding, item);
}
private void updateTo(TxListItemBinding binding, TxEntity item) throws JSONException {
private void updateTo(TxListItemBinding binding, TxEntity item) {
String to = item.getTo();
binding.to.setText(item.getTo());
JSONArray outputs = new JSONArray(to);
binding.to.setText(outputs.getJSONObject(0).getString("address"));
try {
JSONArray outputs = new JSONArray(to);
binding.to.setText(outputs.getJSONObject(0).getString("address"));
} catch (JSONException e) {
e.printStackTrace();
}
}
private void updateFrom(TxListItemBinding binding, TxEntity item) {
String from = item.getFrom();
binding.from.setText(item.getFrom());
try {
JSONArray inputs = new JSONArray(from);
String address = inputs.getJSONObject(0).getString("address");
binding.from.setText(address);
} catch (JSONException e) {
e.printStackTrace();
}
}
}

5
app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/Callback.java

@ -0,0 +1,5 @@
package com.cobo.cold.ui.fragment.main.electrum;
public interface Callback {
void onClick(String file);
}

97
app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumBroadcastTxFragment.java

@ -0,0 +1,97 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.ui.fragment.main.electrum;
import android.os.Bundle;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import androidx.databinding.DataBindingUtil;
import androidx.lifecycle.ViewModelProviders;
import com.cobo.coinlib.utils.Base43;
import com.cobo.cold.R;
import com.cobo.cold.databinding.BroadcastElectrumTxFragmentBinding;
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 com.cobo.cold.viewmodel.CoinListViewModel;
import org.spongycastle.util.encoders.Hex;
import java.util.Objects;
import static com.cobo.cold.ui.fragment.main.electrum.ElectrumTxConfirmFragment.showExportTxnDialog;
public class ElectrumBroadcastTxFragment extends BaseFragment<BroadcastElectrumTxFragmentBinding> {
public static final String KEY_TXID = "txId";
private final View.OnClickListener goHome = v -> navigate(R.id.action_to_home);
private TxEntity txEntity;
@Override
protected int setView() {
return R.layout.broadcast_electrum_tx_fragment;
}
@Override
protected void init(View view) {
Bundle data = Objects.requireNonNull(getArguments());
mBinding.toolbar.setNavigationOnClickListener(goHome);
mBinding.complete.setOnClickListener(goHome);
CoinListViewModel viewModel = ViewModelProviders.of(mActivity).get(CoinListViewModel.class);
viewModel.loadTx(data.getString(KEY_TXID)).observe(this, txEntity -> {
this.txEntity = txEntity;
mBinding.setCoinCode(txEntity.getCoinCode());
String txString = getSignTxString(txEntity);
mBinding.qrcodeLayout.qrcode.setData(txString);
});
mBinding.hint.setOnClickListener(v -> {
if (txEntity != null) {
showExportTxnDialog(mActivity, txEntity.getTxId(), txEntity.getSignedHex());
}
});
mBinding.info.setOnClickListener(v -> showElectrumInfo());
}
private void showElectrumInfo() {
ModalDialog modalDialog = ModalDialog.newInstance();
CommonModalBinding binding = DataBindingUtil.inflate(
LayoutInflater.from(mActivity), R.layout.common_modal,
null, false);
binding.title.setText(R.string.electrum_broadcast_guide);
binding.subTitle.setText(R.string.electrum_broadcast_action_guide);
binding.subTitle.setGravity(Gravity.START);
binding.close.setVisibility(View.GONE);
binding.confirm.setText(R.string.know);
binding.confirm.setOnClickListener(vv -> modalDialog.dismiss());
modalDialog.setBinding(binding);
modalDialog.show(mActivity.getSupportFragmentManager(), "");
}
@Override
protected void initData(Bundle savedInstanceState) {
}
private String getSignTxString(TxEntity txEntity) {
return Base43.encode(Hex.decode(txEntity.getSignedHex()));
}
}

107
app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumExportFragment.java

@ -0,0 +1,107 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.ui.fragment.main.electrum;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import androidx.databinding.DataBindingUtil;
import androidx.lifecycle.ViewModelProviders;
import com.cobo.cold.R;
import com.cobo.cold.databinding.CommonModalBinding;
import com.cobo.cold.databinding.ElectrumExportBinding;
import com.cobo.cold.databinding.ExportSdcardModalBinding;
import com.cobo.cold.ui.fragment.BaseFragment;
import com.cobo.cold.ui.modal.ModalDialog;
import com.cobo.cold.update.utils.Storage;
import com.cobo.cold.viewmodel.ElectrumViewModel;
import static com.cobo.cold.viewmodel.ElectrumViewModel.exportSuccess;
import static com.cobo.cold.viewmodel.ElectrumViewModel.showNoSdcardModal;
import static com.cobo.cold.viewmodel.ElectrumViewModel.writeToSdcard;
public class ElectrumExportFragment extends BaseFragment<ElectrumExportBinding> {
private static final String EXTEND_PUB_FILE_NAME = "CV-P2SH-P2WPKH-pubkey.txt";
private String exPub;
@Override
protected int setView() {
return R.layout.electrum_export;
}
@Override
protected void init(View view) {
mBinding.toolbar.setNavigationOnClickListener(v -> navigateUp());
ElectrumViewModel viewModel = ViewModelProviders.of(this).get(ElectrumViewModel.class);
viewModel.getExPub().observe(this, s -> {
if (!TextUtils.isEmpty(s)) {
exPub = s;
mBinding.qrcode.setData(s);
mBinding.expub.setText(s);
}
});
mBinding.info.setOnClickListener(v -> showElectrumInfo());
mBinding.exportToSdcard.setOnClickListener(v -> {
Storage storage = Storage.createByEnvironment(mActivity);
if (storage == null || storage.getExternalDir() == null) {
showNoSdcardModal(mActivity);
} else {
ModalDialog modalDialog = ModalDialog.newInstance();
ExportSdcardModalBinding binding = DataBindingUtil.inflate(LayoutInflater.from(mActivity),
R.layout.export_sdcard_modal, null, false);
binding.title.setText(R.string.export_xpub_text_file);
binding.fileName.setText(EXTEND_PUB_FILE_NAME);
binding.actionHint.setText(R.string.electrum_import_xpub_action);
binding.cancel.setOnClickListener(vv -> modalDialog.dismiss());
binding.confirm.setOnClickListener(vv -> {
modalDialog.dismiss();
if (writeToSdcard(storage, exPub, EXTEND_PUB_FILE_NAME)) {
exportSuccess(mActivity);
}
});
modalDialog.setBinding(binding);
modalDialog.show(mActivity.getSupportFragmentManager(), "");
}
});
}
private void showElectrumInfo() {
ModalDialog modalDialog = ModalDialog.newInstance();
CommonModalBinding binding = DataBindingUtil.inflate(
LayoutInflater.from(mActivity), R.layout.common_modal,
null, false);
binding.title.setText(R.string.electrum_import_xpub_guide_title);
binding.subTitle.setText(R.string.electrum_import_xpub_action_guide);
binding.subTitle.setGravity(Gravity.START);
binding.close.setVisibility(View.GONE);
binding.confirm.setText(R.string.know);
binding.confirm.setOnClickListener(vv -> modalDialog.dismiss());
modalDialog.setBinding(binding);
modalDialog.show(mActivity.getSupportFragmentManager(), "");
}
@Override
protected void initData(Bundle savedInstanceState) {
}
}

44
app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumGuideFragment.java

@ -0,0 +1,44 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.ui.fragment.main.electrum;
import android.os.Bundle;
import android.view.View;
import com.cobo.cold.R;
import com.cobo.cold.databinding.ElectrumExportGuideBinding;
import com.cobo.cold.ui.fragment.BaseFragment;
public class ElectrumGuideFragment extends BaseFragment<ElectrumExportGuideBinding> {
@Override
protected int setView() {
return R.layout.electrum_export_guide;
}
@Override
protected void init(View view) {
mBinding.toolbar.setNavigationOnClickListener(v -> navigateUp());
mBinding.export.setOnClickListener(v -> navigate(R.id.export_electrum_ypub));
}
@Override
protected void initData(Bundle savedInstanceState) {
}
}

342
app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumTxConfirmFragment.java

@ -0,0 +1,342 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.ui.fragment.main.electrum;
import android.os.Bundle;
import android.os.Handler;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.style.ForegroundColorSpan;
import android.view.LayoutInflater;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import androidx.lifecycle.ViewModelProviders;
import androidx.navigation.Navigation;
import com.cobo.coinlib.coins.BTC.Electrum.ElectrumTx;
import com.cobo.coinlib.utils.Base43;
import com.cobo.cold.R;
import com.cobo.cold.Utilities;
import com.cobo.cold.config.FeatureFlags;
import com.cobo.cold.databinding.ElectrumTxConfirmFragmentBinding;
import com.cobo.cold.databinding.ExportSdcardModalBinding;
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.TxConfirmFragment;
import com.cobo.cold.ui.modal.ModalDialog;
import com.cobo.cold.ui.modal.ProgressModalDialog;
import com.cobo.cold.ui.modal.SigningDialog;
import com.cobo.cold.ui.views.AuthenticateModal;
import com.cobo.cold.update.utils.Storage;
import com.cobo.cold.util.KeyStoreUtil;
import com.cobo.cold.viewmodel.TxConfirmViewModel;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.spongycastle.util.encoders.Hex;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
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.viewmodel.ElectrumViewModel.exportSuccess;
import static com.cobo.cold.viewmodel.ElectrumViewModel.hasSdcard;
import static com.cobo.cold.viewmodel.ElectrumViewModel.showNoSdcardModal;
import static com.cobo.cold.viewmodel.ElectrumViewModel.writeToSdcard;
public class ElectrumTxConfirmFragment extends BaseFragment<ElectrumTxConfirmFragmentBinding> {
private final Runnable forgetPassword = () -> {
Bundle data = new Bundle();
data.putInt(KEY_NAV_ID, R.id.action_to_setPasswordFragment1);
Navigation.findNavController(Objects.requireNonNull(getView()))
.navigate(R.id.action_to_verifyMnemonic, data);
};
private TxConfirmViewModel viewModel;
private SigningDialog signingDialog;
private TxEntity txEntity;
private ModalDialog addingAddressDialog;
private String txnData;
public static void showExportTxnDialog(AppCompatActivity activity, String txId, String hex) {
ModalDialog modalDialog = ModalDialog.newInstance();
ExportSdcardModalBinding binding = DataBindingUtil.inflate(LayoutInflater.from(activity),
R.layout.export_sdcard_modal, null, false);
String fileName = txId.substring(0, 5) + "-signed.txn";
binding.title.setText(R.string.export_signed_txn);
binding.fileName.setText(fileName);
binding.actionHint.setText(R.string.electrum_import_signed_txn);
binding.cancel.setOnClickListener(vv -> modalDialog.dismiss());
binding.confirm.setOnClickListener(vv -> {
modalDialog.dismiss();
if (hasSdcard(activity)) {
Storage storage = Storage.createByEnvironment(activity);
boolean result = writeToSdcard(storage, generateElectrumTxn(hex), fileName);
if (result) {
exportSuccess(activity);
}
} else {
showNoSdcardModal(activity);
}
});
modalDialog.setBinding(binding);
modalDialog.show(activity.getSupportFragmentManager(), "");
}
private static String generateElectrumTxn(String hex) {
JSONObject txn = new JSONObject();
try {
txn.put("hex", hex);
txn.put("complete", true);
txn.put("final", ElectrumTx.isFinal(hex));
return txn.toString();
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}
@Override
protected int setView() {
return R.layout.electrum_tx_confirm_fragment;
}
@Override
protected void init(View view) {
Bundle bundle = Objects.requireNonNull(getArguments());
mBinding.toolbar.setNavigationOnClickListener(v -> navigateUp());
mBinding.txDetail.txIdInfo.setVisibility(View.GONE);
mBinding.txDetail.qrcodeLayout.qrcode.setVisibility(View.GONE);
mBinding.txDetail.export.setVisibility(View.GONE);
txnData = bundle.getString("txn");
viewModel = ViewModelProviders.of(this).get(TxConfirmViewModel.class);
mBinding.setViewModel(viewModel);
subscribeTxEntityState();
mBinding.sign.setOnClickListener(v -> handleSign());
}
private void handleSign() {
boolean fingerprintSignEnable = Utilities.isFingerprintSignEnable(mActivity);
if (txEntity != null) {
if (FeatureFlags.ENABLE_WHITE_LIST) {
if (isAddressInWhiteList()) {
AuthenticateModal.show(mActivity,
getString(R.string.password_modal_title),
"",
fingerprintSignEnable,
signWithVerifyInfo(), forgetPassword);
} else {
Utilities.alert(mActivity, getString(R.string.hint),
getString(R.string.not_in_whitelist_reject),
getString(R.string.confirm),
() -> navigate(R.id.action_to_home));
}
} else {
AuthenticateModal.show(mActivity,
getString(R.string.password_modal_title),
"",
fingerprintSignEnable,
signWithVerifyInfo(), forgetPassword);
}
} else {
navigate(R.id.action_to_home);
}
}
private AuthenticateModal.OnVerify signWithVerifyInfo() {
return token -> {
viewModel.setToken(token);
viewModel.handleSign();
subscribeSignState();
};
}
private void subscribeTxEntityState() {
ProgressModalDialog dialog = new ProgressModalDialog();
dialog.show(mActivity.getSupportFragmentManager(), "");
viewModel.parseTxnData(txnData);
viewModel.getObservableTx().observe(this, txEntity -> {
if (txEntity != null) {
dialog.dismiss();
this.txEntity = txEntity;
mBinding.setTx(txEntity);
refreshAmount();
refreshFromList();
refreshReceiveList();
}
});
viewModel.getAddingAddressState().observe(this, b -> {
if (b) {
addingAddressDialog = ModalDialog.newInstance();
ProgressModalBinding binding = DataBindingUtil.inflate(LayoutInflater.from(mActivity),
R.layout.progress_modal, null, false);
binding.text.setText(R.string.sync_in_progress);
binding.text.setVisibility(View.VISIBLE);
addingAddressDialog.setBinding(binding);
addingAddressDialog.show(mActivity.getSupportFragmentManager(), "");
} else {
if (addingAddressDialog != null) {
addingAddressDialog.dismiss();
}
}
});
viewModel.parseTxException().observe(this, ex -> {
if (ex != null) {
ex.printStackTrace();
dialog.dismiss();
ModalDialog.showCommonModal(mActivity,
getString(R.string.electrum_decode_txn_fail),
getString(R.string.incorrect_tx_data),
getString(R.string.confirm),
null);
navigateUp();
}
});
}
private void refreshAmount() {
SpannableStringBuilder style = new SpannableStringBuilder(txEntity.getAmount());
style.setSpan(new ForegroundColorSpan(mActivity.getColor(R.color.colorAccent)),
0, txEntity.getAmount().indexOf(" "), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
mBinding.txDetail.amount.setText(style);
}
private void refreshReceiveList() {
String to = txEntity.getTo();
List<TxConfirmFragment.TransactionItem> items = new ArrayList<>();
try {
JSONArray outputs = new JSONArray(to);
for (int i = 0; i < outputs.length(); i++) {
items.add(new TxConfirmFragment.TransactionItem(i,
outputs.getJSONObject(i).getLong("value"),
outputs.getJSONObject(i).getString("address")
));
}
} catch (JSONException e) {
return;
}
TxConfirmFragment.TransactionItemAdapter adapter
= new TxConfirmFragment.TransactionItemAdapter(mActivity,
TxConfirmFragment.TransactionItem.ItemType.OUTPUT);
adapter.setItems(items);
mBinding.txDetail.toList.setVisibility(View.VISIBLE);
mBinding.txDetail.toList.setAdapter(adapter);
}
private void refreshFromList() {
String to = txEntity.getFrom();
List<TxConfirmFragment.TransactionItem> items = new ArrayList<>();
try {
JSONArray outputs = new JSONArray(to);
for (int i = 0; i < outputs.length(); i++) {
JSONObject out = outputs.getJSONObject(i);
items.add(new TxConfirmFragment.TransactionItem(i,
out.getLong("value"),
out.getString("address")));
}
} catch (JSONException e) {
return;
}
TxConfirmFragment.TransactionItemAdapter adapter
= new TxConfirmFragment.TransactionItemAdapter(mActivity,
TxConfirmFragment.TransactionItem.ItemType.INPUT);
adapter.setItems(items);
mBinding.txDetail.fromList.setVisibility(View.VISIBLE);
mBinding.txDetail.fromList.setAdapter(adapter);
}
private void subscribeSignState() {
viewModel.getSignState().observe(this, s -> {
if (TxConfirmViewModel.STATE_SIGNING.equals(s)) {
signingDialog = SigningDialog.newInstance();
signingDialog.show(mActivity.getSupportFragmentManager(), "");
} else if (TxConfirmViewModel.STATE_SIGN_SUCCESS.equals(s)) {
if (signingDialog != null) {
signingDialog.setState(SigningDialog.STATE_SUCCESS);
}
new Handler().postDelayed(() -> {
if (signingDialog != null) {
signingDialog.dismiss();
}
signingDialog = null;
onSignSuccess();
}, 500);
} else if (TxConfirmViewModel.STATE_SIGN_FAIL.equals(s)) {
if (signingDialog == null) {
signingDialog = SigningDialog.newInstance();
signingDialog.show(mActivity.getSupportFragmentManager(), "");
}
new Handler().postDelayed(() -> signingDialog.setState(SigningDialog.STATE_FAIL), 1000);
new Handler().postDelayed(() -> {
if (signingDialog != null) {
signingDialog.dismiss();
}
signingDialog = null;
viewModel.getSignState().removeObservers(this);
}, 2000);
}
});
}
private void onSignSuccess() {
handleTxnSignSuccess();
viewModel.getSignState().removeObservers(this);
}
private void handleTxnSignSuccess() {
String hex = viewModel.getTxHex();
String base43 = Base43.encode(Hex.decode(hex));
if (base43.length() <= 1000) {
String txId = viewModel.getTxId();
Bundle data = new Bundle();
data.putString(KEY_TXID, txId);
navigate(R.id.action_to_broadcastElectrumTxFragment, data);
} else {
showExportTxnDialog(mActivity, viewModel.getTxId(), viewModel.getTxHex());
}
}
private boolean isAddressInWhiteList() {
String to = txEntity.getTo();
String encryptedAddress = ByteFormatter.bytes2hex(
new KeyStoreUtil().encrypt(to.getBytes(StandardCharsets.UTF_8)));
return viewModel.isAddressInWhiteList(encryptedAddress);
}
@Override
protected void initData(Bundle savedInstanceState) {
}
}

140
app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumTxFragment.java

@ -0,0 +1,140 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.ui.fragment.main.electrum;
import android.os.Bundle;
import android.os.Handler;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.style.ForegroundColorSpan;
import android.view.View;
import androidx.lifecycle.ViewModelProviders;
import com.cobo.coinlib.utils.Base43;
import com.cobo.cold.R;
import com.cobo.cold.databinding.ElectrumTxBinding;
import com.cobo.cold.db.entity.TxEntity;
import com.cobo.cold.ui.fragment.BaseFragment;
import com.cobo.cold.ui.fragment.main.TxConfirmFragment;
import com.cobo.cold.viewmodel.CoinListViewModel;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.spongycastle.util.encoders.Hex;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static com.cobo.cold.ui.fragment.main.electrum.ElectrumTxConfirmFragment.showExportTxnDialog;
public class ElectrumTxFragment extends BaseFragment<ElectrumTxBinding> {
public static final String KEY_TX_ID = "txid";
private TxEntity txEntity;
@Override
protected int setView() {
return R.layout.electrum_tx;
}
@Override
protected void init(View view) {
Bundle data = Objects.requireNonNull(getArguments());
mBinding.toolbar.setNavigationOnClickListener(v -> navigateUp());
CoinListViewModel viewModel = ViewModelProviders.of(mActivity).get(CoinListViewModel.class);
viewModel.loadTx(data.getString(KEY_TX_ID)).observe(this, txEntity -> {
mBinding.setTx(txEntity);
this.txEntity = txEntity;
String signTx = getSignTxString(txEntity);
if (signTx.length() <= 1000) {
new Handler().postDelayed(() -> mBinding.txDetail.qrcodeLayout.qrcode.setData(signTx), 500);
} else {
mBinding.txDetail.qrcodeLayout.qrcode.setVisibility(View.GONE);
}
refreshAmount();
refreshFromList();
refreshReceiveList();
mBinding.txDetail.exportToSdcard.setOnClickListener(v -> {
showExportTxnDialog(mActivity, txEntity.getTxId(), txEntity.getSignedHex());
});
});
}
private void refreshFromList() {
String from = txEntity.getFrom();
List<TxConfirmFragment.TransactionItem> items = new ArrayList<>();
try {
JSONArray outputs = new JSONArray(from);
for (int i = 0; i < outputs.length(); i++) {
JSONObject out = outputs.getJSONObject(i);
items.add(new TxConfirmFragment.TransactionItem(i,
out.getLong("value"), out.getString("address")));
}
} catch (JSONException e) {
return;
}
TxConfirmFragment.TransactionItemAdapter adapter
= new TxConfirmFragment.TransactionItemAdapter(mActivity,
TxConfirmFragment.TransactionItem.ItemType.INPUT);
adapter.setItems(items);
mBinding.txDetail.fromList.setAdapter(adapter);
}
private void refreshAmount() {
SpannableStringBuilder style = new SpannableStringBuilder(txEntity.getAmount());
style.setSpan(new ForegroundColorSpan(mActivity.getColor(R.color.colorAccent)),
0, txEntity.getAmount().indexOf(" "), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
mBinding.txDetail.amount.setText(style);
}
private void refreshReceiveList() {
String to = txEntity.getTo();
List<TxConfirmFragment.TransactionItem> items = new ArrayList<>();
try {
JSONArray outputs = new JSONArray(to);
for (int i = 0; i < outputs.length(); i++) {
items.add(new TxConfirmFragment.TransactionItem(i,
outputs.getJSONObject(i).getLong("value"),
outputs.getJSONObject(i).getString("address")
));
}
} catch (JSONException e) {
return;
}
TxConfirmFragment.TransactionItemAdapter adapter =
new TxConfirmFragment.TransactionItemAdapter(mActivity, TxConfirmFragment.TransactionItem.ItemType.OUTPUT);
adapter.setItems(items);
mBinding.txDetail.toList.setAdapter(adapter);
}
@Override
protected void initData(Bundle savedInstanceState) {
}
private String getSignTxString(TxEntity txEntity) {
byte[] txData = Hex.decode(txEntity.getSignedHex());
return Base43.encode(txData);
}
}

143
app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumTxnListFragment.java

@ -0,0 +1,143 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.ui.fragment.main.electrum;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.RecyclerView;
import com.cobo.cold.R;
import com.cobo.cold.databinding.ElectrumTxnBinding;
import com.cobo.cold.databinding.TxnListBinding;
import com.cobo.cold.ui.MainActivity;
import com.cobo.cold.ui.common.BaseBindingAdapter;
import com.cobo.cold.ui.fragment.BaseFragment;
import com.cobo.cold.viewmodel.ElectrumViewModel;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.cobo.cold.viewmodel.ElectrumViewModel.hasSdcard;
public class ElectrumTxnListFragment extends BaseFragment<TxnListBinding>
implements Callback {
public static final String TAG = "ElectrumTxnListFragment";
private ElectrumViewModel viewModel;
private TxnAdapter adapter;
private AtomicBoolean showEmpty;
@Override
protected int setView() {
return R.layout.txn_list;
}
@Override
protected void init(View view) {
mActivity.setSupportActionBar(mBinding.toolbar);
mBinding.toolbar.setNavigationOnClickListener(((MainActivity) mActivity)::toggleDrawer);
mBinding.toolbar.setTitle("");
viewModel = ViewModelProviders.of(mActivity).get(ElectrumViewModel.class);
adapter = new TxnAdapter(mActivity, this);
initViews();
}
private void initViews() {
showEmpty = new AtomicBoolean(false);
if (!hasSdcard(mActivity)) {
showEmpty.set(true);
mBinding.emptyTitle.setText(R.string.no_sdcard);
mBinding.emptyMessage.setText(R.string.no_sdcard_hint);
} else {
mBinding.list.setAdapter(adapter);
viewModel.loadUnsignTxn().observe(this, files -> {
if (files.size() > 0) {
adapter.setItems(files);
} else {
showEmpty.set(true);
mBinding.emptyTitle.setText(R.string.no_unsigned_txn);
mBinding.emptyMessage.setText(R.string.no_unsigned_txn_hint);
}
updateUi();
});
}
updateUi();
}
private void updateUi() {
if (showEmpty.get()) {
mBinding.emptyView.setVisibility(View.VISIBLE);
mBinding.list.setVisibility(View.GONE);
} else {
mBinding.emptyView.setVisibility(View.GONE);
mBinding.list.setVisibility(View.VISIBLE);
}
}
@Override
protected void initData(Bundle savedInstanceState) {
}
@Override
public void onClick(String file) {
viewModel.parseTxnFile(file).observe(this, hex -> {
if (!TextUtils.isEmpty(hex)) {
Bundle bundle = new Bundle();
bundle.putString("txn", hex);
bundle.putBoolean("is_file", true);
navigate(R.id.action_to_ElectrumTxConfirmFragment, bundle);
} else {
Toast.makeText(mActivity, R.string.error_txn_file, Toast.LENGTH_SHORT).show();
}
});
}
public static class TxnAdapter extends BaseBindingAdapter<String, ElectrumTxnBinding> {
private Callback callback;
TxnAdapter(Context context, Callback callback) {
super(context);
this.callback = callback;
}
@Override
protected int getLayoutResId(int viewType) {
return R.layout.electrum_txn;
}
@Override
protected void onBindItem(ElectrumTxnBinding binding, String item) {
binding.setFile(item);
binding.setCallback(callback);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
super.onBindViewHolder(holder, position);
}
}
}

59
app/src/main/java/com/cobo/cold/ui/modal/ExportToSdcardDialog.java

@ -0,0 +1,59 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.ui.modal;
import android.app.AlertDialog;
import android.app.Dialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.databinding.DataBindingUtil;
import androidx.fragment.app.DialogFragment;
import com.cobo.cold.R;
public class ExportToSdcardDialog extends DialogFragment {
public static ExportToSdcardDialog newInstance() {
return new ExportToSdcardDialog();
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return super.onCreateView(inflater, container, savedInstanceState);
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
View v = DataBindingUtil.inflate(LayoutInflater.from(getActivity()),
R.layout.export_success,null,false).getRoot();
Dialog dialog = new AlertDialog.Builder(getActivity(), R.style.dialog)
.setView(v)
.create();
dialog.setCanceledOnTouchOutside(false);
return dialog;
}
}

34
app/src/main/java/com/cobo/cold/ui/views/DrawerAdapter.java

@ -45,9 +45,9 @@ public class DrawerAdapter extends RecyclerView.Adapter<DrawerAdapter.Holder> {
new DrawerItem(R.id.drawer_wallet, R.drawable.drawer_wallet, R.string.drawer_menu_my_vault),
new DrawerItem(R.id.drawer_manage, R.drawable.drawer_asset_manager, R.string.drawer_menu_add_remove),
new DrawerItem(R.id.drawer_sync, R.drawable.drawer_asset_observation, R.string.drawer_menu_sync),
new DrawerItem(R.id.drawer_sdcard, R.drawable.drawer_sdcard, R.string.read_sdcard),
new DrawerItem(R.id.drawer_settings, R.drawable.drawer_setting, R.string.drawer_menu_setting),
new DrawerItem(R.id.drawer_about, R.drawable.drawer_about, R.string.drawer_menu_about),
new DrawerItem(R.id.drawer_id, R.drawable.drawer_id, 0)
new DrawerItem(R.id.drawer_about, R.drawable.drawer_about, R.string.drawer_menu_about)
);
public DrawerAdapter(int currentFragmentIndex) {
@ -78,7 +78,7 @@ public class DrawerAdapter extends RecyclerView.Adapter<DrawerAdapter.Holder> {
@Override
public int getItemCount() {
return dataList.size() - 1;
return dataList.size();
}
@NonNull
@ -94,24 +94,16 @@ public class DrawerAdapter extends RecyclerView.Adapter<DrawerAdapter.Holder> {
@Override
public void onBindViewHolder(@NonNull Holder holder, int position) {
final DrawerItem item = dataList.get(position);
if (position == dataList.size() - 1) {
holder.binding.setDrawerItem(item);
holder.binding.icon.setImageResource(item.iconRes);
holder.binding.getRoot().setClickable(false);
holder.binding.text.setText(Utilities.getVaultId(holder.binding.getRoot().getContext()));
holder.binding.text.setTextColor(holder.binding.getRoot().getContext().getColor(R.color.id_color));
} else {
holder.binding.setDrawerItem(item);
holder.binding.executePendingBindings();
holder.binding.getRoot().setOnClickListener(v -> {
dataList.forEach(i -> i.select = false);
item.select = true;
notifyDataSetChanged();
if (listener != null) {
listener.itemClick(item.index);
}
});
}
holder.binding.setDrawerItem(item);
holder.binding.executePendingBindings();
holder.binding.getRoot().setOnClickListener(v -> {
dataList.forEach(i -> i.select = false);
item.select = true;
notifyDataSetChanged();
if (listener != null) {
listener.itemClick(item.index);
}
});
}
public void setOnItemClickListener(OnItemClickListener listener) {

6
app/src/main/java/com/cobo/cold/ui/views/qrcode/DynamicQrCodeView.java

@ -34,7 +34,7 @@ import androidx.databinding.DataBindingUtil;
import com.cobo.cold.AppExecutors;
import com.cobo.cold.R;
import com.cobo.cold.databinding.QrcodeModalBinding;
import com.cobo.cold.databinding.DynamicQrcodeModalBinding;
import com.cobo.cold.encryptioncore.utils.ByteFormatter;
import com.cobo.cold.ui.modal.FullScreenModal;
import com.cobo.cold.update.utils.Digest;
@ -102,8 +102,8 @@ public class DynamicQrCodeView extends LinearLayout implements QrCodeHolder {
private void showModal() {
FullScreenModal dialog = new FullScreenModal();
QrcodeModalBinding binding = DataBindingUtil.inflate(LayoutInflater.from(getContext()),
R.layout.qrcode_modal, null, false);
DynamicQrcodeModalBinding binding = DataBindingUtil.inflate(LayoutInflater.from(getContext()),
R.layout.dynamic_qrcode_modal, null, false);
dialog.setBinding(binding);
binding.close.setOnClickListener(v -> dialog.dismiss());
binding.qrcodeLayout.qrcode.setData(data);

30
app/src/main/java/com/cobo/cold/ui/views/qrcode/QrCodeView.java

@ -19,16 +19,22 @@ package com.cobo.cold.ui.views.qrcode;
import android.content.Context;
import android.graphics.Bitmap;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ProgressBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.view.ViewCompat;
import androidx.databinding.DataBindingUtil;
import com.cobo.cold.AppExecutors;
import com.cobo.cold.R;
import com.cobo.cold.databinding.QrcodeModalBinding;
import com.cobo.cold.ui.modal.FullScreenModal;
public class QrCodeView extends FrameLayout implements QrCodeHolder {
@ -50,10 +56,19 @@ public class QrCodeView extends FrameLayout implements QrCodeHolder {
showQrCode();
}
public void disableModal() {
img.setOnClickListener(null);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
img = findViewById(R.id.img);
img.setOnClickListener(v -> {
if (!TextUtils.isEmpty(data)) {
showModal();
}
});
progressBar = findViewById(R.id.progress);
}
@ -71,6 +86,17 @@ public class QrCodeView extends FrameLayout implements QrCodeHolder {
}
}
public void showModal() {
FullScreenModal dialog = new FullScreenModal();
QrcodeModalBinding binding = DataBindingUtil.inflate(LayoutInflater.from(getContext()),
R.layout.qrcode_modal, null, false);
dialog.setBinding(binding);
binding.close.setOnClickListener(v -> dialog.dismiss());
binding.qrcodeLayout.qrcode.setData(data);
binding.qrcodeLayout.qrcode.disableModal();
dialog.show(((AppCompatActivity) getContext()).getSupportFragmentManager(), "");
}
private void setImageBitmap(Bitmap bm) {
AppExecutors.getInstance().mainThread().execute(() -> {
progressBar.setVisibility(GONE);
@ -86,11 +112,11 @@ public class QrCodeView extends FrameLayout implements QrCodeHolder {
@Override
public int getViewWidth() {
return getWidth();
return img.getWidth();
}
@Override
public int getViewHeight() {
return getHeight();
return img.getHeight();
}
}

10
app/src/main/java/com/cobo/cold/update/utils/FileUtils.java

@ -49,6 +49,16 @@ public class FileUtils {
return builder.toString();
}
public static boolean writeString(@NonNull File file, String content) {
try(FileOutputStream fos = new FileOutputStream(file)) {
fos.write(content.getBytes());
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
@Nullable
public static byte[] bufferlize(@NonNull File file) {
try (InputStream inputStream = new FileInputStream(file)) {

5
app/src/main/java/com/cobo/cold/update/utils/Storage.java

@ -119,6 +119,11 @@ public class Storage {
return dir;
}
@NonNull
public File getElectrumDir() {
return mExternalDir;
}
@NonNull
public File getUpdateZipFile() {
return new File(mExternalDir, UPDATE_ZIP_FILE);

56
app/src/main/java/com/cobo/cold/viewmodel/AddAddressViewModel.java

@ -169,60 +169,4 @@ public class AddAddressViewModel extends AndroidViewModel {
}
}
static class AddAccountAddressTask extends AsyncTask<String, Void, Void> {
private final CoinEntity coinEntity;
private final DataRepository repo;
private final Runnable onComplete;
AddAccountAddressTask(CoinEntity coinEntity, DataRepository repo, Runnable onComplete) {
this.coinEntity = coinEntity;
this.repo = repo;
this.onComplete = onComplete;
}
@Override
protected Void doInBackground(String... path) {
List<AddressEntity> entities = new ArrayList<>();
for (String s : path) {
String exPub = new GetExtendedPublicKeyCallable(s).call();
Account account;
try {
account = Account.parseAccount(path[0]);
int index = account.getValue();
AddressEntity addressEntity = new AddressEntity();
addressEntity.setPath(path[0]);
int coinType = account.getParent().getValue();
AbsDeriver deriver = AbsDeriver.newInstance(Coins.coinCodeOfIndex(coinType));
if (deriver != null) {
String addr = deriver.derive(exPub);
addressEntity.setAddressString(addr);
addressEntity.setCoinId(coinEntity.getCoinId());
addressEntity.setIndex(index);
addressEntity.setName(coinEntity.getCoinCode().toLowerCase() + "-" + index);
addressEntity.setBelongTo(coinEntity.getBelongTo());
entities.add(addressEntity);
}
} catch (InvalidPathException e) {
e.printStackTrace();
return null;
}
}
repo.insertAddress(entities);
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
if (onComplete != null) {
onComplete.run();
}
}
}
}

227
app/src/main/java/com/cobo/cold/viewmodel/ElectrumViewModel.java

@ -0,0 +1,227 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.viewmodel;
import android.app.Application;
import android.content.Context;
import android.os.Handler;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.cobo.coinlib.Util;
import com.cobo.coinlib.coins.BTC.Electrum.ElectrumTx;
import com.cobo.coinlib.coins.BTC.Electrum.TransactionInput;
import com.cobo.coinlib.coins.BTC.Electrum.TransactionOutput;
import com.cobo.coinlib.exception.InvalidPathException;
import com.cobo.coinlib.path.Account;
import com.cobo.coinlib.utils.Coins;
import com.cobo.cold.AppExecutors;
import com.cobo.cold.DataRepository;
import com.cobo.cold.MainApplication;
import com.cobo.cold.R;
import com.cobo.cold.databinding.CommonModalBinding;
import com.cobo.cold.db.entity.AccountEntity;
import com.cobo.cold.db.entity.CoinEntity;
import com.cobo.cold.protobuf.TransactionProtoc;
import com.cobo.cold.ui.modal.ExportToSdcardDialog;
import com.cobo.cold.ui.modal.ModalDialog;
import com.cobo.cold.update.utils.FileUtils;
import com.cobo.cold.update.utils.Storage;
import com.googlecode.protobuf.format.JsonFormat;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.spongycastle.util.encoders.Hex;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ElectrumViewModel extends AndroidViewModel {
public static final String ELECTRUM_SIGN_ID = "electrum_sign_id";
private static Pattern signedTxnPattern = Pattern.compile("[0-9a-f]{5}-signed.txn$");
private final DataRepository mRepo;
private MutableLiveData<String> exPub = new MutableLiveData<>();
private Storage storage;
public ElectrumViewModel(@NonNull Application application) {
super(application);
mRepo = MainApplication.getApplication().getRepository();
storage = Storage.createByEnvironment(application);
}
public static boolean hasSdcard(Context context) {
Storage storage = Storage.createByEnvironment(context);
return storage != null && storage.getExternalDir() != null;
}
public static boolean writeToSdcard(Storage storage, String content, String fileName) {
File file = new File(storage.getElectrumDir(), fileName);
return FileUtils.writeString(file, content);
}
public static void showNoSdcardModal(AppCompatActivity activity) {
ModalDialog modalDialog = ModalDialog.newInstance();
CommonModalBinding binding = DataBindingUtil.inflate(
LayoutInflater.from(activity), R.layout.common_modal,
null, false);
binding.title.setText(R.string.hint);
binding.subTitle.setText(R.string.insert_sdcard_hint);
binding.close.setVisibility(View.GONE);
binding.confirm.setText(R.string.know);
binding.confirm.setOnClickListener(vv -> modalDialog.dismiss());
modalDialog.setBinding(binding);
modalDialog.show(activity.getSupportFragmentManager(), "");
}
public static void exportSuccess(AppCompatActivity activity) {
ExportToSdcardDialog dialog = new ExportToSdcardDialog();
dialog.show(activity.getSupportFragmentManager(), "");
new Handler().postDelayed(dialog::dismiss, 1000);
}
static JSONObject parseElectrumTxHex(String hex) throws JSONException, ElectrumTx.SerializationException {
ElectrumTx tx = ElectrumTx.parse(Hex.decode(hex));
JSONObject btcTx = adapt(tx);
TransactionProtoc.SignTransaction.Builder builder = TransactionProtoc.SignTransaction.newBuilder();
builder.setCoinCode(Coins.BTC.coinCode())
.setSignId(ELECTRUM_SIGN_ID)
.setTimestamp(System.currentTimeMillis())
.setDecimal(8);
String signTransaction = new JsonFormat().printToString(builder.build());
JSONObject signTx = new JSONObject(signTransaction);
signTx.put("btcTx", btcTx);
return signTx;
}
private static JSONObject adapt(ElectrumTx tx) throws JSONException {
JSONObject object = new JSONObject();
JSONArray inputs = new JSONArray();
JSONArray outputs = new JSONArray();
adaptInputs(tx, inputs);
adaptOutputs(tx, outputs);
object.put("inputs", inputs);
object.put("outputs", outputs);
object.put("locktime", tx.getLockTime());
object.put("version", tx.getVersion());
return object;
}
private static void adaptInputs(ElectrumTx tx, JSONArray inputs) throws JSONException {
for (TransactionInput transactionInput : tx.getInputs()) {
JSONObject in = new JSONObject();
JSONObject utxo = new JSONObject();
in.put("hash", transactionInput.preTxId);
in.put("index", transactionInput.preTxIndex);
in.put("sequence", transactionInput.sequence);
utxo.put("publicKey", transactionInput.pubKey.pubkey);
utxo.put("value", transactionInput.value.intValue());
in.put("utxo", utxo);
in.put("hash", transactionInput.preTxId);
in.put("ownerKeyPath", transactionInput.pubKey.hdPath);
inputs.put(in);
}
}
private static void adaptOutputs(ElectrumTx tx, JSONArray outputs) throws JSONException {
for (TransactionOutput transactionOutput : tx.getOutputs()) {
JSONObject out = new JSONObject();
out.put("address", transactionOutput.address);
out.put("value", transactionOutput.value);
outputs.put(out);
}
}
public LiveData<String> getExPub() {
AppExecutors.getInstance().diskIO().execute(() -> {
CoinEntity btc = mRepo.loadCoinSync(Coins.coinIdFromCoinCode("BTC"));
AccountEntity accountEntity = mRepo.loadAccountsForCoin(btc).get(0);
String hdPath = accountEntity.getHdPath();
String expub = accountEntity.getExPub();
try {
Account account = Account.parseAccount(hdPath);
if (account.getParent().getParent().getValue() == 49 && expub.startsWith("xpub")) {
exPub.postValue(Util.convertXpubToYpub(expub));
} else if (expub.startsWith("ypub")) {
exPub.postValue(expub);
}
} catch (InvalidPathException e) {
e.printStackTrace();
}
});
return exPub;
}
private boolean isSignedTxn(String fileName) {
Matcher matcher = signedTxnPattern.matcher(fileName);
return matcher.matches();
}
public LiveData<List<String>> loadUnsignTxn() {
MutableLiveData<List<String>> result = new MutableLiveData<>();
AppExecutors.getInstance().diskIO().execute(() -> {
List<String> fileList = new ArrayList<>();
if (storage != null) {
File[] files = storage.getElectrumDir().listFiles();
if (files != null) {
for (File f : files) {
Log.w("kkk", f.getName() + " " + f.length());
if (f.getName().endsWith(".txn")
&& !isSignedTxn(f.getName())) {
fileList.add(f.getName());
}
}
}
}
result.postValue(fileList);
});
return result;
}
public LiveData<String> parseTxnFile(String file) {
MutableLiveData<String> txnHex = new MutableLiveData<>();
AppExecutors.getInstance().diskIO().execute(() -> {
String content = FileUtils.readString(new File(storage.getExternalDir(), file));
try {
JSONObject object = new JSONObject(content);
String hex = object.getString("hex");
txnHex.postValue(hex);
} catch (JSONException e) {
e.printStackTrace();
txnHex.postValue(null);
}
});
return txnHex;
}
}

7
app/src/main/java/com/cobo/cold/viewmodel/QrScanViewModel.java

@ -206,13 +206,12 @@ public class QrScanViewModel extends AndroidViewModel {
if (!Coins.isCoinSupported(coinCode)) {
throw new CoinNotFindException("not support " + coinCode);
}
Bundle bundle = new Bundle();
bundle.putString(KEY_TX_DATA, object.getJSONObject("signTx").toString());
fragment.navigate(R.id.action_to_txConfirmFragment, bundle);
} catch (JSONException e) {
throw new InvalidTransactionException("invalid transaction");
}
Bundle bundle = new Bundle();
bundle.putString(KEY_TX_DATA, object.toString());
fragment.navigate(R.id.action_to_txConfirmFragment, bundle);
}
private JSONObject parseToJson(ScannedData[] res, String valueType) {

135
app/src/main/java/com/cobo/cold/viewmodel/TxConfirmViewModel.java

@ -32,12 +32,12 @@ import com.cobo.coinlib.coins.AbsDeriver;
import com.cobo.coinlib.coins.AbsTx;
import com.cobo.coinlib.coins.BTC.Btc;
import com.cobo.coinlib.coins.BTC.BtcImpl;
import com.cobo.coinlib.coins.BTC.Electrum.ElectrumTx;
import com.cobo.coinlib.coins.BTC.UtxoTx;
import com.cobo.coinlib.exception.InvalidPathException;
import com.cobo.coinlib.exception.InvalidTransactionException;
import com.cobo.coinlib.interfaces.SignCallback;
import com.cobo.coinlib.interfaces.Signer;
import com.cobo.coinlib.path.Account;
import com.cobo.coinlib.path.AddressIndex;
import com.cobo.coinlib.path.CoinPath;
import com.cobo.coinlib.utils.Coins;
@ -55,6 +55,7 @@ import com.cobo.cold.db.entity.TxEntity;
import com.cobo.cold.encryption.ChipSigner;
import com.cobo.cold.ui.views.AuthenticateModal;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.spongycastle.util.encoders.Hex;
@ -69,6 +70,8 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Stream;
import static com.cobo.cold.viewmodel.ElectrumViewModel.parseElectrumTxHex;
public class TxConfirmViewModel extends AndroidViewModel {
public static final String STATE_SIGNING = "signing";
@ -99,22 +102,16 @@ public class TxConfirmViewModel extends AndroidViewModel {
}
public void parseTxData(String json) {
NumberFormat nf = NumberFormat.getInstance();
nf.setMaximumFractionDigits(20);
AppExecutors.getInstance().diskIO().execute(() -> {
TxEntity tx = new TxEntity();
try {
JSONObject object = new JSONObject(json).getJSONObject("signTx");
JSONObject object = new JSONObject(json);
Log.i(TAG, "object = " + object.toString(4));
transaction = AbsTx.newInstance(object);
if (transaction == null) {
observableTx.postValue(null);
parseTxException.postValue(new InvalidTransactionException("invalid transaction"));
return;
}
if (transaction instanceof UtxoTx) {
if (!checkChangeAddress(transaction)) {
observableTx.postValue(null);
@ -122,17 +119,7 @@ public class TxConfirmViewModel extends AndroidViewModel {
return;
}
}
coinCode = Objects.requireNonNull(transaction).getCoinCode();
tx.setSignId(object.getString("signId"));
tx.setTimeStamp(object.getLong("timestamp"));
tx.setCoinCode(coinCode);
tx.setCoinId(Coins.coinIdFromCoinCode(coinCode));
tx.setFrom(getFromAddress(transaction));
tx.setTo(transaction.getTo());
tx.setAmount(nf.format(transaction.getAmount()) + " " + transaction.getUnit());
tx.setFee(nf.format(transaction.getFee()) + " " + coinCode);
tx.setMemo(transaction.getMemo());
tx.setBelongTo(mRepository.getBelongTo());
TxEntity tx = generateTxEntity(object);
observableTx.postValue(tx);
} catch (JSONException e) {
e.printStackTrace();
@ -140,6 +127,35 @@ public class TxConfirmViewModel extends AndroidViewModel {
});
}
private TxEntity generateTxEntity(JSONObject object) throws JSONException {
TxEntity tx = new TxEntity();
NumberFormat nf = NumberFormat.getInstance();
nf.setMaximumFractionDigits(20);
coinCode = Objects.requireNonNull(transaction).getCoinCode();
tx.setSignId(object.getString("signId"));
tx.setTimeStamp(object.getLong("timestamp"));
tx.setCoinCode(coinCode);
tx.setCoinId(Coins.coinIdFromCoinCode(coinCode));
tx.setFrom(getFromAddress());
tx.setTo(getToAddress());
tx.setAmount(nf.format(transaction.getAmount()) + " " + transaction.getUnit());
tx.setFee(nf.format(transaction.getFee()) + " " + coinCode);
tx.setMemo(transaction.getMemo());
tx.setBelongTo(mRepository.getBelongTo());
return tx;
}
public void parseTxnData(String txnData) {
AppExecutors.getInstance().networkIO().execute(()-> {
try {
JSONObject signTx = parseElectrumTxHex(txnData);
parseTxData(signTx.toString());
} catch (ElectrumTx.SerializationException | JSONException e) {
e.printStackTrace();
}
});
}
private boolean checkChangeAddress(AbsTx utxoTx) {
UtxoTx.ChangeAddressInfo changeAddressInfo = ((UtxoTx) utxoTx).getChangeAddressInfo();
if (changeAddressInfo == null) {
@ -162,9 +178,19 @@ public class TxConfirmViewModel extends AndroidViewModel {
}
}
private String getFromAddress(AbsTx tx) {
if (!TextUtils.isEmpty(tx.getFrom())) {
return tx.getFrom();
private String getToAddress() {
String to = transaction.getTo();
if (transaction instanceof UtxoTx) {
to = ((UtxoTx) transaction).getOutputs().toString();
}
return to;
}
private String getFromAddress() {
if (!TextUtils.isEmpty(transaction.getFrom())) {
return transaction.getFrom();
}
String[] paths = transaction.getHdPath().split(AbsTx.SEPARATOR);
String[] externalPath = Stream.of(paths)
@ -172,14 +198,37 @@ public class TxConfirmViewModel extends AndroidViewModel {
.toArray(String[]::new);
ensureAddressExist(externalPath);
String[] accountPath = Stream.of(paths)
.filter(this::isAccountPath)
.toArray(String[]::new);
try {
if (transaction instanceof UtxoTx) {
JSONArray inputsClone = new JSONArray();
JSONArray inputs = ((UtxoTx) transaction).getInputs();
CoinEntity coin = mRepository.loadCoinSync(Coins.coinIdFromCoinCode(coinCode));
String expub = mRepository.loadAccountsForCoin(coin).get(0).getExPub();
for (int i = 0; i < inputs.length(); i++) {
JSONObject input = inputs.getJSONObject(i);
long value = input.getJSONObject("utxo").getLong("value");
String hdpath = input.getString("ownerKeyPath");
AddressIndex addressIndex = CoinPath.parsePath(hdpath);
int index = addressIndex.getValue();
int change = addressIndex.getParent().getValue();
String from = AbsDeriver.newInstance(transaction.getCoinCode()).derive(expub,change,index);
inputsClone.put(new JSONObject().put("value", value)
.put("address",from));
}
ensureAccountAddressExist(accountPath);
return inputsClone.toString();
}
} catch (JSONException e) {
e.printStackTrace();
} catch (InvalidPathException e) {
e.printStackTrace();
}
return Stream.of(concat(accountPath, externalPath))
return Stream.of(externalPath)
.distinct()
.map(path -> mRepository.loadAddressBypath(path).getAddressString())
.reduce((s1, s2) -> s1 + AbsTx.SEPARATOR + s2)
@ -193,36 +242,18 @@ public class TxConfirmViewModel extends AndroidViewModel {
}
private void ensureAccountAddressExist(String[] accountPath) {
CoinEntity coin = mRepository.loadCoinSync(Coins.coinIdFromCoinCode(coinCode));
accountPath = Stream.of(accountPath).distinct()
.filter(path -> mRepository.loadAddressBypath(path) == null)
.toArray(String[]::new);
final CountDownLatch mLatch = new CountDownLatch(1);
new AddAddressViewModel.AddAccountAddressTask(coin, mRepository, mLatch::countDown)
.execute(accountPath);
private boolean isExternalPath(@NonNull String path) {
try {
mLatch.await();
} catch (InterruptedException e) {
return CoinPath.parsePath(path).getParent().isExternal();
} catch (InvalidPathException e) {
e.printStackTrace();
}
}
private boolean isAccountPath(String path) {
try {
Account.parseAccount(path);
return true;
} catch (InvalidPathException ignore) {
}
return false;
}
private boolean isExternalPath(@NonNull String path) {
private boolean isInternalPath(@NonNull String path) {
try {
return CoinPath.parsePath(path).getParent().isExternal();
return !CoinPath.parsePath(path).getParent().isExternal();
} catch (InvalidPathException e) {
e.printStackTrace();
}
@ -436,6 +467,10 @@ public class TxConfirmViewModel extends AndroidViewModel {
return Objects.requireNonNull(observableTx.getValue()).getTxId();
}
public String getTxHex() {
return Objects.requireNonNull(observableTx.getValue()).getSignedHex();
}
private final ExecutorService sExecutor = Executors.newSingleThreadExecutor();
public boolean isAddressInWhiteList(String address) {

BIN
app/src/main/res/drawable-xhdpi/drawer_sdcard.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 B

BIN
app/src/main/res/drawable-xhdpi/info.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1014 B

BIN
app/src/main/res/drawable-xhdpi/more.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

BIN
app/src/main/res/drawable-xhdpi/sdcard_icon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

136
app/src/main/res/layout/broadcast_electrum_tx_fragment.xml

@ -0,0 +1,136 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Cobo
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="coinCode"
type="String" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:navigationIcon="@drawable/arrow_left"
app:popupTheme="@style/AppTheme.PopupOverlay">
<TextView
android:id="@+id/toolbar_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/broadcast_tx"
android:textColor="@android:color/white"
android:textSize="15sp"
tools:text="@string/broadcast_tx" />
</androidx.appcompat.widget.Toolbar>
<include layout="@layout/divider" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical">
<ImageView
android:id="@+id/icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginTop="20dp"
android:src="@drawable/coin_btc"
tools:ignore="ContentDescription" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="16dp">
<View
android:layout_width="30dp"
android:layout_height="match_parent" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="@string/use_electrum_to_broadcast"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold" />
<ImageView
android:id="@+id/info"
android:layout_width="30dp"
android:layout_height="30dp"
android:padding="5dp"
android:src="@drawable/info" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/transaction_from_electrum"
android:textColor="@color/white40" />
<include
android:id="@+id/qrcode_layout"
layout="@layout/qrcode" />
<com.cobo.cold.ui.views.SpanedTextView
android:id="@+id/hint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="26dp"
android:gravity="center"
android:text="@string/electrum_qrcode_hint"
android:textColor="@color/white"
android:textSize="12sp" />
<Button
android:id="@+id/complete"
style="@style/AcceptButton"
android:layout_width="match_parent"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="20dp"
android:text="@string/complete" />
</LinearLayout>
</ScrollView>
</LinearLayout>
</layout>

4
app/src/main/res/layout/create_vault_modal.xml

@ -17,8 +17,7 @@
~ in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
@ -70,7 +69,6 @@
android:layout_marginBottom="28dp"
android:textSize="15sp"
android:textColor="@color/colorAccent"
tools:text="金库生成中..."
android:gravity="center" />
</LinearLayout>

86
app/src/main/res/layout/dialog_bottom_sheet.xml

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2020 Cobo
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#181717">
<RelativeLayout
android:id="@+id/add_address"
android:layout_width="match_parent"
android:layout_height="53dp"
android:orientation="horizontal"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/add_address"
android:layout_centerVertical="true"
android:paddingHorizontal="16dp"
android:textColor="@color/white" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/arrow_right"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true"
android:layout_marginEnd="16dp" />
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginHorizontal="16dp"
android:background="#12ffffff" />
<RelativeLayout
android:id="@+id/export_xpub_to_electrum"
android:layout_width="match_parent"
android:layout_height="53dp"
android:orientation="horizontal"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/export_to_electrum"
android:layout_centerVertical="true"
android:paddingHorizontal="16dp"
android:textColor="@color/white" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/arrow_right"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true"
android:layout_marginEnd="16dp" />
</RelativeLayout>
</LinearLayout>
</layout>

4
app/src/main/res/layout/divider.xml

@ -19,8 +19,6 @@
-->
<View xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@drawable/divider"
tools:showIn="@layout/asset_list_fragment" />
android:background="@drawable/divider" />

54
app/src/main/res/layout/dynamic_qrcode_modal.xml

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2020 Cobo
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
-->
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
</data>
<FrameLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="530dp"
android:background="#ffffff">
<ImageView
android:id="@+id/close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/close"
android:tint="@color/colorAccent"
android:padding="18dp"
android:layout_gravity="end|top"
tools:ignore="ContentDescription" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center">
<include
android:id="@+id/qrcode_layout"
layout="@layout/dynamic_qrcode" />
</FrameLayout>
</FrameLayout>
</layout>

139
app/src/main/res/layout/electrum_export.xml

@ -0,0 +1,139 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Cobo
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:navigationIcon="@drawable/arrow_left"
app:popupTheme="@style/AppTheme.PopupOverlay">
<TextView
android:id="@+id/toolbar_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginEnd="20dp"
android:ellipsize="middle"
android:singleLine="true"
android:text="@string/export_to_electrum"
android:textColor="@android:color/white"
android:textSize="15sp" />
</androidx.appcompat.widget.Toolbar>
<include layout="@layout/divider" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/use_electrum_scan_xpub"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold" />
<ImageView
android:id="@+id/info"
android:layout_width="30dp"
android:layout_height="30dp"
android:padding="5dp"
android:src="@drawable/info"/>
</LinearLayout>
<com.cobo.cold.ui.views.qrcode.QrCodeView
android:id="@+id/qrcode"
android:layout_width="160dp"
android:layout_height="160dp"
android:layout_marginHorizontal="34dp"
android:layout_marginTop="24dp"
android:background="@color/white"
android:padding="5dp"
android:keepScreenOn="true">
<ImageView
android:id="@+id/img"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible"
tools:ignore="ContentDescription" />
<ProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="visible" />
</com.cobo.cold.ui.views.qrcode.QrCodeView>
<com.cobo.cold.ui.views.SpanedTextView
android:id="@+id/export_to_sdcard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="26dp"
android:gravity="center"
android:text="@string/electrum_qrcode_hint"
android:textColor="@color/white"
android:textSize="12sp" />
<androidx.legacy.widget.Space
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:gravity="center"
android:text="@string/master_xpub"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold" />
<TextView
android:id="@+id/expub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
tools:text="ypub6D3i46Y43SFfjEBYheBK3btYMRm9Cfb8Tt4M5Bv16tArNBw5ATNyJWjdcMyLxoCdHWTvm3ak7j2BWacq5Lw478aYUeARoYm4dvaQgJBAGsb"
android:textColor="@color/white40"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="48dp"
android:textSize="15sp" />
</LinearLayout>
</layout>

86
app/src/main/res/layout/electrum_export_guide.xml

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Cobo
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:navigationIcon="@drawable/arrow_left"
app:popupTheme="@style/AppTheme.PopupOverlay">
<TextView
android:id="@+id/toolbar_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginEnd="20dp"
android:ellipsize="middle"
android:singleLine="true"
android:text="@string/export_to_electrum"
android:textColor="@android:color/white"
android:textSize="15sp" />
</androidx.appcompat.widget.Toolbar>
<include layout="@layout/divider" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:padding="16dp"
android:text="@string/export_to_electrum_guide_hint"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lineSpacingExtra="6dp"
android:padding="16dp"
android:text="@string/export_to_electrum_guide"
android:textColor="@color/white"
android:textSize="13sp" />
<Space
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<Button
android:id="@+id/export"
style="@style/AcceptButton"
android:layout_width="match_parent"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="20dp"
android:text="@string/export_xpub" />
</LinearLayout>
</layout>

69
app/src/main/res/layout/electrum_tx.xml

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2020 Cobo
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="tx"
type="com.cobo.cold.model.Tx" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:navigationIcon="@drawable/arrow_left"
app:popupTheme="@style/AppTheme.PopupOverlay">
<TextView
android:id="@+id/toolbar_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textSize="15sp"
android:textColor="@android:color/white"
android:text="@string/signing_history" />
</androidx.appcompat.widget.Toolbar>
<include layout="@layout/divider" />
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
android:id="@+id/tx_detail"
layout="@layout/electrum_tx_detail"
bind:tx="@{tx}" />
</LinearLayout>
</LinearLayout>
</layout>

89
app/src/main/res/layout/electrum_tx_confirm_fragment.xml

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2020 Cobo
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="coinName"
type="String" />
<variable
name="tx"
type="com.cobo.cold.model.Tx" />
<variable
name="viewModel"
type="com.cobo.cold.viewmodel.TxConfirmViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:navigationIcon="@drawable/arrow_left"
app:popupTheme="@style/AppTheme.PopupOverlay">
<TextView
android:id="@+id/toolbar_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/tx_confirm"
android:textColor="@android:color/white"
android:textSize="15sp" />
</androidx.appcompat.widget.Toolbar>
<include layout="@layout/divider" />
<LinearLayout
android:id="@+id/transaction_info"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical">
<include
android:id="@+id/tx_detail"
layout="@layout/electrum_tx_detail"
bind:tx="@{tx}" />
</LinearLayout>
<Button
android:id="@+id/sign"
style="@style/AcceptButton"
android:layout_width="match_parent"
android:layout_marginTop="16dp"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="20dp"
android:text="@string/sign" />
</LinearLayout>
</layout>

312
app/src/main/res/layout/electrum_tx_detail.xml

@ -0,0 +1,312 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2020 Cobo
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
-->
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="tx"
type="com.cobo.cold.model.Tx" />
</data>
<com.donkingliang.consecutivescroller.ConsecutiveScrollerLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:showIn="@layout/tx_confirm_fragment">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="60dp">
<ImageView
android:id="@+id/icon"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_centerVertical="true"
android:layout_marginStart="16dp"
android:src="@drawable/coin_btc"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/coinId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/coinCode"
android:layout_centerInParent="true"
android:layout_marginStart="12dp"
android:layout_marginTop="2dp"
android:layout_toEndOf="@id/icon"
android:textColor="@color/white40"
android:textSize="12sp"
android:textStyle="bold"
android:text="Bitcoin" />
<TextView
android:id="@+id/unit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginStart="4dp"
android:layout_marginTop="14dp"
android:layout_marginEnd="16dp"
android:text="@{tx.coinCode}"
android:textColor="@color/white"
android:textSize="14sp"
android:textStyle="bold"
tools:text="BTC"
android:visibility="gone" />
<TextView
android:id="@+id/amount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="14dp"
android:layout_toStartOf="@id/unit"
android:layout_alignParentEnd="true"
android:layout_marginEnd="16dp"
android:textColor="@color/white"
android:textSize="14sp"
android:textStyle="bold"
tools:text="2.62407806" />
<TextView
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/amount"
android:layout_alignParentEnd="true"
android:layout_marginStart="12dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="16dp"
android:textColor="@color/white40"
android:textSize="12sp"
android:textStyle="bold"
app:time="@{tx.timeStamp}"
tools:text="2018/06/01 15:40" />
<include
layout="@layout/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentBottom="true" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/tx_id_info"
android:layout_width="match_parent"
android:layout_height="86dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="14dp"
android:text="@string/tx_id"
android:textColor="@color/white40"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:layout_width="184dp"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginTop="14dp"
android:layout_marginEnd="16dp"
android:text="@{tx.txId}"
android:textColor="@color/white"
android:textStyle="bold"
tools:text="84asf56sa5ewf46a4s654f6sa46s4z6x46sa46s46d4sa6f4a64f6ad4f6as4dx556s4a" />
<include
layout="@layout/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentBottom="true" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/tx_source"
android:layout_width="match_parent"
android:layout_height="60dp"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="@string/transaction_source_label"
android:layout_centerVertical="true"
android:textColor="@color/white40"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:layout_width="184dp"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="16dp"
android:textColor="@color/white"
android:textStyle="bold"
android:text="Electrum" />
<include
layout="@layout/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentBottom="true" />
</RelativeLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/from_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/receive_item"
tools:itemCount="1"
android:visibility="visible" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/arrow_down"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
android:layout_marginStart="120dp"
android:src="@drawable/tx_arrow_down"
tools:ignore="ContentDescription" />
</RelativeLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/to_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:itemCount="1"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/receive_item"
android:visibility="visible" />
<include
layout="@layout/divider"
android:layout_width="match_parent"
android:layout_height="1dp" />
<RelativeLayout
android:id="@+id/fee_info"
android:layout_width="match_parent"
android:layout_height="49dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="14dp"
android:text="@string/tx_fee"
android:textColor="@color/white40"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="@+id/fee"
android:layout_width="184dp"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginTop="14dp"
android:layout_marginEnd="16dp"
android:text="@{tx.fee}"
android:textColor="@color/white"
android:textStyle="bold"
tools:text="0.00006840 BTC" />
<include
layout="@layout/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentBottom="true" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:paddingTop="14dp">
<TextView
android:id="@+id/memo_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="@string/tx_memo"
android:textColor="@color/white40"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="@+id/memo_edit"
android:layout_width="184dp"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginBottom="14dp"
android:layout_marginEnd="16dp"
android:maxLines="2"
android:ellipsize="end"
android:text="@{tx.memo}"
android:textColor="@color/white"
android:textStyle="bold"
tools:text="memo" />
<include
layout="@layout/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentBottom="true" />
</RelativeLayout>
<include
android:id="@+id/qrcode_layout"
layout="@layout/qrcode"
android:layout_height="wrap_content"
android:layout_width="match_parent"/>
<LinearLayout
android:id="@+id/export"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/export_to_sdcard"
android:layout_width="match_parent"
style="@style/AcceptButton"
android:layout_marginTop="16dp"
android:layout_marginHorizontal="16dp"
android:text="@string/export_signed_txn_file"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="16dp"
android:paddingHorizontal="16dp"
android:textColor="@color/white"
android:text="@string/export_signed_txn_action_guide"
android:gravity="center"/>
</LinearLayout>
</com.donkingliang.consecutivescroller.ConsecutiveScrollerLayout>
</layout>

46
app/src/main/res/layout/electrum_txn.xml

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="callback"
type="com.cobo.cold.ui.fragment.main.electrum.Callback" />
<variable
name="file"
type="String" />
</data>
<RelativeLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="45dp"
android:paddingHorizontal="16dp"
android:background="?attr/selectableItemBackground"
android:onClick="@{()->callback.onClick(file)}">
<TextView
android:id="@+id/file_name"
android:layout_width="260dp"
android:layout_height="wrap_content"
tools:text="unsigned-1.txn"
android:layout_centerVertical="true"
android:textSize="15sp"
android:text="@{file}"
android:singleLine="true"
android:ellipsize="middle"
android:textColor="@color/white"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/arrow_right"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true"/>
<include
layout="@layout/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentBottom="true" />
</RelativeLayout>
</layout>

117
app/src/main/res/layout/export_sdcard_modal.xml

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Cobo
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center">
<RelativeLayout
android:layout_width="288dp"
android:layout_height="wrap_content"
android:background="@drawable/modal_bg">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="12dp"
android:textColor="@color/black"
android:textSize="15sp"
android:textStyle="bold" />
<TextView
android:id="@+id/file_name_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/title"
android:layout_centerHorizontal="true"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="12dp"
android:gravity="center_horizontal"
android:textSize="13sp"
android:visibility="visible"
android:text="@string/file_name_label" />
<TextView
android:id="@+id/file_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/file_name_label"
android:layout_centerHorizontal="true"
android:layout_marginHorizontal="16dp"
android:gravity="center_horizontal"
android:textColor="@color/black"
android:textSize="13sp"
android:textStyle="bold"
android:visibility="visible"
tools:text="CV-P2PKH-pubkey.txt" />
<TextView
android:id="@+id/action_hint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/file_name"
android:layout_centerHorizontal="true"
android:layout_marginHorizontal="16dp"
android:gravity="center_horizontal"
android:textSize="13sp"
android:visibility="visible" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/action_hint"
android:orientation="horizontal">
<Button
android:id="@+id/cancel"
style="@style/AcceptButton"
android:layout_width="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="20dp"
android:layout_weight="1"
android:background="#EBF0F5"
android:text="@string/cancel"
android:textColor="#8F95AA"
android:textStyle="bold" />
<Button
android:id="@+id/confirm"
style="@style/AcceptButton"
android:layout_width="0dp"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="20dp"
android:layout_weight="1"
android:text="@string/export"
android:textStyle="bold" />
</LinearLayout>
</RelativeLayout>
</FrameLayout>
</layout>

60
app/src/main/res/layout/export_success.xml

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2020 Cobo
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
</data>
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@drawable/modal_bg"
android:paddingVertical="20dp">
<LinearLayout
android:id="@+id/success"
android:layout_width="224dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/circle_positive"
android:tint="@color/colorAccent"
tools:ignore="ContentDescription" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textSize="15sp"
android:textStyle="bold"
android:textColor="@color/black"
android:text="@string/export_success" />
</LinearLayout>
</FrameLayout>
</layout>

58
app/src/main/res/layout/qrcode.xml

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2020 Cobo
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
-->
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
</data>
<com.cobo.cold.ui.views.qrcode.QrCodeView
android:id="@+id/qrcode"
android:layout_width="260dp"
android:layout_height="260dp"
android:layout_marginTop="12dp"
android:layout_gravity="center"
android:orientation="vertical">
<FrameLayout
android:layout_width="260dp"
android:layout_height="260dp"
android:layout_gravity="center_horizontal"
android:background="@color/white"
android:keepScreenOn="true">
<ImageView
android:id="@+id/img"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:visibility="visible"
android:padding="20dp"
tools:ignore="ContentDescription"/>
<ProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="visible" />
</FrameLayout>
</com.cobo.cold.ui.views.qrcode.QrCodeView>
</layout>

2
app/src/main/res/layout/qrcode_modal.xml

@ -46,7 +46,7 @@
<include
android:id="@+id/qrcode_layout"
layout="@layout/dynamic_qrcode" />
layout="@layout/qrcode" />
</FrameLayout>

8
app/src/main/res/layout/receive_item.xml

@ -23,7 +23,7 @@
<variable
name="item"
type="com.cobo.cold.ui.fragment.main.TxConfirmFragment.ReceiveItem" />
type="com.cobo.cold.ui.fragment.main.TxConfirmFragment.TransactionItem" />
</data>
<RelativeLayout
@ -31,25 +31,23 @@
android:layout_height="wrap_content">
<TextView
android:id="@+id/receive_label"
android:id="@+id/label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="14dp"
android:text="@{@string/receive_address(item.id + 1)}"
android:textColor="@color/white40"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="@+id/to"
android:id="@+id/info"
android:layout_width="184dp"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginVertical="14dp"
android:layout_marginEnd="16dp"
android:textColor="@color/white"
android:text="@{item.amount+'\n'+item.address}"
android:textStyle="bold"
tools:text="0.05BTC\n19zhQ 2rKq Xm5r y1pj Co83 JbBK 5zUr bdKgc" />

18
app/src/main/res/layout/tx_detail.xml

@ -166,22 +166,30 @@
android:layout_alignParentEnd="true"
android:layout_marginTop="14dp"
android:layout_marginEnd="16dp"
android:text="@{tx.from}"
android:textColor="@color/white"
android:textStyle="bold"
tools:text="19zhQ 2rKq Xm5r y1pj Co83 JbBK 5zUr bdKgc" />
</RelativeLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/from_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/receive_item"
tools:itemCount="2"
android:visibility="gone" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/arrow_down"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/from"
android:layout_marginVertical="8dp"
android:layout_marginStart="120dp"
android:src="@drawable/tx_arrow_down"
tools:ignore="ContentDescription" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/to_info"
android:layout_width="match_parent"
@ -198,7 +206,7 @@
android:textStyle="bold" />
<TextView
android:id="@+id/to"
android:id="@+id/info"
android:layout_width="184dp"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"

1
app/src/main/res/layout/tx_list_item.xml

@ -46,7 +46,6 @@
android:layout_marginTop="14dp"
android:ellipsize="middle"
android:singleLine="true"
android:text="@{tx.from}"
android:textColor="@color/white"
android:textSize="12sp"
android:textStyle="bold"

95
app/src/main/res/layout/txn_list.xml

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Cobo
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:navigationIcon="@drawable/menu"
app:popupTheme="@style/AppTheme.PopupOverlay">
<TextView
android:id="@+id/toolbar_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="对TF卡内文件签名"
android:textColor="@android:color/white"
android:textSize="15sp" />
</androidx.appcompat.widget.Toolbar>
<include layout="@layout/divider" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/electrum_txn"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
<LinearLayout
android:id="@+id/empty_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="60dp"
android:gravity="center_horizontal"
android:orientation="vertical"
android:visibility="gone">
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
android:src="@drawable/sdcard_icon"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/empty_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold"/>
<TextView
android:id="@+id/empty_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white40"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="8dp"
android:gravity="center"/>
</LinearLayout>
</LinearLayout>
</layout>

37
app/src/main/res/menu/asset_hasmore.xml

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2020 Cobo
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:orderInCategory="100"
android:title="Search"
android:icon="@drawable/search"
android:iconTint="@color/white"
app:showAsAction="always" />
<item
android:id="@+id/action_more"
android:orderInCategory="100"
android:title="More"
android:icon="@drawable/more"
android:iconTint="@color/white"
app:showAsAction="always" />
</menu>

1
app/src/main/res/menu/drawer.xml

@ -21,6 +21,7 @@
<item android:id="@+id/drawer_wallet" android:title=""/>
<item android:id="@+id/drawer_manage" android:title=""/>
<item android:id="@+id/drawer_sync" android:title=""/>
<item android:id="@+id/drawer_sdcard" android:title=""/>
<item android:id="@+id/drawer_settings" android:title=""/>
<item android:id="@+id/drawer_about" android:title=""/>
<item android:id="@+id/drawer_id" android:title=""/>

109
app/src/main/res/navigation/nav_graph_main.xml

@ -46,6 +46,9 @@
<action
android:id="@+id/action_to_syncFragment"
app:destination="@id/syncFragment" />
<action
android:id="@+id/action_to_txnListFragment"
app:destination="@id/txnListFragment" />
<action
android:id="@+id/action_to_assetFragment"
@ -76,6 +79,17 @@
app:popExitAnim="@android:anim/slide_out_right"
app:popUpTo="@id/assetListFragment"
app:popUpToInclusive="false" />
<action
android:id="@+id/action_to_ElectrumTxConfirmFragment"
app:destination="@id/electrumTxConfirmFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right"
app:popUpTo="@id/assetListFragment"
app:popUpToInclusive="false" />
<action
android:id="@+id/action_QRCodeScan_to_result"
app:destination="@id/webAuthResultFragment" />
@ -198,6 +212,22 @@
android:name="com.cobo.cold.ui.fragment.SyncFragment"
tools:layout="@layout/sync_fragment"
android:label="SyncFragment" />
<fragment
android:id="@+id/txnListFragment"
android:name="com.cobo.cold.ui.fragment.main.electrum.ElectrumTxnListFragment"
tools:layout="@layout/txn_list"
android:label="ElectrumTxnListFragment" >
<action
android:id="@id/action_to_ElectrumTxConfirmFragment"
app:destination="@id/electrumTxConfirmFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right"
/>
</fragment>
<fragment
android:id="@+id/mnemonicInputFragment"
android:name="com.cobo.cold.ui.fragment.setup.MnemonicInputFragment"
@ -222,6 +252,38 @@
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
<action
android:id="@+id/action_to_electrumTxFragment"
app:destination="@id/electrumTxFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
<action android:id="@+id/action_to_electrum_guide"
app:destination="@id/electrumGuideFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
</fragment>
<fragment
android:id="@+id/electrumGuideFragment"
android:name="com.cobo.cold.ui.fragment.main.electrum.ElectrumGuideFragment"
tools:layout="@layout/electrum_export_guide"
android:label="ElectrumGuideFragment">
<action android:id="@+id/export_electrum_ypub"
app:destination="@id/electrumExportFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
</fragment>
<fragment
android:id="@+id/electrumExportFragment"
android:name="com.cobo.cold.ui.fragment.main.electrum.ElectrumExportFragment"
tools:layout="@layout/electrum_export"
android:label="ElectrumExportFragment">
</fragment>
<fragment
android:id="@+id/txConfirmFragment"
@ -246,6 +308,35 @@
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
</fragment>
<fragment
android:id="@+id/electrumTxConfirmFragment"
tools:layout="@layout/tx_confirm_fragment"
android:name="com.cobo.cold.ui.fragment.main.electrum.ElectrumTxConfirmFragment"
android:label="TxConfirmFragment">
<action
android:id="@+id/action_to_broadcastElectrumTxFragment"
app:destination="@id/broadcastElectrumTxFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right"/>
<action
android:id="@id/action_to_home"
app:destination="@id/assetListFragment"
app:popUpTo="@id/assetListFragment"
app:popUpToInclusive="false"
app:launchSingleTop="true" />
<action
android:id="@+id/action_to_verifyMnemonic"
app:destination="@id/verifyMnemonicFragment1"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
</fragment>
<fragment
android:id="@+id/broadcastTxFragment"
tools:layout="@layout/broadcast_tx_fragment"
@ -259,6 +350,19 @@
app:launchSingleTop="true" />
</fragment>
<fragment
android:id="@+id/broadcastElectrumTxFragment"
tools:layout="@layout/broadcast_electrum_tx_fragment"
android:name="com.cobo.cold.ui.fragment.main.electrum.ElectrumBroadcastTxFragment"
android:label="ElectrumBroadcastTxFragment">
<action
android:id="@+id/action_to_home"
app:destination="@id/assetListFragment"
app:popUpTo="@id/assetListFragment"
app:popUpToInclusive="false"
app:launchSingleTop="true" />
</fragment>
<fragment
android:id="@+id/receiveCoinFragment"
tools:layout="@layout/receive_fragment"
@ -363,6 +467,11 @@
tools:layout="@layout/tx"
android:name="com.cobo.cold.ui.fragment.main.TxFragment"
android:label="TxFragment" />
<fragment
android:id="@+id/electrumTxFragment"
tools:layout="@layout/electrum_tx"
android:name="com.cobo.cold.ui.fragment.main.electrum.ElectrumTxFragment"
android:label="ElectrumTxFragment" />
<fragment
android:id="@+id/setPatternUnlockFragment"
tools:layout="@layout/set_pattern_unlock"

35
app/src/main/res/values-zh-rCN/strings.xml

@ -264,4 +264,39 @@
<item>"芯片安全检测中…"</item>
<item>"写入安全芯片…"</item>
</string-array>
<string name="export_to_electrum">导出观察钱包到Electrum</string>
<string name="export_to_electrum_guide">1. 打开Electrum,设置钱包名称,点击下一步 \n2. 选择“标准钱包”后点击下一步 \n3. 选择“使用主密钥”后点击下一步,来到输入主公钥页面 \n4. 在输入主公钥页面点击照相机按钮,摄像头打开后扫描Cobo Vault主公钥生成的二维码 \n5. 扫描完成后点击下一步设置钱包密码 \n6. 密码设置完成后进入观察钱包</string>
<string name="export_xpub">导出钱包的主公钥进行适配</string>
<string name="export_to_electrum_guide_hint">导出观察钱包到Electrum指引</string>
<string name="send_address">发送地址 %d</string>
<string name="input">输入 %d</string>
<string name="output">输出 %d</string>
<string name="export_success">导出成功</string>
<string name="read_sdcard">读取TF卡</string>
<string name="no_sdcard">未检测到TF卡</string>
<string name="no_sdcard_hint">请确保TF卡成功插入到该设备中该设备仅支持FAT32格式,容量不能超过32GB</string>
<string name="no_unsigned_txn">未检测到待签文件</string>
<string name="no_unsigned_txn_hint">请检查待签文件是否已成功保存在TF卡中</string>
<string name="electrum_broadcast_guide">Electrum广播交易引导</string>
<string name="electrum_broadcast_action_guide">打开Electrum,点击【工具--加载交易--从二维码】扫描Cobo Vault的二维码\n\n 若扫码苦难,可将已签名交易导出文件,从文件加载交易后进行广播。</string>
<string name="electrum_decode_txn_fail">解析失败</string>
<string name="electrum_import_xpub_guide_title">Electrum扫描主公钥二维码引导</string>
<string name="electrum_import_xpub_action_guide">1. 打开Electrum,设置钱包名称,点击下一步。\n2. 选择“标准钱包”后点击下一步。\n3. 选择“使用主密钥”后点击下一步,来到输入主公钥页面。\n4. 在输入主公钥页面点击相机按钮,摄像头开启后,扫描主公钥二维码。\n\n若扫码困难,可将主密钥导出文本文件,Electrum打开该文件也可导入</string>
<string name="error_txn_file">错误的txn文件</string>
<string name="export_signed_txn">导出文件进行广播</string>
<string name="electrum_import_signed_txn">导出后,electrum--工具--加载交易--从文件,打开该文件</string>
<string name="insert_sdcard_hint">请先插入TF卡(仅支持FAT32格式,容量不能超过32G)</string>
<string name="electrum_import_xpub_action">在Electrum 从一个主密钥创建密钥库 页面打开该文件</string>
<string name="transaction_source_label">热端来源</string>
<string name="use_electrum_to_broadcast">请使用Electrum扫描二维码进行广播</string>
<string name="transaction_from_electrum">该笔交易热端来源于Electrum</string>
<string name="add_address">添加地址</string>
<string name="use_electrum_scan_xpub">请用Electrum扫描主公钥二维码</string>
<string name="export_signed_txn_file">导出已签名文件</string>
<string name="export_signed_txn_action_guide">导出后,用Electrum进行广播\n前往【Electrum--工具--加载交易--从文件】,打开该文件后点击广播</string>
<string name="export">导出</string>
<string name="export_xpub_text_file">导出主密钥文本文件</string>
<string name="file_name_label">该文件的文件名为:</string>
<string name="master_xpub">主公钥(P2SH)</string>
<string name="electrum_qrcode_hint"><![CDATA[扫码困难?点击二维码进行放大<br>或<u>\"点击此处导出文件\"</u>]]></string>
</resources>

35
app/src/main/res/values/strings.xml

@ -287,4 +287,39 @@
<item>"Secure Element verification in progress…"</item>
<item>"Writing Secure Element…"</item>
</string-array>
<string name="export_to_electrum">导出观察钱包到Electrum</string>
<string name="export_to_electrum_guide">1. 打开Electrum,设置钱包名称,点击下一步 \n2. 选择“标准钱包”后点击下一步 \n3. 选择“使用主密钥”后点击下一步,来到输入主公钥页面 \n4. 在输入主公钥页面点击照相机按钮,摄像头打开后扫描Cobo Vault主公钥生成的二维码 \n5. 扫描完成后点击下一步设置钱包密码 \n6. 密码设置完成后进入观察钱包</string>
<string name="export_xpub">导出钱包的主公钥进行适配</string>
<string name="export_to_electrum_guide_hint">导出观察钱包到Electrum指引</string>
<string name="send_address">From %d</string>
<string name="input">input %d</string>
<string name="output">output %d</string>
<string name="export_success">导出成功</string>
<string name="read_sdcard">读取TF卡</string>
<string name="no_sdcard">未检测到TF卡</string>
<string name="no_sdcard_hint">请确保TF卡成功插入到该设备中该设备仅支持FAT32格式,容量不能超过32GB</string>
<string name="no_unsigned_txn">未检测到待签文件</string>
<string name="no_unsigned_txn_hint">请检查待签文件是否已成功保存在TF卡中</string>
<string name="electrum_broadcast_guide">Electrum广播交易引导</string>
<string name="electrum_broadcast_action_guide">打开Electrum,点击【工具--加载交易--从二维码】扫描Cobo Vault的二维码\n\n 若扫码苦难,可将已签名交易导出文件,从文件加载交易后进行广播。</string>
<string name="electrum_decode_txn_fail">解析失败</string>
<string name="electrum_import_xpub_guide_title">Electrum扫描主公钥二维码引导</string>
<string name="electrum_import_xpub_action_guide">1. 打开Electrum,设置钱包名称,点击下一步。\n2. 选择“标准钱包”后点击下一步。\n3. 选择“使用主密钥”后点击下一步,来到输入主公钥页面。\n4. 在输入主公钥页面点击相机按钮,摄像头开启后,扫描主公钥二维码。\n\n若扫码困难,可将主密钥导出文本文件,Electrum打开该文件也可导入</string>
<string name="error_txn_file">错误的txn文件</string>
<string name="export_signed_txn">导出文件进行广播</string>
<string name="electrum_import_signed_txn">导出后,electrum--工具--加载交易--从文件,打开该文件</string>
<string name="insert_sdcard_hint">请先插入TF卡(仅支持FAT32格式,容量不能超过32G)</string>
<string name="electrum_import_xpub_action">在Electrum 从一个主密钥创建密钥库 页面打开该文件</string>
<string name="transaction_source_label">热端来源</string>
<string name="use_electrum_to_broadcast">请使用Electrum扫描二维码进行广播</string>
<string name="transaction_from_electrum">该笔交易热端来源于Electrum</string>
<string name="add_address">添加地址</string>
<string name="use_electrum_scan_xpub">请用Electrum扫描主公钥二维码</string>
<string name="export_signed_txn_file">导出已签名文件</string>
<string name="export_signed_txn_action_guide">导出后,用Electrum进行广播\n前往【Electrum--工具--加载交易--从文件】,打开该文件后点击广播</string>
<string name="export">导出</string>
<string name="export_xpub_text_file">导出主密钥文本文件</string>
<string name="file_name_label">该文件的文件名为:</string>
<string name="master_xpub">主公钥(P2SH)</string>
<string name="electrum_qrcode_hint"><![CDATA[扫码困难?点击二维码进行放大<br>或 <u>\"点击此处导出文件\"</u>]]></string>
</resources>

14
coinlib/src/main/java/com/cobo/coinlib/Util.java

@ -24,7 +24,9 @@ import com.cobo.coinlib.path.AddressIndex;
import com.cobo.coinlib.path.Change;
import com.cobo.coinlib.path.CoinPath;
import org.bitcoinj.core.Base58;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.SignatureDecodeException;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.crypto.HDKeyDerivation;
@ -42,6 +44,8 @@ import java.nio.charset.StandardCharsets;
import java.security.spec.ECPoint;
import java.util.Arrays;
import static com.cobo.coinlib.coins.BTC.Electrum.TxUtils.int2bytes;
public class Util {
public static String pubKeyFromExtentPubKey(String extendPubKey) {
@ -190,4 +194,14 @@ public class Util {
Properties.removeThreadOverride("org.bouncycastle.asn1.allow_unsafe_integer");
}
}
public static String convertXpubToYpub(String xpub) {
byte[] bytes = Base58.decodeChecked(xpub);
byte[] result = new byte[bytes.length + 4];
System.arraycopy(int2bytes(0x049d7cb2), 0, bytes, 0, 4);
byte[] checksum = Sha256Hash.hashTwice(bytes, 0, bytes.length);
System.arraycopy(bytes, 0, result, 0, bytes.length);
System.arraycopy(checksum, 0, result, bytes.length, 4);
return Base58.encode(result);
}
}

3
coinlib/src/main/java/com/cobo/coinlib/coins/AbsTx.java

@ -71,9 +71,6 @@ public abstract class AbsTx {
try {
AddressIndex address = CoinPath.parsePath(hdPath, allHardend);
Change change = address.getParent();
if (!change.isExternal()) {
throw new InvalidTransactionException("invalid hdPath,error change value");
}
Account account = change.getParent();
if (account.getValue() != 0) {

19
coinlib/src/main/java/com/cobo/coinlib/coins/BTC/Btc.java

@ -79,6 +79,25 @@ public class Btc extends AbsCoin {
return changeAddressInfo;
}
@Override
public JSONArray getInputs() {
try {
return metaData.getJSONArray("inputs");
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}
@Override
public JSONArray getOutputs() {
try {
return metaData.getJSONArray("outputs");
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}
@Override
protected JSONObject extractMetaData(JSONObject signTxObject, String coinCode)
throws JSONException {

10
coinlib/src/main/java/com/cobo/coinlib/coins/BTC/Electrum/ElectrumTx.java

@ -19,7 +19,9 @@ package com.cobo.coinlib.coins.BTC.Electrum;
import androidx.annotation.NonNull;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.Utils;
import org.bitcoinj.params.MainNetParams;
import org.bouncycastle.util.encoders.Hex;
import org.json.JSONArray;
import org.json.JSONException;
@ -198,4 +200,12 @@ public class ElectrumTx {
super(errorMessage);
}
}
public static boolean isFinal(@NonNull String hex) {
byte[] raw = Hex.decode(hex);
return new Transaction(MainNetParams.get(), raw)
.getInputs()
.stream()
.noneMatch(input -> input.getSequenceNumber() < 0xffffffffL - 1);
}
}

2
coinlib/src/main/java/com/cobo/coinlib/coins/BTC/Electrum/TxUtils.java

@ -36,7 +36,7 @@ public class TxUtils {
private static final NetworkParameters MAINNET = MainNetParams.get();
private static byte[] int2bytes(int i) {
public static byte[] int2bytes(int i) {
return new byte[]{
(byte) ((i >> 24) & 0xFF),
(byte) ((i >> 16) & 0xFF),

5
coinlib/src/main/java/com/cobo/coinlib/coins/BTC/UtxoTx.java

@ -17,9 +17,14 @@
package com.cobo.coinlib.coins.BTC;
import org.json.JSONArray;
public interface UtxoTx {
ChangeAddressInfo getChangeAddressInfo();
JSONArray getInputs();
JSONArray getOutputs();
class ChangeAddressInfo {
public final String address;
public final String hdPath;

5
coinlib/src/test/java/com/cobo/coinlib/UtilTest.java

@ -55,4 +55,9 @@ public class UtilTest {
bytes = Util.trimOrAddLeadingZeros(Hex.decode("0000000000000000000000000000000000000000000000000000000001"));
assertEquals("0000000000000000000000000000000000000000000000000000000000000001", Hex.toHexString(bytes));
}
@Test
public void testXpubToYpub() {
assertEquals("ypub6XsyMmCyC7o9aXNfXzxwFgz3XPub9HadNzaZraotUtYjRHkJR7YXvaPmdZvvxhrYh9ajWXBJaPNjPsEPo3M4uNG9LyrrPTaYuee44qgWJW3",
Util.convertXpubToYpub("xpub6D3i46Y43SFfjEBYheBK3btYMRm9Cfb8Tt4M5Bv16tArNBw5ATNyJWjdcMyLxoCdHWTvm3ak7j2BWacq5Lw478aYUeARoYm4dvaQgJBAGsb"));
}
}

11
coinlib/src/test/java/com/cobo/coinlib/coin/ElectrumTxTest.java

@ -7,6 +7,7 @@ import org.bouncycastle.util.encoders.Hex;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ElectrumTxTest {
@ -61,4 +62,14 @@ public class ElectrumTxTest {
System.out.println(tx);
}
@Test
public void testFinal() {
String hex = "02000000000101069494a45bd029cd27da7d9de90172702e2bfda40b4ad67bf3cc700c4212e4ac01000000171600141ea4ea0a12d7c2b07e9084d36abd03a2edd72798fdffffff02b80b00000000000017a91472ec889523c81e94fd9d65701417d76b371ae3a5874f0e00000000000017a9141c174319445d98f4da84b51da9c8feb686b4fb198702473044022008ab229e2878e339cfb5c4a8589d5f8072024c74413df0d113f26ca0d680660402202e541f490b1937c15cd45323cefbcba4d07eb7cab089fcdb44f795c92e2c6826012102a18c6e271a995b162348b4332b63f13bf031617192d6232e809210ad5d85c382e5940900";
assertFalse(ElectrumTx.isFinal(hex));
String hex1 = "020000000001025573b0158c7ce0b25bb3d0cff7344e369ca3dbcee2ce4d888840fff49ae17cd00100000017160014e9cf9131d9c02a3a02d246bb4297b5606c6cb2f9ffffffffe014dc2363486a66e337b802da70722a32065f7a9e4fd4daf0e067aa31f8e58900000000171600143007abdafe8f875c3d3b714428e7761494a71f6cffffffff02f78f01000000000017a914915892366a6cdf24afa6e1c480db2ad88c6337808798895b000000000017a914915892366a6cdf24afa6e1c480db2ad88c633780870247304402206b891a2c6b2a95bb7b7e25275544f3dd761269392dde98ab65c8f8187194ce0502203061881a7d0e92fd19ac68b77a1fee35d7b6b7b72e8fce369a35088c1b35cca7012103fbe02e16d35d3c9c6772c75ba5d0d1387573724082266ea667c53b9d00decd72024730440220236f1c70df027dd7c0c862c9b89b28cb3fca6a96d4c73f7f565b7cc4b0fdff6902202262c87aec91d1ad9e661d807d5f9a8aaacd8a634028a74013cc2141c8750d13012102f325a85902d264dbcb0cbe144e9b2463f8252bd0c51bc19666f4c82461e4baa200000000";
assertTrue(ElectrumTx.isFinal(hex1));
}
}

Loading…
Cancel
Save