Add memo field to withdrawal transaction

- "Memo" field is modeled as property of the new object Transaction which is stored in persitant storage.
- Transaction object is modeled in a way that allows extension in the furure for more persisted attributes.
This commit is contained in:
Petr Hejna 2020-05-02 22:11:43 +02:00
parent 5e5d7d1577
commit 8d5f42f122
No known key found for this signature in database
GPG key ID: 3E7F8DE90B479830
10 changed files with 201 additions and 10 deletions

View file

@ -39,6 +39,7 @@ import bisq.core.support.dispute.mediation.MediationDisputeList;
import bisq.core.support.dispute.refund.RefundDisputeList;
import bisq.core.trade.TradableList;
import bisq.core.trade.statistics.TradeStatistics2Store;
import bisq.core.transaction.TransactionsPayload;
import bisq.core.user.PreferencesPayload;
import bisq.core.user.UserPayload;
@ -148,6 +149,8 @@ public class CorePersistenceProtoResolver extends CoreProtoResolver implements P
return UnconfirmedBsqChangeOutputList.fromProto(proto.getUnconfirmedBsqChangeOutputList());
case SIGNED_WITNESS_STORE:
return SignedWitnessStore.fromProto(proto.getSignedWitnessStore());
case TRANSACTIONS_PAYLOAD:
return TransactionsPayload.fromProto(proto.getTransactionsPayload());
default:
throw new ProtobufferRuntimeException("Unknown proto message case(PB.PersistableEnvelope). " +

View file

@ -0,0 +1,29 @@
package bisq.core.transaction;
import bisq.common.Payload;
public class Transaction implements Payload {
private final String txId;
private final String memo;
public Transaction(String txId, String memo) {
this.txId = txId;
this.memo = memo;
}
@Override
public protobuf.Transaction toProtoMessage() {
final protobuf.Transaction.Builder builder = protobuf.Transaction.newBuilder()
.setMemo(memo)
.setTxId(txId);
return builder.build();
}
public static Transaction fromProto(protobuf.Transaction protobufTransaction) {
return new Transaction(protobufTransaction.getTxId(), protobufTransaction.getMemo());
}
public String getTxId() { return txId; }
public String getMemo() { return memo; }
}

View file

@ -0,0 +1,63 @@
package bisq.core.transaction;
import bisq.common.proto.persistable.PersistableEnvelope;
import com.google.protobuf.Message;
import javax.inject.Singleton;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@Slf4j
@Singleton
public class TransactionsPayload implements PersistableEnvelope {
private final Map<String, Transaction> transactions;
public TransactionsPayload(Map<String, Transaction> transactions) {
this.transactions = transactions;
}
public Transaction findTransactionById(String txId) {
@Nullable
Transaction result = this.transactions.get(txId);
return result != null ? result : new Transaction(txId, "");
}
@Override
public Message toProtoMessage() {
protobuf.TransactionsPayload.Builder builder = protobuf.TransactionsPayload.newBuilder();
Optional.ofNullable(transactions)
.ifPresent(transactions -> builder.addAllTransactions(
transactions
.values()
.stream()
.map(Transaction::toProtoMessage)
.collect(Collectors.toList())
)
);
return protobuf.PersistableEnvelope.newBuilder().setTransactionsPayload(builder).build();
}
public void addTransaction(Transaction transaction) {
this.transactions.put(transaction.getTxId(), transaction);
}
public static TransactionsPayload fromProto(protobuf.TransactionsPayload proto) {
Map<String, Transaction> map = new HashMap<>();
proto.getTransactionsList().forEach(
protoTransaction -> map.put(protoTransaction.getTxId(), Transaction.fromProto(protoTransaction))
);
return new TransactionsPayload(map);
}
}

View file

@ -899,6 +899,8 @@ funds.withdrawal.feeExcluded=Amount excludes mining fee
funds.withdrawal.feeIncluded=Amount includes mining fee
funds.withdrawal.fromLabel=Withdraw from address
funds.withdrawal.toLabel=Withdraw to address
funds.withdrawal.memoLabel=Withdrawal memo
funds.withdrawal.memo=Optional text as memo for the withdrawal
funds.withdrawal.withdrawButton=Withdraw selected
funds.withdrawal.noFundsAvailable=No funds are available for withdrawal
funds.withdrawal.confirmWithdrawalRequest=Confirm withdrawal request
@ -936,6 +938,7 @@ funds.tx.noFundsFromDispute=No refund from dispute
funds.tx.receivedFunds=Received funds
funds.tx.withdrawnFromWallet=Withdrawn from wallet
funds.tx.withdrawnFromBSQWallet=BTC withdrawn from BSQ wallet
funds.tx.memo=Memo
funds.tx.noTxAvailable=No transactions available
funds.tx.revert=Revert
funds.tx.txSent=Transaction successfully sent to a new address in the local Bisq wallet.

View file

@ -20,16 +20,21 @@ package bisq.desktop.main.funds.transactions;
import bisq.core.btc.wallet.BsqWalletService;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.dao.DaoFacade;
import bisq.core.transaction.TransactionsPayload;
import bisq.core.user.Preferences;
import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.CoinFormatter;
import bisq.common.storage.Storage;
import org.bitcoinj.core.Transaction;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.HashMap;
import javax.annotation.Nullable;
@ -40,27 +45,38 @@ public class TransactionListItemFactory {
private final DaoFacade daoFacade;
private final CoinFormatter formatter;
private final Preferences preferences;
private final Storage<TransactionsPayload> storage;
@Inject
TransactionListItemFactory(BtcWalletService btcWalletService,
BsqWalletService bsqWalletService,
DaoFacade daoFacade,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
Preferences preferences) {
Preferences preferences,
Storage<TransactionsPayload> storage
) {
this.btcWalletService = btcWalletService;
this.bsqWalletService = bsqWalletService;
this.daoFacade = daoFacade;
this.formatter = formatter;
this.preferences = preferences;
this.storage = storage;
}
TransactionsListItem create(Transaction transaction, @Nullable TransactionAwareTradable tradable) {
TransactionsPayload persisted = storage.initAndGetPersistedWithFileName("TransactionPayload", 100);
if (persisted == null) {
persisted = new TransactionsPayload(new HashMap<>());
}
return new TransactionsListItem(transaction,
btcWalletService,
bsqWalletService,
tradable,
daoFacade,
formatter,
preferences.getIgnoreDustThreshold());
preferences.getIgnoreDustThreshold(),
persisted.findTransactionById(transaction.getHashAsString())
);
}
}

