package com.greenaddress.greenbits.wallets;
import android.util.Log;
import com.blockstream.libwally.Wally;
import com.btchip.BTChipDongle;
import com.btchip.BTChipException;
import com.btchip.BitcoinTransaction;
import com.btchip.comm.BTChipTransport;
import com.btchip.utils.BufferUtils;
import com.google.common.base.Joiner;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import com.greenaddress.greenapi.HDKey;
import com.greenaddress.greenapi.HWWallet;
import com.greenaddress.greenapi.ISigningWallet;
import com.greenaddress.greenapi.Output;
import com.greenaddress.greenapi.PreparedTransaction;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutPoint;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.core.UnsafeByteArrayOutputStream;
import org.bitcoinj.core.VarInt;
import org.bitcoinj.crypto.DeterministicKey;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
public class BTChipHWWallet extends HWWallet {
private static final ListeningExecutorService ES = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1));
private final BTChipDongle mDongle;
private final String mPin;
private DeterministicKey mCachedPubkey;
private final List<Integer> mAddrn;
private static final String TAG = BTChipHWWallet.class.getSimpleName();
private BTChipHWWallet(final BTChipDongle dongle, final String pin, final List<Integer> addrn) {
this.mDongle = dongle;
this.mPin = pin;
this.mAddrn = addrn;
}
public BTChipHWWallet(final BTChipDongle dongle) {
this(dongle, "0000", new LinkedList<Integer>());
}
public BTChipHWWallet(final BTChipTransport transport, final String pin) {
this.mDongle = new BTChipDongle(transport);
this.mPin = pin;
this.mAddrn = new LinkedList<>();
}
public BTChipHWWallet(final BTChipTransport transport) {
this(transport, null);
}
public BTChipHWWallet(final BTChipTransport transport, final String pin, final SettableFuture<Integer> remainingAttemptsFuture) {
this(transport, pin);
ES.submit(new Callable<Object>() {
@Override
public Object call() {
try {
mDongle.verifyPin(BTChipHWWallet.this.mPin.getBytes());
remainingAttemptsFuture.set(-1); // -1 means success
} catch (final BTChipException e) {
e.printStackTrace();
if (e.toString().contains("63c"))
remainingAttemptsFuture.set(
Integer.valueOf(String.valueOf(e.toString().charAt(e.toString().indexOf("63c") + 3))));
else if (e.toString().contains("6985"))
// mDongle is not set up
remainingAttemptsFuture.set(0);
else
remainingAttemptsFuture.setException(e);
} catch (final Exception e) {
e.printStackTrace();
}
return null;
}
});
}
private String outToPath(final Output out) {
final String BRANCH = Integer.toString(HDKey.BRANCH_REGULAR) + '/';
if (out.subAccount != 0)
return "3'/" + out.subAccount + "'/" + BRANCH + out.pointer;
return BRANCH + out.pointer;
}
private List<byte[]> signSegwitInputs(final PreparedTransaction ptx) throws BTChipException, IOException {
final Transaction decoded = ptx.mDecoded;
final List<TransactionInput> txInputs = decoded.getInputs();
final List<Output> prevOuts = ptx.mPrevOutputs;
final BTChipDongle.BTChipInput inputs[] = new BTChipDongle.BTChipInput[txInputs.size()];
final List<byte[]> sigs = new LinkedList<>();
for (int i = 0; i < txInputs.size(); ++i) {
final TransactionInput txInput = txInputs.get(i);
final TransactionOutPoint txOutpoint = txInput.getOutpoint();
final byte[] inputHash = txOutpoint.getHash().getReversedBytes();
final Output prevOut = prevOuts.get(i);
ByteArrayOutputStream inputBuf = new ByteArrayOutputStream();
inputBuf.write(inputHash, 0, inputHash.length);
long index = txOutpoint.getIndex();
BufferUtils.writeUint32LE(inputBuf, index);
BufferUtils.writeUint64LE(inputBuf, prevOut.value);
ByteArrayOutputStream sequenceBuf = new ByteArrayOutputStream();
BufferUtils.writeUint32LE(sequenceBuf, txInput.getSequenceNumber());
inputs[i] = mDongle.createInput(inputBuf.toByteArray(), sequenceBuf.toByteArray(), false, true);
}
// Prepare the pseudo transaction
BTChipDongle.BTChipInput singleInput[] = new BTChipDongle.BTChipInput[1];
final List<TransactionOutput> txOutputs = decoded.getOutputs();
Output output = ptx.mPrevOutputs.get(0);
// Provide the first script instead of a null script to initialize the P2SH confirmation logic
mDongle.startUntrustedTransction(true, 0, inputs, Wally.hex_to_bytes(output.script), true);
final int msgSize = decoded.getMessageSize();
final ByteArrayOutputStream stream = new UnsafeByteArrayOutputStream(msgSize < 32 ? 32 : msgSize + 32);
stream.write(new VarInt(txOutputs.size()).encode());
for (final TransactionOutput out : txOutputs)
out.bitcoinSerialize(stream);
mDongle.finalizeInputFull(stream.toByteArray());
// Sign each input
for (int i = 0; i < txInputs.size(); ++i) {
output = ptx.mPrevOutputs.get(i);
singleInput[0] = inputs[i];
final Output prevOut = prevOuts.get(i);
if (prevOut.scriptType != 14) // sign segwit only
continue;
mDongle.startUntrustedTransction(false, 0, singleInput, Wally.hex_to_bytes(output.script), true);
final ECKey.ECDSASignature sig;
sig = ECKey.ECDSASignature.decodeFromDER(mDongle.untrustedHashSign(outToPath(output),
"0", decoded.getLockTime(), (byte) 1 /* = SIGHASH_ALL */));
sigs.add(ISigningWallet.getTxSignature(sig));
}
return sigs;
}
private List<byte[]> signNonSegwitInputs(final PreparedTransaction ptx) throws BTChipException, IOException {
final Transaction decoded = ptx.mDecoded;
final List<TransactionInput> txInputs = decoded.getInputs();
final List<Output> prevOuts = ptx.mPrevOutputs;
final List<byte[]> sigs = new LinkedList<>();
final BTChipDongle.BTChipInput inputs[] = new BTChipDongle.BTChipInput[txInputs.size()];
if (!mDongle.understandsMultipleOutputs()) {
for (int i = 0; i < txInputs.size(); ++i) {
final TransactionInput txInput = txInputs.get(i);
final TransactionOutPoint txOutpoint = txInput.getOutpoint();
final byte[] inputHash = txOutpoint.getHash().getReversedBytes();
ByteArrayOutputStream inputBuf = new ByteArrayOutputStream();
inputBuf.write(inputHash, 0, inputHash.length);
long index = txOutpoint.getIndex();
BufferUtils.writeUint32LE(inputBuf, index);
ByteArrayOutputStream sequenceBuf = new ByteArrayOutputStream();
BufferUtils.writeUint32LE(sequenceBuf, txInput.getSequenceNumber());
inputs[i] = mDongle.createInput(inputBuf.toByteArray(), sequenceBuf.toByteArray(), false, false);
}
} else {
for (int i = 0; i < txInputs.size(); ++i) {
final TransactionInput txInput = txInputs.get(i);
final TransactionOutPoint txOutpoint = txInput.getOutpoint();
final long index = txOutpoint.getIndex();
final ByteArrayInputStream in = new ByteArrayInputStream(ptx.mPrevoutRawTxs.get(txOutpoint.getHash().toString()).unsafeBitcoinSerialize());
final BitcoinTransaction encodedTx = new BitcoinTransaction(in);
inputs[i] = mDongle.getTrustedInput(encodedTx, index, txInput.getSequenceNumber());
}
}
for (int i = 0; i < txInputs.size(); ++i) {
final List<TransactionOutput> txOutputs = decoded.getOutputs();
final Output output = ptx.mPrevOutputs.get(i);
mDongle.startUntrustedTransction(i == 0, i, inputs, Wally.hex_to_bytes(output.script), false);
final int msgSize = decoded.getMessageSize();
final ByteArrayOutputStream stream = new UnsafeByteArrayOutputStream(msgSize < 32 ? 32 : msgSize + 32);
stream.write(new VarInt(txOutputs.size()).encode());
for (final TransactionOutput out : txOutputs)
out.bitcoinSerialize(stream);
mDongle.finalizeInputFull(stream.toByteArray());
final Output prevOut = prevOuts.get(i);
if (prevOut.scriptType == 14) // don't sign segwit
continue;
final ECKey.ECDSASignature sig;
sig = ECKey.ECDSASignature.decodeFromDER(mDongle.untrustedHashSign(outToPath(output),
"0", decoded.getLockTime(), (byte) 1 /* = SIGHASH_ALL */));
sigs.add(ISigningWallet.getTxSignature(sig));
}
return sigs;
}
@Override
public List<byte[]> signTransaction(final PreparedTransaction ptx) {
final Transaction decoded = ptx.mDecoded;
final List<TransactionInput> txInputs = decoded.getInputs();
final List<Output> prevOuts = ptx.mPrevOutputs;
final List<byte[]> sigs = new LinkedList<>();
try {
boolean segwit = false, nonSegwit = false;
for (int i = 0; i < txInputs.size(); ++i) {
final Output prevOut = prevOuts.get(i);
if (prevOut.scriptType.equals(14)) {
segwit = true;
} else {
nonSegwit = true;
}
}
// Sanity check on the firmware version, in case devices have been swapped
if (segwit && !mDongle.shouldUseNewSigningApi()) {
throw new RuntimeException("Segwit not supported");
}
final List<byte[]> segwitSigs, nonSegwitSigs;
if (segwit) {
segwitSigs = signSegwitInputs(ptx);
} else {
segwitSigs = new LinkedList<>();
}
if (nonSegwit) {
nonSegwitSigs = signNonSegwitInputs(ptx);
} else {
nonSegwitSigs = new LinkedList<>();
}
for (int i = 0; i < txInputs.size(); ++i) {
final Output prevOut = prevOuts.get(i);
if (prevOut.scriptType.equals(14)) {
sigs.add(segwitSigs.remove(0));
} else {
sigs.add(nonSegwitSigs.remove(0));
}
}
return sigs;
} catch (final BTChipException | IOException e) {
throw new RuntimeException(e.getMessage());
}
}
@Override
public List<byte[]> signTransaction(final Transaction tx, final PreparedTransaction ptx, final List<Output> prevOuts) {
// see TODOs in TrezorHWWallet.signTransaction
ptx.mDecoded = tx;
ptx.mPrevOutputs = prevOuts;
return signTransaction(ptx);
}
@Override
public DeterministicKey getPubKey() {
try {
return internalGetPubKey();
} catch (final BTChipException e) {
e.printStackTrace();
return null;
}
}
private DeterministicKey internalGetPubKey() throws BTChipException {
if (mCachedPubkey == null) {
final BTChipDongle.BTChipPublicKey walletKey = mDongle.getWalletPublicKey(getPath());
mCachedPubkey = HDKey.createMasterKey(walletKey.getChainCode(), walletKey.getPublicKey());
}
return mCachedPubkey;
}
@Override
protected ECKey.ECDSASignature signMessage(final String message) {
try {
mDongle.signMessagePrepare(getPath(), message.getBytes());
final BTChipDongle.BTChipSignature sig = mDongle.signMessageSign(new byte[]{0});
return ECKey.ECDSASignature.decodeFromDER(sig.getSignature());
} catch (final BTChipException e) {
throw new RuntimeException(e.getMessage());
}
}
private String getPath() {
final List<String> pathStr = new LinkedList<>();
for (final Integer i : mAddrn) {
String s = String.valueOf(i & ~0x80000000);
if ((i & 0x80000000) != 0)
s = s + '\'';
pathStr.add(s);
}
return Joiner.on("/").join(pathStr);
}
@Override
protected HWWallet derive(final Integer childNumber) {
final LinkedList<Integer> addrn_child = new LinkedList<>(mAddrn);
addrn_child.add(childNumber);
return new BTChipHWWallet(mDongle, mPin, addrn_child);
}
public BTChipDongle getDongle() {
return mDongle;
}
public boolean checkConnected() {
try {
Log.d(TAG, "Connection check");
mDongle.getFirmwareVersion();
Log.d(TAG, "Connection ok");
return true;
}
catch(final Exception e) {
Log.d(TAG, "Connection not connected");
try {
mDongle.getTransport().close();
Log.d(TAG, "Connection closed");
}
catch(final Exception e1) {
}
return false;
}
}
public void setTransport(final BTChipTransport transport) {
mDongle.setTransport(transport);
try {
mDongle.verifyPin(BTChipHWWallet.this.mPin.getBytes());
}
catch(final Exception e) {
}
}
@Override
public Object[] getChallengeArguments() {
return getChallengeArguments(false);
}
}