package com.greenaddress.greenbits.ui;
import android.app.Dialog;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.view.ViewPager;
import android.util.Log;
import android.util.Pair;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.Switch;
import android.widget.TextView;
import android.widget.Toast;
import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog;
import com.blockstream.libwally.Wally;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.greenaddress.greenapi.ConfidentialAddress;
import com.greenaddress.greenapi.CryptoHelper;
import com.greenaddress.greenapi.ElementsTransaction;
import com.greenaddress.greenapi.ElementsTransactionOutput;
import com.greenaddress.greenapi.GATx;
import com.greenaddress.greenapi.JSONMap;
import com.greenaddress.greenapi.Network;
import com.greenaddress.greenapi.Output;
import com.greenaddress.greenapi.PreparedTransaction;
import com.greenaddress.greenbits.GaService;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.params.MainNetParams;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.core.TransactionWitness;
import org.bitcoinj.script.Script;
import org.bitcoinj.uri.BitcoinURI;
import org.bitcoinj.uri.BitcoinURIParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import de.schildbach.wallet.ui.ScanActivity;
public class SendFragment extends SubaccountFragment {
private static final String TAG = SendFragment.class.getSimpleName();
private Dialog mSummary;
private Dialog mTwoFactor;
private EditText mAmountEdit;
private EditText mAmountFiatEdit;
private TextView mAmountBtcWithCommission;
private EditText mRecipientEdit;
private EditText mNoteText;
private CheckBox mInstantConfirmationCheckbox;
private TextView mNoteIcon;
private Button mSendButton;
private Switch mMaxButton;
private TextView mMaxLabel;
private TextView mScanIcon;
private Map<?, ?> mPayreqData;
private boolean mFromIntentURI;
private int mSubaccount;
private AmountFields mAmountFields;
private boolean mIsExchanger;
private Exchanger mExchanger;
private void processBitcoinURI(final BitcoinURI URI) {
processBitcoinURI(URI, null, null);
}
private void processBitcoinURI(final BitcoinURI URI, final String confidentialAddress, Coin amount) {
final GaService service = getGAService();
final GaActivity gaActivity = getGaActivity();
if (URI != null && URI.getPaymentRequestUrl() != null) {
final ProgressBar bip70Progress = UI.find(mView, R.id.sendBip70ProgressBar);
UI.show(bip70Progress);
mRecipientEdit.setEnabled(false);
mSendButton.setEnabled(false);
UI.hide(mNoteIcon);
Futures.addCallback(service.processBip70URL(URI.getPaymentRequestUrl()),
new CB.Toast<Map<?, ?>>(gaActivity) {
@Override
public void onSuccess(final Map<?, ?> result) {
mPayreqData = result;
final String name;
if (result.get("merchant_cn") != null)
name = (String) result.get("merchant_cn");
else
name = (String) result.get("request_url");
long amount = 0;
for (final Map<?, ?> out : (ArrayList<Map>) result.get("outputs"))
amount += ((Number) out.get("amount")).longValue();
final CharSequence amountStr;
if (amount > 0) {
amountStr = UI.setCoinText(service, null, null, Coin.valueOf(amount));
} else
amountStr = "";
gaActivity.runOnUiThread(new Runnable() {
public void run() {
mRecipientEdit.setText(name);
mSendButton.setEnabled(true);
if (!amountStr.toString().isEmpty()) {
mAmountEdit.setText(amountStr);
mAmountFields.convertBtcToFiat();
mAmountEdit.setEnabled(false);
mAmountFiatEdit.setEnabled(false);
UI.hide(mMaxButton, mMaxLabel);
}
UI.hide(bip70Progress);
}
});
}
@Override
public void onFailure(final Throwable t) {
super.onFailure(t);
gaActivity.runOnUiThread(new Runnable() {
public void run() {
UI.hide(bip70Progress);
mRecipientEdit.setEnabled(true);
mSendButton.setEnabled(true);
UI.show(mNoteIcon);
}
});
}
});
} else {
if (confidentialAddress != null) {
mRecipientEdit.setText(confidentialAddress);
} else {
mRecipientEdit.setText(URI.getAddress().toString());
amount = URI.getAmount();
}
if (amount == null)
return;
final Coin uriAmount = amount;
Futures.addCallback(service.getSubaccountBalance(mSubaccount), new CB.Op<Map<String, Object>>() {
@Override
public void onSuccess(final Map<String, Object> result) {
gaActivity.runOnUiThread(new Runnable() {
public void run() {
UI.setCoinText(service, null, mAmountEdit, uriAmount);
mAmountFields.convertBtcToFiat();
UI.disable(mAmountEdit, mAmountFiatEdit);
UI.hide(mMaxButton, mMaxLabel);
}
});
}
}, service.getExecutor());
}
}
@Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
Log.d(TAG, "onCreateView -> " + TAG);
if (isZombieNoView())
return null;
final GaService service = getGAService();
final GaActivity gaActivity = getGaActivity();
if (savedInstanceState != null)
mIsExchanger = savedInstanceState.getBoolean("isExchanger", false);
final int viewId = mIsExchanger ? R.layout.fragment_exchanger_sell : R.layout.fragment_send;
mView = inflater.inflate(viewId, container, false);
if (mIsExchanger)
mExchanger = new Exchanger(getContext(), service, mView, false, null);
else
mExchanger = null;
mAmountFields = new AmountFields(service, getContext(), mView, mExchanger);
if (savedInstanceState != null) {
final Boolean pausing = savedInstanceState.getBoolean("pausing", false);
mAmountFields.setIsPausing(pausing);
}
mSubaccount = service.getCurrentSubAccount();
mSendButton = UI.find(mView, R.id.sendSendButton);
mMaxButton = UI.find(mView, R.id.sendMaxButton);
mMaxLabel = UI.find(mView, R.id.sendMaxLabel);
mNoteText = UI.find(mView, R.id.sendToNoteText);
mNoteIcon = UI.find(mView, R.id.sendToNoteIcon);
mInstantConfirmationCheckbox = UI.find(mView, R.id.instantConfirmationCheckBox);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
// pre-Material Design the label was already a part of the switch
UI.hide(mMaxLabel);
}
mAmountEdit = UI.find(mView, R.id.sendAmountEditText);
mAmountFiatEdit = UI.find(mView, R.id.sendAmountFiatEditText);
if (mIsExchanger)
mAmountBtcWithCommission = UI.find(mView, R.id.amountBtcWithCommission);
mRecipientEdit = UI.find(mView, R.id.sendToEditText);
mScanIcon = UI.find(mView, R.id.sendScanIcon);
if (mIsExchanger && GaService.IS_ELEMENTS) {
mRecipientEdit.setHint(R.string.send_to_address);
UI.hide(mAmountFiatEdit);
}
final TextView bitcoinUnitText = UI.find(mView, R.id.sendBitcoinUnitText);
UI.setCoinText(service, bitcoinUnitText, null, null);
if (container.getTag(R.id.tag_amount) != null)
mAmountEdit.setText((String) container.getTag(R.id.tag_amount));
if (container.getTag(R.id.tag_bitcoin_uri) != null) {
final Uri uri = (Uri) container.getTag(R.id.tag_bitcoin_uri);
BitcoinURI bitcoinUri = null;
if (GaService.IS_ELEMENTS) {
String addr = null;
Coin amount = null;
try {
final Pair<String, Coin> res = ConfidentialAddress.parseBitcoinURI(Network.NETWORK, uri.toString());
addr = res.first;
amount = res.second;
} catch (final BitcoinURIParseException e) {
gaActivity.toast(R.string.err_send_invalid_bitcoin_uri);
}
if (addr != null)
processBitcoinURI(null, addr, amount);
} else {
try {
bitcoinUri = new BitcoinURI(uri.toString());
} catch (final BitcoinURIParseException e) {
gaActivity.toast(R.string.err_send_invalid_bitcoin_uri);
}
if (bitcoinUri != null)
processBitcoinURI(bitcoinUri);
}
// set intent uri flag only if the call arrives from non internal qr scan
if (container.getTag(R.id.internal_qr) == null) {
mFromIntentURI = true;
container.setTag(R.id.internal_qr, null);
}
container.setTag(R.id.tag_bitcoin_uri, null);
}
mSendButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
// FIXME: Instead of checking the state here, enable/disable sendButton when state changes
if (!service.isLoggedIn()) {
gaActivity.toast(R.string.err_send_not_connected_will_resume);
return;
}
final String recipient = UI.getText(mRecipientEdit);
if (recipient.isEmpty()) {
gaActivity.toast(R.string.err_send_need_recipient);
return;
}
onSendButtonClicked(recipient);
}
});
if (GaService.IS_ELEMENTS) {
UI.disable(mMaxButton); // FIXME: Sweeping not available in elements
UI.hide(mMaxButton, mMaxLabel, mInstantConfirmationCheckbox);
} else {
mMaxButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(final CompoundButton v, final boolean isChecked) {
UI.disableIf(isChecked, mAmountEdit, mAmountFiatEdit);
mAmountEdit.setText(isChecked ? R.string.send_max_amount : R.string.empty);
}
});
}
mScanIcon.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
//New Marshmallow permissions paradigm
final String[] perms = {"android.permission.CAMERA"};
if (Build.VERSION.SDK_INT>Build.VERSION_CODES.LOLLIPOP_MR1 &&
gaActivity.checkSelfPermission(perms[0]) != PackageManager.PERMISSION_GRANTED) {
final int permsRequestCode = 100;
gaActivity.requestPermissions(perms, permsRequestCode);
} else {
final Intent qrcodeScanner = new Intent(gaActivity, ScanActivity.class);
qrcodeScanner.putExtra("sendAmount", mAmountEdit.getText().toString());
int requestCode = TabbedMainActivity.REQUEST_SEND_QR_SCAN;
if (mIsExchanger)
requestCode = TabbedMainActivity.REQUEST_SEND_QR_SCAN_EXCHANGER;
gaActivity.startActivityForResult(qrcodeScanner, requestCode);
}
}
}
);
mNoteIcon.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
if (mNoteText.getVisibility() == View.VISIBLE) {
mNoteIcon.setText(R.string.fa_pencil);
UI.clear(mNoteText);
UI.hide(mNoteText);
} else {
mNoteIcon.setText(R.string.fa_remove);
UI.show(mNoteText);
mNoteText.requestFocus();
}
}
});
hideInstantIf2of3();
makeBalanceObserver(mSubaccount);
if (service.getCoinBalance(mSubaccount) != null)
onBalanceUpdated();
registerReceiver();
return mView;
}
@Override
public void onViewStateRestored(final Bundle savedInstanceState) {
Log.d(TAG, "onViewStateRestored -> " + TAG);
super.onViewStateRestored(savedInstanceState);
if (mAmountFields != null)
mAmountFields.setIsPausing(false);
if (mIsExchanger)
mExchanger.conversionFinish();
}
private void hideInstantIf2of3() {
if (getGAService().findSubaccountByType(mSubaccount, "2of3") == null) {
if (!GaService.IS_ELEMENTS)
UI.show(mInstantConfirmationCheckbox);
return;
}
UI.hide(mInstantConfirmationCheckbox);
mInstantConfirmationCheckbox.setChecked(false);
}
@Override
protected void onBalanceUpdated() {
final GaService service = getGAService();
final TextView sendSubAccountBalanceUnit = UI.find(mView, R.id.sendSubAccountBalanceUnit);
final TextView sendSubAccountBalance = UI.find(mView, R.id.sendSubAccountBalance);
final Coin balance = service.getCoinBalance(mSubaccount);
UI.setCoinText(service, sendSubAccountBalanceUnit, sendSubAccountBalance, balance);
final int nChars = sendSubAccountBalance.getText().length() + sendSubAccountBalanceUnit.getText().length();
final int size = Math.min(50 - nChars, 34);
sendSubAccountBalance.setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
sendSubAccountBalanceUnit.setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
if (service.showBalanceInTitle())
UI.hide(sendSubAccountBalance, sendSubAccountBalanceUnit);
}
@Override
public void onResume() {
super.onResume();
Log.d(TAG, "onResume -> " + TAG);
if (mAmountFields != null)
mAmountFields.setIsPausing(false);
}
@Override
public void onPause() {
super.onPause();
Log.d(TAG, "onPause -> " + TAG);
if (mAmountFields != null)
mAmountFields.setIsPausing(true);
}
@Override
public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
if (mAmountFields != null)
outState.putBoolean("pausing", mAmountFields.isPausing());
outState.putBoolean("isExchanger", mIsExchanger);
}
public void onDestroyView() {
super.onDestroyView();
Log.d(TAG, "onDestroyView -> " + TAG);
if (mSummary != null)
mSummary.dismiss();
if (mTwoFactor != null)
mTwoFactor.dismiss();
}
@Override
protected void onSubaccountChanged(final int newSubAccount) {
mSubaccount = newSubAccount;
if (!IsPageSelected()) {
Log.d(TAG, "Subaccount changed while page hidden");
setIsDirty(true);
return;
}
updateBalance();
}
private void updateBalance() {
Log.d(TAG, "Updating balance");
if (isZombie())
return;
hideInstantIf2of3();
makeBalanceObserver(mSubaccount);
getGAService().updateBalance(mSubaccount);
}
public void setPageSelected(final boolean isSelected) {
final boolean needReload = isDirty();
super.setPageSelected(isSelected);
if (needReload && isSelected) {
Log.d(TAG, "Dirty, reloading");
updateBalance();
if (!isZombie())
setIsDirty(false);
}
}
private Coin getSendAmount() {
try {
final TextView amountEdit = mIsExchanger ? mAmountBtcWithCommission : mAmountEdit;
return UI.parseCoinValue(getGAService(), UI.getText(amountEdit));
} catch (final IllegalArgumentException e) {
return Coin.ZERO;
}
}
private void onSendButtonClicked(final String recipient) {
final GaService service = getGAService();
final GaActivity gaActivity = getGaActivity();
final JSONMap privateData = new JSONMap();
final String memo = UI.getText(mNoteText);
if (!memo.isEmpty())
privateData.mData.put("memo", memo);
if (mIsExchanger)
privateData.mData.put("memo", Exchanger.TAG_EXCHANGER_TX_MEMO);
if (mSubaccount != 0)
privateData.mData.put("subaccount", mSubaccount);
final boolean isInstant = mInstantConfirmationCheckbox.isChecked();
if (isInstant)
privateData.mData.put("instant", true);
final Coin amount = getSendAmount();
if (mPayreqData != null) {
final ListenableFuture<PreparedTransaction> ptxFn;
ptxFn = service.preparePayreq(amount, mPayreqData, privateData);
UI.disable(mSendButton);
CB.after(ptxFn, new CB.Toast<PreparedTransaction>(gaActivity, mSendButton) {
@Override
public void onSuccess(final PreparedTransaction ptx) {
onTransactionPrepared(ptx, recipient, amount, privateData);
}
});
} else {
final boolean sendAll = mMaxButton.isChecked();
final boolean validAddress = GaService.isValidAddress(recipient);
final boolean validAmount = sendAll || amount.isGreaterThan(Coin.ZERO);
int messageId = 0;
if (!validAddress && !validAmount)
messageId = R.string.invalidAmountAndAddress;
else if (!validAddress)
messageId = R.string.invalidAddress;
else if (!validAmount)
messageId = R.string.invalidAmount;
if (messageId != 0) {
gaActivity.toast(messageId);
return;
}
UI.disable(mSendButton);
final int numConfs;
if (isInstant)
numConfs = 6; // Instant requires at least 6 confs
else if (Network.NETWORK == MainNetParams.get())
numConfs = 1; // Require 1 conf before spending on mainnet
else
numConfs = 0; // Allow 0 conf for networks with no real-world value
// For 2of2 accounts we first try to spend older coins to avoid
// having to re-deposit them. If that fails (and always for 2of3
// accounts) we try to use the minimum number of utxos instead.
final boolean is2Of3 = service.findSubaccountByType(mSubaccount, "2of3") != null;
final boolean minimizeInputs = is2Of3;
final boolean filterAsset = true;
CB.after(service.getAllUnspentOutputs(numConfs, mSubaccount, filterAsset),
new CB.Toast<List<JSONMap>>(gaActivity, mSendButton) {
@Override
public void onSuccess(final List<JSONMap> utxos) {
int ret = R.string.insufficientFundsText;
if (!utxos.isEmpty()) {
GATx.sortUtxos(utxos, minimizeInputs);
ret = createRawTransaction(utxos, recipient, amount, privateData, sendAll);
if (ret == R.string.insufficientFundsText && !minimizeInputs) {
// Not enough money using nlocktime outputs first:
// Try again using the largest values first
GATx.sortUtxos(utxos, true);
ret = createRawTransaction(utxos, recipient, amount, privateData, sendAll);
}
}
if (ret != 0)
gaActivity.toast(ret, mSendButton);
}
});
}
}
private void onTransactionPrepared(final PreparedTransaction ptx,
final String recipient, final Coin amount,
final JSONMap privateData) {
final GaService service = getGAService();
final GaActivity gaActivity = getGaActivity();
final Coin verifyAmount = mMaxButton.isChecked() ? null : amount;
CB.after(service.validateTx(ptx, recipient, verifyAmount), new CB.Toast<Coin>(gaActivity, mSendButton) {
@Override
public void onSuccess(final Coin fee) {
final Map<?, ?> twoFacConfig = service.getTwoFactorConfig();
// can be non-UI because validation talks to USB if hw wallet is used
gaActivity.runOnUiThread(new Runnable() {
public void run() {
mSendButton.setEnabled(true);
final Coin sendAmount, sendFee;
if (mMaxButton.isChecked()) {
// 'fee' is actually the sent amount when passed amount=null
sendAmount = fee;
sendFee = service.getCoinBalance(mSubaccount).subtract(sendAmount);
} else {
sendAmount = amount;
sendFee = fee;
}
final boolean skipChoice = !ptx.mRequiresTwoFactor ||
twoFacConfig == null || !((Boolean) twoFacConfig.get("any"));
mTwoFactor = UI.popupTwoFactorChoice(gaActivity, service, skipChoice,
new CB.Runnable1T<String>() {
@Override
public void run(final String method) {
onTransactionValidated(ptx, null, recipient, sendAmount, method,
sendFee, privateData, null);
}
});
if (mTwoFactor != null)
mTwoFactor.show();
}
});
}
});
}
private void onTransactionValidated(final PreparedTransaction ptx,
final Transaction signedRawTx,
final String recipient, final Coin amount,
final String method, final Coin fee,
final JSONMap privateData,
final Map<String, Object> underLimits) {
Log.i(TAG, "onTransactionValidated( params " + method + ' ' + fee + ' ' + amount + ' ' + recipient + ')');
final GaService service = getGAService();
final GaActivity gaActivity = getGaActivity();
final View v = gaActivity.getLayoutInflater().inflate(R.layout.dialog_new_transaction, null, false);
UI.setCoinText(service, v, R.id.newTxAmountUnitText, R.id.newTxAmountText, amount);
UI.setCoinText(service, v, R.id.newTxFeeUnit, R.id.newTxFeeText, fee);
final TextView recipientText = UI.find(v, R.id.newTxRecipientText);
final TextView twoFAText = UI.find(v, R.id.newTx2FATypeText);
final EditText newTx2FACodeText = UI.find(v, R.id.newTx2FACodeText);
if (mPayreqData != null)
recipientText.setText(recipient);
else
recipientText.setText(String.format("%s\n%s\n%s",
recipient.substring(0, 12),
recipient.substring(12, 24),
recipient.substring(24)));
UI.showIf(method != null && !method.equals("limit"), twoFAText, newTx2FACodeText);
final Map<String, Object> twoFacData;
if (method == null)
twoFacData = null;
else if (method.equals("limit")) {
twoFacData = new HashMap<>();
twoFacData.put("try_under_limits_spend", underLimits);
} else {
twoFacData = new HashMap<>();
twoFacData.put("method", method);
twoFAText.setText(String.format("2FA %s code", method));
if (!method.equals("gauth")) {
if (underLimits != null)
for (final String key : underLimits.keySet())
twoFacData.put("send_raw_tx_" + key, underLimits.get(key));
if (GaService.IS_ELEMENTS) {
underLimits.remove("ephemeral_privkeys");
underLimits.remove("blinding_pubkeys");
}
service.requestTwoFacCode(method, ptx == null ? "send_raw_tx" : "send_tx", underLimits);
}
}
mSummary = UI.popup(gaActivity, R.string.newTxTitle, 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 && !method.equals("limit"))
twoFacData.put("code", UI.getText(newTx2FACodeText));
if (signedRawTx != null) {
final ListenableFuture<Map<String,Object>> sendFuture;
sendFuture = service.sendRawTransaction(signedRawTx, twoFacData, privateData, false);
Futures.addCallback(sendFuture, new CB.Toast<Map<String,Object>>(gaActivity, mSendButton) {
@Override
public void onSuccess(final Map result) {
if (GaService.IS_ELEMENTS && twoFacData != null && method.equals("limit")) {
// FIXME: Store limits for non-elements w/configurable m/u/bits units
service.cfg().edit().putString(
"twoFacLimits",
UI.formatCoinValue(
service, Coin.valueOf(((Number) result.get("new_limit")).longValue())
)
).apply();
}
onTransactionSent();
}
}, service.getExecutor());
} else {
final ListenableFuture<String> sendFuture = service.signAndSendTransaction(ptx, twoFacData);
Futures.addCallback(sendFuture, new CB.Toast<String>(gaActivity, mSendButton) {
@Override
public void onSuccess(final String result) {
onTransactionSent();
}
}, service.getExecutor());
}
}
}).build();
UI.mapEnterToPositive(mSummary, R.id.newTx2FACodeText);
mSummary.show();
}
private void onTransactionSent() {
final GaActivity gaActivity = getGaActivity();
gaActivity.runOnUiThread(new Runnable() {
public void run() {
UI.toast(gaActivity, R.string.transactionCompleted, Toast.LENGTH_LONG);
if (mIsExchanger) {
final float fiatAmount = Float.valueOf(mAmountFiatEdit.getText().toString());
mExchanger.sellBtc(fiatAmount);
}
if (mFromIntentURI) {
gaActivity.finish();
return;
}
UI.clear(mAmountEdit, mRecipientEdit);
UI.enable(mAmountEdit, mRecipientEdit);
if (!GaService.IS_ELEMENTS) {
mMaxButton.setChecked(false);
UI.show(mMaxButton, mMaxLabel);
}
mNoteIcon.setText(R.string.fa_pencil);
UI.clear(mNoteText);
UI.hide(mNoteText);
if (!mIsExchanger) {
final ViewPager viewPager = UI.find(gaActivity, R.id.container);
viewPager.setCurrentItem(1);
} else {
gaActivity.toast(R.string.transactionCompleted);
gaActivity.finish();
}
}
});
}
private static double extractRate(final Map feeEstimates, final Integer blockNum) {
final Map estimate = (Map) feeEstimates.get(Integer.toString(blockNum));
return Double.parseDouble(estimate.get("feerate").toString());
}
// FIXME: Duplicated in TransactionActivity.java
// Return the best estimate of the fee rate in satoshi/1000 bytes
private static Coin getFeeEstimate(final GaService service, final boolean isInstant) {
final Map<String, Object> feeEstimates = service.getFeeEstimates();
Double bestInstant = null;
// Iterate the estimates from shortest to longest confirmation time
final SortedSet<Integer> keys = new TreeSet<>();
for (final String block : feeEstimates.keySet())
keys.add(Integer.parseInt(block));
for (final Integer blockNum : keys) {
if (!isInstant && blockNum < 6)
continue; // Non-instant: Use 6 confirmation rate and later only
double rate = extractRate(feeEstimates, blockNum);
if (rate <= 0.0)
continue; // No estimate available: Try next confirmation rate
if (isInstant) {
// For instant, increase the rate to increase the likelyhood of confirmation.
// We use the lowest value of:
// a) 1.1 * the 1st or 2nd block fee rate
// b) 2.0 * the first rate later than 2 blocks
if (blockNum <= 2) {
if (bestInstant == null)
bestInstant = rate * 1.1; // Save earliest fast confirmation rate
continue; // Continue to find the first non-fast rate
} else
rate *= 2.0;
}
if (bestInstant != null && bestInstant < rate)
rate = bestInstant; // Use the lowest instant rate found
return Coin.valueOf((long) (rate * 1000 * 1000 * 100));
}
if (bestInstant != null) {
// No non-fast confirmation rate, return the fast confirmation rate
return Coin.valueOf((long) (bestInstant * 1000 * 1000 * 100));
}
// We don't have a usable fee rate estimate, use a default.
if (GaService.IS_ELEMENTS)
return Coin.valueOf(1);
if (Network.NETWORK == MainNetParams.get())
return Coin.valueOf((isInstant ? 200 : 120) * 1000);
return Coin.valueOf((isInstant ? 75 : 60) * 1000);
}
private Coin addUtxo(final Transaction tx,
final List<JSONMap> utxos, final List<JSONMap> used) {
return addUtxo(tx, utxos, used, null, null, null, null);
}
private Coin addUtxo(final Transaction tx,
final List<JSONMap> utxos, final List<JSONMap> used,
final List<Long> inValues, final List<byte[]> inAssetIds,
final List<byte[]> inAbfs, final List<byte[]> inVbfs) {
final JSONMap utxo = utxos.get(0);
final GaService service = getGAService();
utxos.remove(0);
if (utxo.getBool("confidential")) {
inAssetIds.add(utxo.getBytes("assetId"));
inAbfs.add(utxo.getBytes("abf"));
inVbfs.add(utxo.getBytes("vbf"));
}
used.add(utxo);
GATx.addInput(service, tx, utxo);
if (inValues != null)
inValues.add(utxo.getLong("value"));
return utxo.getCoin("value");
}
private int createRawTransaction(final List<JSONMap> utxos, final String recipient,
final Coin amount, final JSONMap privateData,
final boolean sendAll) {
if (GaService.IS_ELEMENTS)
return createRawElementsTransaction(utxos, recipient, amount, privateData, sendAll);
final GaActivity gaActivity = getGaActivity();
final GaService service = getGAService();
final List<JSONMap> used = new ArrayList<>();
final Coin feeRate = getFeeEstimate(service, privateData.getBool("instant"));
final Transaction tx = new Transaction(Network.NETWORK);
tx.addOutput(amount, Address.fromBase58(Network.NETWORK, recipient));
Coin total = Coin.ZERO;
Coin fee;
boolean randomizedChange = false;
Pair<TransactionOutput, Integer> changeOutput = null;
// First add inputs until we cover the amount to send
while ((sendAll || total.isLessThan(amount)) && !utxos.isEmpty())
total = total.add(addUtxo(tx, utxos, used));
// Then add inputs until we cover amount + fee/change
while (true) {
fee = GATx.getTxFee(service, tx, feeRate);
final Coin minChange = changeOutput == null ? Coin.ZERO : service.getDustThreshold();
final int cmp = sendAll ? 0 : total.compareTo(amount.add(fee).add(minChange));
if (cmp < 0) {
// Need more inputs to cover amount + fee/change
if (utxos.isEmpty())
return R.string.insufficientFundsText; // None left, fail
total = total.add(addUtxo(tx, utxos, used));
continue;
}
if (cmp == 0 || changeOutput != null) {
// Inputs exactly match amount + fee/change, or are greater
// and we have a change output for the excess
break;
}
// Inputs greater than amount + fee, add a change output and try again
changeOutput = GATx.addChangeOutput(service, tx, mSubaccount);
if (changeOutput == null)
return R.string.unable_to_create_change;
}
if (changeOutput != null) {
// Set the value of the change output
changeOutput.first.setValue(total.subtract(amount).subtract(fee));
randomizedChange = GATx.randomizeChange(tx);
}
final Coin actualAmount;
if (!sendAll)
actualAmount = amount;
else {
actualAmount = total.subtract(fee);
if (!actualAmount.isGreaterThan(Coin.ZERO))
return R.string.insufficientFundsText;
tx.getOutputs().get(0).setValue(actualAmount);
}
tx.setLockTime(service.getCurrentBlock()); // Prevent fee sniping
// Fetch previous outputs
final List<Output> prevOuts = GATx.createPrevouts(service, used);
final PreparedTransaction ptx = new PreparedTransaction(
changeOutput == null ? null : changeOutput.second,
mSubaccount, tx, service.findSubaccountByType(mSubaccount, "2of3")
);
ptx.mPrevoutRawTxs = new HashMap<>();
for (final Transaction prevTx : GATx.getPreviousTransactions(service, tx))
ptx.mPrevoutRawTxs.put(Wally.hex_from_bytes(prevTx.getHash().getBytes()), prevTx);
final boolean isSegwitEnabled = service.isSegwitEnabled();
// Sign the tx
final List<byte[]> signatures = service.signTransaction(tx, ptx, prevOuts);
for (int i = 0; i < signatures.size(); ++i) {
final byte[] sig = signatures.get(i);
// FIXME: Massive duplication with TransactionActivity
final JSONMap utxo = used.get(i);
final int scriptType = utxo.getInt("script_type");
final byte[] outscript = GATx.createOutScript(service, utxo);
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.P2SH_P2WSH_FORTIFIED_OUT) {
// Replace the witness data with just the user signature:
// the server will recreate the witness data to include the
// dummy OP_CHECKMULTISIG push, user + server sigs and script.
final TransactionWitness witness = new TransactionWitness(1);
witness.setPush(0, sig);
tx.setWitness(i, witness);
}
}
final Map<?, ?> twoFacConfig = service.getTwoFactorConfig();
final Coin sendFee = fee;
final Map underLimits = new HashMap();
underLimits.put("asset", "BTC");
underLimits.put("amount", amount.add(sendFee).getValue());
underLimits.put("fee", sendFee.getValue());
underLimits.put("change_idx", changeOutput == null ? -1 : (randomizedChange ? 0 : 1));
gaActivity.runOnUiThread(new Runnable() {
public void run() {
mSendButton.setEnabled(true);
final boolean skipChoice = /* FIXME: !ptx.mRequiresTwoFactor || */
twoFacConfig == null || !((Boolean) twoFacConfig.get("any"));
mTwoFactor = UI.popupTwoFactorChoice(gaActivity, service, skipChoice,
new CB.Runnable1T<String>() {
@Override
public void run(final String method) {
onTransactionValidated(null, tx, recipient, actualAmount,
method, sendFee, privateData, underLimits);
}
});
if (mTwoFactor != null)
mTwoFactor.show();
}
});
return 0;
}
private void arraycpy(final byte[] dest, final int i, final byte[] src) {
System.arraycopy(src, 0, dest, src.length * i, src.length);
}
private int createRawElementsTransaction(final List<JSONMap> utxos, final String recipient,
final Coin amount, final JSONMap privateData,
final boolean sendAll) {
// FIXME: sendAll
final GaService service = getGAService();
final GaActivity gaActivity = getGaActivity();
final List<JSONMap> used = new ArrayList<>();
final Coin feeRate = getFeeEstimate(service, privateData.getBool("instant"));
final ElementsTransaction tx = new ElementsTransaction(Network.NETWORK);
final ElementsTransactionOutput feeOutput = new ElementsTransactionOutput(Network.NETWORK, tx, Coin.ZERO);
feeOutput.setUnblindedAssetTagFromAssetId(service.mAssetId);
feeOutput.setValue(Coin.valueOf(1)); // updated below, necessary for serialization for fee calculation
tx.addOutput(feeOutput);
TransactionOutput changeOutput = null;
tx.addOutput(service.mAssetId, amount, ConfidentialAddress.fromBase58(Network.NETWORK, recipient));
Coin total = Coin.ZERO;
Coin fee;
final List<Long> inValues = new ArrayList<>();
final List<byte[]> inAssetIds = new ArrayList<>();
final List<byte[]> inAbfs = new ArrayList<>();
final List<byte[]> inVbfs = new ArrayList<>();
// First add inputs until we cover the amount to send
while (total.isLessThan(amount) && !utxos.isEmpty())
total = total.add(addUtxo(tx, utxos, used, inValues, inAssetIds, inAbfs, inVbfs));
// Then add inputs until we cover amount + fee/change
while (true) {
fee = GATx.getTxFee(service, tx, feeRate);
final Coin minChange = changeOutput == null ? Coin.ZERO : service.getDustThreshold();
final int cmp = total.compareTo(amount.add(fee).add(minChange));
if (cmp < 0) {
// Need more inputs to cover amount + fee/change
if (utxos.isEmpty())
return R.string.insufficientFundsText; // None left, fail
total = total.add(addUtxo(tx, utxos, used, inValues, inAssetIds, inAbfs, inVbfs));
continue;
}
if (cmp == 0 || changeOutput != null) {
// Inputs exactly match amount + fee/change, or are greater
// and we have a change output for the excess
break;
}
// Inputs greater than amount + fee, add a change output and try again
final JSONMap addr = service.getNewAddress(mSubaccount);
if (addr == null)
return R.string.unable_to_create_change;
final byte[] script = addr.getBytes("script");
changeOutput = tx.addOutput(
service.mAssetId, Coin.ZERO,
ConfidentialAddress.fromP2SHHash(
Network.NETWORK, Wally.hash160(script),
service.getBlindingPubKey(mSubaccount, addr.getInt("pointer"))
)
);
}
if (changeOutput != null) {
// Set the value of the change output
((ElementsTransactionOutput)changeOutput).setUnblindedValue(total.subtract(amount).subtract(fee).getValue());
// TODO: randomize change
// GATx.randomizeChange(tx);
}
feeOutput.setValue(fee);
// FIXME: tx.setLockTime(latestBlock); // Prevent fee sniping
// Fetch previous outputs
final List<Output> prevOuts = GATx.createPrevouts(service, used);
final Map<?, ?> twoFacConfig = service.getTwoFactorConfig();
final Coin sendFee = fee;
final int numInputs = tx.getInputs().size();
final int numOutputs = tx.getOutputs().size();
final int numInOuts = numInputs + numOutputs;
final long[] values = new long[numInOuts];
final byte[] abfs = new byte[32 * (numInOuts)];
final byte[] vbfs = new byte[32 * (numInOuts - 1)];
final byte[] assetids = new byte[32 * numInputs];
final byte[] ags = new byte[33 * numInputs];
for (int i = 0; i < numInputs; ++i) {
values[i] = inValues.get(i);
arraycpy(abfs, i, inAbfs.get(i));
arraycpy(vbfs, i, inVbfs.get(i));
arraycpy(assetids, i, inAssetIds.get(i));
arraycpy(ags, i, Wally.asset_generator_from_bytes(inAssetIds.get(i), inAbfs.get(i)));
}
for (int i = 0; i < numOutputs; ++i) {
final ElementsTransactionOutput output = (ElementsTransactionOutput) tx.getOutput(i);
// Fee: FIXME: Assumes fee is the first output
values[numInputs + i] = i == 0 ? output.getValue().getValue() : output.getUnblindedValue();
arraycpy(abfs, numInputs + i, output.getAbf());
if (i == numOutputs - 1) {
// Compute the final VBF
output.setAbfVbf(null, Wally.asset_final_vbf(values, numInputs, abfs, vbfs), service.mAssetId);
} else
arraycpy(vbfs, numInputs + i, output.getVbf());
}
final boolean isSegwitEnabled = service.isSegwitEnabled();
// fee output:
tx.addOutWitness(new byte[0], new byte[0], new byte[0]);
final ArrayList<String> ephemeralKeys = new ArrayList<>();
final ArrayList<String> blindingKeys = new ArrayList<>();
ephemeralKeys.add("00");
blindingKeys.add("00");
for (int i = 1; i < numOutputs; ++i) {
final ElementsTransactionOutput out = (ElementsTransactionOutput) tx.getOutput(i);
final byte[] ephemeral = CryptoHelper.randomBytes(32);
ephemeralKeys.add(Wally.hex_from_bytes(ephemeral));
blindingKeys.add(Wally.hex_from_bytes(out.getBlindingPubKey()));
final byte[] rangeproof = Wally.asset_rangeproof(
out.getUnblindedValue(), out.getBlindingPubKey(), ephemeral,
out.getAssetId(), out.getAbf(), out.getVbf(),
out.getCommitment(), out.getAssetTag()
);
final byte[] surjectionproof = Wally.asset_surjectionproof(
out.getAssetId(), out.getAbf(), out.getAssetTag(),
CryptoHelper.randomBytes(32),
assetids, Arrays.copyOf(abfs, 32 * numInputs), ags
);
final byte[] nonceCommitment = Wally.ec_public_key_from_private_key(ephemeral);
tx.addOutWitness(surjectionproof, rangeproof, nonceCommitment);
}
// FIXME: Implement HW Signing
/*
final PreparedTransaction ptx = new PreparedTransaction(
changeOutput == null ? null : changeOutput.second,
mSubaccount, tx, service.findSubaccountByType(mSubaccount, "2of3")
);
ptx.mPrevoutRawTxs = new HashMap<>();
for (final Transaction prevTx : GATx.getPreviousTransactions(service, tx))
ptx.mPrevoutRawTxs.put(Wally.hex_from_bytes(prevTx.getHash().getBytes()), prevTx);
*/
final PreparedTransaction ptx = null;
// Sign the tx
final List<byte[]> signatures = service.signTransaction(tx, ptx, prevOuts);
for (int i = 0; i < signatures.size(); ++i) {
final byte[] sig = signatures.get(i);
// FIXME: Massive duplication with TransactionActivity
final JSONMap utxo = used.get(i);
final int scriptType = utxo.getInt("script_type");
final byte[] outscript = GATx.createOutScript(service, utxo);
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.P2SH_P2WSH_FORTIFIED_OUT) {
final TransactionWitness witness = new TransactionWitness(1);
witness.setPush(0, sig);
tx.setWitness(i, witness);
}
}
final Map underLimits = new HashMap();
underLimits.put("asset_id", Wally.hex_from_bytes(service.mAssetId)); // FIXME: Others
underLimits.put("amount", amount.add(sendFee).getValue());
underLimits.put("fee", sendFee.getValue());
underLimits.put("change_idx", changeOutput == null ? -1 : 2);
underLimits.put("ephemeral_privkeys", ephemeralKeys);
underLimits.put("blinding_pubkeys", blindingKeys);
final Coin finalFee = fee;
gaActivity.runOnUiThread(new Runnable() {
public void run() {
mSendButton.setEnabled(true);
final String limit = service.cfg().getString("twoFacLimits", "0");
final boolean skipChoice = /* FIXME: !ptx.mRequiresTwoFactor || */
twoFacConfig == null || !((Boolean) twoFacConfig.get("any")) ||
amount.add(finalFee).getValue() < Float.valueOf(limit)*100;
mTwoFactor = UI.popupTwoFactorChoice(gaActivity, service, skipChoice,
new CB.Runnable1T<String>() {
@Override
public void run(String method) {
if (twoFacConfig != null && ((Boolean) twoFacConfig.get("any")) &&
amount.add(finalFee).getValue() < Float.valueOf(limit) * 100) {
method = "limit";
}
onTransactionValidated(null, tx, recipient, amount, method,
sendFee, privateData, underLimits);
}
});
if (mTwoFactor != null)
mTwoFactor.show();
}
});
return 0;
}
public void setIsExchanger(final boolean isExchanger) {
mIsExchanger = isExchanger;
}
}