View file

@ -67,6 +67,7 @@ class TransactionsListItem {
private boolean received;
private boolean detailsAvailable;
private Coin amountAsCoin = Coin.ZERO;
private String memo = "";
private int confirmations = 0;
@Getter
private final boolean isDustAttackTx;
@ -88,9 +89,12 @@ class TransactionsListItem {
TransactionAwareTradable transactionAwareTradable,
DaoFacade daoFacade,
CoinFormatter formatter,
long ignoreDustThreshold) {
long ignoreDustThreshold,
bisq.core.transaction.Transaction storedTransaction
) {
this.btcWalletService = btcWalletService;
this.formatter = formatter;
this.memo = storedTransaction.getMemo();
txId = transaction.getHashAsString();
@ -340,5 +344,7 @@ class TransactionsListItem {
public String getNumConfirmations() {
return String.valueOf(confirmations);
}
public String getMemo() { return memo; }
}

View file

@ -34,6 +34,7 @@
<TableColumn fx:id="addressColumn" minWidth="260"/>
<TableColumn fx:id="transactionColumn" minWidth="180"/>
<TableColumn fx:id="amountColumn" minWidth="130" maxWidth="130"/>
<TableColumn fx:id="memoColumn" minWidth="50" maxWidth="50"/>
<TableColumn fx:id="confidenceColumn" minWidth="130" maxWidth="130"/>
<TableColumn fx:id="revertTxColumn" sortable="false" minWidth="110" maxWidth="110" visible="false"/>
</columns>

View file

@ -89,7 +89,7 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
@FXML
TableView<TransactionsListItem> tableView;
@FXML
TableColumn<TransactionsListItem, TransactionsListItem> dateColumn, detailsColumn, addressColumn, transactionColumn, amountColumn, confidenceColumn, revertTxColumn;
TableColumn<TransactionsListItem, TransactionsListItem> dateColumn, detailsColumn, addressColumn, transactionColumn, amountColumn, memoColumn, confidenceColumn, revertTxColumn;
@FXML
AutoTooltipButton exportButton;
@ -136,6 +136,7 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
addressColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.address")));
transactionColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.txId", Res.getBaseCurrencyCode())));
amountColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amountWithCur", Res.getBaseCurrencyCode())));
memoColumn.setGraphic(new AutoTooltipLabel(Res.get("funds.tx.memo")));
confidenceColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.confirmations", Res.getBaseCurrencyCode())));
revertTxColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.revert", Res.getBaseCurrencyCode())));
@ -147,6 +148,7 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
setAddressColumnCellFactory();
setTransactionColumnCellFactory();
setAmountColumnCellFactory();
setMemoColumnCellFactory();
setConfidenceColumnCellFactory();
setRevertTxColumnCellFactory();
@ -237,13 +239,14 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
return columns;
};
CSVEntryConverter<TransactionsListItem> contentConverter = item -> {
String[] columns = new String[6];
String[] columns = new String[7];
columns[0] = item.getDateString();
columns[1] = item.getDetails();
columns[2] = item.getDirection() + " " + item.getAddressString();
columns[3] = item.getTxId();
columns[4] = item.getAmount();
columns[5] = item.getNumConfirmations();
columns[5] = item.getMemo();
columns[6] = item.getNumConfirmations();
return columns;
};
@ -453,6 +456,33 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
});
}
private void setMemoColumnCellFactory() {
memoColumn.setCellValueFactory((addressListItem) ->
new ReadOnlyObjectWrapper<>(addressListItem.getValue()));
memoColumn.setCellFactory(
new Callback<>() {
@Override
public TableCell<TransactionsListItem, TransactionsListItem> call(TableColumn<TransactionsListItem,
TransactionsListItem> column) {
return new TableCell<>() {
@Override
public void updateItem(final TransactionsListItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
setGraphic(new AutoTooltipLabel(item.getMemo()));
} else {
setGraphic(null);
}
}
};
}
});
}
private void setConfidenceColumnCellFactory() {
confidenceColumn.getStyleClass().add("last-column");
confidenceColumn.setCellValueFactory((addressListItem) ->

View file

@ -39,6 +39,7 @@ import bisq.core.btc.wallet.Restrictions;
import bisq.core.locale.Res;
import bisq.core.trade.Trade;
import bisq.core.trade.TradeManager;
import bisq.core.transaction.TransactionsPayload;
import bisq.core.user.Preferences;
import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.CoinFormatter;
@ -49,6 +50,7 @@ import bisq.core.util.validation.BtcAddressValidator;
import bisq.network.p2p.P2PService;
import bisq.common.UserThread;
import bisq.common.storage.Storage;
import bisq.common.util.Tuple3;
import bisq.common.util.Tuple4;
@ -98,10 +100,9 @@ import javafx.util.Callback;
import org.spongycastle.crypto.params.KeyParameter;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@ -125,7 +126,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
private RadioButton useAllInputsRadioButton, useCustomInputsRadioButton, feeExcludedRadioButton;
private Label amountLabel;
private TextField amountTextField, withdrawFromTextField, withdrawToTextField;
private TextField amountTextField, withdrawFromTextField, withdrawToTextField, withdrawMemoTextField;
private final BtcWalletService walletService;
private final TradeManager tradeManager;
@ -135,6 +136,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
private final Preferences preferences;
private final BtcAddressValidator btcAddressValidator;
private final WalletPasswordWindow walletPasswordWindow;
private final Storage<TransactionsPayload> transactionPayloadStorage;
private final ObservableList<WithdrawalListItem> observableList = FXCollections.observableArrayList();
private final SortedList<WithdrawalListItem> sortedList = new SortedList<>(observableList);
private Set<WithdrawalListItem> selectedItems = new HashSet<>();
@ -164,7 +166,9 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
Preferences preferences,
BtcAddressValidator btcAddressValidator,
WalletPasswordWindow walletPasswordWindow) {
WalletPasswordWindow walletPasswordWindow,
Storage<TransactionsPayload> transactionPayloadStorage
) {
this.walletService = walletService;
this.tradeManager = tradeManager;
this.p2PService = p2PService;
@ -173,6 +177,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
this.preferences = preferences;
this.btcAddressValidator = btcAddressValidator;
this.walletPasswordWindow = walletPasswordWindow;
this.transactionPayloadStorage = transactionPayloadStorage;
}
@Override
@ -220,6 +225,10 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
Res.get("funds.withdrawal.toLabel", Res.getBaseCurrencyCode())).second;
withdrawToTextField.setMaxWidth(380);
withdrawMemoTextField = addTopLabelInputTextField(gridPane, ++rowIndex,
Res.get("funds.withdrawal.memoLabel", Res.getBaseCurrencyCode())).second;
withdrawMemoTextField.setMaxWidth(380);
final Button withdrawButton = addButton(gridPane, ++rowIndex, Res.get("funds.withdrawal.withdrawButton"), 15);
withdrawButton.setOnAction(event -> onWithdraw());
@ -385,6 +394,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
public void onSuccess(@javax.annotation.Nullable Transaction transaction) {
if (transaction != null) {
log.debug("onWithdraw onSuccess tx ID:{}", transaction.getHashAsString());
storeTransaction(transaction);
} else {
log.error("onWithdraw transaction is null");
}
@ -420,6 +430,19 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
}
}
private void storeTransaction(@NotNull Transaction transaction) {
bisq.core.transaction.Transaction transactionToBeStored = new bisq.core.transaction.Transaction(
transaction.getHashAsString(),
withdrawMemoTextField.getText()
);
TransactionsPayload transactionsPayload = transactionPayloadStorage.initAndGetPersistedWithFileName("TransactionPayload", 100);
if (transactionsPayload == null) {
transactionsPayload = new TransactionsPayload(new HashMap<>());
}
transactionsPayload.addTransaction(transactionToBeStored);
transactionPayloadStorage.queueUpForSave(transactionsPayload);
}
private void selectForWithdrawal(WithdrawalListItem item) {
if (item.isSelected())
selectedItems.add(item);
@ -528,6 +551,9 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
withdrawToTextField.setText("");
withdrawToTextField.setPromptText(Res.get("funds.withdrawal.fillDestAddress"));
withdrawMemoTextField.setText("");
withdrawMemoTextField.setPromptText(Res.get("funds.withdrawal.memo"));
selectedItems.clear();
tableView.getSelectionModel().clearSelection();
}

View file

@ -1145,6 +1145,7 @@ message PersistableEnvelope {
SignedWitnessStore signed_witness_store = 28;
MediationDisputeList mediation_dispute_list = 29;
RefundDisputeList refund_dispute_list = 30;
TransactionsPayload transactions_payload = 31;
}
}
@ -2069,3 +2070,16 @@ message MockPayload {
string message_version = 1;
string message = 2;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Transaction
///////////////////////////////////////////////////////////////////////////////////////////
message Transaction {
string txId = 1;
string memo = 2;
}
message TransactionsPayload {
repeated Transaction transactions = 1;
}