package io.emax.cosigner.bitcoin;
import io.emax.cosigner.api.core.ServerStatus;
import io.emax.cosigner.api.currency.CurrencyAdmin;
import io.emax.cosigner.api.currency.Wallet;
import io.emax.cosigner.api.validation.Validatable;
import io.emax.cosigner.bitcoin.bitcoindrpc.BitcoindRpc;
import io.emax.cosigner.bitcoin.bitcoindrpc.BlockChainInfo;
import io.emax.cosigner.bitcoin.bitcoindrpc.MultiSig;
import io.emax.cosigner.bitcoin.bitcoindrpc.Outpoint;
import io.emax.cosigner.bitcoin.bitcoindrpc.OutpointDetails;
import io.emax.cosigner.bitcoin.bitcoindrpc.Output;
import io.emax.cosigner.bitcoin.bitcoindrpc.Payment;
import io.emax.cosigner.bitcoin.bitcoindrpc.Payment.PaymentCategory;
import io.emax.cosigner.bitcoin.bitcoindrpc.RawInput;
import io.emax.cosigner.bitcoin.bitcoindrpc.RawOutput;
import io.emax.cosigner.bitcoin.bitcoindrpc.RawTransaction;
import io.emax.cosigner.bitcoin.bitcoindrpc.SigHash;
import io.emax.cosigner.bitcoin.bitcoindrpc.SignedTransaction;
import io.emax.cosigner.bitcoin.common.BitcoinTools;
import io.emax.cosigner.common.ByteUtilities;
import io.emax.cosigner.common.Json;
import io.emax.cosigner.common.crypto.Secp256k1;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class BitcoinWallet implements Wallet, Validatable, CurrencyAdmin {
private static final Logger LOGGER = LoggerFactory.getLogger(BitcoinWallet.class);
private final BitcoindRpc bitcoindRpc = BitcoinResource.getResource().getBitcoindRpc();
private static final String PUBKEY_PREFIX = "PK-";
BitcoinConfiguration config;
private final HashMap<String, String> multiSigRedeemScripts = new HashMap<>();
private Thread multiSigSubscription = new Thread(() -> {
//noinspection InfiniteLoopStatement
while (true) {
try {
LOGGER.info("Scanning BTC multi-sig addresses");
scanForAddresses();
Thread.sleep(60000);
} catch (Exception e) {
LOGGER.debug("Multisig scan interrupted.");
}
}
});
private Thread rescanThread = new Thread(() -> {
if (config.getRescanTimer() == 0) {
return;
}
//noinspection InfiniteLoopStatement
while (true) {
try {
try {
LOGGER.debug("Initiating blockchain rescan...");
byte[] key = Secp256k1.generatePrivateKey();
String privateKey = BitcoinTools.encodePrivateKey(ByteUtilities.toHexString(key));
String address = BitcoinTools.getPublicAddress(privateKey, true);
bitcoindRpc.importaddress(address, "RESCAN", true);
} catch (Exception e) {
LOGGER.debug("Rescan thread interrupted, or import timed out (expected)", e);
}
Thread.sleep(config.getRescanTimer() * 60L * 60L * 1000L);
} catch (Exception e) {
LOGGER.debug("Rescan thread interrupted, or import timed out (expected)", e);
}
}
});
public BitcoinWallet(BitcoinConfiguration conf) {
config = conf;
if (!multiSigSubscription.isAlive()) {
multiSigSubscription.setDaemon(true);
multiSigSubscription.start();
}
if (!rescanThread.isAlive()) {
rescanThread.setDaemon(true);
rescanThread.start();
}
}
@Override
public String createAddress(String name) {
return createAddress(name, 0);
}
@Override
public String createAddress(String name, int skipNumber) {
int rounds = 1 + skipNumber;
String privateKey =
BitcoinTools.getDeterministicPrivateKey(name, config.getServerPrivateKey(), rounds);
String newAddress = BitcoinTools.getPublicAddress(privateKey, true);
String pubKey = BitcoinTools.getPublicKey(privateKey);
String internalName = PUBKEY_PREFIX + pubKey;
String[] existingAddresses = bitcoindRpc.getaddressesbyaccount(internalName);
boolean oldAddress = true;
while (oldAddress && rounds <= config.getMaxDeterministicAddresses()) {
oldAddress = false;
for (String existingAddress : existingAddresses) {
if (existingAddress.equalsIgnoreCase(newAddress)) {
oldAddress = true;
rounds++;
privateKey =
BitcoinTools.getDeterministicPrivateKey(name, config.getServerPrivateKey(), rounds);
newAddress = BitcoinTools.getPublicAddress(privateKey, true);
break;
}
}
}
bitcoindRpc.importaddress(newAddress, internalName, false);
return newAddress;
}
@Override
public boolean registerAddress(String address) {
bitcoindRpc.importaddress(address, "", false);
return true;
}
@Override
public String generatePrivateKey() {
String key = ByteUtilities.toHexString(Secp256k1.generatePrivateKey());
return BitcoinTools.encodePrivateKey(key);
}
@Override
public String createAddressFromKey(String key, boolean isPrivateKey) {
return BitcoinTools.getPublicAddress(key, isPrivateKey);
}
@Override
public String generatePublicKey(String privateKey) {
return ByteUtilities.toHexString(BitcoinTools.getPublicKeyBytes(privateKey));
}
@Override
public Iterable<String> getAddresses(String name) {
// Hash the user's key so it's not stored in the wallet
String internalName = BitcoinTools.encodeUserKey(name);
String[] addresses = bitcoindRpc.getaddressesbyaccount(internalName);
return Arrays.asList(addresses);
}
@Override
public String getMultiSigAddress(Iterable<String> addresses, String name) {
// Hash the user's key so it's not stored in the wallet
String internalName = BitcoinTools.encodeUserKey(name);
String newAddress = generateMultiSigAddress(addresses, name);
bitcoindRpc.importaddress(newAddress, internalName, false);
return newAddress;
}
private void scanForAddresses() {
try {
Map<String, BigDecimal> knownAccounts = bitcoindRpc.listaccounts(0, true);
knownAccounts.keySet().forEach(account -> {
// Look for any known PK/Single accounts and generate the matching multisig in memory
Pattern pattern = Pattern.compile("^" + PUBKEY_PREFIX + "(.*)");
Matcher matcher = pattern.matcher(account);
if (matcher.matches()) {
String pubKey = matcher.group(1);
try {
generateMultiSigAddress(Collections.singletonList(pubKey), null);
} catch (Exception e) {
LOGGER.info(account + " appears to be an invalid account - ignoring");
}
}
});
} catch (Exception e) {
LOGGER.debug("No accounts found when scanning");
}
}
private String generateMultiSigAddress(Iterable<String> addresses, String name) {
LinkedList<String> multisigAddresses = new LinkedList<>();
addresses.forEach(address -> {
// Check if any of the addresses belong to the user
int rounds = 1;
String userPrivateKey =
BitcoinTools.getDeterministicPrivateKey(name, config.getServerPrivateKey(), rounds);
String userAddress = BitcoinTools.NOKEY;
if (!userPrivateKey.equalsIgnoreCase(BitcoinTools.NOKEY)) {
userAddress = BitcoinTools.getPublicAddress(userPrivateKey, true);
while (!address.equalsIgnoreCase(userAddress) && rounds <= config
.getMaxDeterministicAddresses()) {
rounds++;
userPrivateKey =
BitcoinTools.getDeterministicPrivateKey(name, config.getServerPrivateKey(), rounds);
userAddress = BitcoinTools.getPublicAddress(userPrivateKey, true);
}
}
if (address.equalsIgnoreCase(userAddress)) {
multisigAddresses.add(BitcoinTools.getPublicKey(userPrivateKey));
} else {
multisigAddresses.add(address);
}
});
for (String account : config.getMultiSigAccounts()) {
if (!account.isEmpty()) {
multisigAddresses.add(account);
}
}
for (String accountKey : config.getMultiSigKeys()) {
if (!accountKey.isEmpty()) {
multisigAddresses.add(BitcoinTools.getPublicKey(accountKey));
}
}
String[] addressArray = new String[multisigAddresses.size()];
MultiSig newAddress = bitcoindRpc
.createmultisig(config.getMinSignatures(), multisigAddresses.toArray(addressArray));
if (name != null && !name.isEmpty()) {
// Bitcoind refuses to connect the address it has to the p2sh script even when provided.
// Simplest to just load it, it still doesn't have the private keys.
bitcoindRpc
.addmultisigaddress(config.getMinSignatures(), multisigAddresses.toArray(addressArray),
BitcoinTools.encodeUserKey(name));
}
multiSigRedeemScripts.put(newAddress.getAddress(), newAddress.getRedeemScript());
return newAddress.getAddress();
}
@Override
public String getBalance(String address) {
BigDecimal balance = BigDecimal.ZERO;
try {
Output[] outputs = bitcoindRpc
.listunspent(config.getMinConfirmations(), config.getMaxConfirmations(),
new String[]{address});
for (Output output : outputs) {
balance = balance.add(output.getAmount());
}
} catch (Exception e) {
LOGGER.debug(null, e);
}
return balance.toPlainString();
}
@Override
public String getPendingBalance(String address) {
BigDecimal balance = BigDecimal.ZERO;
try {
Output[] outputs =
bitcoindRpc.listunspent(0, config.getMinConfirmations(), new String[]{address});
for (Output output : outputs) {
balance = balance.add(output.getAmount());
}
} catch (Exception e) {
LOGGER.debug(null, e);
}
return balance.toPlainString();
}
@Override
public String createTransaction(Iterable<String> fromAddresses, Iterable<Recipient> toAddresses) {
return createTransaction(fromAddresses, toAddresses, null);
}
private boolean getOption(String options, String option) {
if (options != null && !options.isEmpty()) {
try {
LinkedList<String> optionList =
(LinkedList) Json.objectifyString(LinkedList.class, options);
for (String optionPossiblilty : optionList) {
if (optionPossiblilty.equalsIgnoreCase(option)) {
return true;
}
}
} catch (Exception e) {
LOGGER.debug("Bad options");
}
}
return false;
}
@Override
public String createTransaction(Iterable<String> fromAddress, Iterable<Recipient> toAddress,
String options) {
boolean includeFees = false;
if (getOption(options, "includeFees")) {
includeFees = true;
}
List<String> fromAddresses = new LinkedList<>();
fromAddress.forEach(fromAddresses::add);
String[] addresses = new String[fromAddresses.size()];
Outpoint[] outputs = bitcoindRpc
.listunspent(config.getMinConfirmations(), config.getMaxConfirmations(),
fromAddresses.toArray(addresses));
List<Outpoint> usedOutputs = new LinkedList<>();
Map<String, BigDecimal> txnOutput = new HashMap<>();
BigDecimal total = BigDecimal.ZERO;
BigDecimal subTotal = BigDecimal.ZERO;
Iterator<Recipient> recipients = toAddress.iterator();
Recipient recipient = recipients.next();
boolean filledAllOutputs = false;
for (Outpoint output : outputs) {
total = total.add(output.getAmount());
subTotal = subTotal.add(output.getAmount());
usedOutputs.add(output);
if (subTotal.compareTo(recipient.getAmount()) >= 0) {
LOGGER.debug("Recipient: " + recipient.getRecipientAddress());
txnOutput.put(recipient.getRecipientAddress(), recipient.getAmount());
subTotal = subTotal.subtract(recipient.getAmount());
if (recipients.hasNext()) {
recipient = recipients.next();
} else {
// 0.0001 BTC * 1000 Bytes suggested by spec
int byteSize = 0;
// inputs for normal TXs should only be ~181 bytes
byteSize += usedOutputs.size() * 181;
// outputs should be ~34 bytes
byteSize += (txnOutput.size() + 1) * 34;
// tx overhead should be ~10 bytes
byteSize += 10;
// round up to the nearest KB.
LOGGER.debug("Estimated tx size: " + byteSize);
byteSize = (int) Math.ceil(((double) byteSize) / 1000);
BigDecimal fees =
BigDecimal.valueOf((double) byteSize).multiply(BigDecimal.valueOf(0.0001));
LOGGER.debug("Expecting fees of: " + fees.toPlainString());
// Only set a change address if there's change.
if (!includeFees && subTotal.compareTo(fees) > 0) {
subTotal = subTotal.subtract(fees);
if (subTotal.compareTo(BigDecimal.ZERO) > 0) {
LOGGER.debug("We have change: " + subTotal.toPlainString());
txnOutput.put(fromAddress.iterator().next(), subTotal);
}
} else {
BigDecimal feeTotal = fees;
if (!includeFees) {
feeTotal = fees.subtract(subTotal);
subTotal = BigDecimal.ZERO;
}
try {
// Split fees over all recipients if sender can't cover it.
BigDecimal individualFees =
feeTotal.divide(BigDecimal.valueOf(txnOutput.size()), BigDecimal.ROUND_HALF_UP);
txnOutput.forEach((address, amount) -> {
txnOutput.put(address, amount.subtract(individualFees));
});
if (subTotal.compareTo(BigDecimal.ZERO) > 0) {
LOGGER.debug("We have change: " + subTotal.toPlainString());
txnOutput.put(fromAddress.iterator().next(), subTotal);
}
} catch (Exception e) {
LOGGER.debug(null, e);
throw e;
}
}
filledAllOutputs = true;
break;
}
}
}
// We don't have enough to complete the transaction
if (!filledAllOutputs) {
return null;
}
RawTransaction rawTx = new RawTransaction();
rawTx.setVersion(1);
rawTx.setInputCount(usedOutputs.size());
usedOutputs.forEach(input -> {
RawInput rawInput = new RawInput();
rawInput.setTxHash(input.getTransactionId());
rawInput.setTxIndex((int) input.getOutputIndex());
rawInput.setSequence(-1);
rawTx.getInputs().add(rawInput);
});
rawTx.setOutputCount(txnOutput.size());
txnOutput.forEach((address, amount) -> {
RawOutput rawOutput = new RawOutput();
rawOutput.setAmount(amount.multiply(BigDecimal.valueOf(100000000)).longValue());
LOGGER.debug("Address: " + address);
String decodedAddress = BitcoinTools.decodeAddress(address);
LOGGER.debug("Decoded address: " + decodedAddress);
byte[] addressBytes = ByteUtilities.toByteArray(decodedAddress);
String scriptData = "";
if (!BitcoinTools.isMultiSigAddress(address)) {
// Regular address
scriptData = "76a914";
scriptData += ByteUtilities.toHexString(addressBytes);
scriptData += "88ac";
} else {
// Multi-sig address
scriptData = "a914";
scriptData += ByteUtilities.toHexString(addressBytes);
scriptData += "87";
}
rawOutput.setScript(scriptData);
rawTx.getOutputs().add(rawOutput);
});
rawTx.setLockTime(0);
return rawTx.encode();
}
@Override
public Iterable<String> getSignersForTransaction(String transaction) {
RawTransaction tx = RawTransaction.parse(transaction);
LinkedList<String> addresses = new LinkedList<>();
Outpoint[] outputs = bitcoindRpc
.listunspent(config.getMinConfirmations(), config.getMaxConfirmations(), new String[]{});
tx.getInputs().forEach(input -> {
LOGGER.debug("Checking input: " + input.getTxHash());
for (Outpoint output : outputs) {
if (output.getTransactionId().equalsIgnoreCase(input.getTxHash())
&& output.getOutputIndex() == input.getTxIndex()) {
LOGGER.debug("Found match: " + output.getTransactionId());
String redeemScript = multiSigRedeemScripts.get(output.getAddress());
LOGGER.debug("Found redeem script: " + redeemScript);
Iterable<String> publicKeys = RawTransaction.decodeRedeemScript(redeemScript);
LOGGER.debug("Got public keys: " + publicKeys.toString());
publicKeys.forEach(key -> addresses.add(BitcoinTools.getPublicAddress(key, false)));
}
}
});
return addresses;
}
@Override
public String signTransaction(String transaction, String address) {
return signTransaction(transaction, address, null);
}
@Override
public String signTransaction(String transaction, String address, String name) {
return signTransaction(transaction, address, name, null);
}
@Override
public String signTransaction(String transaction, String address, String name, String options) {
LOGGER.debug("Attempting to sign a transaction");
int rounds = 1;
String privateKey;
String userAddress;
SignedTransaction signedTransaction;
LOGGER.debug("Options: " + options);
boolean onlyMatching = false;
if (getOption(options, "onlyMatching")) {
LOGGER.debug("onlyMatching");
onlyMatching = true;
}
if (name != null) {
LOGGER.debug("User key has value, trying to determine private key");
privateKey =
BitcoinTools.getDeterministicPrivateKey(name, config.getServerPrivateKey(), rounds);
userAddress = BitcoinTools.getPublicAddress(privateKey, true);
while (!(userAddress != null && userAddress.equalsIgnoreCase(address))
&& !generateMultiSigAddress(Collections.singletonList(userAddress), name)
.equalsIgnoreCase(address) && rounds < config.getMaxDeterministicAddresses()) {
rounds++;
privateKey =
BitcoinTools.getDeterministicPrivateKey(name, config.getServerPrivateKey(), rounds);
userAddress = BitcoinTools.getPublicAddress(privateKey, true);
}
// If we hit max addresses/user bail out
if (!(userAddress != null && userAddress.equalsIgnoreCase(address))
&& !generateMultiSigAddress(Collections.singletonList(userAddress), name)
.equalsIgnoreCase(address)) {
LOGGER.debug("Too many rounds, failed to sign");
return transaction;
}
LOGGER.debug("We can sign for " + userAddress);
// BTC TX is re-verified since we look up the outputs in getSigString
signedTransaction = new SignedTransaction();
Iterable<Iterable<String>> signatureData = getSigString(transaction, address);
signatureData = signWithPrivateKey(signatureData, privateKey, onlyMatching);
signedTransaction.setTransaction(applySignature(transaction, address, signatureData));
} else {
try {
LOGGER.debug("Asking bitcoind to sign...");
signedTransaction =
bitcoindRpc.signrawtransaction(transaction, new OutpointDetails[]{}, null, SigHash.ALL);
} catch (Exception e) {
signedTransaction = new SignedTransaction();
signedTransaction.setTransaction(transaction);
}
for (String accountKey : config.getMultiSigKeys()) {
if (!accountKey.isEmpty()) {
transaction = signedTransaction.getTransaction();
address = BitcoinTools.getPublicAddress(accountKey, true);
signedTransaction = new SignedTransaction();
Iterable<Iterable<String>> signatureData = getSigString(transaction, address);
signatureData = signWithPrivateKey(signatureData, accountKey, onlyMatching);
signedTransaction.setTransaction(applySignature(transaction, address, signatureData));
}
}
}
return signedTransaction.getTransaction();
}
@Override
public Iterable<Iterable<String>> getSigString(String transaction, String address) {
LinkedList<Iterable<String>> signatureData = new LinkedList<>();
Outpoint[] outputs = bitcoindRpc
.listunspent(config.getMinConfirmations(), config.getMaxConfirmations(), new String[]{});
RawTransaction rawTx = RawTransaction.parse(transaction);
for (RawInput input : rawTx.getInputs()) {
for (Outpoint output : outputs) {
LOGGER.debug("Looking for outputs we can sign");
if (output.getTransactionId().equalsIgnoreCase(input.getTxHash())
&& output.getOutputIndex() == input.getTxIndex()) {
OutpointDetails outpoint = new OutpointDetails();
outpoint.setTransactionId(output.getTransactionId());
outpoint.setOutputIndex(output.getOutputIndex());
outpoint.setScriptPubKey(output.getScriptPubKey());
outpoint.setRedeemScript(multiSigRedeemScripts.get(output.getAddress()));
if (output.getAddress().equalsIgnoreCase(address)) {
RawTransaction signingTx = RawTransaction.stripInputScripts(rawTx);
byte[] sigData;
LOGGER.debug("Found an output, matching to inputs in the transaction");
for (RawInput sigInput : signingTx.getInputs()) {
if (sigInput.getTxHash().equalsIgnoreCase(outpoint.getTransactionId())
&& sigInput.getTxIndex() == outpoint.getOutputIndex()) {
// This is the input we're processing, fill it and sign it
if (BitcoinTools.isMultiSigAddress(address)) {
sigInput.setScript(outpoint.getRedeemScript());
} else {
sigInput.setScript(outpoint.getScriptPubKey());
}
byte[] hashTypeBytes =
ByteUtilities.stripLeadingNullBytes(BigInteger.valueOf(1).toByteArray());
hashTypeBytes = ByteUtilities.leftPad(hashTypeBytes, 4, (byte) 0x00);
hashTypeBytes = ByteUtilities.flipEndian(hashTypeBytes);
String sigString = signingTx.encode() + ByteUtilities.toHexString(hashTypeBytes);
LOGGER.debug("Signing: " + sigString);
try {
sigData = ByteUtilities.toByteArray(sigString);
MessageDigest md = MessageDigest.getInstance("SHA-256");
sigData = md.digest(md.digest(sigData));
LinkedList<String> inputSigData = new LinkedList<>();
inputSigData.add(input.getTxHash());
inputSigData.add(Integer.toString(input.getTxIndex()));
inputSigData.add(output.getRedeemScript());
inputSigData.add(ByteUtilities.toHexString(sigData));
signatureData.add(inputSigData);
} catch (Exception e) {
LOGGER.error(null, e);
}
}
}
}
}
}
}
return signatureData;
}
@Override
public Iterable<Iterable<String>> signWithPrivateKey(Iterable<Iterable<String>> data,
String privateKey) {
return signWithPrivateKey(data, privateKey, false);
}
private Iterable<Iterable<String>> signWithPrivateKey(Iterable<Iterable<String>> data,
String privateKey, boolean onlyMatching) {
final byte[] addressData = BitcoinTools.getPublicKeyBytes(privateKey);
final byte[] privateKeyBytes =
ByteUtilities.toByteArray(BitcoinTools.decodeAddress(privateKey));
Iterator<Iterable<String>> signatureData = data.iterator();
LinkedList<Iterable<String>> signatures = new LinkedList<>();
while (signatureData.hasNext()) {
Iterator<String> signatureEntry = signatureData.next().iterator();
LinkedList<String> signatureResults = new LinkedList<>();
// Hash
signatureResults.add(signatureEntry.next());
// Index
signatureResults.add(signatureEntry.next());
// Redeem Script
String redeemScript = signatureEntry.next();
signatureResults.add(redeemScript);
Iterable<String> possibleSigners = RawTransaction.decodeRedeemScript(redeemScript);
if (possibleSigners.iterator().hasNext()) {
LOGGER.debug("There are possible signers");
}
boolean foundSigner = false;
if (onlyMatching && possibleSigners.iterator().hasNext()) {
LOGGER.debug("Checking that signer is on the script.");
for (String signer : possibleSigners) {
if (BitcoinTools.getPublicKey(privateKey).equalsIgnoreCase(signer)) {
foundSigner = true;
LOGGER.debug("Signer is OK.");
}
}
if (!foundSigner) {
LOGGER.debug("Signer not on the script.");
continue;
}
}
signatureResults.add(ByteUtilities.toHexString(addressData));
// Sig string
byte[] sigData = ByteUtilities.toByteArray(signatureEntry.next());
byte[][] sigResults = Secp256k1.signTransaction(sigData, privateKeyBytes);
// BIP62
BigInteger lowSlimit =
new BigInteger("007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0", 16);
BigInteger ourSvalue = new BigInteger(1, sigResults[1]);
while (ourSvalue.compareTo(lowSlimit) > 0) {
sigResults = Secp256k1.signTransaction(sigData, privateKeyBytes);
ourSvalue = new BigInteger(1, sigResults[1]);
}
StringBuilder signature = new StringBuilder();
// Only want R & S, don't need V
for (int i = 0; i < 2; i++) {
byte[] sig = sigResults[i];
signature.append("02");
byte[] sigSize = BigInteger.valueOf(sig.length).toByteArray();
sigSize = ByteUtilities.stripLeadingNullBytes(sigSize);
signature.append(ByteUtilities.toHexString(sigSize));
signature.append(ByteUtilities.toHexString(sig));
}
byte[] sigBytes = ByteUtilities.toByteArray(signature.toString());
byte[] sigSize = BigInteger.valueOf(sigBytes.length).toByteArray();
sigSize = ByteUtilities.stripLeadingNullBytes(sigSize);
String signatureString = ByteUtilities.toHexString(sigSize) + signature.toString();
signatureString = "30" + signatureString;
signatureResults.add(signatureString);
signatures.add(signatureResults);
}
return signatures;
}
@Override
public String applySignature(String transaction, String address,
Iterable<Iterable<String>> signatureData) {
Iterator<Iterable<String>> signatures = signatureData.iterator();
RawTransaction rawTx = RawTransaction.parse(transaction);
while (signatures.hasNext()) {
Iterable<String> signature = signatures.next();
Iterator<String> sigDataIterator = signature.iterator();
rawTx = RawTransaction.parse(transaction);
String signedTxHash = sigDataIterator.next();
String signedTxIndex = sigDataIterator.next();
String signedTxRedeemScript = sigDataIterator.next();
byte[] addressData = ByteUtilities.toByteArray(sigDataIterator.next());
byte[] sigData = ByteUtilities.toByteArray(sigDataIterator.next());
// Determine how we need to format the sig data
if (BitcoinTools.isMultiSigAddress(address)) {
for (RawInput signedInput : rawTx.getInputs()) {
if (signedInput.getTxHash().equalsIgnoreCase(signedTxHash) && Integer
.toString(signedInput.getTxIndex()).equalsIgnoreCase(signedTxIndex)) {
// Merge the new signature with existing ones.
signedInput.stripMultiSigRedeemScript(signedTxRedeemScript);
String scriptData = signedInput.getScript();
if (scriptData.isEmpty()) {
scriptData += "00";
}
byte[] dataSize = RawTransaction.writeVariableStackInt(sigData.length + 1);
scriptData += ByteUtilities.toHexString(dataSize);
scriptData += ByteUtilities.toHexString(sigData);
scriptData += "01";
byte[] redeemScriptBytes = ByteUtilities.toByteArray(signedTxRedeemScript);
dataSize = RawTransaction.writeVariableStackInt(redeemScriptBytes.length);
scriptData += ByteUtilities.toHexString(dataSize);
scriptData += ByteUtilities.toHexString(redeemScriptBytes);
signedInput.setScript(scriptData);
break;
}
}
} else {
for (RawInput signedInput : rawTx.getInputs()) {
if (signedInput.getTxHash().equalsIgnoreCase(signedTxHash) && Integer
.toString(signedInput.getTxIndex()).equalsIgnoreCase(signedTxIndex)) {
// Sig then pubkey
String scriptData = "";
byte[] dataSize = RawTransaction.writeVariableStackInt(sigData.length + 1);
scriptData += ByteUtilities.toHexString(dataSize);
scriptData += ByteUtilities.toHexString(sigData);
scriptData += "01"; // SIGHASH.ALL
dataSize = RawTransaction.writeVariableStackInt(addressData.length);
scriptData += ByteUtilities.toHexString(dataSize);
scriptData += ByteUtilities.toHexString(addressData);
signedInput.setScript(scriptData);
break;
}
}
}
transaction = rawTx.encode();
}
return rawTx.encode();
}
@Override
public String sendTransaction(String transaction) {
if (transactionsEnabled) {
return bitcoindRpc.sendrawtransaction(transaction, false);
} else {
return "Transactions temporarily disabled";
}
}
@Override
public TransactionDetails[] getTransactions(String address, int numberToReturn, int skipNumber) {
LinkedList<TransactionDetails> txDetails = new LinkedList<>();
int pageSize = 1000;
int pageNumber = 0;
while (txDetails.size() < (numberToReturn + skipNumber)) {
Payment[] payments = bitcoindRpc.listtransactions("*", pageSize, pageNumber * pageSize, true);
if (payments.length == 0) {
break;
}
for (Payment payment : Arrays.asList(payments)) {
// Lookup the txid and vout/vin based on the sign of the amount (+/-)
// Determine the address involved
try {
String rawTx = bitcoindRpc.getrawtransaction(payment.getTxid());
RawTransaction tx = RawTransaction.parse(rawTx);
if (payment.getCategory() == PaymentCategory.receive) {
// Paid to the account
if (payment.getAddress().equalsIgnoreCase(address)) {
TransactionDetails detail = new TransactionDetails();
detail.setAmount(payment.getAmount().abs());
detail.setTxDate(new Date(payment.getBlocktime().toInstant().toEpochMilli() * 1000L));
Map txData = bitcoindRpc.gettransaction(payment.getTxid(), true);
detail
.setConfirmed(config.getMinConfirmations() <= (int) txData.get("confirmations"));
detail.setConfirmations((int) txData.get("confirmations"));
detail.setMinConfirmations(config.getMinConfirmations());
// Senders
HashSet<String> senders = new HashSet<>();
tx.getInputs().forEach(input -> {
try {
String rawSenderTx = bitcoindRpc.getrawtransaction(input.getTxHash());
RawTransaction senderTx = RawTransaction.parse(rawSenderTx);
String script = senderTx.getOutputs().get(input.getTxIndex()).getScript();
String scriptAddress = RawTransaction.decodePubKeyScript(script);
senders.add(scriptAddress);
} catch (Exception e) {
LOGGER.debug(null, e);
senders.add(null);
}
});
detail.setFromAddress(senders.toArray(new String[senders.size()]));
detail.setToAddress(new String[]{address});
detail.setTxHash(payment.getTxid());
txDetails.add(detail);
}
} else if (payment.getCategory() == PaymentCategory.send) {
// Sent from the account
tx.getInputs().forEach(input -> {
String rawSenderTx = bitcoindRpc.getrawtransaction(input.getTxHash());
RawTransaction senderTx = RawTransaction.parse(rawSenderTx);
String script = senderTx.getOutputs().get(input.getTxIndex()).getScript();
String scriptAddress = RawTransaction.decodePubKeyScript(script);
if (scriptAddress != null && scriptAddress.equalsIgnoreCase(address)) {
TransactionDetails detail = new TransactionDetails();
detail
.setTxDate(new Date(payment.getBlocktime().toInstant().toEpochMilli() * 1000L));
detail.setTxHash(payment.getTxid());
detail.setAmount(payment.getAmount().abs());
detail.setFromAddress(new String[]{address});
detail.setToAddress(new String[]{payment.getAddress()});
Map txData = bitcoindRpc.gettransaction(payment.getTxid(), true);
detail.setConfirmed(
config.getMinConfirmations() <= (int) txData.get("confirmations"));
detail.setConfirmations((int) txData.get("confirmations"));
detail.setMinConfirmations(config.getMinConfirmations());
txDetails.add(detail);
}
});
}
} catch (Exception e) {
LOGGER.debug(null, e);
}
}
LinkedList<TransactionDetails> removeThese = new LinkedList<>();
for (TransactionDetails detail : txDetails) {
boolean noMatch = false;
for (String from : Arrays.asList(detail.getFromAddress())) {
boolean subMatch = false;
for (String to : Arrays.asList(detail.getToAddress())) {
if (to.equalsIgnoreCase(from)) {
subMatch = true;
break;
}
}
if (!subMatch) {
noMatch = true;
break;
}
}
// If the from & to's match then it's just a return amount, simpler if we don't list it.
if (!noMatch) {
removeThese.add(detail);
}
}
removeThese.forEach(txDetails::remove);
pageNumber++;
}
for (int i = 0; i < skipNumber; i++) {
txDetails.removeFirst();
}
while (txDetails.size() > numberToReturn) {
txDetails.removeLast();
}
return txDetails.toArray(new TransactionDetails[txDetails.size()]);
}
@Override
public TransactionDetails decodeRawTransaction(String transaction) {
RawTransaction tx = RawTransaction.parse(transaction);
Set<String> senders = new HashSet<>();
tx.getInputs().forEach(input -> {
String rawSenderTx = bitcoindRpc.getrawtransaction(input.getTxHash());
RawTransaction senderTx = RawTransaction.parse(rawSenderTx);
String script = senderTx.getOutputs().get(input.getTxIndex()).getScript();
String scriptAddress = RawTransaction.decodePubKeyScript(script);
senders.add(scriptAddress);
});
Set<String> recipients = new HashSet<>();
List<Long> satoshis = new LinkedList<>();
tx.getOutputs().forEach(output -> {
String scriptAddress = RawTransaction.decodePubKeyScript(output.getScript());
if (senders.contains(scriptAddress)) {
// Skip if it's change returned.
return;
}
recipients.add(scriptAddress);
satoshis.add(output.getAmount());
});
BigDecimal totalAmount = BigDecimal.ZERO;
for (long amount : satoshis) {
totalAmount = totalAmount.add(BigDecimal.valueOf(amount));
}
totalAmount = totalAmount.divide(BigDecimal.valueOf(100000000), BigDecimal.ROUND_HALF_UP);
TransactionDetails txDetails = new TransactionDetails();
txDetails.setAmount(totalAmount);
txDetails.setFromAddress(senders.toArray(new String[senders.size()]));
txDetails.setToAddress(recipients.toArray(new String[recipients.size()]));
return txDetails;
}
@Override
public TransactionDetails getTransaction(String transactionId) {
Map txData = bitcoindRpc.gettransaction(transactionId, true);
TransactionDetails txDetail = new TransactionDetails();
txDetail.setTxHash(txData.get("txid").toString());
txDetail.setConfirmed(config.getMinConfirmations() <= (int) txData.get("confirmations"));
txDetail.setAmount(new BigDecimal(txData.get("amount").toString()));
txDetail.setTxDate(new Date(((int) txData.get("blocktime")) * 1000L));
txDetail.setConfirmations((int) txData.get("confirmations"));
txDetail.setMinConfirmations(config.getMinConfirmations());
LinkedList<String> senders = new LinkedList<>();
LinkedList<String> recipients = new LinkedList<>();
ArrayList<Map<String, Object>> txd = (ArrayList<Map<String, Object>>) txData.get("details");
txd.forEach((txdMap) -> {
if (txdMap.get("category").toString().equalsIgnoreCase("send")) {
senders.add(txdMap.get("address").toString());
} else if (txdMap.get("category").toString().equalsIgnoreCase("receive")) {
recipients.add(txdMap.get("address").toString());
}
});
txDetail.setFromAddress(senders.toArray(new String[senders.size()]));
txDetail.setToAddress(recipients.toArray(new String[recipients.size()]));
return txDetail;
}
@Override
public ServerStatus getWalletStatus() {
try {
bitcoindRpc.getblockchaininfo().getChain();
return ServerStatus.CONNECTED;
} catch (Exception e) {
return ServerStatus.DISCONNECTED;
}
}
@Override
public Map<String, String> getConfiguration() {
HashMap<String, String> configSummary = new HashMap<>();
configSummary.put("Currency Symbol", config.getCurrencySymbol());
configSummary.put("Bitcoind Connection", config.getDaemonConnectionString());
configSummary.put("Minimum Signatures", ((Integer) config.getMinSignatures()).toString());
configSummary.put("Minimum Confirmations", ((Integer) config.getMinConfirmations()).toString());
configSummary.put("Rescan Timer", ((Integer) config.getRescanTimer()).toString());
configSummary
.put("Maximum Transaction Value", config.getMaxAmountPerTransaction().toPlainString());
configSummary
.put("Maximum Transaction Value Per Hour", config.getMaxAmountPerHour().toPlainString());
configSummary
.put("Maximum Transaction Value Per Day", config.getMaxAmountPerDay().toPlainString());
return configSummary;
}
private boolean transactionsEnabled = true;
@Override
public void enableTransactions() {
transactionsEnabled = true;
}
@Override
public void disableTransactions() {
transactionsEnabled = false;
}
@Override
public boolean transactionsEnabled() {
return transactionsEnabled;
}
@Override
public long getBlockchainHeight() {
BlockChainInfo chainInfo = bitcoindRpc.getblockchaininfo();
return chainInfo.getBlocks();
}
@Override
public long getLastBlockTime() {
BlockChainInfo chainInfo = bitcoindRpc.getblockchaininfo();
String blockHash = bitcoindRpc.getBlockHash(chainInfo.getBlocks());
Map<String, Object> block = bitcoindRpc.getBlock(blockHash);
return Long.parseLong(String.valueOf(block.get("time")));
}
}