/* * This file is part of Bisq. * * Bisq is free software: you can redistribute it and/or modify it * under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or (at * your option) any later version. * * Bisq is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Bisq. If not, see <http://www.gnu.org/licenses/>. */ package bisq.core.btc.wallet; import bisq.core.btc.AddressEntry; import bisq.core.btc.AddressEntryException; import bisq.core.btc.AddressEntryList; import bisq.core.btc.InsufficientFundsException; import bisq.core.btc.Restrictions; import bisq.core.btc.exceptions.TransactionVerificationException; import bisq.core.btc.exceptions.WalletException; import bisq.core.provider.fee.FeeService; import bisq.core.user.Preferences; import bisq.common.handlers.ErrorMessageHandler; import org.bitcoinj.core.Address; import org.bitcoinj.core.AddressFormatException; import org.bitcoinj.core.Coin; import org.bitcoinj.core.InsufficientMoneyException; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionConfidence; import org.bitcoinj.core.TransactionInput; import org.bitcoinj.core.TransactionOutPoint; import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.crypto.DeterministicKey; import org.bitcoinj.crypto.KeyCrypterScrypt; import org.bitcoinj.script.ScriptBuilder; import org.bitcoinj.wallet.SendRequest; import org.bitcoinj.wallet.Wallet; import javax.inject.Inject; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import org.spongycastle.crypto.params.KeyParameter; import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; public class BtcWalletService extends WalletService { private static final Logger log = LoggerFactory.getLogger(BtcWalletService.class); private final AddressEntryList addressEntryList; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject public BtcWalletService(WalletsSetup walletsSetup, AddressEntryList addressEntryList, Preferences preferences, FeeService feeService) { super(walletsSetup, preferences, feeService); this.addressEntryList = addressEntryList; walletsSetup.addSetupCompletedHandler(() -> { wallet = walletsSetup.getBtcWallet(); wallet.addEventListener(walletEventListener); walletsSetup.getChain().addNewBestBlockListener(block -> chainHeightProperty.set(block.getHeight())); chainHeightProperty.set(walletsSetup.getChain().getBestChainHeight()); }); } /////////////////////////////////////////////////////////////////////////////////////////// // Overridden Methods /////////////////////////////////////////////////////////////////////////////////////////// @Override void decryptWallet(@NotNull KeyParameter key) { super.decryptWallet(key); addressEntryList.stream().forEach(e -> { final DeterministicKey keyPair = e.getKeyPair(); if (keyPair.isEncrypted()) e.setDeterministicKey(keyPair.decrypt(key)); }); addressEntryList.persist(); } @Override void encryptWallet(KeyCrypterScrypt keyCrypterScrypt, KeyParameter key) { super.encryptWallet(keyCrypterScrypt, key); addressEntryList.stream().forEach(e -> { final DeterministicKey keyPair = e.getKeyPair(); if (keyPair.isEncrypted()) e.setDeterministicKey(keyPair.encrypt(keyCrypterScrypt, key)); }); addressEntryList.persist(); } @Override String getWalletAsString(boolean includePrivKeys) { StringBuilder sb = new StringBuilder(); getAddressEntryListAsImmutableList().stream().forEach(e -> sb.append(e.toString()).append("\n")); return "Address entry list:\n" + sb.toString() + "\n\n" + wallet.toString(includePrivKeys, true, true, walletsSetup.getChain()) + "\n\n" + "All pubKeys as hex:\n" + wallet.printAllPubKeysAsHex(); } /////////////////////////////////////////////////////////////////////////////////////////// // Public Methods /////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////// // CompensationRequest tx /////////////////////////////////////////////////////////////////////////////////////////// public Transaction completePreparedCompensationRequestTx(Coin issuanceAmount, Address issuanceAddress, Transaction feeTx, byte[] opReturnData) throws TransactionVerificationException, WalletException, InsufficientMoneyException { // (BsqFee)tx has following structure: // inputs [1-n] BSQ inputs (fee) // outputs [0-1] BSQ request fee change output (>= 546 Satoshi) // preparedCompensationRequestTx has following structure: // inputs [1-n] BSQ inputs for request fee // inputs [1-n] BTC inputs for BSQ issuance and miner fee // outputs [1] Mandatory BSQ request fee change output (>= 546 Satoshi) // outputs [1] Potentially BSQ issuance output (>= 546 Satoshi) // outputs [0-1] BTC change output from issuance and miner fee inputs (>= 546 Satoshi) // outputs [1] OP_RETURN with opReturnData and amount 0 // mining fee: BTC mining fee + burned BSQ fee Transaction preparedTx = new Transaction(params); // Copy inputs from BSQ fee tx feeTx.getInputs().forEach(preparedTx::addInput); int indexOfBtcFirstInput = feeTx.getInputs().size(); // Need to be first because issuance is not guaranteed to be valid and would otherwise burn change output! // BSQ change outputs from BSQ fee inputs. feeTx.getOutputs().forEach(preparedTx::addOutput); // BSQ issuance output preparedTx.addOutput(issuanceAmount, issuanceAddress); // safety check counter to avoid endless loops int counter = 0; // estimated size of input sig final int sigSizePerInput = 106; // typical size for a tx with 3 inputs int txSizeWithUnsignedInputs = 300; final Coin txFeePerByte = feeService.getTxFeePerByte(); Address changeAddress = getFreshAddressEntry().getAddress(); checkNotNull(changeAddress, "changeAddress must not be null"); final BtcCoinSelector coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE)); final List<TransactionInput> preparedBsqTxInputs = preparedTx.getInputs(); final List<TransactionOutput> preparedBsqTxOutputs = preparedTx.getOutputs(); int numInputs = preparedBsqTxInputs.size(); Transaction resultTx = null; boolean isFeeOutsideTolerance; do { counter++; if (counter >= 10) { checkNotNull(resultTx, "resultTx must not be null"); log.error("Could not calculate the fee. Tx=" + resultTx); break; } Transaction tx = new Transaction(params); preparedBsqTxInputs.stream().forEach(tx::addInput); preparedBsqTxOutputs.stream().forEach(tx::addOutput); SendRequest sendRequest = SendRequest.forTx(tx); sendRequest.shuffleOutputs = false; sendRequest.aesKey = aesKey; // signInputs needs to be false as it would try to sign all inputs (BSQ inputs are not in this wallet) sendRequest.signInputs = false; sendRequest.fee = txFeePerByte.multiply(txSizeWithUnsignedInputs + sigSizePerInput * numInputs); sendRequest.feePerKb = Coin.ZERO; sendRequest.ensureMinRequiredFee = false; sendRequest.coinSelector = coinSelector; sendRequest.changeAddress = changeAddress; wallet.completeTx(sendRequest); resultTx = sendRequest.tx; // add OP_RETURN output resultTx.addOutput(new TransactionOutput(params, resultTx, Coin.ZERO, ScriptBuilder.createOpReturnScript(opReturnData).getProgram())); numInputs = resultTx.getInputs().size(); txSizeWithUnsignedInputs = resultTx.bitcoinSerialize().length; final long estimatedFeeAsLong = txFeePerByte.multiply(txSizeWithUnsignedInputs + sigSizePerInput * numInputs).value; // calculated fee must be inside of a tolerance range with tx fee isFeeOutsideTolerance = Math.abs(resultTx.getFee().value - estimatedFeeAsLong) > 1000; } while (isFeeOutsideTolerance); // Sign all BTC inputs signAllBtcInputs(indexOfBtcFirstInput, resultTx); checkWalletConsistency(wallet); verifyTransaction(resultTx); // printTx("BTC wallet: Signed tx", resultTx); return resultTx; } //TODO Similar like completePreparedCompensationRequestTx but without second output for BSQ issuance public Transaction completePreparedProposalTx(Transaction preparedBurnFeeTx, byte[] opReturnData) { try { //TODO dummy return completePreparedCompensationRequestTx(Coin.valueOf(10000), getFreshAddressEntry().getAddress(), preparedBurnFeeTx, opReturnData); } catch (TransactionVerificationException | InsufficientMoneyException | WalletException e) { e.printStackTrace(); } throw new RuntimeException("completePreparedGenericProposalTx not implemented yet."); } /////////////////////////////////////////////////////////////////////////////////////////// // Blind vote tx /////////////////////////////////////////////////////////////////////////////////////////// // We add BTC inputs to pay miner fees and sign the BTC tx inputs // (BsqFee)tx has following structure: // inputs [1-n] BSQ inputs (fee + stake) // outputs [1] BSQ stake // outputs [0-1] BSQ change output (>= 546 Satoshi) // preparedVoteTx has following structure: // inputs [1-n] BSQ inputs (fee + stake) // inputs [1-n] BTC inputs for miner fee // outputs [1] BSQ stake // outputs [0-1] BSQ change output (>= 546 Satoshi) // outputs [0-1] BTC change output from miner fee inputs (>= 546 Satoshi) // outputs [1] OP_RETURN with opReturnData and amount 0 // mining fee: BTC mining fee + burned BSQ fee public Transaction completePreparedBlindVoteTx(Transaction preparedTx, byte[] opReturnData) throws TransactionVerificationException, WalletException, InsufficientMoneyException { // First input index for btc inputs (they get added after bsq inputs) return completePreparedBsqTxWithBtcFee(preparedTx, opReturnData); } private Transaction completePreparedBsqTxWithBtcFee(Transaction preparedTx, byte[] opReturnData) throws InsufficientMoneyException, TransactionVerificationException, WalletException { // Remember index for first BTC input int indexOfBtcFirstInput = preparedTx.getInputs().size(); Transaction tx = addInputsForMinerFee(preparedTx, opReturnData); signAllBtcInputs(indexOfBtcFirstInput, tx); checkWalletConsistency(wallet); verifyTransaction(tx); // printTx("BTC wallet: Signed tx", tx); return tx; } private Transaction addInputsForMinerFee(Transaction preparedTx, byte[] opReturnData) throws InsufficientMoneyException { // safety check counter to avoid endless loops int counter = 0; // estimated size of input sig final int sigSizePerInput = 106; // typical size for a tx with 3 inputs int txSizeWithUnsignedInputs = 300; final Coin txFeePerByte = feeService.getTxFeePerByte(); Address changeAddress = getFreshAddressEntry().getAddress(); checkNotNull(changeAddress, "changeAddress must not be null"); final BtcCoinSelector coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE)); final List<TransactionInput> preparedBsqTxInputs = preparedTx.getInputs(); final List<TransactionOutput> preparedBsqTxOutputs = preparedTx.getOutputs(); int numInputs = preparedBsqTxInputs.size(); Transaction resultTx = null; boolean isFeeOutsideTolerance; do { counter++; if (counter >= 10) { checkNotNull(resultTx, "resultTx must not be null"); log.error("Could not calculate the fee. Tx=" + resultTx); break; } Transaction tx = new Transaction(params); preparedBsqTxInputs.forEach(tx::addInput); preparedBsqTxOutputs.forEach(tx::addOutput); SendRequest sendRequest = SendRequest.forTx(tx); sendRequest.shuffleOutputs = false; sendRequest.aesKey = aesKey; // signInputs needs to be false as it would try to sign all inputs (BSQ inputs are not in this wallet) sendRequest.signInputs = false; sendRequest.fee = txFeePerByte.multiply(txSizeWithUnsignedInputs + sigSizePerInput * numInputs); sendRequest.feePerKb = Coin.ZERO; sendRequest.ensureMinRequiredFee = false; sendRequest.coinSelector = coinSelector; sendRequest.changeAddress = changeAddress; wallet.completeTx(sendRequest); resultTx = sendRequest.tx; // add OP_RETURN output resultTx.addOutput(new TransactionOutput(params, resultTx, Coin.ZERO, ScriptBuilder.createOpReturnScript(opReturnData).getProgram())); numInputs = resultTx.getInputs().size(); txSizeWithUnsignedInputs = resultTx.bitcoinSerialize().length; final long estimatedFeeAsLong = txFeePerByte.multiply(txSizeWithUnsignedInputs + sigSizePerInput * numInputs).value; // calculated fee must be inside of a tolerance range with tx fee isFeeOutsideTolerance = Math.abs(resultTx.getFee().value - estimatedFeeAsLong) > 1000; } while (isFeeOutsideTolerance); return resultTx; } private void signAllBtcInputs(int indexOfBtcFirstInput, Transaction tx) throws TransactionVerificationException { for (int i = indexOfBtcFirstInput; i < tx.getInputs().size(); i++) { TransactionInput input = tx.getInputs().get(i); checkArgument(input.getConnectedOutput() != null && input.getConnectedOutput().isMine(wallet), "input.getConnectedOutput() is not in our wallet. That must not happen."); signTransactionInput(wallet, aesKey, tx, input, i); checkScriptSig(tx, input, i); } } /////////////////////////////////////////////////////////////////////////////////////////// // Vote reveal tx /////////////////////////////////////////////////////////////////////////////////////////// // We add BTC fees to the prepared reveal tx // (BsqFee)tx has following structure: // inputs [1] BSQ input (stake) // output [1] BSQ unlocked stake // preparedVoteTx has following structure: // inputs [1] BSQ inputs (stake) // inputs [1-n] BTC inputs for miner fee // outputs [1] BSQ unlocked stake // outputs [0-1] BTC change output from miner fee inputs (>= 546 Satoshi) // outputs [1] OP_RETURN with opReturnData and amount 0 // mining fee: BTC mining fee + burned BSQ fee public Transaction completePreparedVoteRevealTx(Transaction preparedTx, byte[] opReturnData) throws TransactionVerificationException, WalletException, InsufficientMoneyException { return completePreparedBsqTxWithBtcFee(preparedTx, opReturnData); } /////////////////////////////////////////////////////////////////////////////////////////// // Add fee input to prepared BSQ send tx /////////////////////////////////////////////////////////////////////////////////////////// public Transaction completePreparedSendBsqTx(Transaction preparedBsqTx, boolean isSendTx) throws TransactionVerificationException, WalletException, InsufficientMoneyException { // preparedBsqTx has following structure: // inputs [1-n] BSQ inputs // outputs [0-1] BSQ receivers output // outputs [0-1] BSQ change output // We add BTC mining fee. Result tx looks like: // inputs [1-n] BSQ inputs // inputs [1-n] BTC inputs // outputs [0-1] BSQ receivers output // outputs [0-1] BSQ change output // outputs [0-1] BTC change output // mining fee: BTC mining fee return completePreparedBsqTx(preparedBsqTx, isSendTx, null); } public Transaction completePreparedBsqTx(Transaction preparedBsqTx, boolean useCustomTxFee, @Nullable byte[] opReturnData) throws TransactionVerificationException, WalletException, InsufficientMoneyException { // preparedBsqTx has following structure: // inputs [1-n] BSQ inputs // outputs [0-1] BSQ receivers output // outputs [0-1] BSQ change output // mining fee: optional burned BSQ fee (only if opReturnData != null) // We add BTC mining fee. Result tx looks like: // inputs [1-n] BSQ inputs // inputs [1-n] BTC inputs // outputs [0-1] BSQ receivers output // outputs [0-1] BSQ change output // outputs [0-1] BTC change output // outputs [0-1] OP_RETURN with opReturnData (only if opReturnData != null) // mining fee: BTC mining fee + optional burned BSQ fee (only if opReturnData != null) // In case of txs for burned BSQ fees we have no receiver output and it might be that there is no change outputs // We need to guarantee that min. 1 valid output is added (OP_RETURN does not count). So we use a higher input // for BTC to force an additional change output. // safety check counter to avoid endless loops int counter = 0; // estimated size of input sig final int sigSizePerInput = 106; // typical size for a tx with 2 inputs int txSizeWithUnsignedInputs = 203; // If useCustomTxFee we allow overriding the estimated fee from preferences final Coin txFeePerByte = useCustomTxFee ? getTxFeeForWithdrawalPerByte() : feeService.getTxFeePerByte(); // In case there are no change outputs we force a change by adding min dust to the BTC input Coin forcedChangeValue = Coin.ZERO; Address changeAddress = getFreshAddressEntry().getAddress(); checkNotNull(changeAddress, "changeAddress must not be null"); final BtcCoinSelector coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE)); final List<TransactionInput> preparedBsqTxInputs = preparedBsqTx.getInputs(); final List<TransactionOutput> preparedBsqTxOutputs = preparedBsqTx.getOutputs(); int numInputs = preparedBsqTxInputs.size() + 1; // We add 1 for the BTC fee input Transaction resultTx = null; boolean isFeeOutsideTolerance; boolean opReturnIsOnlyOutput; do { counter++; if (counter >= 10) { checkNotNull(resultTx, "resultTx must not be null"); log.error("Could not calculate the fee. Tx=" + resultTx); break; } Transaction tx = new Transaction(params); preparedBsqTxInputs.stream().forEach(tx::addInput); if (forcedChangeValue.isZero()) { preparedBsqTxOutputs.stream().forEach(tx::addOutput); } else { //TODO test that case checkArgument(preparedBsqTxOutputs.size() == 0, "preparedBsqTxOutputs.size must be null in that code branch"); tx.addOutput(forcedChangeValue, changeAddress); } SendRequest sendRequest = SendRequest.forTx(tx); sendRequest.shuffleOutputs = false; sendRequest.aesKey = aesKey; // signInputs needs to be false as it would try to sign all inputs (BSQ inputs are not in this wallet) sendRequest.signInputs = false; sendRequest.fee = txFeePerByte.multiply(txSizeWithUnsignedInputs + sigSizePerInput * numInputs); sendRequest.feePerKb = Coin.ZERO; sendRequest.ensureMinRequiredFee = false; sendRequest.coinSelector = coinSelector; sendRequest.changeAddress = changeAddress; wallet.completeTx(sendRequest); resultTx = sendRequest.tx; // We might have the rare case that both inputs matched the required fees, so both did not require // a change output. // In such cases we need to add artificially a change output (OP_RETURN is not allowed as only output) opReturnIsOnlyOutput = resultTx.getOutputs().size() == 0; forcedChangeValue = opReturnIsOnlyOutput ? Restrictions.getMinNonDustOutput() : Coin.ZERO; // add OP_RETURN output if (opReturnData != null) resultTx.addOutput(new TransactionOutput(params, resultTx, Coin.ZERO, ScriptBuilder.createOpReturnScript(opReturnData).getProgram())); numInputs = resultTx.getInputs().size(); txSizeWithUnsignedInputs = resultTx.bitcoinSerialize().length; final long estimatedFeeAsLong = txFeePerByte.multiply(txSizeWithUnsignedInputs + sigSizePerInput * numInputs).value; // calculated fee must be inside of a tolerance range with tx fee isFeeOutsideTolerance = Math.abs(resultTx.getFee().value - estimatedFeeAsLong) > 1000; } while (opReturnIsOnlyOutput || isFeeOutsideTolerance || resultTx.getFee().value < txFeePerByte.multiply(resultTx.bitcoinSerialize().length).value); // Sign all BTC inputs signAllBtcInputs(preparedBsqTxInputs.size(), resultTx); checkWalletConsistency(wallet); verifyTransaction(resultTx); printTx("BTC wallet: Signed tx", resultTx); return resultTx; } /////////////////////////////////////////////////////////////////////////////////////////// // Commit tx /////////////////////////////////////////////////////////////////////////////////////////// public void commitTx(Transaction tx) { wallet.commitTx(tx); // printTx("BTC commit Tx", tx); } /////////////////////////////////////////////////////////////////////////////////////////// // AddressEntry /////////////////////////////////////////////////////////////////////////////////////////// public Optional<AddressEntry> getAddressEntry(String offerId, @SuppressWarnings("SameParameterValue") AddressEntry.Context context) { return getAddressEntryListAsImmutableList().stream() .filter(e -> offerId.equals(e.getOfferId())) .filter(e -> context == e.getContext()) .findAny(); } public AddressEntry getOrCreateAddressEntry(String offerId, AddressEntry.Context context) { Optional<AddressEntry> addressEntry = getAddressEntryListAsImmutableList().stream() .filter(e -> offerId.equals(e.getOfferId())) .filter(e -> context == e.getContext()) .findAny(); if (addressEntry.isPresent()) { return addressEntry.get(); } else { // We try to use available and not yet used entries Optional<AddressEntry> emptyAvailableAddressEntry = getAddressEntryListAsImmutableList().stream() .filter(e -> AddressEntry.Context.AVAILABLE == e.getContext()) .filter(e -> isAddressUnused(e.getAddress())) .findAny(); if (emptyAvailableAddressEntry.isPresent()) { return addressEntryList.swapAvailableToAddressEntryWithOfferId(emptyAvailableAddressEntry.get(), context, offerId); } else { AddressEntry entry = new AddressEntry(wallet.freshReceiveKey(), context, offerId); addressEntryList.addAddressEntry(entry); return entry; } } } public AddressEntry getArbitratorAddressEntry() { AddressEntry.Context context = AddressEntry.Context.ARBITRATOR; Optional<AddressEntry> addressEntry = getAddressEntryListAsImmutableList().stream() .filter(e -> context == e.getContext()) .findAny(); return getOrCreateAddressEntry(context, addressEntry); } public AddressEntry getFreshAddressEntry() { AddressEntry.Context context = AddressEntry.Context.AVAILABLE; Optional<AddressEntry> addressEntry = getAddressEntryListAsImmutableList().stream() .filter(e -> context == e.getContext()) .filter(e -> isAddressUnused(e.getAddress())) .findAny(); return getOrCreateAddressEntry(context, addressEntry); } private AddressEntry getOrCreateAddressEntry(AddressEntry.Context context, Optional<AddressEntry> addressEntry) { if (addressEntry.isPresent()) { return addressEntry.get(); } else { AddressEntry entry = new AddressEntry(wallet.freshReceiveKey(), context); addressEntryList.addAddressEntry(entry); return entry; } } private Optional<AddressEntry> findAddressEntry(String address, AddressEntry.Context context) { return getAddressEntryListAsImmutableList().stream() .filter(e -> address.equals(e.getAddressString())) .filter(e -> context == e.getContext()) .findAny(); } public List<AddressEntry> getAvailableAddressEntries() { return getAddressEntryListAsImmutableList().stream() .filter(addressEntry -> AddressEntry.Context.AVAILABLE == addressEntry.getContext()) .collect(Collectors.toList()); } public List<AddressEntry> getAddressEntriesForOpenOffer() { return getAddressEntryListAsImmutableList().stream() .filter(addressEntry -> AddressEntry.Context.OFFER_FUNDING == addressEntry.getContext() || AddressEntry.Context.RESERVED_FOR_TRADE == addressEntry.getContext()) .collect(Collectors.toList()); } public List<AddressEntry> getAddressEntriesForTrade() { return getAddressEntryListAsImmutableList().stream() .filter(addressEntry -> AddressEntry.Context.MULTI_SIG == addressEntry.getContext() || AddressEntry.Context.TRADE_PAYOUT == addressEntry.getContext()) .collect(Collectors.toList()); } public List<AddressEntry> getAddressEntries(AddressEntry.Context context) { return getAddressEntryListAsImmutableList().stream() .filter(addressEntry -> context == addressEntry.getContext()) .collect(Collectors.toList()); } public List<AddressEntry> getFundedAvailableAddressEntries() { return getAvailableAddressEntries().stream() .filter(addressEntry -> getBalanceForAddress(addressEntry.getAddress()).isPositive()) .collect(Collectors.toList()); } public List<AddressEntry> getAddressEntryListAsImmutableList() { return ImmutableList.copyOf(addressEntryList.getList()); } public void swapTradeEntryToAvailableEntry(String offerId, AddressEntry.Context context) { Optional<AddressEntry> addressEntryOptional = getAddressEntryListAsImmutableList().stream() .filter(e -> offerId.equals(e.getOfferId())) .filter(e -> context == e.getContext()) .findAny(); addressEntryOptional.ifPresent(e -> { log.info("swap addressEntry with address {} and offerId {} from context {} to available", e.getAddressString(), e.getOfferId(), context); addressEntryList.swapToAvailable(e); saveAddressEntryList(); }); } public void resetAddressEntriesForOpenOffer(String offerId) { swapTradeEntryToAvailableEntry(offerId, AddressEntry.Context.OFFER_FUNDING); swapTradeEntryToAvailableEntry(offerId, AddressEntry.Context.RESERVED_FOR_TRADE); } public void resetAddressEntriesForPendingTrade(String offerId) { swapTradeEntryToAvailableEntry(offerId, AddressEntry.Context.MULTI_SIG); // Don't swap TRADE_PAYOUT as it might be still open in the last trade step to be used for external transfer } public void swapAnyTradeEntryContextToAvailableEntry(String offerId) { resetAddressEntriesForOpenOffer(offerId); resetAddressEntriesForPendingTrade(offerId); } public void saveAddressEntryList() { addressEntryList.persist(); } public DeterministicKey getMultiSigKeyPair(String tradeId, byte[] pubKey) { Optional<AddressEntry> multiSigAddressEntryOptional = getAddressEntry(tradeId, AddressEntry.Context.MULTI_SIG); DeterministicKey multiSigKeyPair; if (multiSigAddressEntryOptional.isPresent()) { AddressEntry multiSigAddressEntry = multiSigAddressEntryOptional.get(); multiSigKeyPair = multiSigAddressEntry.getKeyPair(); if (!Arrays.equals(pubKey, multiSigAddressEntry.getPubKey())) { log.error("Pub Key from AddressEntry does not match key pair from trade data. Trade ID={}\n" + "We try to find the keypair in the wallet with the pubKey we found in the trade data.", tradeId); multiSigKeyPair = findKeyFromPubKey(pubKey); } } else { log.error("multiSigAddressEntry not found for trade ID={}.\n" + "We try to find the keypair in the wallet with the pubKey we found in the trade data.", tradeId); multiSigKeyPair = findKeyFromPubKey(pubKey); } return multiSigKeyPair; } /////////////////////////////////////////////////////////////////////////////////////////// // Balance /////////////////////////////////////////////////////////////////////////////////////////// public Coin getSavingWalletBalance() { return Coin.valueOf(getFundedAvailableAddressEntries().stream() .mapToLong(addressEntry -> getBalanceForAddress(addressEntry.getAddress()).value) .sum()); } /////////////////////////////////////////////////////////////////////////////////////////// // Double spend unconfirmed transaction (unlock in case we got into a tx with a too low mining fee) /////////////////////////////////////////////////////////////////////////////////////////// public void doubleSpendTransaction(String txId, Runnable resultHandler, ErrorMessageHandler errorMessageHandler) throws InsufficientFundsException { AddressEntry addressEntry = getFreshAddressEntry(); checkNotNull(addressEntry.getAddress(), "addressEntry.getAddress() must not be null"); Optional<Transaction> transactionOptional = wallet.getTransactions(true).stream() .filter(t -> t.getHashAsString().equals(txId)) .findAny(); if (transactionOptional.isPresent()) { Transaction txToDoubleSpend = transactionOptional.get(); Address toAddress = addressEntry.getAddress(); final TransactionConfidence.ConfidenceType confidenceType = txToDoubleSpend.getConfidence().getConfidenceType(); if (confidenceType == TransactionConfidence.ConfidenceType.PENDING) { log.debug("txToDoubleSpend no. of inputs " + txToDoubleSpend.getInputs().size()); Transaction newTransaction = new Transaction(params); txToDoubleSpend.getInputs().stream().forEach(input -> { final TransactionOutput connectedOutput = input.getConnectedOutput(); if (connectedOutput != null && connectedOutput.isMine(wallet) && connectedOutput.getParentTransaction() != null && connectedOutput.getParentTransaction().getConfidence() != null && input.getValue() != null) { //if (connectedOutput.getParentTransaction().getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING) { newTransaction.addInput(new TransactionInput(params, newTransaction, new byte[]{}, new TransactionOutPoint(params, input.getOutpoint().getIndex(), new Transaction(params, connectedOutput.getParentTransaction().bitcoinSerialize())), Coin.valueOf(input.getValue().value))); /* } else { log.warn("Confidence of parent tx is not of type BUILDING: ConfidenceType=" + connectedOutput.getParentTransaction().getConfidence().getConfidenceType()); }*/ } } ); log.info("newTransaction no. of inputs " + newTransaction.getInputs().size()); log.info("newTransaction size in kB " + newTransaction.bitcoinSerialize().length / 1024); if (!newTransaction.getInputs().isEmpty()) { Coin amount = Coin.valueOf(newTransaction.getInputs().stream() .mapToLong(input -> input.getValue() != null ? input.getValue().value : 0) .sum()); newTransaction.addOutput(amount, toAddress); try { Coin fee; int counter = 0; int txSize = 0; Transaction tx; SendRequest sendRequest; Coin txFeeForWithdrawalPerByte = getTxFeeForWithdrawalPerByte(); do { counter++; fee = txFeeForWithdrawalPerByte.multiply(txSize); newTransaction.clearOutputs(); newTransaction.addOutput(amount.subtract(fee), toAddress); sendRequest = SendRequest.forTx(newTransaction); sendRequest.fee = fee; sendRequest.feePerKb = Coin.ZERO; sendRequest.ensureMinRequiredFee = false; sendRequest.aesKey = aesKey; sendRequest.coinSelector = new BtcCoinSelector(toAddress); sendRequest.changeAddress = toAddress; wallet.completeTx(sendRequest); tx = sendRequest.tx; txSize = tx.bitcoinSerialize().length; printTx("FeeEstimationTransaction", tx); sendRequest.tx.getOutputs().forEach(o -> log.debug("Output value " + o.getValue().toFriendlyString())); } while (feeEstimationNotSatisfied(counter, tx)); if (counter == 10) log.error("Could not calculate the fee. Tx=" + tx); Wallet.SendResult sendResult = null; try { sendRequest = SendRequest.forTx(newTransaction); sendRequest.fee = fee; sendRequest.feePerKb = Coin.ZERO; sendRequest.ensureMinRequiredFee = false; sendRequest.aesKey = aesKey; sendRequest.coinSelector = new BtcCoinSelector(toAddress); sendRequest.changeAddress = toAddress; sendResult = wallet.sendCoins(sendRequest); } catch (InsufficientMoneyException e) { // in some cases getFee did not calculate correctly and we still get an InsufficientMoneyException log.warn("We still have a missing fee " + (e.missing != null ? e.missing.toFriendlyString() : "")); amount = amount.subtract(e.missing); newTransaction.clearOutputs(); newTransaction.addOutput(amount, toAddress); sendRequest = SendRequest.forTx(newTransaction); sendRequest.fee = fee; sendRequest.feePerKb = Coin.ZERO; sendRequest.ensureMinRequiredFee = false; sendRequest.aesKey = aesKey; sendRequest.coinSelector = new BtcCoinSelector(toAddress, false); sendRequest.changeAddress = toAddress; try { sendResult = wallet.sendCoins(sendRequest); printTx("FeeEstimationTransaction", newTransaction); } catch (InsufficientMoneyException e2) { errorMessageHandler.handleErrorMessage("We did not get the correct fee calculated. " + (e2.missing != null ? e2.missing.toFriendlyString() : "")); } } if (sendResult != null) { log.info("Broadcasting double spending transaction. " + sendResult.tx); Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<Transaction>() { @Override public void onSuccess(Transaction result) { log.info("Double spending transaction published. " + result); resultHandler.run(); } @Override public void onFailure(@NotNull Throwable t) { log.error("Broadcasting double spending transaction failed. " + t.getMessage()); errorMessageHandler.handleErrorMessage(t.getMessage()); } }); } } catch (InsufficientMoneyException e) { throw new InsufficientFundsException("The fees for that transaction exceed the available funds " + "or the resulting output value is below the min. dust value:\n" + "Missing " + (e.missing != null ? e.missing.toFriendlyString() : "null")); } } else { String errorMessage = "We could not find inputs we control in the transaction we want to double spend."; log.warn(errorMessage); errorMessageHandler.handleErrorMessage(errorMessage); } } else if (confidenceType == TransactionConfidence.ConfidenceType.BUILDING) { errorMessageHandler.handleErrorMessage("That transaction is already in the blockchain so we cannot double spend it."); } else if (confidenceType == TransactionConfidence.ConfidenceType.DEAD) { errorMessageHandler.handleErrorMessage("One of the inputs of that transaction has been already double spent."); } } } /////////////////////////////////////////////////////////////////////////////////////////// // Withdrawal Fee calculation /////////////////////////////////////////////////////////////////////////////////////////// public Transaction getFeeEstimationTransaction(String fromAddress, String toAddress, Coin amount, AddressEntry.Context context) throws AddressFormatException, AddressEntryException, InsufficientFundsException { Optional<AddressEntry> addressEntry = findAddressEntry(fromAddress, context); if (!addressEntry.isPresent()) throw new AddressEntryException("WithdrawFromAddress is not found in our wallet."); checkNotNull(addressEntry.get().getAddress(), "addressEntry.get().getAddress() must nto be null"); try { Coin fee; int counter = 0; int txSize = 0; Transaction tx; Coin txFeeForWithdrawalPerByte = getTxFeeForWithdrawalPerByte(); do { counter++; fee = txFeeForWithdrawalPerByte.multiply(txSize); SendRequest sendRequest = getSendRequest(fromAddress, toAddress, amount, fee, aesKey, context); wallet.completeTx(sendRequest); tx = sendRequest.tx; txSize = tx.bitcoinSerialize().length; printTx("FeeEstimationTransaction", tx); } while (feeEstimationNotSatisfied(counter, tx)); if (counter == 10) log.error("Could not calculate the fee. Tx=" + tx); return tx; } catch (InsufficientMoneyException e) { throw new InsufficientFundsException("The fees for that transaction exceed the available funds " + "or the resulting output value is below the min. dust value:\n" + "Missing " + (e.missing != null ? e.missing.toFriendlyString() : "null")); } } public Transaction getFeeEstimationTransactionForMultipleAddresses(Set<String> fromAddresses, Coin amount) throws AddressFormatException, AddressEntryException, InsufficientFundsException { Set<AddressEntry> addressEntries = fromAddresses.stream() .map(address -> { Optional<AddressEntry> addressEntryOptional = findAddressEntry(address, AddressEntry.Context.AVAILABLE); if (!addressEntryOptional.isPresent()) addressEntryOptional = findAddressEntry(address, AddressEntry.Context.OFFER_FUNDING); if (!addressEntryOptional.isPresent()) addressEntryOptional = findAddressEntry(address, AddressEntry.Context.TRADE_PAYOUT); if (!addressEntryOptional.isPresent()) addressEntryOptional = findAddressEntry(address, AddressEntry.Context.ARBITRATOR); return addressEntryOptional; }) .filter(Optional::isPresent) .map(Optional::get) .collect(Collectors.toSet()); if (addressEntries.isEmpty()) throw new AddressEntryException("No Addresses for withdraw found in our wallet"); try { Coin fee; int counter = 0; int txSize = 0; Transaction tx; Coin txFeeForWithdrawalPerByte = getTxFeeForWithdrawalPerByte(); do { counter++; fee = txFeeForWithdrawalPerByte.multiply(txSize); // We use a dummy address for the output final String dummyReceiver = getFreshAddressEntry().getAddressString(); SendRequest sendRequest = getSendRequestForMultipleAddresses(fromAddresses, dummyReceiver, amount, fee, null, aesKey); wallet.completeTx(sendRequest); tx = sendRequest.tx; txSize = tx.bitcoinSerialize().length; printTx("FeeEstimationTransactionForMultipleAddresses", tx); } while (feeEstimationNotSatisfied(counter, tx)); if (counter == 10) log.error("Could not calculate the fee. Tx=" + tx); return tx; } catch (InsufficientMoneyException e) { throw new InsufficientFundsException("The fees for that transaction exceed the available funds " + "or the resulting output value is below the min. dust value:\n" + "Missing " + (e.missing != null ? e.missing.toFriendlyString() : "null")); } } private boolean feeEstimationNotSatisfied(int counter, Transaction tx) { long targetFee = getTxFeeForWithdrawalPerByte().multiply(tx.bitcoinSerialize().length).value; return counter < 10 && (tx.getFee().value < targetFee || tx.getFee().value - targetFee > 1000); } /////////////////////////////////////////////////////////////////////////////////////////// // Withdrawal Send /////////////////////////////////////////////////////////////////////////////////////////// public String sendFunds(String fromAddress, String toAddress, Coin receiverAmount, Coin fee, @Nullable KeyParameter aesKey, @SuppressWarnings("SameParameterValue") AddressEntry.Context context, FutureCallback<Transaction> callback) throws AddressFormatException, AddressEntryException, InsufficientMoneyException { SendRequest sendRequest = getSendRequest(fromAddress, toAddress, receiverAmount, fee, aesKey, context); Wallet.SendResult sendResult = wallet.sendCoins(sendRequest); Futures.addCallback(sendResult.broadcastComplete, callback); printTx("sendFunds", sendResult.tx); return sendResult.tx.getHashAsString(); } public String sendFundsForMultipleAddresses(Set<String> fromAddresses, String toAddress, Coin receiverAmount, Coin fee, @Nullable String changeAddress, @Nullable KeyParameter aesKey, FutureCallback<Transaction> callback) throws AddressFormatException, AddressEntryException, InsufficientMoneyException { SendRequest request = getSendRequestForMultipleAddresses(fromAddresses, toAddress, receiverAmount, fee, changeAddress, aesKey); Wallet.SendResult sendResult = wallet.sendCoins(request); Futures.addCallback(sendResult.broadcastComplete, callback); printTx("sendFunds", sendResult.tx); return sendResult.tx.getHashAsString(); } private SendRequest getSendRequest(String fromAddress, String toAddress, Coin amount, Coin fee, @Nullable KeyParameter aesKey, AddressEntry.Context context) throws AddressFormatException, AddressEntryException { Transaction tx = new Transaction(params); final Coin receiverAmount = amount.subtract(fee); Preconditions.checkArgument(Restrictions.isAboveDust(receiverAmount), "The amount is too low (dust limit)."); tx.addOutput(receiverAmount, Address.fromBase58(params, toAddress)); SendRequest sendRequest = SendRequest.forTx(tx); sendRequest.fee = fee; sendRequest.feePerKb = Coin.ZERO; sendRequest.ensureMinRequiredFee = false; sendRequest.aesKey = aesKey; sendRequest.shuffleOutputs = false; Optional<AddressEntry> addressEntry = findAddressEntry(fromAddress, context); if (!addressEntry.isPresent()) throw new AddressEntryException("WithdrawFromAddress is not found in our wallet."); checkNotNull(addressEntry.get(), "addressEntry.get() must not be null"); checkNotNull(addressEntry.get().getAddress(), "addressEntry.get().getAddress() must not be null"); sendRequest.coinSelector = new BtcCoinSelector(addressEntry.get().getAddress()); sendRequest.changeAddress = addressEntry.get().getAddress(); return sendRequest; } private SendRequest getSendRequestForMultipleAddresses(Set<String> fromAddresses, String toAddress, Coin amount, Coin fee, @Nullable String changeAddress, @Nullable KeyParameter aesKey) throws AddressFormatException, AddressEntryException, InsufficientMoneyException { Transaction tx = new Transaction(params); checkArgument(Restrictions.isAboveDust(amount), "The amount is too low (dust limit)."); final Coin netValue = amount.subtract(fee); if (netValue.isNegative()) throw new InsufficientMoneyException(netValue.multiply(-1), "The mining fee for that transaction exceed the available amount."); tx.addOutput(netValue, Address.fromBase58(params, toAddress)); SendRequest sendRequest = SendRequest.forTx(tx); sendRequest.fee = fee; sendRequest.feePerKb = Coin.ZERO; sendRequest.ensureMinRequiredFee = false; sendRequest.aesKey = aesKey; sendRequest.shuffleOutputs = false; Set<AddressEntry> addressEntries = fromAddresses.stream() .map(address -> { Optional<AddressEntry> addressEntryOptional = findAddressEntry(address, AddressEntry.Context.AVAILABLE); if (!addressEntryOptional.isPresent()) addressEntryOptional = findAddressEntry(address, AddressEntry.Context.OFFER_FUNDING); if (!addressEntryOptional.isPresent()) addressEntryOptional = findAddressEntry(address, AddressEntry.Context.TRADE_PAYOUT); if (!addressEntryOptional.isPresent()) addressEntryOptional = findAddressEntry(address, AddressEntry.Context.ARBITRATOR); return addressEntryOptional; }) .filter(Optional::isPresent) .map(Optional::get) .collect(Collectors.toSet()); if (addressEntries.isEmpty()) throw new AddressEntryException("No Addresses for withdraw found in our wallet"); sendRequest.coinSelector = new BtcCoinSelector(walletsSetup.getAddressesFromAddressEntries(addressEntries)); Optional<AddressEntry> addressEntryOptional = Optional.<AddressEntry>empty(); AddressEntry changeAddressAddressEntry = null; if (changeAddress != null) addressEntryOptional = findAddressEntry(changeAddress, AddressEntry.Context.AVAILABLE); changeAddressAddressEntry = addressEntryOptional.orElseGet(() -> getFreshAddressEntry()); checkNotNull(changeAddressAddressEntry, "change address must not be null"); sendRequest.changeAddress = changeAddressAddressEntry.getAddress(); return sendRequest; } }