package com.greenaddress.greenbits.ui;
import android.app.Dialog;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.Html;
import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog;
import com.blockstream.libwally.Wally;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.greenaddress.greenapi.ConfidentialAddress;
import com.greenaddress.greenapi.GAException;
import com.greenaddress.greenapi.GATx;
import com.greenaddress.greenapi.HDKey;
import com.greenaddress.greenapi.Network;
import com.greenaddress.greenapi.JSONMap;
import com.greenaddress.greenapi.Output;
import com.greenaddress.greenapi.PreparedTransaction;
import com.greenaddress.greenbits.GaService;
import com.greenaddress.greenbits.wallets.TrezorHWWallet;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.core.TransactionWitness;
import org.bitcoinj.core.Utils;
import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.script.Script;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class TransactionActivity extends GaActivity {
private static final String TAG = TransactionActivity.class.getSimpleName();
private static final String FEE_BLOCK_NUMBERS[] = {"1", "3", "6"};
// For debug regtest builds, always allow RBF (Useful for development/testing)
private static final boolean ALWAYS_ALLOW_RBF = BuildConfig.DEBUG &&
Network.NETWORK == RegTestParams.get();
private Menu mMenu;
private TextView mUnconfirmedText;
private TextView mUnconfirmedEstimatedBlocks;
private TextView mUnconfirmedRecommendation;
private Button mUnconfirmedIncreaseFee;
private View mMemoView;
private View mMemoTitle;
private TextView mMemoIcon;
private TextView mMemoText;
private TextView mMemoEditText;
private Button mMemoSaveButton;
private Dialog mSummary;
private Dialog mTwoFactor;
@Override
protected int getMainViewId() { return R.layout.activity_transaction; }
@Override
protected void onCreateWithService(final Bundle savedInstanceState) {
setResult(RESULT_OK);
mMemoView = UI.find(this, R.id.txMemoMargin);
mMemoTitle = UI.find(this, R.id.txMemoTitle);
mMemoIcon = UI.find(this, R.id.sendToNoteIcon);
mMemoText = UI.find(this, R.id.txMemoText);
mMemoEditText = UI.find(this, R.id.sendToNoteText);
mMemoSaveButton = UI.find(this, R.id.saveMemo);
mUnconfirmedText = UI.find(this, R.id.txUnconfirmedText);
mUnconfirmedEstimatedBlocks = UI.find(this, R.id.txUnconfirmedEstimatedBlocks);
mUnconfirmedRecommendation = UI.find(this, R.id.txUnconfirmedRecommendation);
mUnconfirmedIncreaseFee = UI.find(this, R.id.txUnconfirmedIncreaseFee);
final TextView doubleSpentByText = UI.find(this, R.id.txDoubleSpentByText);
final TextView doubleSpentByTitle = UI.find(this, R.id.txDoubleSpentByTitle);
final TextView recipientText = UI.find(this, R.id.txRecipientText);
final TextView recipientTitle = UI.find(this, R.id.txRecipientTitle);
// Hide outgoing-only widgets by default
UI.hide(mUnconfirmedRecommendation, mUnconfirmedEstimatedBlocks, mUnconfirmedIncreaseFee);
final TransactionItem txItem = (TransactionItem) getIntent().getSerializableExtra("TRANSACTION");
final TextView hashText = UI.find(this, R.id.txHashText);
openInBrowser(hashText, txItem.txHash.toString(), Network.BLOCKEXPLORER_TX, null);
final Coin feePerKb = txItem.getFeePerKilobyte();
final boolean isWatchOnly = mService.isWatchOnly();
if (GaService.IS_ELEMENTS) {
UI.hide(UI.find(this, R.id.txUnconfirmed));
} else if (txItem.type == TransactionItem.TYPE.OUT || txItem.type == TransactionItem.TYPE.REDEPOSIT || txItem.isSpent) {
if (txItem.getConfirmations() > 0)
UI.hide(UI.find(this, R.id.txUnconfirmed)); // Confirmed: hide warning
else if (txItem.type == TransactionItem.TYPE.OUT || txItem.type == TransactionItem.TYPE.REDEPOSIT)
showUnconfirmed(txItem, feePerKb);
} else {
// unspent incoming output
if (txItem.getConfirmations() > 0)
if (isWatchOnly || txItem.spvVerified)
UI.hide(UI.find(this, R.id.txUnconfirmed));
else {
final int blocksLeft = mService.getSPVBlocksRemaining();
final String message = getResources().getString(R.string.txUnverifiedTx);
if (blocksLeft != Integer.MAX_VALUE)
mUnconfirmedText.setText(String.format("%s %s", message, blocksLeft));
else
mUnconfirmedText.setText(String.format("%s %s", message,
"Not yet connected to SPV!"));
}
}
UI.setCoinText(this, R.id.txBitcoinUnit, R.id.txAmountText,
Coin.valueOf(txItem.amount));
final TextView feeUnit = UI.find(this, R.id.txFeeUnit);
final TextView feeInfoText = UI.find(this, R.id.txFeeInfoText);
feeInfoText.setText(UI.setCoinText(mService, feeUnit, null, Coin.valueOf(txItem.fee)) +
" / " + String.valueOf(txItem.size) + " / " +
UI.setCoinText(mService, feeUnit, null, feePerKb));
final TextView dateText = UI.find(this, R.id.txDateText);
dateText.setText(SimpleDateFormat.getInstance().format(txItem.date));
// FIXME: use a list instead of reusing a TextView to show all double spends to allow
// for a warning to be shown before the browser is open
// this is to prevent to accidentally leak to block explorers your addresses
if (txItem.doubleSpentBy != null || !txItem.replacedHashes.isEmpty()) {
CharSequence res = "";
if (txItem.doubleSpentBy != null) {
if (txItem.doubleSpentBy.equals("malleability") || txItem.doubleSpentBy.equals("update"))
res = txItem.doubleSpentBy;
else
res = Html.fromHtml("<a href=\"" + Network.BLOCKEXPLORER_TX + txItem.doubleSpentBy + "\">" + txItem.doubleSpentBy + "</a>");
if (!txItem.replacedHashes.isEmpty())
res = TextUtils.concat(res, "; ");
}
if (!txItem.replacedHashes.isEmpty()) {
res = TextUtils.concat(res, Html.fromHtml("replaces transactions:<br/>"));
for (int i = 0; i < txItem.replacedHashes.size(); ++i) {
if (i > 0)
res = TextUtils.concat(res, Html.fromHtml("<br/>"));
final String txHashHex = txItem.replacedHashes.get(i).toString();
final String link = "<a href=\"" + Network.BLOCKEXPLORER_TX + txHashHex + "\">" + txHashHex + "</a>";
res = TextUtils.concat(res, Html.fromHtml(link));
}
}
doubleSpentByText.setText(res);
} else
UI.hide(doubleSpentByText, doubleSpentByTitle,
UI.find(this, R.id.txDoubleSpentByMargin));
if (!TextUtils.isEmpty(txItem.counterparty))
recipientText.setText(txItem.counterparty);
else
UI.hide(recipientText, recipientTitle,
UI.find(this, R.id.txRecipientMargin));
final TextView receivedOnText = UI.find(this, R.id.txReceivedOnText);
if (!TextUtils.isEmpty(txItem.receivedOn))
openInBrowser(
receivedOnText, txItem.receivedOn, Network.BLOCKEXPLORER_ADDRESS,
txItem.receivedOnEp
);
else {
final View receivedOnTitle = UI.find(this, R.id.txReceivedOnTitle);
final View receivedOnMargin = UI.find(this, R.id.txReceivedOnMargin);
UI.hide(receivedOnText, receivedOnTitle, receivedOnMargin);
}
// Memo
if (!TextUtils.isEmpty(txItem.memo)) {
mMemoText.setText(txItem.memo);
UI.hideIf(isWatchOnly, mMemoIcon);
} else {
UI.hide(mMemoText, mMemoView);
UI.hideIf(isWatchOnly, mMemoTitle, mMemoIcon);
}
if (isWatchOnly)
return;
mMemoIcon.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
final boolean editInProgress = mMemoEditText.getVisibility() == View.VISIBLE;
mMemoEditText.setText(UI.getText(mMemoText));
UI.hideIf(editInProgress, mMemoEditText, mMemoSaveButton);
UI.showIf(editInProgress, mMemoText);
}
});
mMemoSaveButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
saveMemo(txItem.txHash);
}
});
}
private void showUnconfirmed(final TransactionItem txItem, final Coin feePerKb) {
UI.show(mUnconfirmedEstimatedBlocks);
final Map<String, Object> feeEstimates = mService.getFeeEstimates();
int currentEstimate = 25;
for (final String atBlock : FEE_BLOCK_NUMBERS)
if (!feePerKb.isLessThan(getFeeEstimate(feeEstimates, atBlock))) {
currentEstimate = (Integer) ((Map) feeEstimates.get(atBlock)).get("blocks");
break;
}
mUnconfirmedEstimatedBlocks.setText(String.format(getResources().getString(R.string.willConfirmAfter), currentEstimate));
if (mService.isWatchOnly() || GaService.IS_ELEMENTS || !txItem.replaceable)
return; // FIXME: Implement RBF for elements
final int bestEstimate = (Integer) ((Map) feeEstimates.get("1")).get("blocks");
if (ALWAYS_ALLOW_RBF || (bestEstimate < currentEstimate)) {
UI.show(mUnconfirmedRecommendation, mUnconfirmedIncreaseFee);
if (bestEstimate == 1)
mUnconfirmedRecommendation.setText(R.string.recommendationSingleBlock);
else
mUnconfirmedRecommendation.setText(getString(R.string.recommendationBlocks, bestEstimate));
mUnconfirmedIncreaseFee.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
replaceByFee(txItem, getFeeEstimate(feeEstimates, "1"), null, 0);
}
});
}
}
@Override
public void onResumeWithService() {
if (mService.isForcedOff()) {
// FIXME: Should pass flag to activity so it shows it was forced logged out
startActivity(new Intent(this, FirstScreenActivity.class));
finish();
return;
}
setMenuItemVisible(mMenu, R.id.action_share, !mService.isLoggedIn());
}
@Override
public void onPauseWithService() {
if (mSummary != null)
mSummary.dismiss();
if (mTwoFactor != null)
mTwoFactor.dismiss();
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.menu_transaction, menu);
mMenu = menu;
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch(item.getItemId()) {
case R.id.action_share:
final TransactionItem txItem = (TransactionItem) getIntent().getSerializableExtra("TRANSACTION");
final Intent sendIntent = new Intent(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, Network.BLOCKEXPLORER_TX + txItem.txHash.toString());
sendIntent.setType("text/plain");
startActivity(sendIntent);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void onFinishedSavingMemo() {
runOnUiThread(new Runnable() {
@Override
public void run() {
mMemoText.setText(UI.getText(mMemoEditText));
UI.hide(mMemoEditText, mMemoSaveButton);
UI.hideIf(UI.getText(mMemoText).isEmpty(),
mMemoText, mMemoView);
hideKeyboardFrom(mMemoEditText);
}
});
}
private void saveMemo(final Sha256Hash txHash) {
final String newMemo = UI.getText(mMemoEditText);
if (newMemo.equals(UI.getText(mMemoText))) {
onFinishedSavingMemo();
return;
}
CB.after(mService.changeMemo(txHash, newMemo),
new CB.Toast<Boolean>(this) {
@Override
public void onSuccess(final Boolean result) {
onFinishedSavingMemo();
}
});
}
private static Coin getFeeEstimate(final Map<String, Object> feeEstimates, final String atBlock) {
final double rate = Double.parseDouble(((Map)feeEstimates.get(atBlock)).get("feerate").toString());
return Coin.valueOf((long) (rate * 1000 * 1000 * 100));
}
private void openInBrowser(final TextView textView, final String identifier, final String url, final JSONMap confidentialData) {
if (confidentialData == null)
textView.setText(identifier);
else {
final int subaccount = confidentialData.getInt("subaccount", 0);
final int pointer = confidentialData.getInt("pubkey_pointer");
textView.setText(ConfidentialAddress.fromP2SHHash(
Network.NETWORK,
Wally.hash160(mService.createOutScript(subaccount, pointer)),
mService.getBlindingPubKey(subaccount, pointer)
).toString());
}
textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
if (url == null)
return;
String domain = url;
try {
domain = new URI(url).getHost();
} catch (final URISyntaxException e) {
e.printStackTrace();
}
final String stripped = domain.startsWith("www.") ? domain.substring(4) : domain;
UI.popup(TransactionActivity.this, R.string.warning, R.string.continueText, R.string.cancel)
.content(getString(R.string.view_block_explorer, stripped))
.onPositive(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(final MaterialDialog dlg, final DialogAction which) {
final String fullUrl = TextUtils.concat(url, identifier).toString();
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(fullUrl)));
}
}).build().show();
}
});
}
private void replaceByFee(final TransactionItem txItem, final Coin feerate, Integer txSize, final int level) {
if (level > 10)
throw new RuntimeException("Recursion limit exceeded");
final Transaction tx = GaService.buildTransaction(txItem.data);
Integer change_pointer = null;
final int subAccount = mService.getCurrentSubAccount();
// requiredFeeDelta assumes mintxfee = 1000, and inputs increasing
// by at most 4 bytes per input (signatures have variable lengths)
if (txSize == null)
txSize = tx.getMessageSize();
final long requiredFeeDelta = txSize + tx.getInputs().size() * 4;
final List<TransactionInput> oldInputs = new ArrayList<>(tx.getInputs());
tx.clearInputs();
final List<JSONMap> tx_eps = new ArrayList<>();
for (final JSONMap ep : txItem.eps) {
if (ep.getBool("is_credit"))
continue;
final TransactionInput oldInput = oldInputs.get(ep.getInt("pt_idx"));
final TransactionInput newInput = new TransactionInput(
Network.NETWORK,
null,
oldInput.getScriptBytes(),
oldInput.getOutpoint(),
ep.getCoin("value")
);
newInput.setSequenceNumber(0); // This ensures nlocktime is recognized
tx.addInput(newInput);
tx_eps.add(ep);
}
final Coin oldFee = tx.getFee();
final Coin newFeeWithRate = feerate.multiply(txSize).divide(1000);
final Coin feeDelta = Coin.valueOf(Math.max(
newFeeWithRate.subtract(oldFee).longValue(),
requiredFeeDelta
));
Coin remainingFeeDelta = feeDelta;
final List<TransactionOutput> origOuts = new ArrayList<>(tx.getOutputs());
tx.clearOutputs();
for (final JSONMap ep : txItem.eps) {
if (!ep.getBool("is_credit"))
continue;
if (!ep.getBool("is_relevant"))
// keep non-change/non-redeposit intact
tx.addOutput(origOuts.get(ep.getInt("pt_idx")));
else {
if (ep.getInt("subaccount", 0).equals(subAccount))
change_pointer = ep.getInt("pubkey_pointer");
// change/redeposit
final Coin value = ep.getCoin("value");
if (!value.isGreaterThan(remainingFeeDelta)) {
// smaller than remaining fee -- get rid of this output
remainingFeeDelta = remainingFeeDelta.subtract(value);
} else {
// larger than remaining fee -- subtract the remaining fee
final TransactionOutput out = origOuts.get(ep.getInt("pt_idx"));
out.setValue(out.getValue().subtract(remainingFeeDelta));
tx.addOutput(out);
remainingFeeDelta = Coin.ZERO;
}
}
}
if (!remainingFeeDelta.isGreaterThan(Coin.ZERO)) {
doReplaceByFee(txItem, feerate, tx, tx_eps, change_pointer, subAccount, oldFee, null, level);
return;
}
final Coin finalRemaining = remainingFeeDelta;
final boolean filterAsset = true; // TODO: Elements doesn't support RBF yet
CB.after(mService.getAllUnspentOutputs(1, subAccount, filterAsset),
new CB.Toast<List<JSONMap>>(TransactionActivity.this) {
@Override
public void onSuccess(final List<JSONMap> result) {
Coin remaining = finalRemaining;
final List<JSONMap> moreInputs = new ArrayList<>();
for (final JSONMap utxo : result) {
remaining = remaining.subtract(utxo.getCoin("value"));
moreInputs.add(utxo);
if (!remaining.isGreaterThan(Coin.ZERO))
break;
}
final int remainingCmp = remaining.compareTo(Coin.ZERO);
if (remainingCmp > 0) {
// Not enough funds
TransactionActivity.this.toast(R.string.insufficientFundsText);
return;
}
if (remainingCmp == 0) {
// Funds available exactly match the required value
doReplaceByFee(txItem, feerate, tx, tx_eps, null, subAccount,
oldFee, moreInputs, level);
return;
}
// Funds left over - add a new change output
final Coin changeValue = remaining.multiply(-1);
final JSONMap addr = mService.getNewAddress(subAccount);
if (addr == null) {
TransactionActivity.this.toast(R.string.unable_to_create_change);
return;
}
final byte[] script = addr.getBytes("script");
tx.addOutput(changeValue,
Address.fromP2SHHash(Network.NETWORK, Utils.sha256hash160(script)));
doReplaceByFee(txItem, feerate, tx, tx_eps, addr.getInt("pointer"),
subAccount, oldFee, moreInputs, level);
}
});
}
private void doReplaceByFee(final TransactionItem txItem, final Coin feerate,
final Transaction tx, final List<JSONMap> tx_eps,
final Integer change_pointer, final int subAccount,
final Coin oldFee, final List<JSONMap> moreInputs,
final int level) {
final PreparedTransaction ptx;
ptx = new PreparedTransaction(change_pointer, subAccount, tx,
mService.findSubaccountByType(subAccount, "2of3"));
for (final JSONMap ep : txItem.eps) {
if (ep.getBool("is_credit"))
continue;
final Integer prevIndex = ep.get("pt_idx");
final TransactionInput oldInput = tx.getInput(prevIndex);
final int scriptType = ep.getInt("script_type");
ptx.mPrevOutputs.add(new Output(
(Integer) ep.get("subaccount"),
(Integer) ep.get("pubkey_pointer"),
HDKey.BRANCH_REGULAR,
GATx.getOutScriptType(scriptType),
Wally.hex_from_bytes(GATx.createOutScript(mService, ep)),
oldInput.getValue().longValue()
));
}
if (moreInputs != null) {
for (final JSONMap ep : moreInputs) {
final int scriptType = ep.getInt("script_type");
final byte[] outscript = GATx.createOutScript(mService, ep);
ptx.mPrevOutputs.add(new Output(
ep.getInt("subaccount"),
ep.getInt("pointer"),
1,
GATx.getOutScriptType(scriptType),
Wally.hex_from_bytes(outscript),
ep.getLong("value")
));
GATx.addInput(mService, tx, ep);
tx_eps.add(ep);
}
}
// verify if the increased fee is enough to achieve wanted feerate
// (can be too small in case of added inputs)
final int estimatedSize = tx.getMessageSize() + tx.getInputs().size() * 4;
if (feerate.multiply(estimatedSize).divide(1000).isGreaterThan(tx.getFee())) {
replaceByFee(txItem, feerate, estimatedSize, level + 1);
return;
}
// also verify if it's enough for 'bandwidth fee increment' condition
// of RBF
final Coin bandwidthAdjustedFee = Coin.valueOf(tx.getMessageSize() + tx.getInputs().size() * 4);
if (tx.getFee().subtract(oldFee).isLessThan(bandwidthAdjustedFee)) {
replaceByFee(txItem, feerate, estimatedSize, level + 1);
return;
}
final List<TransactionInput> inputs = tx.getInputs();
final List<ListenableFuture<Void>> outpointToRaw = new ArrayList<>(inputs.size() + 1);
if (!(mService.getSigningWallet() instanceof TrezorHWWallet))
outpointToRaw.add(Futures.immediateFuture((Void) null));
else
for (final TransactionInput inp : inputs) {
final Sha256Hash hash = inp.getOutpoint().getHash();
outpointToRaw.add(Futures.transform(mService.getRawOutput(hash),
new Function<Transaction, Void>() {
@Override
public Void apply(final Transaction input) {
ptx.mPrevoutRawTxs.put(Wally.hex_from_bytes(hash.getReversedBytes()), input);
return null;
}
}));
}
final ListenableFuture<List<Void>> prevouts = Futures.allAsList(outpointToRaw);
final ListenableFuture<List<byte[]>> signed = Futures.transform(prevouts, new AsyncFunction<List<Void>, List<byte[]>>() {
@Override
public ListenableFuture<List<byte[]>> apply(final List<Void> input) throws Exception {
return mService.signTransaction(ptx);
}
});
final boolean isSegwitEnabled = mService.isSegwitEnabled();
CB.after(signed, new CB.Toast<List<byte[]>>(TransactionActivity.this) {
@Override
public void onSuccess(final List<byte[]> signatures) {
int i = 0;
for (final byte[] sig : signatures) {
final JSONMap ep = tx_eps.get(i);
final int scriptType = ep.getInt("script_type");
final byte[] outscript = GATx.createOutScript(mService, ep);
final List<byte[]> userSigs = ImmutableList.of(new byte[]{0}, sig);
final byte[] inscript = GATx.createInScript(userSigs, outscript, scriptType);
tx.getInput(i).setScriptSig(new Script(inscript));
if (isSegwitEnabled && scriptType == GATx.REDEEM_P2SH_P2WSH_FORTIFIED) {
final TransactionWitness witness = new TransactionWitness(1);
witness.setPush(0, sig);
tx.setWitness(i, witness);
}
i++;
}
final Map<String, Object> twoFacData = new HashMap<>();
twoFacData.put("try_under_limits_bump", tx.getFee().subtract(oldFee).longValue());
final ListenableFuture<Map<String,Object>> sendFuture;
sendFuture = mService.sendRawTransaction(tx, twoFacData, null, true);
Futures.addCallback(sendFuture, new FutureCallback<Map<String,Object>>() {
@Override
public void onSuccess(final Map result) {
// FIXME: Add notification with "Transaction sent"?
finishOnUiThread();
}
@Override
public void onFailure(final Throwable t) {
if (!(t instanceof GAException) || !t.getMessage().equals("http://greenaddressit.com/error#auth")) {
toast(t);
return;
}
// 2FA is required, prompt the user
runOnUiThread(new Runnable() {
@Override
public void run() {
final boolean skipChoice = false;
mTwoFactor = UI.popupTwoFactorChoice(TransactionActivity.this, mService, skipChoice,
new CB.Runnable1T<String>() {
@Override
public void run(final String method) {
showIncreaseSummary(method, oldFee, tx.getFee(), tx);
}
});
if (mTwoFactor != null)
mTwoFactor.show();
}
});
}
}, mService.getExecutor());
}
});
}
private void showIncreaseSummary(final String method, final Coin oldFee, final Coin newFee, final Transaction signedTx) {
Log.i(TAG, "showIncreaseSummary( params " + method + ' ' + oldFee + ' ' + newFee + ')');
final View v = getLayoutInflater().inflate(R.layout.dialog_new_transaction, null, false);
final TextView amountLabel = UI.find(v, R.id.newTxAmountLabel);
amountLabel.setText(R.string.newFeeText);
final TextView feeLabel = UI.find(v, R.id.newTxFeeLabel);
feeLabel.setText(R.string.oldFeeText);
UI.hide(UI.find(v, R.id.newTxRecipientLabel), UI.find(v, R.id.newTxRecipientText));
final TextView twoFAText = UI.find(v, R.id.newTx2FATypeText);
final EditText newTx2FACodeText = UI.find(v, R.id.newTx2FACodeText);
UI.setCoinText(mService, v, R.id.newTxAmountUnitText, R.id.newTxAmountText, newFee);
UI.setCoinText(mService, v, R.id.newTxFeeUnit, R.id.newTxFeeText, oldFee);
final Map<String, Object> twoFacData;
if (method == null) {
UI.hide(twoFAText, newTx2FACodeText);
twoFacData = null;
} else {
twoFAText.setText(String.format("2FA %s code", method));
twoFacData = new HashMap<>();
twoFacData.put("method", method);
twoFacData.put("bump_fee_amount", newFee.subtract(oldFee).longValue());
if (!method.equals("gauth")) {
final Map<String, Long> amount = new HashMap<>();
amount.put("amount", newFee.subtract(oldFee).longValue());
mService.requestTwoFacCode(method, "bump_fee", amount);
}
}
mSummary = UI.popup(this, R.string.feeIncreaseTitle, R.string.send, R.string.cancel)
.customView(v, true)
.onPositive(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(final MaterialDialog dialog, final DialogAction which) {
if (twoFacData != null)
twoFacData.put("code", UI.getText(newTx2FACodeText));
final ListenableFuture<Map<String,Object>> sendFuture;
sendFuture = mService.sendRawTransaction(signedTx, twoFacData, null, false);
Futures.addCallback(sendFuture, new CB.Toast<Map<String,Object>>(TransactionActivity.this) {
@Override
public void onSuccess(final Map result) {
// FIXME: Add notification with "Transaction sent"?
finishOnUiThread();
}
}, mService.getExecutor());
}
})
.onNegative(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(final MaterialDialog dialog, final DialogAction which) {
Log.i(TAG, "SHOWN ON CLOSE!");
}
})
.build();
UI.mapEnterToPositive(mSummary, R.id.newTx2FACodeText);
mSummary.show();
}
}