Add UTXO handling of orphan transactions, Add SquBlockchainManager, add Unittests

This commit is contained in:
Manfred Karrer 2017-01-17 02:56:30 +01:00
parent 37a8d6af94
commit 8f871e2629
30 changed files with 1116 additions and 437 deletions

View file

@ -50,6 +50,7 @@ public class Utilities {
public static final String LB = System.getProperty("line.separator");
public static final String LB2 = LB + LB;
// TODO check out Jackson lib
public static String objectToJson(Object object) {
Gson gson = new GsonBuilder()
.setExclusionStrategies(new AnnotationExclusionStrategy())

View file

@ -128,14 +128,13 @@ public class BtcWalletService extends WalletService {
// Add fee input to prepared SQU send tx
///////////////////////////////////////////////////////////////////////////////////////////
public Transaction completePreparedSquTx(Transaction preparedSquTx, boolean isSendTx, @Nullable byte[] opReturnData) throws
TransactionVerificationException, WalletException, InsufficientFundsException, InsufficientMoneyException {
public Transaction completePreparedSendSquTx(Transaction preparedSquTx, boolean isSendTx) throws
TransactionVerificationException, WalletException, InsufficientFundsException, InsufficientMoneyException {
// preparedSquTx has following structure:
// inputs [1-n] SQU inputs
// outputs [0-1] SQU receivers output
// outputs [0-1] SQU change output
// mining fee: optional burned SQU fee
// We add BTC mining fee. Result tx looks like:
// inputs [1-n] SQU inputs
@ -143,8 +142,27 @@ public class BtcWalletService extends WalletService {
// outputs [0-1] SQU receivers output
// outputs [0-1] SQU change output
// outputs [0-1] BTC change output
// outputs [0-1] OP_RETURN with opReturnData
// mining fee: BTC mining fee + optional burned SQU fee
// mining fee: BTC mining fee
return completePreparedSquTx(preparedSquTx, isSendTx, null);
}
public Transaction completePreparedSquTx(Transaction preparedSquTx, boolean isSendTx, @Nullable byte[] opReturnData) throws
TransactionVerificationException, WalletException, InsufficientFundsException, InsufficientMoneyException {
// preparedSquTx has following structure:
// inputs [1-n] SQU inputs
// outputs [0-1] SQU receivers output
// outputs [0-1] SQU change output
// mining fee: optional burned SQU fee (only if opReturnData != null)
// We add BTC mining fee. Result tx looks like:
// inputs [1-n] SQU inputs
// inputs [1-n] BTC inputs
// outputs [0-1] SQU receivers output
// outputs [0-1] SQU 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 SQU fee (only if opReturnData != null)
// In case of txs for burned SQU 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

View file

@ -25,7 +25,7 @@ import io.bitsquare.btc.exceptions.WalletException;
import io.bitsquare.btc.provider.fee.FeeService;
import io.bitsquare.common.handlers.ErrorMessageHandler;
import io.bitsquare.common.handlers.ResultHandler;
import io.bitsquare.dao.blockchain.BlockchainService;
import io.bitsquare.dao.blockchain.SquBlockchainManager;
import io.bitsquare.dao.blockchain.SquUTXO;
import io.bitsquare.user.Preferences;
import org.bitcoinj.core.*;
@ -51,7 +51,7 @@ import static com.google.common.base.Preconditions.checkArgument;
public class SquWalletService extends WalletService {
private static final Logger log = LoggerFactory.getLogger(SquWalletService.class);
private final BlockchainService blockchainService;
private final SquBlockchainManager squBlockchainManager;
private final SquCoinSelector squCoinSelector;
@ -61,13 +61,13 @@ public class SquWalletService extends WalletService {
@Inject
public SquWalletService(WalletsSetup walletsSetup,
BlockchainService blockchainService,
SquBlockchainManager squBlockchainManager,
Preferences preferences,
FeeService feeService) {
super(walletsSetup,
preferences,
feeService);
this.blockchainService = blockchainService;
this.squBlockchainManager = squBlockchainManager;
this.squCoinSelector = new SquCoinSelector(true);
walletsSetup.addSetupCompletedHandler(() -> {
@ -147,12 +147,12 @@ public class SquWalletService extends WalletService {
///////////////////////////////////////////////////////////////////////////////////////////
public void requestSquUtxo(@Nullable ResultHandler resultHandler, @Nullable ErrorMessageHandler errorMessageHandler) {
if (blockchainService.isUtxoAvailable()) {
applyUtxoSetToUTXOProvider(blockchainService.getUtxoByTxIdMap());
if (squBlockchainManager.isUtxoAvailable()) {
applyUtxoSetToUTXOProvider(squBlockchainManager.getUtxoByTxIdMap());
if (resultHandler != null)
resultHandler.handleResult();
} else {
blockchainService.addUtxoListener(utxoByTxIdMap -> {
squBlockchainManager.addUtxoListener(utxoByTxIdMap -> {
applyUtxoSetToUTXOProvider(utxoByTxIdMap);
if (resultHandler != null)
resultHandler.handleResult();

View file

@ -56,6 +56,8 @@ import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
// Derived from WalletAppKit
// TODO use same seed for both wallets.
// TODo add rolling backup for both wallets
public class WalletConfig extends AbstractIdleService {
private static final Logger log = LoggerFactory.getLogger(WalletConfig.class);

View file

@ -570,7 +570,7 @@ public abstract class WalletService {
///////////////////////////////////////////////////////////////////////////////////////////
class BitsquareWalletEventListener extends AbstractWalletEventListener {
public class BitsquareWalletEventListener extends AbstractWalletEventListener {
@Override
public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
notifyBalanceListeners(tx);

View file

@ -20,21 +20,21 @@ package io.bitsquare.dao;
import com.google.inject.Inject;
import io.bitsquare.btc.provider.squ.SquUtxoFeedService;
import io.bitsquare.btc.wallet.SquWalletService;
import io.bitsquare.dao.blockchain.BlockchainException;
import io.bitsquare.dao.blockchain.BlockchainService;
import io.bitsquare.dao.blockchain.SquBlockchainException;
import io.bitsquare.dao.blockchain.SquBlockchainManager;
import io.bitsquare.dao.compensation.CompensationRequestManager;
import io.bitsquare.dao.vote.VoteManager;
import io.bitsquare.dao.vote.VotingManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DaoManager {
private static final Logger log = LoggerFactory.getLogger(DaoManager.class);
private final BlockchainService blockchainService;
private final SquBlockchainManager squBlockchainManager;
private final SquWalletService squWalletService;
private final DaoPeriodService daoPeriodService;
private final SquUtxoFeedService squUtxoFeedService;
private final VoteManager voteManager;
private final VotingManager voteManager;
private final CompensationRequestManager compensationRequestManager;
///////////////////////////////////////////////////////////////////////////////////////////
@ -42,13 +42,13 @@ public class DaoManager {
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public DaoManager(BlockchainService blockchainService,
public DaoManager(SquBlockchainManager squBlockchainManager,
SquWalletService squWalletService,
DaoPeriodService daoPeriodService,
SquUtxoFeedService squUtxoFeedService,
VoteManager voteManager,
VotingManager voteManager,
CompensationRequestManager compensationRequestManager) {
this.blockchainService = blockchainService;
this.squBlockchainManager = squBlockchainManager;
this.squWalletService = squWalletService;
this.daoPeriodService = daoPeriodService;
this.squUtxoFeedService = squUtxoFeedService;
@ -56,12 +56,12 @@ public class DaoManager {
this.compensationRequestManager = compensationRequestManager;
}
public void onAllServicesInitialized() throws BlockchainException {
public void onAllServicesInitialized() throws SquBlockchainException {
daoPeriodService.onAllServicesInitialized();
squUtxoFeedService.onAllServicesInitialized();
voteManager.onAllServicesInitialized();
compensationRequestManager.onAllServicesInitialized();
blockchainService.onAllServicesInitialized();
squBlockchainManager.onAllServicesInitialized();
}

View file

@ -19,12 +19,13 @@ package io.bitsquare.dao;
import com.google.inject.Singleton;
import io.bitsquare.app.AppModule;
import io.bitsquare.dao.blockchain.BlockchainRpcService;
import io.bitsquare.dao.blockchain.BlockchainService;
import io.bitsquare.dao.blockchain.RpcOptionKeys;
import io.bitsquare.dao.blockchain.SquBlockchainManager;
import io.bitsquare.dao.blockchain.SquBlockchainRpcService;
import io.bitsquare.dao.blockchain.SquBlockchainService;
import io.bitsquare.dao.compensation.CompensationRequestManager;
import io.bitsquare.dao.vote.VoteManager;
import io.bitsquare.dao.vote.VotingDefaultValues;
import io.bitsquare.dao.vote.VotingManager;
import io.bitsquare.dao.vote.VotingService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -42,12 +43,13 @@ public class DaoModule extends AppModule {
@Override
protected void configure() {
bind(DaoManager.class).in(Singleton.class);
bind(BlockchainService.class).to(BlockchainRpcService.class).in(Singleton.class);
bind(SquBlockchainManager.class).in(Singleton.class);
bind(SquBlockchainService.class).to(SquBlockchainRpcService.class).in(Singleton.class);
bind(DaoPeriodService.class).in(Singleton.class);
bind(VotingService.class).in(Singleton.class);
bind(CompensationRequestManager.class).in(Singleton.class);
bind(VoteManager.class).in(Singleton.class);
bind(VotingManager.class).in(Singleton.class);
bind(DaoService.class).in(Singleton.class);
bind(VotingDefaultValues.class).in(Singleton.class);

View file

@ -1,295 +0,0 @@
/*
* This file is part of Bitsquare.
*
* Bitsquare 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.
*
* Bitsquare 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 Bitsquare. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bitsquare.dao.blockchain;
import com.google.inject.Inject;
import io.bitsquare.common.handlers.ErrorMessageHandler;
import io.bitsquare.common.handlers.ResultHandler;
import org.bitcoinj.core.Coin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import static com.google.common.base.Preconditions.checkArgument;
abstract public class BlockchainService {
private static final Logger log = LoggerFactory.getLogger(BlockchainService.class);
// regtest
public static final String GENESIS_TX_ID = "f3eb7bd51d792ecd7df7f74905a1452a43aa4f678ff2c0ded853939b1d2b590e";
public static final int GENESIS_BLOCK_HEIGHT = 102; //at regtest: 700, 447000
protected Map<String, Map<Integer, SquUTXO>> utxoByTxIdMap;
private List<UtxoListener> utxoListeners = new ArrayList<>();
private boolean isUtxoAvailable;
protected int chainHeadHeight;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public BlockchainService() {
}
///////////////////////////////////////////////////////////////////////////////////////////
// Public methods
///////////////////////////////////////////////////////////////////////////////////////////
public void onAllServicesInitialized() {
setup(this::setupComplete, errorMessage -> {
log.error("setup failed" + errorMessage);
});
}
public Map<String, Map<Integer, SquUTXO>> getUtxoByTxIdMap() {
return utxoByTxIdMap;
}
public void addUtxoListener(UtxoListener utxoListener) {
utxoListeners.add(utxoListener);
}
public void removeUtxoListener(UtxoListener utxoListener) {
utxoListeners.remove(utxoListener);
}
public boolean isUtxoAvailable() {
return isUtxoAvailable;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Protected methods
///////////////////////////////////////////////////////////////////////////////////////////
protected void setupComplete() {
syncFromGenesis(utxoByTxIdMap -> {
this.utxoByTxIdMap = utxoByTxIdMap;
isUtxoAvailable = true;
utxoListeners.stream().forEach(e -> e.onUtxoChanged(utxoByTxIdMap));
syncFromGenesisCompete();
}, errorMessage -> {
log.error("syncFromGenesis failed" + errorMessage);
});
}
abstract protected void syncFromGenesisCompete();
abstract protected void setup(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler);
abstract protected void syncFromGenesis(Consumer<Map<String, Map<Integer, SquUTXO>>> resultHandler, ErrorMessageHandler errorMessageHandler);
abstract void parseBlockchainFromGenesis(Map<String, Map<Integer, SquUTXO>> utxoByTxIdMap, int genesisBlockHeight, String genesisTxId) throws BlockchainException;
abstract SquTransaction getSquTransaction(String txId) throws BlockchainException;
void parseBlock(SquBlock block, String genesisTxId, Map<String, Map<Integer, SquUTXO>> utxoByTxIdMap) {
int blockHeight = block.blockHeight;
log.debug("Parse block at height={} ", blockHeight);
for (String txId : block.txIds) {
parseTransaction(txId, genesisTxId, blockHeight, utxoByTxIdMap);
}
}
SquTransaction parseTransaction(String txId, String genesisTxId, int blockHeight, Map<String, Map<Integer, SquUTXO>> utxoByTxIdMap) {
log.debug("Parse transaction with id={} ", txId);
try {
SquTransaction squTransaction = getSquTransaction(txId);
List<SquTxOutput> outputs = squTransaction.outputs;
if (txId.equals(genesisTxId)) {
// Genesis tx uses all outputs as SQU outputs
Map<Integer, SquUTXO> utxoByIndexMap = new HashMap<>();
for (int i = 0; i < outputs.size(); i++) {
SquTxOutput output = outputs.get(i);
List<String> addresses = output.addresses;
// Only at raw MS outputs addresses have more then 1 entry
// We do not support raw MS for SQU
if (addresses.size() == 1) {
String address = addresses.get(0);
//TODO set coinbase to true after testing
SquUTXO utxo = new SquUTXO(txId,
output.index,
output.value,
blockHeight,
false,
output.script,
address);
utxoByIndexMap.put(i, utxo);
}
}
checkArgument(!utxoByIndexMap.isEmpty(), "Genesis tx must have squ utxo");
boolean wasEmpty = utxoByTxIdMap.put(txId, utxoByIndexMap) == null;
checkArgument(wasEmpty, "We must not have that tx in the map. txId=" + txId);
} else if (isIssuanceTx(txId)) {
// Issuance tx has the funding output at index 0 and the SQU output at index 1
if (outputs.size() > 1) {
if (isValidFundingOutput(outputs.get(0))) {
// We have a valid funding tx so we add out issuance output to the utxo
SquTxOutput squOutput = outputs.get(1);
if (isValidIssuanceOutput(squOutput)) {
List<String> addresses = squOutput.addresses;
// Only at raw MS outputs addresses have more then 1 entry
// We do not support raw MS for SQU
if (addresses.size() == 1) {
String address = addresses.get(0);
//TODO set coinbase to true after testing
SquUTXO utxo = new SquUTXO(txId,
squOutput.index,
squOutput.value,
blockHeight,
false,
squOutput.script,
address);
Map<Integer, SquUTXO> utxoByIndexMap = new HashMap<>();
utxoByIndexMap.put(1, utxo);
boolean wasEmpty = utxoByTxIdMap.put(txId, utxoByIndexMap) == null;
checkArgument(wasEmpty, "We must not have that tx in the map. txId=" + txId);
}
}
}
}
} else {
// Other txs
Coin availableValue = Coin.ZERO;
for (SquTxInput input : squTransaction.inputs) {
String spendingTxId = input.spendingTxId;
if (utxoByTxIdMap.containsKey(spendingTxId)) {
Map<Integer, SquUTXO> utxoByIndexMap = utxoByTxIdMap.get(spendingTxId);
Integer index = input.index;
if (utxoByIndexMap.containsKey(index)) {
SquUTXO utxo = utxoByIndexMap.get(index);
availableValue = availableValue.add(utxo.getValue());
utxoByIndexMap.remove(index);
if (utxoByIndexMap.isEmpty()) {
// If no more entries by index we can remove the whole entry by txId
utxoByTxIdMap.remove(spendingTxId);
}
}
}
}
// If we have an input spending tokens we iterate the outputs
if (availableValue.isPositive()) {
Map<Integer, SquUTXO> utxoByIndexMap = utxoByTxIdMap.containsKey(txId) ?
utxoByTxIdMap.get(txId) :
new HashMap<>();
// We sort by index, inputs are tokens as long there is enough input value
for (int i = 0; i < outputs.size(); i++) {
SquTxOutput squOutput = outputs.get(i);
List<String> addresses = squOutput.addresses;
// Only at raw MS outputs addresses have more then 1 entry
// We do not support raw MS for SQU
if (addresses.size() == 1) {
String address = addresses.get(0);
availableValue = availableValue.subtract(squOutput.value);
if (!availableValue.isNegative()) {
// We are spending available tokens
SquUTXO utxo = new SquUTXO(txId,
squOutput.index,
squOutput.value,
blockHeight,
false,
squOutput.script,
address);
utxoByIndexMap.put(i, utxo);
} else {
// As soon we have spent our inputs we can break
break;
}
}
}
if (!utxoByIndexMap.isEmpty() && !utxoByTxIdMap.containsKey(txId)) {
boolean wasEmpty = utxoByTxIdMap.put(txId, utxoByIndexMap) == null;
checkArgument(wasEmpty, "We must not have that tx in the map. txId=" + txId);
}
}
}
if (isRequestPhase(blockHeight)) {
/* Check if tx is in time interval of phase 1
Check if OP_RETURN data starts with version byte and has latest version
Check if burned SQU have been mature
Check if SQU is a valid SQU input
Check if fee payment is correct
Check if there is min. 1 Btc output (small payment to own compensation receiving address)
*/
} else if (isVotingPhase(blockHeight)) {
} else if (isFundingPhase(blockHeight)) {
}
return squTransaction;
} catch (BlockchainException e) {
//TODO throw
log.error(e.toString());
return null;
}
}
boolean isValidIssuanceOutput(SquTxOutput output) {
//TODO
return true;
}
boolean isValidFundingOutput(SquTxOutput output) {
//TODO
return true;
}
boolean isIssuanceTx(String txId) {
// TODO
return false;
}
boolean isRequestPhase(int blockHeight) {
return false;
}
boolean isVotingPhase(int blockHeight) {
return false;
}
boolean isFundingPhase(int blockHeight) {
return false;
}
protected void printUtxoMap(Map<String, Map<Integer, SquUTXO>> utxoByTxIdMap) {
StringBuilder sb = new StringBuilder("utxoByTxIdMap:\n");
utxoByTxIdMap.entrySet().stream().forEach(e -> {
sb.append("TxId: ").append(e.getKey()).append("\n");
e.getValue().entrySet().stream().forEach(a -> {
sb.append(" [").append(a.getKey()).append("] {")
.append(a.getValue().toString()).append("}\n");
});
});
log.info(sb.toString());
}
public int getChainHeadHeight() {
return chainHeadHeight;
}
}

View file

@ -20,16 +20,62 @@ package io.bitsquare.dao.blockchain;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class SquBlock {
private static final Logger log = LoggerFactory.getLogger(SquBlock.class);
public final List<String> txIds;
public final int blockHeight;
public final List<String> txIds;
private Map<String, SquTransaction> squTransactions = new HashMap<>();
public SquBlock(List<String> txIds, int blockHeight) {
this.txIds = txIds;
this.blockHeight = blockHeight;
}
public void addSquTransaction(SquTransaction squTransaction) {
squTransactions.put(squTransaction.txId, squTransaction);
}
public SquTransaction getSquTransaction(String txId) {
return squTransactions.get(txId);
}
public Map<String, SquTransaction> getTransactions() {
return squTransactions;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SquBlock squBlock = (SquBlock) o;
if (blockHeight != squBlock.blockHeight) return false;
if (txIds != null ? !txIds.equals(squBlock.txIds) : squBlock.txIds != null) return false;
return !(squTransactions != null ? !squTransactions.equals(squBlock.squTransactions) : squBlock.squTransactions != null);
}
@Override
public int hashCode() {
int result = blockHeight;
result = 31 * result + (txIds != null ? txIds.hashCode() : 0);
result = 31 * result + (squTransactions != null ? squTransactions.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "SquBlock{" +
"blockHeight=" + blockHeight +
", txIds=" + txIds +
", squTransactions=" + squTransactions +
'}';
}
}

View file

@ -17,12 +17,12 @@
package io.bitsquare.dao.blockchain;
public class BlockchainException extends Exception {
public BlockchainException(String message) {
public class SquBlockchainException extends Exception {
public SquBlockchainException(String message) {
super(message);
}
public BlockchainException(String message, Throwable cause) {
public SquBlockchainException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -0,0 +1,147 @@
/*
* This file is part of Bitsquare.
*
* Bitsquare 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.
*
* Bitsquare 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 Bitsquare. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bitsquare.dao.blockchain;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.inject.Inject;
import io.bitsquare.common.UserThread;
import io.bitsquare.common.util.Tuple2;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class SquBlockchainManager {
private static final Logger log = LoggerFactory.getLogger(SquBlockchainManager.class);
private final SquBlockchainService blockchainService;
// regtest
public static final String GENESIS_TX_ID = "c01129ff48082f8f9613dd505899359227cb71aa457903359cfd0ca9c152dcd6";
public static final int GENESIS_BLOCK_HEIGHT = 103;
protected Map<String, Map<Integer, SquUTXO>> utxoByTxIdMap;
private List<UtxoListener> utxoListeners = new ArrayList<>();
private boolean isUtxoAvailable;
protected int chainHeadHeight;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public SquBlockchainManager(SquBlockchainService blockchainService) {
this.blockchainService = blockchainService;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Public methods
///////////////////////////////////////////////////////////////////////////////////////////
public void onAllServicesInitialized() {
blockchainService.setup(this::setupComplete, errorMessage -> {
log.error("setup failed" + errorMessage);
});
}
public Map<String, Map<Integer, SquUTXO>> getUtxoByTxIdMap() {
return utxoByTxIdMap;
}
public void addUtxoListener(UtxoListener utxoListener) {
utxoListeners.add(utxoListener);
}
public void removeUtxoListener(UtxoListener utxoListener) {
utxoListeners.remove(utxoListener);
}
public boolean isUtxoAvailable() {
return isUtxoAvailable;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Protected methods
///////////////////////////////////////////////////////////////////////////////////////////
protected void setupComplete() {
ListenableFuture<Tuple2<Map<String, Map<Integer, SquUTXO>>, Integer>> future = blockchainService.syncFromGenesis(GENESIS_BLOCK_HEIGHT, GENESIS_TX_ID);
Futures.addCallback(future, new FutureCallback<Tuple2<Map<String, Map<Integer, SquUTXO>>, Integer>>() {
@Override
public void onSuccess(Tuple2<Map<String, Map<Integer, SquUTXO>>, Integer> tulpe) {
UserThread.execute(() -> {
SquBlockchainManager.this.utxoByTxIdMap = tulpe.first;
chainHeadHeight = tulpe.second;
isUtxoAvailable = true;
utxoListeners.stream().forEach(e -> e.onUtxoChanged(utxoByTxIdMap));
blockchainService.syncFromGenesisCompete(GENESIS_TX_ID,
GENESIS_BLOCK_HEIGHT,
block -> {
UserThread.execute(() -> {
log.error("### new block");
try {
blockchainService.parseBlock(new SquBlock(block.getTx(), block.getHeight()),
GENESIS_BLOCK_HEIGHT,
GENESIS_TX_ID,
utxoByTxIdMap);
} catch (SquBlockchainException e) {
//TODO
e.printStackTrace();
}
blockchainService.printUtxoMap(utxoByTxIdMap);
});
});
});
}
@Override
public void onFailure(Throwable throwable) {
UserThread.execute(() -> log.error("syncFromGenesis failed" + throwable.toString()));
}
});
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getters
///////////////////////////////////////////////////////////////////////////////////////////
public int getChainHeadHeight() {
return chainHeadHeight;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Setters
///////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////
// Private methods
///////////////////////////////////////////////////////////////////////////////////////////
}

View file

@ -27,12 +27,11 @@ import com.neemre.btcdcli4j.core.CommunicationException;
import com.neemre.btcdcli4j.core.client.BtcdClientImpl;
import com.neemre.btcdcli4j.core.domain.Block;
import com.neemre.btcdcli4j.core.domain.RawTransaction;
import com.neemre.btcdcli4j.core.domain.Transaction;
import com.neemre.btcdcli4j.daemon.BtcdDaemonImpl;
import com.neemre.btcdcli4j.daemon.event.BlockListener;
import com.neemre.btcdcli4j.daemon.event.WalletListener;
import io.bitsquare.common.handlers.ErrorMessageHandler;
import io.bitsquare.common.handlers.ResultHandler;
import io.bitsquare.common.util.Tuple2;
import io.bitsquare.common.util.Utilities;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
@ -56,8 +55,8 @@ import java.util.stream.Collectors;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.bitcoinj.core.Utils.HEX;
public class BlockchainRpcService extends BlockchainService {
private static final Logger log = LoggerFactory.getLogger(BlockchainRpcService.class);
public class SquBlockchainRpcService extends SquBlockchainService {
private static final Logger log = LoggerFactory.getLogger(SquBlockchainRpcService.class);
private final String rpcUser;
private final String rpcPassword;
@ -75,11 +74,11 @@ public class BlockchainRpcService extends BlockchainService {
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public BlockchainRpcService(@Named(RpcOptionKeys.RPC_USER) String rpcUser,
@Named(RpcOptionKeys.RPC_PASSWORD) String rpcPassword,
@Named(RpcOptionKeys.RPC_PORT) String rpcPort,
@Named(RpcOptionKeys.RPC_BLOCK_PORT) String rpcBlockPort,
@Named(RpcOptionKeys.RPC_WALLET_PORT) String rpcWalletPort) {
public SquBlockchainRpcService(@Named(RpcOptionKeys.RPC_USER) String rpcUser,
@Named(RpcOptionKeys.RPC_PASSWORD) String rpcPassword,
@Named(RpcOptionKeys.RPC_PORT) String rpcPort,
@Named(RpcOptionKeys.RPC_BLOCK_PORT) String rpcBlockPort,
@Named(RpcOptionKeys.RPC_WALLET_PORT) String rpcWalletPort) {
this.rpcUser = rpcUser;
this.rpcPassword = rpcPassword;
this.rpcPort = rpcPort;
@ -93,7 +92,7 @@ public class BlockchainRpcService extends BlockchainService {
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected void setup(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
void setup(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
ListenableFuture<BtcdClientImpl> future = setupExecutorService.submit(() -> {
long startTs = System.currentTimeMillis();
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
@ -114,16 +113,16 @@ public class BlockchainRpcService extends BlockchainService {
log.info("Setup took {} ms", System.currentTimeMillis() - startTs);
return client;
} catch (IOException | BitcoindException | CommunicationException e) {
throw new BlockchainException(e.getMessage(), e);
throw new SquBlockchainException(e.getMessage(), e);
}
} catch (URISyntaxException | IOException e) {
throw new BlockchainException(e.getMessage(), e);
throw new SquBlockchainException(e.getMessage(), e);
}
});
Futures.addCallback(future, new FutureCallback<BtcdClientImpl>() {
public void onSuccess(BtcdClientImpl client) {
BlockchainRpcService.this.client = client;
SquBlockchainRpcService.this.client = client;
resultHandler.handleResult();
}
@ -134,83 +133,61 @@ public class BlockchainRpcService extends BlockchainService {
}
@Override
protected void syncFromGenesis(Consumer<Map<String, Map<Integer, SquUTXO>>> resultHandler, ErrorMessageHandler errorMessageHandler) {
ListenableFuture<Map<String, Map<Integer, SquUTXO>>> future = rpcRequestsExecutor.submit(() -> {
protected ListenableFuture<Tuple2<Map<String, Map<Integer, SquUTXO>>, Integer>> syncFromGenesis(int genesisBlockHeight, String genesisTxId) {
return rpcRequestsExecutor.submit(() -> {
long startTs = System.currentTimeMillis();
Map<String, Map<Integer, SquUTXO>> utxoByTxIdMap = new HashMap<>();
try {
parseBlockchainFromGenesis(utxoByTxIdMap, GENESIS_BLOCK_HEIGHT, GENESIS_TX_ID);
} catch (BlockchainException e) {
throw new BlockchainException(e.getMessage(), e);
}
int chainHeadHeight = requestChainHeadHeight();
parseBlockchain(utxoByTxIdMap, chainHeadHeight, genesisBlockHeight, genesisTxId);
log.info("syncFromGenesis took {} ms", System.currentTimeMillis() - startTs);
return utxoByTxIdMap;
});
Futures.addCallback(future, new FutureCallback<Map<String, Map<Integer, SquUTXO>>>() {
public void onSuccess(Map<String, Map<Integer, SquUTXO>> utxoByTxIdMap) {
resultHandler.accept(utxoByTxIdMap);
}
public void onFailure(@NotNull Throwable throwable) {
errorMessageHandler.handleErrorMessage(throwable.getMessage());
}
return new Tuple2<>(utxoByTxIdMap, chainHeadHeight);
});
}
@Override
protected void syncFromGenesisCompete() {
protected void syncFromGenesisCompete(String genesisTxId, int genesisBlockHeight, Consumer<Block> onNewBlockHandler) {
daemon.addBlockListener(new BlockListener() {
@Override
public void blockDetected(Block block) {
log.info("blockDetected " + block.getHash());
parseBlock(new SquBlock(block.getTx(), block.getHeight()), GENESIS_TX_ID, utxoByTxIdMap);
printUtxoMap(utxoByTxIdMap);
if (onNewBlockHandler != null)
onNewBlockHandler.accept(block);
}
});
daemon.addWalletListener(new WalletListener() {
/* daemon.addWalletListener(new WalletListener() {
@Override
public void walletChanged(Transaction transaction) {
log.info("walletChanged " + transaction.getTxId());
try {
parseTransaction(transaction.getTxId(), GENESIS_TX_ID, client.getBlockCount(), utxoByTxIdMap);
*//* try {
// parseTransaction(transaction.getTxId(), GENESIS_TX_ID, client.getBlockCount(), tempUtxoByTxIdMap, utxoByTxIdMap);
printUtxoMap(utxoByTxIdMap);
} catch (BitcoindException | CommunicationException e) {
} catch (BitcoindException | CommunicationException | SquBlockchainException e) {
//TODO
e.printStackTrace();
}
}*//*
}
});
});*/
}
@Override
void parseBlockchainFromGenesis(Map<String, Map<Integer, SquUTXO>> utxoByTxIdMap, int genesisBlockHeight, String genesisTxId) throws BlockchainException {
try {
chainHeadHeight = client.getBlockCount();
log.info("blockCount=" + chainHeadHeight);
long startTs = System.currentTimeMillis();
for (int i = genesisBlockHeight; i <= chainHeadHeight; i++) {
String blockHash = client.getBlockHash(i);
Block block = client.getBlock(blockHash);
log.info("blockHeight=" + i);
parseBlock(new SquBlock(block.getTx(), block.getHeight()), genesisTxId, utxoByTxIdMap);
}
printUtxoMap(utxoByTxIdMap);
log.info("Took {} ms", System.currentTimeMillis() - startTs);
} catch (BitcoindException | CommunicationException e) {
throw new BlockchainException(e.getMessage(), e);
}
int requestChainHeadHeight() throws BitcoindException, CommunicationException {
return client.getBlockCount();
}
@Override
SquTransaction getSquTransaction(String txId) throws BlockchainException {
Block requestBlock(int blockHeight) throws BitcoindException, CommunicationException {
return client.getBlock(client.getBlockHash(blockHeight));
}
@Override
SquTransaction requestTransaction(String txId) throws SquBlockchainException {
try {
RawTransaction rawTransaction = (RawTransaction) client.getRawTransaction(txId, 1);
RawTransaction rawTransaction = getRawTransaction(txId);
return new SquTransaction(txId,
rawTransaction.getVIn()
.stream()
.filter(e -> e != null && e.getVOut() != null && e.getTxId() != null)
.map(e -> new SquTxInput(e.getVOut(), e.getTxId()))
.filter(rawInput -> rawInput != null && rawInput.getVOut() != null && rawInput.getTxId() != null)
.map(rawInput -> new SquTxInput(rawInput.getVOut(), rawInput.getTxId(), rawTransaction.getHex()))
.collect(Collectors.toList()),
rawTransaction.getVOut()
.stream()
@ -218,10 +195,14 @@ public class BlockchainRpcService extends BlockchainService {
.map(e -> new SquTxOutput(e.getN(),
Coin.valueOf(e.getValue().movePointRight(8).longValue()),
e.getScriptPubKey().getAddresses(),
new Script(HEX.decode(e.getScriptPubKey().getHex()))))
e.getScriptPubKey().getHex() != null ? new Script(HEX.decode(e.getScriptPubKey().getHex())) : null))
.collect(Collectors.toList()));
} catch (BitcoindException | CommunicationException e) {
throw new BlockchainException(e.getMessage(), e);
throw new SquBlockchainException(e.getMessage(), e);
}
}
protected RawTransaction getRawTransaction(String txId) throws BitcoindException, CommunicationException {
return (RawTransaction) client.getRawTransaction(txId, 1);
}
}

View file

@ -24,15 +24,16 @@ import org.slf4j.LoggerFactory;
import java.io.File;
public class BlockchainRpcServiceMain {
private static final Logger log = LoggerFactory.getLogger(BlockchainRpcServiceMain.class);
public class SquBlockchainRpcServiceMain {
private static final Logger log = LoggerFactory.getLogger(SquBlockchainRpcServiceMain.class);
public static void main(String[] args) throws BlockchainException {
public static void main(String[] args) throws SquBlockchainException {
Log.setup(System.getProperty("user.home") + File.separator + "BlockchainRpcServiceMain");
Log.setLevel(Level.WARN);
// regtest uses port 18332, mainnet 8332
BlockchainRpcService blockchainRpcService = new BlockchainRpcService(args[0], args[1], args[2], args[3], args[4]);
blockchainRpcService.onAllServicesInitialized();
SquBlockchainRpcService blockchainRpcService = new SquBlockchainRpcService(args[0], args[1], args[2], args[3], args[4]);
SquBlockchainManager squBlockchainManager = new SquBlockchainManager(blockchainRpcService);
squBlockchainManager.onAllServicesInitialized();
}
}

View file

@ -0,0 +1,291 @@
/*
* This file is part of Bitsquare.
*
* Bitsquare 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.
*
* Bitsquare 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 Bitsquare. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bitsquare.dao.blockchain;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.inject.Inject;
import com.neemre.btcdcli4j.core.BitcoindException;
import com.neemre.btcdcli4j.core.CommunicationException;
import com.neemre.btcdcli4j.core.domain.Block;
import io.bitsquare.common.handlers.ErrorMessageHandler;
import io.bitsquare.common.handlers.ResultHandler;
import io.bitsquare.common.util.Tuple2;
import org.bitcoinj.core.Coin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static com.google.common.base.Preconditions.checkArgument;
abstract public class SquBlockchainService {
private static final Logger log = LoggerFactory.getLogger(SquBlockchainService.class);
protected Map<String, Map<Integer, SquUTXO>> utxoByTxIdMap;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public SquBlockchainService() {
}
///////////////////////////////////////////////////////////////////////////////////////////
// Protected methods
///////////////////////////////////////////////////////////////////////////////////////////
abstract void setup(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler);
abstract ListenableFuture<Tuple2<Map<String, Map<Integer, SquUTXO>>, Integer>> syncFromGenesis(int genesisBlockHeight, String genesisTxId);
abstract void syncFromGenesisCompete(String genesisTxId, int genesisBlockHeight, Consumer<Block> onNewBlockHandler);
abstract int requestChainHeadHeight() throws BitcoindException, CommunicationException;
abstract Block requestBlock(int i) throws BitcoindException, CommunicationException;
abstract SquTransaction requestTransaction(String txId) throws SquBlockchainException;
Map<String, Map<Integer, SquUTXO>> parseBlockchain(Map<String, Map<Integer, SquUTXO>> utxoByTxIdMap,
int chainHeadHeight,
int genesisBlockHeight,
String genesisTxId)
throws SquBlockchainException {
try {
log.info("blockCount=" + chainHeadHeight);
long startTs = System.currentTimeMillis();
for (int blockHeight = genesisBlockHeight; blockHeight <= chainHeadHeight; blockHeight++) {
Block block = requestBlock(blockHeight);
log.info("blockHeight=" + blockHeight);
parseBlock(new SquBlock(block.getTx(), block.getHeight()),
genesisBlockHeight,
genesisTxId,
utxoByTxIdMap);
}
printUtxoMap(utxoByTxIdMap);
log.info("Took {} ms", System.currentTimeMillis() - startTs);
} catch (BitcoindException | CommunicationException e) {
throw new SquBlockchainException(e.getMessage(), e);
}
return utxoByTxIdMap;
}
void parseBlock(SquBlock block,
int genesisBlockHeight,
String genesisTxId,
Map<String, Map<Integer, SquUTXO>> utxoByTxIdMap)
throws SquBlockchainException {
int blockHeight = block.blockHeight;
log.debug("Parse block at height={} ", blockHeight);
// We add all transactions to the block
List<String> txIds = block.txIds;
for (String txId : txIds) {
block.addSquTransaction(requestTransaction(txId));
}
// First we check for the genesis tx
Map<String, SquTransaction> transactionsMap = block.getTransactions();
if (blockHeight == genesisBlockHeight) {
transactionsMap.entrySet().stream()
.filter(entry -> entry.getKey().equals(genesisTxId))
.forEach(entry -> parseGenesisTx(entry.getValue(), blockHeight, utxoByTxIdMap));
}
resolveConnectedTxs(transactionsMap.values(), utxoByTxIdMap, blockHeight, 0, 100);
}
void resolveConnectedTxs(Collection<SquTransaction> transactions,
Map<String, Map<Integer, SquUTXO>> utxoByTxIdMap,
int blockHeight,
int recursions,
int maxRecursions) {
// The set of txIds of txs which are used for inputs in a tx in that block
Set<String> spendingTxIdSet = getSpendingTxIdSet(transactions);
// We check if the tx has only connections to the UTXO set, if so we add it to the connectedTxs, otherwise it
// is an orphaned tx.
// connectedTxs: Those who have inputs in the UTXO set
// orphanTxs: Those who have inputs from other txs in the same block
Set<SquTransaction> connectedTxs = new HashSet<>();
Set<SquTransaction> orphanTxs = new HashSet<>();
outerLoop:
for (SquTransaction transaction : transactions) {
boolean isConnected = false;
for (SquTxInput input : transaction.inputs) {
String spendingTxId = input.spendingTxId;
if (spendingTxIdSet.contains(spendingTxId)) {
// We have an input in one of the blocks transactions, so we cannot process that tx now.
// We break out here if at least 1 input points to a tx in the same block
orphanTxs.add(transaction);
continue outerLoop;
} else if (utxoByTxIdMap.containsKey(spendingTxId)) {
// If we find the tx in the utxo set we set the isConnected flag.
Map<Integer, SquUTXO> utxoByIndexMap = utxoByTxIdMap.get(spendingTxId);
if (utxoByIndexMap != null && utxoByIndexMap.containsKey(input.spendingOuptuIndex)) {
// Our input has a connection to an tx from the utxo set
isConnected = true;
}
}
}
if (isConnected)
connectedTxs.add(transaction);
}
// Now we check if our connected txs are valid SQU transactions
for (SquTransaction transaction : connectedTxs) {
verifyTransaction(transaction, blockHeight, utxoByTxIdMap);
}
log.info("orphanTxs " + orphanTxs);
if (!orphanTxs.isEmpty() && recursions < maxRecursions)
resolveConnectedTxs(orphanTxs, utxoByTxIdMap, blockHeight, ++recursions, maxRecursions);
}
private Set<String> getSpendingTxIdSet(Collection<SquTransaction> transactions) {
Set<String> txIdSet = transactions.stream().map(tx -> tx.txId).collect(Collectors.toSet());
Set<String> spendingTxIdSet = new HashSet<>();
transactions.stream()
.forEach(transaction -> transaction.inputs.stream()
.forEach(input -> {
String spendingTxId = input.spendingTxId;
if (txIdSet.contains(spendingTxId))
spendingTxIdSet.add(spendingTxId);
}));
return spendingTxIdSet;
}
private void verifyTransaction(SquTransaction squTransaction,
int blockHeight,
Map<String, Map<Integer, SquUTXO>> utxoByTxIdMap
) {
String txId = squTransaction.txId;
List<SquTxOutput> outputs = squTransaction.outputs;
Coin availableValue = Coin.ZERO;
for (SquTxInput input : squTransaction.inputs) {
String spendingTxId = input.spendingTxId;
if (utxoByTxIdMap.containsKey(spendingTxId)) {
Map<Integer, SquUTXO> utxoByIndexMap = utxoByTxIdMap.get(spendingTxId);
Integer index = input.spendingOuptuIndex;
if (utxoByIndexMap.containsKey(index)) {
SquUTXO utxo = utxoByIndexMap.get(index);
utxoByIndexMap.remove(index);
availableValue = availableValue.add(utxo.getValue());
if (utxoByIndexMap.isEmpty()) {
// If no more entries by index we can remove the whole entry by txId
utxoByTxIdMap.remove(spendingTxId);
}
}
}
}
// If we have an input spending tokens we iterate the outputs
if (availableValue.isPositive()) {
Map<Integer, SquUTXO> utxoByIndexMap = utxoByTxIdMap.containsKey(txId) ?
utxoByTxIdMap.get(txId) :
new HashMap<>();
// We sort by index, inputs are tokens as long there is enough input value
for (int i = 0; i < outputs.size(); i++) {
SquTxOutput squOutput = outputs.get(i);
List<String> addresses = squOutput.addresses;
// Only at raw MS outputs addresses have more then 1 entry
// We do not support raw MS for SQU
if (addresses.size() == 1) {
String address = addresses.get(0);
availableValue = availableValue.subtract(squOutput.value);
if (!availableValue.isNegative()) {
// We are spending available tokens
SquUTXO utxo = new SquUTXO(txId,
squOutput.index,
squOutput.value,
blockHeight,
false,
squOutput.script,
address);
utxoByIndexMap.put(i, utxo);
} else {
log.warn("We tried to spend more SQU as we have in our inputs");
break;
}
} else {
log.warn("addresses.size() is not 1.");
}
}
//TODO write that warning to a handler
if (availableValue.isPositive()) {
log.warn("SQU have been left which was not spent. Burned SQU amount={}, tx={}",
availableValue.value,
squTransaction.toString());
}
if (!utxoByIndexMap.isEmpty() && !utxoByTxIdMap.containsKey(txId)) {
boolean wasEmpty = utxoByTxIdMap.put(txId, utxoByIndexMap) == null;
checkArgument(wasEmpty, "We must not have that tx in the map. txId=" + txId);
}
}
}
@VisibleForTesting
void parseGenesisTx(SquTransaction squTransaction, int blockHeight, Map<String, Map<Integer, SquUTXO>> utxoByTxIdMap) {
String txId = squTransaction.txId;
List<SquTxOutput> outputs = squTransaction.outputs;
// Genesis tx uses all outputs as SQU outputs
Map<Integer, SquUTXO> utxoByIndexMap = new HashMap<>();
for (int i = 0; i < outputs.size(); i++) {
SquTxOutput output = outputs.get(i);
List<String> addresses = output.addresses;
// Only at raw MS outputs addresses have more then 1 entry
// We do not support raw MS for SQU
if (addresses.size() == 1) {
String address = addresses.get(0);
//TODO set coinbase to true after testing
SquUTXO utxo = new SquUTXO(txId,
output.index,
output.value,
blockHeight,
false,
output.script,
address);
utxoByIndexMap.put(i, utxo);
}
}
checkArgument(!utxoByIndexMap.isEmpty(), "Genesis tx must have squ utxo");
boolean wasEmpty = utxoByTxIdMap.put(txId, utxoByIndexMap) == null;
checkArgument(wasEmpty, "We must not have that tx in the map. txId=" + txId);
}
void printUtxoMap(Map<String, Map<Integer, SquUTXO>> utxoByTxIdMap) {
StringBuilder sb = new StringBuilder("utxoByTxIdMap:\n");
utxoByTxIdMap.entrySet().stream().forEach(e -> {
sb.append("TxId: ").append(e.getKey()).append("\n");
e.getValue().entrySet().stream().forEach(a -> {
sb.append(" [").append(a.getKey()).append("] {")
.append(a.getValue().toString()).append("}\n");
});
});
log.info(sb.toString());
}
}

View file

@ -34,4 +34,34 @@ public class SquTransaction {
this.inputs = inputs;
this.outputs = outputs;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SquTransaction that = (SquTransaction) o;
if (txId != null ? !txId.equals(that.txId) : that.txId != null) return false;
if (inputs != null ? !inputs.equals(that.inputs) : that.inputs != null) return false;
return !(outputs != null ? !outputs.equals(that.outputs) : that.outputs != null);
}
@Override
public int hashCode() {
int result = txId != null ? txId.hashCode() : 0;
result = 31 * result + (inputs != null ? inputs.hashCode() : 0);
result = 31 * result + (outputs != null ? outputs.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "SquTransaction{" +
"txId='" + txId + '\'' +
", inputs=" + inputs +
", outputs=" + outputs +
'}';
}
}

View file

@ -23,11 +23,43 @@ import org.slf4j.LoggerFactory;
public class SquTxInput {
private static final Logger log = LoggerFactory.getLogger(SquTxInput.class);
public final int index;
public final int spendingOuptuIndex;
public final String spendingTxId;
public final String txId;
public SquTxInput(int index, String spendingTxId) {
this.index = index;
public SquTxInput(int spendingOuptuIndex, String spendingTxId, String txId) {
this.spendingOuptuIndex = spendingOuptuIndex;
this.spendingTxId = spendingTxId;
this.txId = txId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SquTxInput that = (SquTxInput) o;
if (spendingOuptuIndex != that.spendingOuptuIndex) return false;
if (spendingTxId != null ? !spendingTxId.equals(that.spendingTxId) : that.spendingTxId != null) return false;
return !(txId != null ? !txId.equals(that.txId) : that.txId != null);
}
@Override
public int hashCode() {
int result = spendingOuptuIndex;
result = 31 * result + (spendingTxId != null ? spendingTxId.hashCode() : 0);
result = 31 * result + (txId != null ? txId.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "SquTxInput{" +
"spendingOuptuIndex=" + spendingOuptuIndex +
", spendingTxId='" + spendingTxId + '\'' +
", txId='" + txId + '\'' +
'}';
}
}

View file

@ -38,4 +38,37 @@ public class SquTxOutput {
this.addresses = addresses;
this.script = script;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SquTxOutput that = (SquTxOutput) o;
if (index != that.index) return false;
if (value != null ? !value.equals(that.value) : that.value != null) return false;
if (addresses != null ? !addresses.equals(that.addresses) : that.addresses != null) return false;
return !(script != null ? !script.equals(that.script) : that.script != null);
}
@Override
public int hashCode() {
int result = index;
result = 31 * result + (value != null ? value.hashCode() : 0);
result = 31 * result + (addresses != null ? addresses.hashCode() : 0);
result = 31 * result + (script != null ? script.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "SquTxOutput{" +
"index=" + index +
", value=" + value +
", addresses=" + addresses +
", script=" + script +
'}';
}
}

View file

@ -25,6 +25,9 @@ import org.bitcoinj.script.Script;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// Estimation for UTXO set: 1 UTXO object has 78 byte
// 1000 UTXOs - 10 000 UTXOs: 78kb -780kb
public class SquUTXO extends UTXO {
private static final Logger log = LoggerFactory.getLogger(SquUTXO.class);

View file

@ -41,8 +41,8 @@ import java.util.stream.Collectors;
import static com.google.common.base.Preconditions.checkArgument;
public class VoteManager {
private static final Logger log = LoggerFactory.getLogger(VoteManager.class);
public class VotingManager {
private static final Logger log = LoggerFactory.getLogger(VotingManager.class);
public static final String ERROR_MSG_MISSING_BYTE = "We need to have at least 1 more byte for the voting value.";
public static final String ERROR_MSG_WRONG_SIZE = "sizeOfCompReqVotesInBytes must be 0 or multiple of 2. sizeOfCompReqVotesInBytes=";
@ -60,13 +60,13 @@ public class VoteManager {
private VoteItemsList activeVoteItemsList;
@Inject
public VoteManager(BtcWalletService btcWalletService,
SquWalletService squWalletService,
FeeService feeService,
Storage<ArrayList<VoteItemsList>> voteItemCollectionsStorage,
CompensationRequestManager compensationRequestManager,
DaoPeriodService daoPeriodService,
VotingDefaultValues votingDefaultValues) {
public VotingManager(BtcWalletService btcWalletService,
SquWalletService squWalletService,
FeeService feeService,
Storage<ArrayList<VoteItemsList>> voteItemCollectionsStorage,
CompensationRequestManager compensationRequestManager,
DaoPeriodService daoPeriodService,
VotingDefaultValues votingDefaultValues) {
this.btcWalletService = btcWalletService;
this.squWalletService = squWalletService;
this.feeService = feeService;
@ -81,7 +81,7 @@ public class VoteManager {
}
@VisibleForTesting
VoteManager(VotingDefaultValues votingDefaultValues) {
VotingManager(VotingDefaultValues votingDefaultValues) {
this.btcWalletService = null;
this.squWalletService = null;
this.feeService = null;

View file

@ -40,6 +40,8 @@ public enum VotingType {
COMP_REQUEST_MAPS((byte) 0x50);
// TODO max growth rate of SQU
public final Byte code;
VotingType(Byte code) {

View file

@ -0,0 +1,349 @@
/*
* This file is part of Bitsquare.
*
* Bitsquare 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.
*
* Bitsquare 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 Bitsquare. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bitsquare.dao.blockchain;
import com.neemre.btcdcli4j.core.BitcoindException;
import com.neemre.btcdcli4j.core.CommunicationException;
import com.neemre.btcdcli4j.core.domain.*;
import org.bitcoinj.core.Utils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
import java.util.*;
import static org.junit.Assert.assertEquals;
/*
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static org.junit.Assert.assertEquals;
*/
public class SquBlockchainServiceTest {
private static final Logger log = LoggerFactory.getLogger(SquBlockchainServiceTest.class);
private MockSquBlockchainService squBlockchainService;
@Before
public void setup() {
squBlockchainService = new MockSquBlockchainService();
}
@After
public void tearDown() {
}
@Test
public void testGenesisBlock() throws SquBlockchainException, BitcoindException, CommunicationException {
int genesisBlockHeight = 0;
String genesisTxId = "000000a4d94cb612b5d722d531083f59f317d5dea1db4a191f61b2ab34af2627";
buildGenesisBlock(genesisBlockHeight, genesisTxId);
Map<String, Map<Integer, SquUTXO>> utxoByTxIdMap = squBlockchainService.parseBlockchain(new HashMap<>(),
squBlockchainService.requestChainHeadHeight(),
genesisBlockHeight,
genesisTxId);
SquUTXO squUTXO1 = utxoByTxIdMap.get(genesisTxId).get(0);
SquUTXO squUTXO2 = utxoByTxIdMap.get(genesisTxId).get(1);
assertEquals(1, utxoByTxIdMap.size());
assertEquals("addressGen1", squUTXO1.getAddress());
assertEquals("addressGen2", squUTXO2.getAddress());
}
@Test
public void testGenToTx1Block1() throws SquBlockchainException, BitcoindException, CommunicationException {
int genesisBlockHeight = 0;
String genesisTxId = "000000a4d94cb612b5d722d531083f59f317d5dea1db4a191f61b2ab34af2627";
buildGenesisBlock(genesisBlockHeight, genesisTxId);
// We spend from output 1 of gen tx
String txId = "100000a4d94cb612b5d722d531083f59f317d5dea1db4a191f61b2ab34af2627";
buildSpendingTx(genesisBlockHeight,
genesisTxId,
txId,
1,
0,
0.00001000,
"addressTx1");
Map<String, Map<Integer, SquUTXO>> utxoByTxIdMap = squBlockchainService.parseBlockchain(new HashMap<>(),
squBlockchainService.requestChainHeadHeight(),
genesisBlockHeight,
genesisTxId);
SquUTXO squUTXO1 = utxoByTxIdMap.get(genesisTxId).get(0);
SquUTXO squUTXO2 = utxoByTxIdMap.get(txId).get(0);
assertEquals(2, utxoByTxIdMap.size());
assertEquals("addressGen1", squUTXO1.getAddress());
assertEquals("addressTx1", squUTXO2.getAddress());
}
@Test
public void testGenToTx1toTx2Block1() throws SquBlockchainException, BitcoindException, CommunicationException {
int genesisBlockHeight = 0;
String genesisTxId = "000000a4d94cb612b5d722d531083f59f317d5dea1db4a191f61b2ab34af2627";
buildGenesisBlock(genesisBlockHeight, genesisTxId);
// We spend from output 1 of gen tx
String tx1Id = "100000a4d94cb612b5d722d531083f59f317d5dea1db4a191f61b2ab34af2627";
buildSpendingTx(genesisBlockHeight,
genesisTxId,
tx1Id,
1,
0,
0.00001000,
"addressTx1");
// We spend from output 0 of tx1 (same block)
String tx2Id = "200000a4d94cb612b5d722d531083f59f317d5dea1db4a191f61b2ab34af2627";
buildSpendingTx(1,
tx1Id,
tx2Id,
0,
0,
0.00001000,
"addressTx2");
Map<String, Map<Integer, SquUTXO>> utxoByTxIdMap = squBlockchainService.parseBlockchain(new HashMap<>(),
squBlockchainService.requestChainHeadHeight(),
genesisBlockHeight,
genesisTxId);
SquUTXO squUTXO1 = utxoByTxIdMap.get(genesisTxId).get(0);
SquUTXO squUTXO2 = utxoByTxIdMap.get(tx2Id).get(0);
assertEquals(2, utxoByTxIdMap.size());
assertEquals("addressGen1", squUTXO1.getAddress());
assertEquals("addressTx2", squUTXO2.getAddress());
}
@Test
public void testGenToTx1toTx2AndGenToTx2Block1() throws SquBlockchainException, BitcoindException, CommunicationException {
int genesisBlockHeight = 0;
String genesisTxId = "000000a4d94cb612b5d722d531083f59f317d5dea1db4a191f61b2ab34af2627";
buildGenesisBlock(genesisBlockHeight, genesisTxId);
// We spend from output 1 of gen tx
String tx1Id = "100000a4d94cb612b5d722d531083f59f317d5dea1db4a191f61b2ab34af2627";
buildSpendingTx(genesisBlockHeight,
genesisTxId,
tx1Id,
1,
0,
0.00001000,
"addressTx1");
// We spend from output 0 of tx1 (same block)
String tx2Id = "200000a4d94cb612b5d722d531083f59f317d5dea1db4a191f61b2ab34af2627";
RawTransaction tx2 = buildSpendingTx(1,
tx1Id,
tx2Id,
0,
0,
0.00001000,
"addressTx3a");
// We spend from output 0 of gen tx to tx2
List<RawInput> rawInputs = tx2.getVIn();
rawInputs.add(getRawInput(0, genesisTxId));
tx2.setVIn(rawInputs);
List<RawOutput> rawOutputs = tx2.getVOut();
rawOutputs.add(getRawOutput(0, 0.00005000, "addressTx3b"));
tx2.setVOut(rawOutputs);
Map<String, Map<Integer, SquUTXO>> utxoByTxIdMap = squBlockchainService.parseBlockchain(new HashMap<>(),
squBlockchainService.requestChainHeadHeight(),
genesisBlockHeight,
genesisTxId);
SquUTXO squUTXO1 = utxoByTxIdMap.get(tx2Id).get(0);
SquUTXO squUTXO2 = utxoByTxIdMap.get(tx2Id).get(1);
assertEquals(1, utxoByTxIdMap.size());
assertEquals("addressTx3a", squUTXO1.getAddress());
assertEquals("addressTx3b", squUTXO2.getAddress());
}
private RawTransaction buildSpendingTx(int inputTxBlockHeight,
String inputTxId,
String txId,
int inputIndex,
int outputIndex,
double outputValue,
String outputAddress) {
RawTransaction rawTransaction = getRawTransaction(inputTxBlockHeight + 1, txId);
List<RawInput> rawInputs = new ArrayList<>();
rawInputs.add(getRawInput(inputIndex, inputTxId));
rawTransaction.setVIn(rawInputs);
List<RawOutput> rawOutputs = new ArrayList<>();
rawOutputs.add(getRawOutput(outputIndex, outputValue, outputAddress));
rawTransaction.setVOut(rawOutputs);
squBlockchainService.addTxToBlock(1, rawTransaction);
squBlockchainService.buildBlocks(0, 1);
return rawTransaction;
}
private void buildGenesisBlock(int genesisBlockHeight, String genesisTxId) throws SquBlockchainException, BitcoindException, CommunicationException {
RawTransaction genesisRawTransaction = getRawTransaction(genesisBlockHeight, genesisTxId);
List<RawInput> rawInputs = new ArrayList<>();
rawInputs.add(getRawInput(0, "000001a4d94cb612b5d722d531083f59f317d5dea1db4a191f61b2ab34af2627"));
genesisRawTransaction.setVIn(rawInputs);
List<RawOutput> rawOutputs = new ArrayList<>();
rawOutputs.add(getRawOutput(0, 0.00005000, "addressGen1"));
rawOutputs.add(getRawOutput(1, 0.00001000, "addressGen2"));
genesisRawTransaction.setVOut(rawOutputs);
squBlockchainService.setGenesisTx(genesisTxId, genesisBlockHeight);
squBlockchainService.addTxToBlock(0, genesisRawTransaction);
squBlockchainService.buildBlocks(0, 0);
}
private RawTransaction getRawTransaction(int genesisBlockHeight, String txId) {
RawTransaction genesisRawTransaction = new RawTransaction();
genesisRawTransaction.setBlockHash("BlockHash" + genesisBlockHeight);
genesisRawTransaction.setTxId(txId);
return genesisRawTransaction;
}
private RawOutput getRawOutput(int index, double value, String address) {
RawOutput rawOutput = new RawOutput();
rawOutput.setN(index);
rawOutput.setValue(BigDecimal.valueOf((long) (value * 100000000), 8));
PubKeyScript scriptPubKey = new PubKeyScript();
scriptPubKey.setAddresses(Arrays.asList(address));
rawOutput.setScriptPubKey(scriptPubKey);
return rawOutput;
}
private RawInput getRawInput(int index, String spendingTxId) {
RawInput rawInput = new RawInput();
rawInput.setTxId(spendingTxId);
rawInput.setVOut(index);
return rawInput;
}
private String getHex(String txId) {
byte[] bytes = new byte[32];
byte[] inputBytes = txId.getBytes();
for (int i = 0; i < 32; i++) {
if (inputBytes.length > i)
bytes[i] = inputBytes[i];
else
bytes[i] = 0x00;
}
return Utils.HEX.encode(bytes);
}
}
class MockSquBlockchainService extends SquBlockchainRpcService {
private static final Logger log = LoggerFactory.getLogger(MockSquBlockchainService.class);
private List<Block> blocks;
private int chainHeadHeight;
private String genesisTxId;
private int genesisBlockHeight;
private Map<String, RawTransaction> txsByIsMap = new HashMap<>();
private Map<Integer, List<RawTransaction>> txsInBlockMap = new HashMap<>();
private Map<Integer, List<String>> txIdsInBlockMap = new HashMap<>();
public MockSquBlockchainService() {
super(null, null, null, null, null);
}
public void buildBlocks(int from, int to) {
this.chainHeadHeight = to;
blocks = new ArrayList<>();
for (int blockIndex = from; blockIndex <= to; blockIndex++) {
blocks.add(getBlock(blockIndex, txIdsInBlockMap.get(blockIndex)));
}
}
public void addTxToBlock(int blockHeight, RawTransaction transaction) {
List<RawTransaction> txs;
if (txsInBlockMap.containsKey(blockHeight)) {
txs = txsInBlockMap.get(blockHeight);
} else {
txs = new ArrayList<>();
txsInBlockMap.put(blockHeight, txs);
}
txs.add(transaction);
List<String> ids;
if (txIdsInBlockMap.containsKey(blockHeight)) {
ids = txIdsInBlockMap.get(blockHeight);
} else {
ids = new ArrayList<>();
txIdsInBlockMap.put(blockHeight, ids);
}
String txId = transaction.getTxId();
ids.add(txId);
txsByIsMap.put(txId, transaction);
}
public void buildTxList(int from, int to) {
blocks = new ArrayList<>();
for (int blockIndex = from; blockIndex < to; blockIndex++) {
blocks.add(getBlock(blockIndex, getTxList(blockIndex)));
}
}
private Block getBlock(int blockIndex, List<String> txList) {
Block block = new Block();
block.setHeight(blockIndex);
block.setHash("hash" + blockIndex);
block.setTx(txList);
return block;
}
private List<String> getTxList(int blockIndex) {
List<String> txList = new ArrayList<>();
if (blockIndex == genesisBlockHeight) {
txList.add(genesisTxId);
}
return txList;
}
@Override
int requestChainHeadHeight() throws BitcoindException, CommunicationException {
return chainHeadHeight;
}
@Override
Block requestBlock(int index) throws BitcoindException, CommunicationException {
return blocks.get(index);
}
public void setGenesisTx(String genesisTxId, int genesisBlockHeight) {
this.genesisTxId = genesisTxId;
this.genesisBlockHeight = genesisBlockHeight;
}
@Override
protected RawTransaction getRawTransaction(String txId) throws BitcoindException, CommunicationException {
return txsByIsMap.get(txId);
}
}

View file

@ -31,7 +31,7 @@ public class VoteManagerTest {
@Test
public void testGetVoteItemListFromOpReturnData() {
VoteManager votingManager = new VoteManager(new VotingDefaultValues());
VotingManager votingManager = new VotingManager(new VotingDefaultValues());
byte[] opReturnData;
VoteItemsList voteItemsList;
@ -55,7 +55,7 @@ public class VoteManagerTest {
votingManager.getVoteItemListFromOpReturnData(opReturnData);
fail("Expected an IllegalArgumentException to be thrown");
} catch (IllegalArgumentException e) {
assertThat(e.getMessage(), startsWith(VoteManager.ERROR_MSG_WRONG_SIZE));
assertThat(e.getMessage(), startsWith(VotingManager.ERROR_MSG_WRONG_SIZE));
}
// compensation requests set but no votes
@ -77,7 +77,7 @@ public class VoteManagerTest {
votingManager.getVoteItemListFromOpReturnData(opReturnData);
fail("Expected an IllegalArgumentException to be thrown");
} catch (IllegalArgumentException e) {
assertThat(e.getMessage(), is(VoteManager.ERROR_MSG_INVALID_COMP_REQ_MAPS));
assertThat(e.getMessage(), is(VotingManager.ERROR_MSG_INVALID_COMP_REQ_MAPS));
}
// has voted on item 2, but we found a accepted flag at item 1
@ -90,7 +90,7 @@ public class VoteManagerTest {
votingManager.getVoteItemListFromOpReturnData(opReturnData);
fail("Expected an IllegalArgumentException to be thrown");
} catch (IllegalArgumentException e) {
assertThat(e.getMessage(), is(VoteManager.ERROR_MSG_INVALID_COMP_REQ_VAL));
assertThat(e.getMessage(), is(VotingManager.ERROR_MSG_INVALID_COMP_REQ_VAL));
}
// has voted on all and are declined
@ -120,7 +120,7 @@ public class VoteManagerTest {
votingManager.getVoteItemListFromOpReturnData(opReturnData);
fail("Expected an IllegalArgumentException to be thrown");
} catch (IllegalArgumentException e) {
assertThat(e.getMessage(), is(VoteManager.ERROR_MSG_MISSING_BYTE));
assertThat(e.getMessage(), is(VotingManager.ERROR_MSG_MISSING_BYTE));
}
// wrong length
@ -134,7 +134,7 @@ public class VoteManagerTest {
votingManager.getVoteItemListFromOpReturnData(opReturnData);
fail("Expected an IllegalArgumentException to be thrown");
} catch (IllegalArgumentException e) {
assertThat(e.getMessage(), is(VoteManager.ERROR_MSG_MISSING_BYTE));
assertThat(e.getMessage(), is(VotingManager.ERROR_MSG_MISSING_BYTE));
}
// Invalid param vote 255 is not allowed only 0-254
@ -177,7 +177,7 @@ public class VoteManagerTest {
@Test
public void testCalculateHash() {
VoteManager votingManager = new VoteManager(new VotingDefaultValues());
VotingManager votingManager = new VotingManager(new VotingDefaultValues());
// assertEquals(10, votingManager.calculateHash(100, 0));
}

View file

@ -67,6 +67,7 @@
<!-- exclude signatures, the bundling process breaks them for some reason -->
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/maven/**/pom.properties</exclude>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>

View file

@ -42,7 +42,7 @@ import io.bitsquare.common.Timer;
import io.bitsquare.common.UserThread;
import io.bitsquare.common.crypto.*;
import io.bitsquare.dao.DaoManager;
import io.bitsquare.dao.blockchain.BlockchainException;
import io.bitsquare.dao.blockchain.SquBlockchainException;
import io.bitsquare.filter.FilterManager;
import io.bitsquare.gui.Navigation;
import io.bitsquare.gui.common.model.ViewModel;
@ -551,7 +551,7 @@ public class MainViewModel implements ViewModel {
try {
daoManager.onAllServicesInitialized();
} catch (BlockchainException e) {
} catch (SquBlockchainException e) {
new Popup<>().error(e.toString()).show();
}

View file

@ -76,7 +76,7 @@ public class VoteView extends ActivatableView<GridPane, Void> {
private FeeService feeService;
private SQUFormatter squFormatter;
private BSFormatter btcFormatter;
private VoteManager voteManager;
private VotingManager voteManager;
private Button voteButton;
private List<CompensationRequest> compensationRequests;
private TitledGroupBg compensationRequestsTitledGroupBg, parametersTitledGroupBg;
@ -94,7 +94,7 @@ public class VoteView extends ActivatableView<GridPane, Void> {
@Inject
private VoteView(CompensationRequestManager compensationRequestManager, SquWalletService squWalletService,
BtcWalletService btcWalletService, FeeService feeService, SQUFormatter squFormatter,
BSFormatter btcFormatter, VoteManager voteManager) {
BSFormatter btcFormatter, VotingManager voteManager) {
this.compensationRequestManager = compensationRequestManager;
this.squWalletService = squWalletService;
this.btcWalletService = btcWalletService;

View file

@ -17,17 +17,17 @@
package io.bitsquare.gui.main.dao.wallet;
import io.bitsquare.btc.listeners.BalanceListener;
import io.bitsquare.btc.wallet.SquWalletService;
import io.bitsquare.gui.main.overlays.popups.Popup;
import io.bitsquare.gui.util.SQUFormatter;
import javafx.scene.control.TextField;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.*;
import org.bitcoinj.script.Script;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.util.List;
public class BalanceUtil {
private static final Logger log = LoggerFactory.getLogger(BalanceUtil.class);
@ -35,7 +35,7 @@ public class BalanceUtil {
private final SquWalletService squWalletService;
private final SQUFormatter formatter;
private TextField balanceTextField;
private BalanceListener balanceListener;
private WalletEventListener walletEventListener;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
@ -52,31 +52,59 @@ public class BalanceUtil {
}
public void initialize() {
balanceListener = new BalanceListener() {
walletEventListener = new WalletEventListener() {
@Override
public void onBalanceChanged(Coin balance, Transaction tx) {
updateBalance(balance);
public void onKeysAdded(List<ECKey> keys) {
}
@Override
public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
requestUtxo();
}
@Override
public void onCoinsSent(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
requestUtxo();
}
@Override
public void onReorganize(Wallet wallet) {
requestUtxo();
}
@Override
public void onTransactionConfidenceChanged(Wallet wallet, Transaction tx) {
requestUtxo();
}
@Override
public void onWalletChanged(Wallet wallet) {
}
@Override
public void onScriptsChanged(Wallet wallet, List<Script> scripts, boolean isAddingScripts) {
}
};
}
public void activate() {
private void requestUtxo() {
squWalletService.requestSquUtxo(() -> {
updateBalance(squWalletService.getAvailableBalance());
balanceTextField.setText(formatter.formatCoinWithCode(squWalletService.getAvailableBalance()));
}, errorMessage -> {
new Popup<>().warning(errorMessage);
});
squWalletService.addBalanceListener(balanceListener);
}
updateBalance(squWalletService.getAvailableBalance());
public void activate() {
requestUtxo();
squWalletService.addEventListener(walletEventListener);
}
public void deactivate() {
squWalletService.removeBalanceListener(balanceListener);
}
private void updateBalance(Coin balance) {
balanceTextField.setText(formatter.formatCoinWithCode(balance));
squWalletService.removeEventListener(walletEventListener);
}
}

View file

@ -107,7 +107,7 @@ public class TokenSendView extends ActivatableView<GridPane, Void> {
Coin receiverAmount = squFormatter.parseToCoin(amountInputTextField.getText());
try {
Transaction preparedSendTx = squWalletService.getPreparedSendTx(receiversAddressString, receiverAmount);
Transaction txWithBtcFee = btcWalletService.completePreparedSquTx(preparedSendTx, true, null);
Transaction txWithBtcFee = btcWalletService.completePreparedSendSquTx(preparedSendTx, true);
Transaction signedTx = squWalletService.signTx(txWithBtcFee);
Coin miningFee = signedTx.getFee();

View file

@ -730,6 +730,7 @@ public class Connection implements MessageListener {
Object rawInputObject = objectInputStream.readObject();
// Throttle inbound messages
long now = System.currentTimeMillis();
long elapsed = now - lastReadTimeStamp;

View file

@ -5,7 +5,7 @@ mkdir -p gui/deploy
set -e
version="0.4.9.8"
version="0.5.0.0"
mvn clean package -DskipTests -Dmaven.javadoc.skip=true

View file

@ -160,6 +160,12 @@
<artifactId>jsr305</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.12</version>
<scope>provided</scope>
</dependency>
<!--logging-->
<dependency>