From 30df6f9721d6a95e4b447a6b10c2f7025d90b73a Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Mon, 10 Apr 2017 00:04:46 -0500 Subject: [PATCH] Add spend infor to tx outputs. adopt json for txo. add custom encoding for bsq addresses. --- .../main/java/io/bisq/common/app/Version.java | 2 + .../dao/blockchain/BsqBlockchainManager.java | 122 +++++----- .../blockchain/BsqBlockchainRpcService.java | 32 +-- .../dao/blockchain/BsqBlockchainService.java | 37 ++- .../io/bisq/core/dao/blockchain/BsqUTXO.java | 6 +- .../bisq/core/dao/blockchain/SpendInfo.java | 27 +++ .../io/bisq/core/dao/blockchain/TxOutput.java | 89 +++++++- ...bKeyJson.java => ScriptPubKeyForJson.java} | 2 +- ...entInfoJson.java => SpentInfoForJson.java} | 8 +- .../dao/blockchain/json/TxOutputForJson.java | 30 ++- .../dao/wallet/receive/BsqReceiveView.java | 16 +- .../gui/main/dao/wallet/send/BsqSendView.java | 6 +- .../gui/main/dao/wallet/tx/BsqTxListItem.java | 10 +- .../gui/main/dao/wallet/tx/BsqTxView.java | 7 +- .../main/java/io/bisq/gui/util/Base58Bsq.java | 211 ++++++++++++++++++ .../java/io/bisq/gui/util/BsqFormatter.java | 51 +++++ .../util/validation/BsqAddressValidator.java | 12 +- 17 files changed, 514 insertions(+), 154 deletions(-) create mode 100644 core/src/main/java/io/bisq/core/dao/blockchain/SpendInfo.java rename core/src/main/java/io/bisq/core/dao/blockchain/json/{ScriptPubKeyJson.java => ScriptPubKeyForJson.java} (96%) rename core/src/main/java/io/bisq/core/dao/blockchain/json/{SpentInfoJson.java => SpentInfoForJson.java} (89%) create mode 100644 gui/src/main/java/io/bisq/gui/util/Base58Bsq.java diff --git a/common/src/main/java/io/bisq/common/app/Version.java b/common/src/main/java/io/bisq/common/app/Version.java index 40ca43064b..7095a1408e 100644 --- a/common/src/main/java/io/bisq/common/app/Version.java +++ b/common/src/main/java/io/bisq/common/app/Version.java @@ -43,6 +43,8 @@ public class Version { public static final int TRADE_PROTOCOL_VERSION = 1; private static int p2pMessageVersion; + public static final String BSQ_TX_VERSION = "1"; + public static int getP2PMessageVersion() { // TODO investigate why a changed NETWORK_PROTOCOL_VERSION for the serialized objects does not trigger // reliable a disconnect., but java serialisation should be replaced anyway, so using one existing field diff --git a/core/src/main/java/io/bisq/core/dao/blockchain/BsqBlockchainManager.java b/core/src/main/java/io/bisq/core/dao/blockchain/BsqBlockchainManager.java index ca5b7070e3..c8372e7a60 100644 --- a/core/src/main/java/io/bisq/core/dao/blockchain/BsqBlockchainManager.java +++ b/core/src/main/java/io/bisq/core/dao/blockchain/BsqBlockchainManager.java @@ -21,18 +21,17 @@ 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 com.neemre.btcdcli4j.core.domain.PubKeyScript; import io.bisq.common.UserThread; import io.bisq.common.handlers.ErrorMessageHandler; -import io.bisq.common.storage.FileManager; import io.bisq.common.storage.PlainTextWrapper; import io.bisq.common.storage.Storage; -import io.bisq.common.util.MathUtils; import io.bisq.common.util.Utilities; import io.bisq.core.app.BisqEnvironment; import io.bisq.core.btc.BitcoinNetwork; import io.bisq.core.dao.RpcOptionKeys; -import io.bisq.core.dao.blockchain.json.ScriptPubKeyJson; -import io.bisq.core.dao.blockchain.json.SpentInfoJson; +import io.bisq.core.dao.blockchain.json.ScriptPubKeyForJson; +import io.bisq.core.dao.blockchain.json.SpentInfoForJson; import io.bisq.core.dao.blockchain.json.TxOutputForJson; import lombok.Getter; import org.jetbrains.annotations.NotNull; @@ -41,10 +40,10 @@ import org.slf4j.LoggerFactory; import javax.inject.Named; import java.io.File; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; public class BsqBlockchainManager { private static final Logger log = LoggerFactory.getLogger(BsqBlockchainManager.class); @@ -86,6 +85,7 @@ public class BsqBlockchainManager { private int chainHeadHeight; @Getter private boolean isUtxoSyncWithChainHeadHeight; + private final Storage jsonStorage; /////////////////////////////////////////////////////////////////////////////////////////// @@ -96,9 +96,11 @@ public class BsqBlockchainManager { public BsqBlockchainManager(BsqBlockchainService blockchainService, BisqEnvironment bisqEnvironment, @Named(Storage.DIR_KEY) File storageDir, + Storage jsonStorage, @Named(RpcOptionKeys.DUMP_BLOCKCHAIN_DATA) boolean dumpBlockchainData) { this.blockchainService = blockchainService; this.storageDir = storageDir; + this.jsonStorage = jsonStorage; this.dumpBlockchainData = dumpBlockchainData; this.bitcoinNetwork = bisqEnvironment.getBitcoinNetwork(); @@ -110,7 +112,7 @@ public class BsqBlockchainManager { bsqTXOMap.addBurnedBSQTxMapListener(c -> onBsqTXOChanged()); if (dumpBlockchainData) { - + this.jsonStorage.initWithFileName("txo.json"); /* p2PService.addP2PServiceListener(new BootstrapListener() { @Override public void onBootstrapComplete() { @@ -132,63 +134,67 @@ public class BsqBlockchainManager { } private void doDumpBlockchainData() { - bsqTXOMap.getMap().values().stream() - .forEach(txOutput -> { - final boolean coinBase = false; - final int height = -1; - final int index = txOutput.getIndex(); - final boolean invalid = false; - final int n = index; - final int output_index = index; + List list = bsqTXOMap.getMap().values().stream() + .map(this::getTxOutputForJson) + .collect(Collectors.toList()); - final ScriptPubKeyJson scriptPubKey = new ScriptPubKeyJson(txOutput.getAddresses(), - "asm", - "hex", - -1, - "type"); - final SpentInfoJson spent_info = new SpentInfoJson(-1, -1, "txId"); + list.sort((o1, o2) -> (o1.getSortData().compareTo(o2.getSortData()))); + TxOutputForJson[] array = new TxOutputForJson[list.size()]; + list.toArray(array); + jsonStorage.queueUpForSave(new PlainTextWrapper(Utilities.objectToJson(array)), 5000); - final long squ_amount = txOutput.getValue(); - final String status = "?"; - final String transaction_version = "?"; - final long tx_time = -1; - final String tx_type_str = "?"; - final String txid = txOutput.getTxId(); - final boolean validated = false; - final double value = MathUtils.scaleDownByPowerOf10(squ_amount, 8); - final long valueSat = squ_amount; + // keep the individual file storage option as code as we dont know yet what we will use. + /* log.error("txOutputForJson " + txOutputForJson); + File txoDir = new File(Paths.get(storageDir.getAbsolutePath(), "txo").toString()); + if (!txoDir.exists()) + if (!txoDir.mkdir()) + log.warn("make txoDir failed.\ntxoDir=" + txoDir.getAbsolutePath()); + File txoFile = new File(Paths.get(txoDir.getAbsolutePath(), + txOutput.getTxId() + ":" + outputIndex + ".json").toString()); - // TODO WIP... - TxOutputForJson txOutputForJson = new TxOutputForJson(coinBase, - height, - index, - invalid, - n, - output_index, - scriptPubKey, - spent_info, - squ_amount, - status, - transaction_version, - tx_time, - tx_type_str, - txid, - validated, - value, - valueSat - ); - // log.error("txOutputForJson " + txOutputForJson); - File txoDir = new File(Paths.get(storageDir.getAbsolutePath(), "txo").toString()); - if (!txoDir.exists()) - if (!txoDir.mkdir()) - log.warn("make txoDir failed.\ntxoDir=" + txoDir.getAbsolutePath()); - File txoFile = new File(Paths.get(txoDir.getAbsolutePath(), txid + ":" + index + ".json").toString()); + // Nr of write requests might be a bit heavy, consider write whole list to one file + FileManager fileManager = new FileManager<>(storageDir, txoFile, 1); + fileManager.saveLater(new PlainTextWrapper(Utilities.objectToJson(txOutputForJson)));*/ + } - // Nr of write requests might be a bit heavy, consider write whole list to one file - FileManager fileManager = new FileManager<>(storageDir, txoFile, 1); - fileManager.saveLater(new PlainTextWrapper(Utilities.objectToJson(txOutputForJson))); - }); + private TxOutputForJson getTxOutputForJson(TxOutput txOutput) { + String txId = txOutput.getTxId(); + int outputIndex = txOutput.getIndex(); + final long bsqAmount = txOutput.getValue(); + final int height = txOutput.getBlockHeight(); + final boolean isBsqCoinBase = txOutput.isBsqCoinBase(); + final boolean verified = txOutput.isVerified(); + final long burnedFee = txOutput.getBurnedFee(); + final long btcTxFee = txOutput.getBtcTxFee(); + PubKeyScript pubKeyScript = txOutput.getPubKeyScript(); + final ScriptPubKeyForJson scriptPubKey = new ScriptPubKeyForJson(pubKeyScript.getAddresses(), + pubKeyScript.getAsm(), + pubKeyScript.getHex(), + pubKeyScript.getReqSigs(), + pubKeyScript.getType().toString()); + SpentInfoForJson spentInfoJson = null; + SpendInfo spendInfo = txOutput.getSpendInfo(); + if (spendInfo != null) + spentInfoJson = new SpentInfoForJson(spendInfo.getBlockHeight(), + spendInfo.getInputIndex(), + spendInfo.getTxId()); + + final long time = txOutput.getTime(); + final String txVersion = txOutput.getTxVersion(); + return new TxOutputForJson(txId, + outputIndex, + bsqAmount, + height, + isBsqCoinBase, + verified, + burnedFee, + btcTxFee, + scriptPubKey, + spentInfoJson, + time, + txVersion + ); } private void onBsqUTXOChanged() { diff --git a/core/src/main/java/io/bisq/core/dao/blockchain/BsqBlockchainRpcService.java b/core/src/main/java/io/bisq/core/dao/blockchain/BsqBlockchainRpcService.java index e4214d3ec7..07a9afba6a 100644 --- a/core/src/main/java/io/bisq/core/dao/blockchain/BsqBlockchainRpcService.java +++ b/core/src/main/java/io/bisq/core/dao/blockchain/BsqBlockchainRpcService.java @@ -37,7 +37,6 @@ import io.bisq.core.dao.RpcOptionKeys; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; -import org.bitcoinj.script.Script; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,7 +50,6 @@ import java.util.function.Consumer; import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkNotNull; -import static org.bitcoinj.core.Utils.HEX; public class BsqBlockchainRpcService extends BsqBlockchainService { private static final Logger log = LoggerFactory.getLogger(BsqBlockchainRpcService.class); @@ -198,35 +196,27 @@ public class BsqBlockchainRpcService extends BsqBlockchainService { } @Override - Tx requestTransaction(String txId) throws BsqBlockchainException { + Tx requestTransaction(String txId, int blockHeight) throws BsqBlockchainException { try { RawTransaction rawTransaction = getRawTransaction(txId); + final long time = rawTransaction.getTime() * 1000; final List txInputs = rawTransaction.getVIn() .stream() .filter(rawInput -> rawInput != null && rawInput.getVOut() != null && rawInput.getTxId() != null) .map(rawInput -> new TxInput(rawInput.getVOut(), rawInput.getTxId(), rawTransaction.getHex())) .collect(Collectors.toList()); + final List txOutputs = rawTransaction.getVOut() .stream() .filter(e -> e != null && e.getN() != null && e.getValue() != null && e.getScriptPubKey() != null) - .map(e -> { - byte[] scriptProgramBytes = new byte[]{}; - try { - scriptProgramBytes = e.getScriptPubKey().getHex() != null ? - new Script(HEX.decode(e.getScriptPubKey().getHex())).getProgram() : new byte[]{}; - } catch (Throwable t) { - // expected for tx: 0f8e9655b29d76c5e01147704a64faf95a9c90433baacdd39d4c1d2667e570a2 - log.error(e.getScriptPubKey().getAsm()); - log.error(e.getScriptPubKey().getHex()); - log.error(e.getScriptPubKey().toString()); - log.error(t.toString()); - t.printStackTrace(); - } - return new TxOutput(e.getN(), - e.getValue().movePointRight(8).longValue(), - e.getScriptPubKey().getAddresses(), + .map(rawOutput -> { + + return new TxOutput(rawOutput.getN(), + rawOutput.getValue().movePointRight(8).longValue(), rawTransaction.getTxId(), - scriptProgramBytes); + rawOutput.getScriptPubKey(), + blockHeight, + time); }) .collect(Collectors.toList()); @@ -234,7 +224,7 @@ public class BsqBlockchainRpcService extends BsqBlockchainService { return new Tx(txId, txInputs, txOutputs, - rawTransaction.getTime() * 1000); + time); } catch (BitcoindException | CommunicationException e) { throw new BsqBlockchainException(e.getMessage(), e); } diff --git a/core/src/main/java/io/bisq/core/dao/blockchain/BsqBlockchainService.java b/core/src/main/java/io/bisq/core/dao/blockchain/BsqBlockchainService.java index 586d9f658a..9107b555b0 100644 --- a/core/src/main/java/io/bisq/core/dao/blockchain/BsqBlockchainService.java +++ b/core/src/main/java/io/bisq/core/dao/blockchain/BsqBlockchainService.java @@ -71,7 +71,7 @@ abstract public class BsqBlockchainService { abstract Block requestBlock(int i) throws BitcoindException, CommunicationException; - abstract Tx requestTransaction(String txId) throws BsqBlockchainException; + abstract Tx requestTransaction(String txId, int blockHeight) throws BsqBlockchainException; @VisibleForTesting void parseBlockchain(BsqUTXOMap bsqUTXOMap, @@ -138,7 +138,7 @@ abstract public class BsqBlockchainService { List txIds = block.getTxIds(); Tx genesisTx = null; for (String txId : txIds) { - final Tx tx = requestTransaction(txId); + final Tx tx = requestTransaction(txId, blockHeight); block.addTx(tx); if (txId.equals(genesisTxId)) genesisTx = tx; @@ -270,12 +270,19 @@ abstract public class BsqBlockchainService { long availableValue = 0; long totalAvailableBSQInputs = 0; long totalSpendBSQOutputs = 0; - for (TxInput input : tx.getInputs()) { + final String txId = tx.getId(); + for (int inputIndex = 0; inputIndex < tx.getInputs().size(); inputIndex++) { + TxInput input = tx.getInputs().get(inputIndex); String spendingTxId = input.getSpendingTxId(); final int spendingTxOutputIndex = input.getSpendingTxOutputIndex(); + + if (bsqTXOMap.containsTuple(spendingTxId, spendingTxOutputIndex)) { + TxOutput txOutputFromSpendingTx = bsqTXOMap.getByTuple(spendingTxId, spendingTxOutputIndex); + txOutputFromSpendingTx.setSpendInfo(new SpendInfo(blockHeight, txId, inputIndex)); + } + if (bsqUTXOMap.containsTuple(spendingTxId, spendingTxOutputIndex)) { BsqUTXO bsqUTXO = bsqUTXOMap.getByTuple(spendingTxId, spendingTxOutputIndex); - log.debug("input value " + bsqUTXO.getValue()); availableValue = availableValue + bsqUTXO.getValue(); totalAvailableBSQInputs += bsqUTXO.getValue(); bsqUTXOMap.removeByTuple(spendingTxId, spendingTxOutputIndex); @@ -292,14 +299,8 @@ abstract public class BsqBlockchainService { for (TxOutput txOutput : outputs) { availableValue = availableValue - txOutput.getValue(); if (availableValue >= 0) { - if (txOutput.getAddresses().size() != 1) { - final String msg = "We got a address list with more or less than 1 address for a BsqUTXO. " + - "Seems to be a raw MS. Raw MS are not supported with BSQ.\n" + this.toString(); - log.warn(msg); - if (DevEnv.DEV_MODE) - throw new RuntimeException(msg); - } // We are spending available tokens + txOutput.setVerified(true); bsqUTXOMap.add(new BsqUTXO(blockHeight, txOutput, false)); bsqTXOMap.add(txOutput); totalSpendBSQOutputs += txOutput.getValue(); @@ -315,11 +316,12 @@ abstract public class BsqBlockchainService { } } - if (totalAvailableBSQInputs - totalSpendBSQOutputs > 0) { + final long burnedAmount = totalAvailableBSQInputs - totalSpendBSQOutputs; + if (burnedAmount > 0) { log.debug("BSQ have been left which was not spent. Burned BSQ amount={}, tx={}", availableValue, tx.toString()); - + tx.getOutputs().stream().forEach(e -> e.setBurnedFee(burnedAmount)); bsqTXOMap.addBurnedBSQTx(tx); } @@ -336,13 +338,8 @@ abstract public class BsqBlockchainService { //TODO use BsqTXO not BsqUTXO as we dont know if they are unspent // Genesis tx uses all outputs as BSQ outputs for (TxOutput txOutput : outputs) { - if (txOutput.getAddresses().size() != 1) { - final String msg = "We got a address list with more or less than 1 address. " + - "Seems to be a raw MS. Raw MS are not supported with BSQ.\n" + this.toString(); - log.warn(msg); - if (DevEnv.DEV_MODE) - throw new RuntimeException(msg); - } + txOutput.setVerified(true); + txOutput.setBsqCoinBase(true); bsqUTXOMap.add(new BsqUTXO(blockHeight, txOutput, true)); bsqTXOMap.add(txOutput); } diff --git a/core/src/main/java/io/bisq/core/dao/blockchain/BsqUTXO.java b/core/src/main/java/io/bisq/core/dao/blockchain/BsqUTXO.java index cd80e3004b..2c5125b12a 100644 --- a/core/src/main/java/io/bisq/core/dao/blockchain/BsqUTXO.java +++ b/core/src/main/java/io/bisq/core/dao/blockchain/BsqUTXO.java @@ -21,7 +21,6 @@ import lombok.Value; import lombok.extern.slf4j.Slf4j; import java.io.Serializable; -import java.util.List; // Estimation for UTXO set: 1 UTXO object has 78 byte // 1000 UTXOs - 10 000 UTXOs: 78kb -780kb @@ -43,10 +42,7 @@ public class BsqUTXO implements Serializable { } public String getAddress() { - // Only at raw MS outputs addresses have more then 1 entry - // We do not support raw MS for BSQ but lets see if is needed anyway, might be removed - final List addresses = txOutput.getAddresses(); - return addresses.size() == 1 ? addresses.get(0) : addresses.toString(); + return txOutput.getAddress(); } public long getValue() { diff --git a/core/src/main/java/io/bisq/core/dao/blockchain/SpendInfo.java b/core/src/main/java/io/bisq/core/dao/blockchain/SpendInfo.java new file mode 100644 index 0000000000..f60300f2f5 --- /dev/null +++ b/core/src/main/java/io/bisq/core/dao/blockchain/SpendInfo.java @@ -0,0 +1,27 @@ +/* + * 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 . + */ + +package io.bisq.core.dao.blockchain; + +import lombok.Value; + +@Value +public class SpendInfo { + private final long blockHeight; + private final String txId; + private final int inputIndex; +} diff --git a/core/src/main/java/io/bisq/core/dao/blockchain/TxOutput.java b/core/src/main/java/io/bisq/core/dao/blockchain/TxOutput.java index 20e9e5454a..f976581170 100644 --- a/core/src/main/java/io/bisq/core/dao/blockchain/TxOutput.java +++ b/core/src/main/java/io/bisq/core/dao/blockchain/TxOutput.java @@ -17,28 +17,103 @@ package io.bisq.core.dao.blockchain; -import lombok.Value; -import org.bouncycastle.util.encoders.Hex; +import com.neemre.btcdcli4j.core.domain.PubKeyScript; +import io.bisq.common.app.DevEnv; +import io.bisq.common.app.Version; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import javax.annotation.Nullable; import java.io.Serializable; import java.util.List; -@Value +@Getter +@EqualsAndHashCode +@Slf4j public class TxOutput implements Serializable { + // Immutable private final int index; private final long value; - private final List addresses; private final String txId; - private final byte[] scriptProgramBytes; + private final PubKeyScript pubKeyScript; + private final int blockHeight; + private final long time; + private final String txVersion = Version.BSQ_TX_VERSION; + + // Mutable + @Setter + private boolean isBsqCoinBase; + @Setter + private boolean isVerified; + @Setter + private long burnedFee; + + @Nullable + @Setter + private long btcTxFee; + + @Nullable + @Setter + private SpendInfo spendInfo; + + // Lazy set + @Nullable + private String address; + + + public TxOutput(int index, + long value, + String txId, + PubKeyScript pubKeyScript, + int blockHeight, + long time) { + this.index = index; + this.value = value; + this.txId = txId; + this.pubKeyScript = pubKeyScript; + this.blockHeight = blockHeight; + this.time = time; + } + + public List getAddresses() { + return pubKeyScript.getAddresses(); + } + + public String getAddress() { + if (address == null) { + // Only at raw MS outputs addresses have more then 1 entry + // We do not support raw MS for BSQ but lets see if is needed anyway, might be removed + final List addresses = pubKeyScript.getAddresses(); + if (addresses.size() == 1) { + address = addresses.get(0); + } else if (addresses.size() > 1) { + final String msg = "We got a raw Multisig script. That is not supported for BSQ tokens."; + log.warn(msg); + address = addresses.toString(); + if (DevEnv.DEV_MODE) + throw new RuntimeException(msg); + } else if (addresses.isEmpty()) { + final String msg = "We got no address. Unsupported pubKeyScript"; + log.warn(msg); + address = ""; + if (DevEnv.DEV_MODE) + throw new RuntimeException(msg); + } + } + return address; + } @Override public String toString() { return "TxOutput{" + "\n index=" + index + ",\n value=" + value + - ",\n addresses=" + addresses + + ",\n address=" + getAddress() + ",\n txId=" + txId + - ",\n scriptProgramBytes=" + Hex.toHexString(scriptProgramBytes) + + ",\n pubKeyScript=" + pubKeyScript + + ",\n spendInfo=" + spendInfo + "\n" + " }"; } diff --git a/core/src/main/java/io/bisq/core/dao/blockchain/json/ScriptPubKeyJson.java b/core/src/main/java/io/bisq/core/dao/blockchain/json/ScriptPubKeyForJson.java similarity index 96% rename from core/src/main/java/io/bisq/core/dao/blockchain/json/ScriptPubKeyJson.java rename to core/src/main/java/io/bisq/core/dao/blockchain/json/ScriptPubKeyForJson.java index 6a304ec9a2..99817177ff 100644 --- a/core/src/main/java/io/bisq/core/dao/blockchain/json/ScriptPubKeyJson.java +++ b/core/src/main/java/io/bisq/core/dao/blockchain/json/ScriptPubKeyForJson.java @@ -22,7 +22,7 @@ import lombok.Value; import java.util.List; @Value -public class ScriptPubKeyJson { +public class ScriptPubKeyForJson { private final List addresses; private final String asm; private final String hex; diff --git a/core/src/main/java/io/bisq/core/dao/blockchain/json/SpentInfoJson.java b/core/src/main/java/io/bisq/core/dao/blockchain/json/SpentInfoForJson.java similarity index 89% rename from core/src/main/java/io/bisq/core/dao/blockchain/json/SpentInfoJson.java rename to core/src/main/java/io/bisq/core/dao/blockchain/json/SpentInfoForJson.java index 5be7b148d0..1a05eae57e 100644 --- a/core/src/main/java/io/bisq/core/dao/blockchain/json/SpentInfoJson.java +++ b/core/src/main/java/io/bisq/core/dao/blockchain/json/SpentInfoForJson.java @@ -20,8 +20,8 @@ package io.bisq.core.dao.blockchain.json; import lombok.Value; @Value -public class SpentInfoJson { +public class SpentInfoForJson { private final long height; - private final int index; - private final String txid; -} + private final int inputIndex; + private final String txId; +} \ No newline at end of file diff --git a/core/src/main/java/io/bisq/core/dao/blockchain/json/TxOutputForJson.java b/core/src/main/java/io/bisq/core/dao/blockchain/json/TxOutputForJson.java index 11226de2e4..ff0a2727c4 100644 --- a/core/src/main/java/io/bisq/core/dao/blockchain/json/TxOutputForJson.java +++ b/core/src/main/java/io/bisq/core/dao/blockchain/json/TxOutputForJson.java @@ -21,23 +21,21 @@ import lombok.Value; @Value public class TxOutputForJson { - private final boolean coinBase; + private final String txId; + private final int outputIndex; + private final long bsqAmount; private final int height; - private final int index; - private final boolean invalid; - private final int n; - private final int output_index; + private final boolean isBsqCoinBase; + private final boolean isVerified; + private final long burnedFee; + private final long btcTxFee; + private final ScriptPubKeyForJson scriptPubKey; + private final SpentInfoForJson spentInfo; + private final long time; + private final String txVersion; - private final ScriptPubKeyJson scriptPubKey; - private final SpentInfoJson spent_info; + public String getSortData() { + return height + txId + outputIndex; + } - private final long squ_amount; - private final String status; - private final String transaction_version; - private final long tx_time; - private final String tx_type_str; - private final String txid; - private final boolean validated; - private final double value; - private final long valueSat; } diff --git a/gui/src/main/java/io/bisq/gui/main/dao/wallet/receive/BsqReceiveView.java b/gui/src/main/java/io/bisq/gui/main/dao/wallet/receive/BsqReceiveView.java index 6e04466f19..e63d468c2a 100644 --- a/gui/src/main/java/io/bisq/gui/main/dao/wallet/receive/BsqReceiveView.java +++ b/gui/src/main/java/io/bisq/gui/main/dao/wallet/receive/BsqReceiveView.java @@ -38,6 +38,7 @@ import javafx.scene.image.ImageView; import javafx.scene.layout.GridPane; import net.glxn.qrgen.QRCode; import net.glxn.qrgen.image.ImageType; +import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; import org.bitcoinj.uri.BitcoinURI; import org.fxmisc.easybind.EasyBind; @@ -58,7 +59,7 @@ public class BsqReceiveView extends ActivatableView { private TextField balanceTextField; private final BsqWalletService bsqWalletService; - private final BsqFormatter formatter; + private final BsqFormatter bsqFormatter; private final BalanceUtil balanceUtil; private int gridRow = 0; @@ -71,9 +72,9 @@ public class BsqReceiveView extends ActivatableView { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - private BsqReceiveView(BsqWalletService bsqWalletService, BsqFormatter formatter, BalanceUtil balanceUtil) { + private BsqReceiveView(BsqWalletService bsqWalletService, BsqFormatter bsqFormatter, BalanceUtil balanceUtil) { this.bsqWalletService = bsqWalletService; - this.formatter = formatter; + this.bsqFormatter = bsqFormatter; this.balanceUtil = balanceUtil; paymentLabelString = Res.get("dao.wallet.receive.fundBSQWallet"); } @@ -108,7 +109,7 @@ public class BsqReceiveView extends ActivatableView { balanceUtil.activate(); amountTextFieldSubscription = EasyBind.subscribe(amountTextField.textProperty(), t -> { - addressTextField.setAmountAsCoin(formatter.parseToCoin(t)); + addressTextField.setAmountAsCoin(bsqFormatter.parseToCoin(t)); updateQRCode(); }); qrCodeImageView.setOnMouseClicked(e -> GUIUtil.showFeeInfoBeforeExecute( @@ -116,7 +117,7 @@ public class BsqReceiveView extends ActivatableView { () -> new QRCodeWindow(getBitcoinURI()).show(), 200, TimeUnit.MILLISECONDS) )); - addressTextField.setAddress(bsqWalletService.freshReceiveAddress().toString()); + addressTextField.setAddress(bsqFormatter.getBsqAddressStringFromAddress(bsqWalletService.freshReceiveAddress())); updateQRCode(); } @@ -142,11 +143,12 @@ public class BsqReceiveView extends ActivatableView { } private Coin getAmountAsCoin() { - return formatter.parseToCoin(amountTextField.getText()); + return bsqFormatter.parseToCoin(amountTextField.getText()); } private String getBitcoinURI() { - return BitcoinURI.convertToBitcoinURI(addressTextField.getAddress(), + Address addressFromBsqAddress = bsqFormatter.getAddressFromBsqAddress(addressTextField.getAddress()); + return BitcoinURI.convertToBitcoinURI(addressFromBsqAddress, getAmountAsCoin(), paymentLabelString, null); diff --git a/gui/src/main/java/io/bisq/gui/main/dao/wallet/send/BsqSendView.java b/gui/src/main/java/io/bisq/gui/main/dao/wallet/send/BsqSendView.java index afaad64491..68ba17678c 100644 --- a/gui/src/main/java/io/bisq/gui/main/dao/wallet/send/BsqSendView.java +++ b/gui/src/main/java/io/bisq/gui/main/dao/wallet/send/BsqSendView.java @@ -53,7 +53,7 @@ public class BsqSendView extends ActivatableView { private final BsqWalletService bsqWalletService; private final BtcWalletService btcWalletService; - private final BSFormatter bsqFormatter; + private final BsqFormatter bsqFormatter; private final BSFormatter btcFormatter; private final BalanceUtil balanceUtil; private BsqValidator bsqValidator; @@ -113,7 +113,7 @@ public class BsqSendView extends ActivatableView { } sendButton.setOnAction((event) -> { - String receiversAddressString = receiversAddressInputTextField.getText(); + String receiversAddressString = bsqFormatter.getAddressFromBsqAddress(receiversAddressInputTextField.getText()).toString(); Coin receiverAmount = bsqFormatter.parseToCoin(amountInputTextField.getText()); try { Transaction preparedSendTx = bsqWalletService.getPreparedSendTx(receiversAddressString, receiverAmount); @@ -124,7 +124,7 @@ public class BsqSendView extends ActivatableView { new Popup().headLine(Res.get("dao.wallet.send.sendFunds.headline")) .confirmation(Res.get("dao.wallet.send.sendFunds.details", bsqFormatter.formatCoinWithCode(receiverAmount), - receiversAddressString, + receiversAddressInputTextField.getText(), btcFormatter.formatCoinWithCode(miningFee), CoinUtil.getFeePerByte(miningFee, txSize), txSize / 1000d, diff --git a/gui/src/main/java/io/bisq/gui/main/dao/wallet/tx/BsqTxListItem.java b/gui/src/main/java/io/bisq/gui/main/dao/wallet/tx/BsqTxListItem.java index f816785013..70c8aa7b3b 100644 --- a/gui/src/main/java/io/bisq/gui/main/dao/wallet/tx/BsqTxListItem.java +++ b/gui/src/main/java/io/bisq/gui/main/dao/wallet/tx/BsqTxListItem.java @@ -23,6 +23,7 @@ import io.bisq.core.btc.wallet.BsqWalletService; import io.bisq.core.btc.wallet.BtcWalletService; import io.bisq.core.btc.wallet.WalletUtils; import io.bisq.gui.components.indicator.TxConfidenceIndicator; +import io.bisq.gui.util.BsqFormatter; import io.bisq.gui.util.GUIUtil; import javafx.scene.control.Tooltip; import lombok.Getter; @@ -61,6 +62,7 @@ class BsqTxListItem { private boolean received; @Getter private boolean isBurnedBsqTx; + private BsqFormatter bsqFormatter; @Getter private TxConfidenceIndicator txConfidenceIndicator; @@ -69,11 +71,13 @@ class BsqTxListItem { public BsqTxListItem(Transaction transaction, BsqWalletService bsqWalletService, BtcWalletService btcWalletService, - boolean isBurnedBsqTx) { + boolean isBurnedBsqTx, + BsqFormatter bsqFormatter) { this.transaction = transaction; this.bsqWalletService = bsqWalletService; this.btcWalletService = btcWalletService; this.isBurnedBsqTx = isBurnedBsqTx; + this.bsqFormatter = bsqFormatter; txId = transaction.getHashAsString(); date = transaction.getUpdateTime(); @@ -106,7 +110,7 @@ class BsqTxListItem { WalletUtils.isOutputScriptConvertableToAddress(output)) { // We don't support send txs with multiple outputs to multiple receivers, so we can // assume that only one output is not from our own wallets. - foreignReceiverAddress = WalletUtils.getAddressStringFromOutput(output); + foreignReceiverAddress = bsqFormatter.getBsqAddressStringFromAddress(WalletUtils.getAddressFromOutput(output)); break; } } @@ -117,7 +121,7 @@ class BsqTxListItem { if (foreignReceiverAddress != null) { for (TransactionOutput output : transaction.getOutputs()) { if (WalletUtils.isOutputScriptConvertableToAddress(output)) { - ownReceiverAddress = WalletUtils.getAddressStringFromOutput(output); + ownReceiverAddress = bsqFormatter.getBsqAddressStringFromAddress(WalletUtils.getAddressFromOutput(output)); break; } } diff --git a/gui/src/main/java/io/bisq/gui/main/dao/wallet/tx/BsqTxView.java b/gui/src/main/java/io/bisq/gui/main/dao/wallet/tx/BsqTxView.java index b5cecaf83d..1d5f084fb4 100644 --- a/gui/src/main/java/io/bisq/gui/main/dao/wallet/tx/BsqTxView.java +++ b/gui/src/main/java/io/bisq/gui/main/dao/wallet/tx/BsqTxView.java @@ -134,9 +134,10 @@ public class BsqTxView extends ActivatableView { Map burnedBSQTxIdMap = bsqBlockchainManager.getBsqTXOMap().getBurnedBSQTxMap(); Set list = bsqWalletService.getWalletBsqTransactions().stream() .map(transaction -> new BsqTxListItem(transaction, - bsqWalletService, - btcWalletService, - burnedBSQTxIdMap.containsKey(transaction.getHashAsString()))) + bsqWalletService, + btcWalletService, + burnedBSQTxIdMap.containsKey(transaction.getHashAsString()), bsqFormatter) + ) .collect(Collectors.toSet()); observableList.setAll(list); diff --git a/gui/src/main/java/io/bisq/gui/util/Base58Bsq.java b/gui/src/main/java/io/bisq/gui/util/Base58Bsq.java new file mode 100644 index 0000000000..a9efbcc849 --- /dev/null +++ b/gui/src/main/java/io/bisq/gui/util/Base58Bsq.java @@ -0,0 +1,211 @@ +/** + * Copyright 2011 Google Inc. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.bisq.gui.util; + +import lombok.extern.slf4j.Slf4j; +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.Sha256Hash; +import org.bitcoinj.core.Utils; +import org.bitcoinj.core.VersionedChecksummedBytes; + +import java.math.BigInteger; +import java.util.Arrays; + +/** + *

Base58 is a way to encode Bitcoin addresses as numbers and letters. Note that this is not the same base58 as used by + * Flickr, which you may see reference to around the internet.

+ * + *

You may instead wish to work with {@link VersionedChecksummedBytes}, which adds support for testing the prefix + * and suffix bytes commonly found in addresses.

+ * + *

Satoshi says: why base-58 instead of standard base-64 encoding?

+ * + *

    + *
  • Don't want 0OIl characters that look the same in some fonts and + * could be used to create visually identical looking account numbers.
  • + *
  • A string with non-alphanumeric characters is not as easily accepted as an account number.
  • + *
  • E-mail usually won't line-break if there's no punctuation to break at.
  • + *
  • Doubleclicking selects the whole number as one word if it's all alphanumeric.
  • + *
+ */ + +// We use Base58 with a different alphabet order, so we would get a checksum failure if the user mixes up btc and bsq +@Slf4j +public class Base58Bsq { + // public static final char[] ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray(); + // we wan tot have B at beginning, usually its 1 for btc addresses + public static final char[] ALPHABET = "BCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz123456789A".toCharArray(); + + private static final int[] INDEXES = new int[128]; + + static { + for (int i = 0; i < INDEXES.length; i++) { + INDEXES[i] = -1; + } + for (int i = 0; i < ALPHABET.length; i++) { + INDEXES[ALPHABET[i]] = i; + } + } + + /** Encodes the given bytes in base58. No checksum is appended. */ + public static String encode(byte[] input) { + if (input.length == 0) { + return ""; + } + input = copyOfRange(input, 0, input.length); + // Count leading zeroes. + int zeroCount = 0; + while (zeroCount < input.length && input[zeroCount] == 0) { + ++zeroCount; + } + // The actual encoding. + byte[] temp = new byte[input.length * 2]; + int j = temp.length; + + int startAt = zeroCount; + while (startAt < input.length) { + byte mod = divmod58(input, startAt); + if (input[startAt] == 0) { + ++startAt; + } + temp[--j] = (byte) ALPHABET[mod]; + } + + // Strip extra '1' if there are some after decoding. + while (j < temp.length && temp[j] == ALPHABET[0]) { + ++j; + } + // Add as many leading '1' as there were leading zeros. + while (--zeroCount >= 0) { + temp[--j] = (byte) ALPHABET[0]; + } + + byte[] output = copyOfRange(temp, j, temp.length); + return Utils.toString(output, "US-ASCII"); + } + + public static byte[] decode(String input) throws AddressFormatException { + if (input.length() == 0) { + return new byte[0]; + } + byte[] input58 = new byte[input.length()]; + // Transform the String to a base58 byte sequence + for (int i = 0; i < input.length(); ++i) { + char c = input.charAt(i); + + int digit58 = -1; + if (c >= 0 && c < 128) { + digit58 = INDEXES[c]; + } + if (digit58 < 0) { + throw new AddressFormatException("Illegal character " + c + " at " + i); + } + + input58[i] = (byte) digit58; + } + // Count leading zeroes + int zeroCount = 0; + while (zeroCount < input58.length && input58[zeroCount] == 0) { + ++zeroCount; + } + // The encoding + byte[] temp = new byte[input.length()]; + int j = temp.length; + + int startAt = zeroCount; + while (startAt < input58.length) { + byte mod = divmod256(input58, startAt); + if (input58[startAt] == 0) { + ++startAt; + } + + temp[--j] = mod; + } + // Do no add extra leading zeroes, move j to first non null byte. + while (j < temp.length && temp[j] == 0) { + ++j; + } + + return copyOfRange(temp, j - zeroCount, temp.length); + } + + public static BigInteger decodeToBigInteger(String input) throws AddressFormatException { + return new BigInteger(1, decode(input)); + } + + /** + * Uses the checksum in the last 4 bytes of the decoded data to verify the rest are correct. The checksum is + * removed from the returned data. + * + * @throws AddressFormatException if the input is not base 58 or the checksum does not validate. + */ + public static byte[] decodeChecked(String input) throws AddressFormatException { + byte[] tmp = decode(input); + if (tmp.length < 4) + throw new AddressFormatException("Input too short"); + byte[] bytes = copyOfRange(tmp, 0, tmp.length - 4); + byte[] checksum = copyOfRange(tmp, tmp.length - 4, tmp.length); + + tmp = Sha256Hash.hashTwice(bytes); + byte[] hash = copyOfRange(tmp, 0, 4); + if (!Arrays.equals(checksum, hash)) + throw new AddressFormatException("Checksum does not validate"); + + return bytes; + } + + // + // number -> number / 58, returns number % 58 + // + private static byte divmod58(byte[] number, int startAt) { + int remainder = 0; + for (int i = startAt; i < number.length; i++) { + int digit256 = (int) number[i] & 0xFF; + int temp = remainder * 256 + digit256; + + number[i] = (byte) (temp / 58); + + remainder = temp % 58; + } + + return (byte) remainder; + } + + // + // number -> number / 256, returns number % 256 + // + private static byte divmod256(byte[] number58, int startAt) { + int remainder = 0; + for (int i = startAt; i < number58.length; i++) { + int digit58 = (int) number58[i] & 0xFF; + int temp = remainder * 58 + digit58; + + number58[i] = (byte) (temp / 256); + + remainder = temp % 256; + } + + return (byte) remainder; + } + + private static byte[] copyOfRange(byte[] source, int from, int to) { + byte[] range = new byte[to - from]; + System.arraycopy(source, from, range, 0, range.length); + + return range; + } +} diff --git a/gui/src/main/java/io/bisq/gui/util/BsqFormatter.java b/gui/src/main/java/io/bisq/gui/util/BsqFormatter.java index a653b6a4aa..75e9e00e4d 100644 --- a/gui/src/main/java/io/bisq/gui/util/BsqFormatter.java +++ b/gui/src/main/java/io/bisq/gui/util/BsqFormatter.java @@ -17,6 +17,10 @@ package io.bisq.gui.util; +import io.bisq.core.btc.wallet.WalletUtils; +import org.bitcoinj.core.Address; +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.utils.MonetaryFormat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,6 +29,7 @@ import javax.inject.Inject; public class BsqFormatter extends BSFormatter { private static final Logger log = LoggerFactory.getLogger(BsqFormatter.class); + private static final boolean useBsqAddressFormat = false; @Inject private BsqFormatter() { @@ -32,4 +37,50 @@ public class BsqFormatter extends BSFormatter { coinFormat = new MonetaryFormat().shift(5).code(5, "BSQ").minDecimals(3); } + /** + * Returns the base-58 encoded String representation of this + * object, including version and checksum bytes. + */ + public String getBsqAddressStringFromAddress(Address address) { + if (useBsqAddressFormat) { + byte[] bytes = address.getHash160(); + int version = address.getVersion(); + // A stringified buffer is: + // 1 byte version + data bytes + 4 bytes check code (a truncated hash) + byte[] addressBytes = new byte[1 + bytes.length + 4]; + addressBytes[0] = (byte) version; + System.arraycopy(bytes, 0, addressBytes, 1, bytes.length); + byte[] checksum = Sha256Hash.hashTwice(addressBytes, 0, bytes.length + 1); + System.arraycopy(checksum, 0, addressBytes, bytes.length + 1, 4); + // return "BSQ" + Base58Bsq.encode(addressBytes); + return Base58Bsq.encode(addressBytes); + } else { + return address.toString(); + } + } + + + public Address getAddressFromBsqAddress(String encoded) { + if (useBsqAddressFormat) { + try { + //encoded = encoded.substring(3, encoded.length()); + byte[] versionAndDataBytes = Base58Bsq.decodeChecked(encoded); + byte[] bytes = new byte[versionAndDataBytes.length - 1]; + System.arraycopy(versionAndDataBytes, 1, bytes, 0, versionAndDataBytes.length - 1); + return new Address(WalletUtils.getParameters(), bytes); + } catch (AddressFormatException e) { + log.error(e.toString()); + e.printStackTrace(); + throw new RuntimeException(e); + } + } else { + try { + return new Address(WalletUtils.getParameters(), encoded); + } catch (AddressFormatException e) { + log.error(e.toString()); + e.printStackTrace(); + throw new RuntimeException(e); + } + } + } } diff --git a/gui/src/main/java/io/bisq/gui/util/validation/BsqAddressValidator.java b/gui/src/main/java/io/bisq/gui/util/validation/BsqAddressValidator.java index 6f273b2b12..5cef630337 100644 --- a/gui/src/main/java/io/bisq/gui/util/validation/BsqAddressValidator.java +++ b/gui/src/main/java/io/bisq/gui/util/validation/BsqAddressValidator.java @@ -18,20 +18,20 @@ package io.bisq.gui.util.validation; import io.bisq.common.locale.Res; -import io.bisq.core.btc.wallet.WalletUtils; import io.bisq.core.user.Preferences; -import org.bitcoinj.core.Address; -import org.bitcoinj.core.AddressFormatException; +import io.bisq.gui.util.BsqFormatter; import javax.inject.Inject; public final class BsqAddressValidator extends InputValidator { private final Preferences preferences; + private BsqFormatter bsqFormatter; @Inject - public BsqAddressValidator(Preferences preferences) { + public BsqAddressValidator(Preferences preferences, BsqFormatter bsqFormatter) { this.preferences = preferences; + this.bsqFormatter = bsqFormatter; } @Override @@ -46,9 +46,9 @@ public final class BsqAddressValidator extends InputValidator { private ValidationResult validateBsqAddress(String input) { try { - new Address(WalletUtils.getParameters(), input); + bsqFormatter.getAddressFromBsqAddress(input); return new ValidationResult(true); - } catch (AddressFormatException e) { + } catch (Throwable e) { return new ValidationResult(false, Res.get("validation.bsq.invalidFormat")); } }