package com.greenaddress.greenapi;
import android.util.Pair;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Bytes;
import com.greenaddress.greenbits.GaService;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOptions;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.core.TransactionOutPoint;
import org.bitcoinj.core.TransactionWitness;
import org.bitcoinj.core.Utils;
import org.bitcoinj.core.VarInt;
import org.bitcoinj.script.ScriptBuilder;
import com.blockstream.libwally.Wally;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import android.util.Log;
public class GATx {
private static final String TAG = GaService.class.getSimpleName();
private static final int SIG_LEN = 71; // Average signature length
private static final List<byte[]> EMPTY_SIGS = ImmutableList.of(new byte[SIG_LEN], new byte[SIG_LEN]);
private static final byte[] EMPTY_WITNESS_DATA = new byte[0];
private static final byte[] EMPTY_WITNESS_SIG = new byte[SIG_LEN + 1]; // 1=Sighash flag byte
// Script types in end points
public static final int P2SH_FORTIFIED_OUT = 10;
public static final int P2SH_P2WSH_FORTIFIED_OUT = 14;
public static final int REDEEM_P2SH_FORTIFIED = 150;
public static final int REDEEM_P2SH_P2WSH_FORTIFIED = 159;
public static final int MAX_BLOCK_NUM = 500000000 - 1; // From nTimeLock field definition
public static int getOutScriptType(final int scriptType) {
switch (scriptType) {
case REDEEM_P2SH_FORTIFIED:
return P2SH_FORTIFIED_OUT;
case REDEEM_P2SH_P2WSH_FORTIFIED:
return P2SH_P2WSH_FORTIFIED_OUT;
default:
return scriptType;
}
}
public static void sortUtxos(final List<JSONMap> utxos, final boolean minimizeInputs) {
Collections.sort(utxos, new Comparator<JSONMap>() {
@Override
public int compare(final JSONMap lhs, final JSONMap rhs) {
int cmp = 0;
if (!minimizeInputs) {
// When not minimizing inputs, prefer earlier block times;
// By spending earlier utxos we can avoid re-deposits.
cmp = lhs.getInt("block_height", MAX_BLOCK_NUM).compareTo(rhs.getInt("block_height", MAX_BLOCK_NUM));
}
if (cmp == 0)
cmp = lhs.getBigInteger("value").compareTo(rhs.getBigInteger("value"));
return cmp;
}
});
}
public static byte[] createOutScript(final GaService service, final JSONMap ep) {
return service.createOutScript(ep.getInt("subaccount", 0),
ep.getInt(ep.getKey("pubkey_pointer", "pointer")));
}
public static byte[] createInScript(final List<byte[]> sigs, final byte[] outScript,
final int scriptType) {
if (scriptType == P2SH_FORTIFIED_OUT || scriptType == REDEEM_P2SH_FORTIFIED)
// FIXME: investigate P2SH_ vs REDEEM_P2SH_ and ideally make it consistent here
return ScriptBuilder.createMultiSigInputScriptBytes(sigs, outScript).getProgram();
// REDEEM_P2SH_P2WSH_FORTIFIED: PUSH(OP_0 PUSH(sha256(outScript)))
return Bytes.concat(Wally.hex_to_bytes("220020"), Wally.sha256(outScript));
}
public static void addInput(final GaService service, final Transaction tx, final JSONMap ep) {
final int scriptType = ep.getInt("script_type");
final byte[] outscript = createOutScript(service, ep);
final byte[] inscript = createInScript(EMPTY_SIGS, outscript, scriptType);
final TransactionOutPoint op;
op = new TransactionOutPoint(Network.NETWORK, ep.getInt("pt_idx"), ep.getHash("txhash"));
final TransactionInput in = new TransactionInput(Network.NETWORK, null, inscript, op, ep.getCoin("value"));
TransactionWitness witness = null;
if (getOutScriptType(scriptType) == P2SH_P2WSH_FORTIFIED_OUT) {
// To calculate the tx weight correctly, we must set the witness data
// to the correct number of pushes of data of the correct size.
// Before sending the transaction, we replace this witness data with
// just the users signature, since the server recreates the witness
// data itself to replace the server sig and script placeholders.
witness = new TransactionWitness(4);
witness.setPush(0, EMPTY_WITNESS_DATA); // Dummy for off by 1 in OP_CHECKMULTISIG
witness.setPush(1, EMPTY_WITNESS_SIG); // Users signature
witness.setPush(2, EMPTY_WITNESS_SIG); // GA Server signature
witness.setPush(3, outscript); // Outscript
}
in.setSequenceNumber(0); // This ensures nlocktime is recognized
tx.addInput(in);
if (witness != null)
tx.setWitness(tx.getInputs().size() - 1, witness);
}
/* Add a new change output to a tx */
public static Pair<TransactionOutput, Integer> addChangeOutput(final GaService service, final Transaction tx,
final int subaccount) {
final JSONMap addr = service.getNewAddress(subaccount);
if (addr == null)
return null;
final byte[] script;
if (addr.getString("addr_type").equals("p2wsh")) {
script = ScriptBuilder.createP2WSHOutputScript(
Wally.sha256(addr.getBytes("script"))).getProgram();
} else {
script = addr.getBytes("script");
}
return new Pair<>(
tx.addOutput(Coin.ZERO, Address.fromP2SHHash(Network.NETWORK,
Utils.sha256hash160(script))),
addr.getInt("pointer")
);
}
/* Create previous outputs for tx construction from uxtos */
public static List<Output> createPrevouts(final GaService service, final List<JSONMap> utxos) {
final List<Output> prevOuts = new ArrayList<>();
for (final JSONMap utxo : utxos)
prevOuts.add(new Output(utxo.getInt("subaccount"),
utxo.getInt("pointer"),
HDKey.BRANCH_REGULAR,
getOutScriptType(utxo.getInt("script_type")),
Wally.hex_from_bytes(createOutScript(service, utxo)),
utxo.getLong("value")));
return prevOuts;
}
/* Return the previous transactions for each of a txs inputs */
public static List<Transaction> getPreviousTransactions(final GaService service, final Transaction tx) {
final List<Transaction> previousTxs = new ArrayList<>();
try {
for (final TransactionInput in : tx.getInputs()) {
final String txhex = service.getRawOutputHex(in.getOutpoint().getHash());
previousTxs.add(GaService.buildTransaction(txhex));
}
} catch (final Exception e) {
e.printStackTrace();
return null;
}
return previousTxs;
}
// Estimate the size of Elements specific parts of a tx
private static int estimateElementsSize(final Transaction tx) {
if (!GaService.IS_ELEMENTS)
return 0;
final int sjSize = Wally.asset_surjectionproof_size(tx.getInputs().size());
final int cmtSize = Wally.EC_PUBLIC_KEY_LEN;
// Estimate the rangeproof len as 160 bytes per 2 bits used to express the
// output value (currently 32), plus fixed overhead of 128 (this is a slight
// over-estimate given that only up to +100 has been seen in the wild).
// FIXME: This assumes 32 bit maximum amounts as per current wally impl.
final int rpSize = ((32 / 2) * 160) + 128;
final int singleOutputSize = sjSize + VarInt.sizeOf(sjSize) +
cmtSize + VarInt.sizeOf(cmtSize) +
rpSize + VarInt.sizeOf(rpSize);
return singleOutputSize * tx.getOutputs().size();
}
// Calculate the fee that must be paid for a tx
public static Coin getTxFee(final GaService service, final Transaction tx, final Coin feeRate) {
final Coin minRate = service.getMinFeeRate();
final Coin rate = feeRate.isLessThan(minRate) ? minRate : feeRate;
final int vSize;
Log.d(TAG, "getTxFee(rates): " + rate.value + '/' + feeRate.value + '/' + minRate.value);
if (!GaService.IS_ELEMENTS && !service.isSegwitEnabled()) {
vSize = tx.unsafeBitcoinSerialize().length;
Log.d(TAG, "getTxFee(non-sw): " + vSize);
} else {
/* For segwit, the fee is based on the weighted size of the tx */
tx.transactionOptions = TransactionOptions.NONE;
final int nonSwSize = tx.unsafeBitcoinSerialize().length;
tx.transactionOptions = TransactionOptions.ALL;
final int swSize = tx.unsafeBitcoinSerialize().length;
final int fullSize = swSize + estimateElementsSize(tx);
vSize = (int) Math.ceil((nonSwSize * 3 + fullSize) / 4.0);
Log.d(TAG, "getTxFee(sw): " + nonSwSize + '/' + swSize + '/' + vSize);
}
final double fee = (double) vSize * rate.value / 1000.0;
final long roundedFee = (long) Math.ceil(fee); // Round up
Log.d(TAG, "getTxFee: fee is " + roundedFee);
return Coin.valueOf(roundedFee);
}
// Swap the change and recipient output in a tx with 50% probability */
public static boolean randomizeChange(final Transaction tx) {
if (CryptoHelper.randomBytes(1)[0] < 0)
return false;
final TransactionOutput a = tx.getOutput(0);
final TransactionOutput b = tx.getOutput(1);
tx.clearOutputs();
tx.addOutput(b);
tx.addOutput(a);
return true;
}
}