Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add feature for cloning an offer with shared maker fee #6675

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4f08f9f
Feat: OCO Offers
Mar 13, 2023
1cc42ae
Bug fix.
Mar 27, 2023
27dcec0
Code review changes.
Apr 2, 2023
e66517f
Code review changes.
Apr 7, 2023
acbff2b
Use domain data as requested by reviewer.
May 2, 2023
966b9d8
Add getAllRecentTransactions method.
HenrikJannsen May 4, 2023
f6c31aa
Remove added code with entryWithSameContextStillExists as it is not n…
HenrikJannsen May 4, 2023
cadf207
Fix updateReservedBalance.
HenrikJannsen May 4, 2023
992854c
Various refactorings and changes
HenrikJannsen May 4, 2023
3403b16
User maker fee as column name (instead of group)
HenrikJannsen May 4, 2023
6036da3
Add cloneItemColumn
HenrikJannsen May 4, 2023
8b91ed7
Only reset if there are no other shared maker fee offers
HenrikJannsen May 5, 2023
a621973
Add translation string
HenrikJannsen May 5, 2023
384173c
Rename translation string
HenrikJannsen May 5, 2023
4ae89e7
Use link icon for cloned offers
HenrikJannsen May 5, 2023
21672a1
Add clone offer tab
HenrikJannsen May 5, 2023
d428956
Publish cloned offer if activated
HenrikJannsen May 5, 2023
13bcfb3
Use new offerId and fresh data at clone offer
HenrikJannsen May 5, 2023
2651a88
Do not reset addressEntries if its from cloned offers at cleanUpAddre…
HenrikJannsen May 5, 2023
60fad3a
Fix incorrect filtering of offers with shared maker fee
HenrikJannsen May 10, 2023
f843345
Use sourceOfferPayload when cloning the OfferPayload for the fields w…
HenrikJannsen May 12, 2023
63ca40d
Remove code for adjusting the security deposit.
HenrikJannsen May 12, 2023
e6dc26a
Further restrict cloning
HenrikJannsen May 12, 2023
b11511e
Do not use sourceOfferPayload for extraDataMap
HenrikJannsen May 13, 2023
4262e91
Reapply original code from @jmacxx in AddressEntryList
HenrikJannsen May 13, 2023
13f7b39
Do not show BSQ accounts in CloneOfferView
HenrikJannsen May 13, 2023
8c37ddd
Use trigger price from model instead from sourceOpenOffer
HenrikJannsen May 20, 2023
dbd1098
Add isCloneOfferViewActivated flag to avoid repeated code execution i…
HenrikJannsen May 20, 2023
a7ade00
Do not reset content in case the currentTab is editOpenOfferTab, dupl…
HenrikJannsen May 20, 2023
114af3f
Add link to wiki
HenrikJannsen May 21, 2023
1cbdf04
Fix bug with selection of account combobox
HenrikJannsen May 22, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/src/main/java/bisq/core/api/CoreOffersService.java
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,7 @@ private void placeOffer(Offer offer,
openOfferManager.placeOffer(offer,
buyerSecurityDepositPct,
useSavingsWallet,
false,
triggerPriceAsLong,
resultHandler::accept,
log::error);
Expand Down
9 changes: 6 additions & 3 deletions core/src/main/java/bisq/core/btc/Balances.java
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,19 @@ private void updateReservedBalance() {
.map(openOffer -> btcWalletService.getAddressEntry(openOffer.getId(), AddressEntry.Context.RESERVED_FOR_TRADE)
.orElse(null))
.filter(Objects::nonNull)
.mapToLong(addressEntry -> btcWalletService.getBalanceForAddress(addressEntry.getAddress()).value)
.map(AddressEntry::getAddress)
.distinct()
.mapToLong(address -> btcWalletService.getBalanceForAddress(address).value)
.sum();
reservedBalance.set(Coin.valueOf(sum));
}

private void updateLockedBalance() {
Stream<Trade> lockedTrades = Stream.concat(closedTradableManager.getTradesStreamWithFundsLockedIn(), failedTradesManager.getTradesStreamWithFundsLockedIn());
lockedTrades = Stream.concat(lockedTrades, tradeManager.getTradesStreamWithFundsLockedIn());
long sum = lockedTrades.map(trade -> btcWalletService.getAddressEntry(trade.getId(), AddressEntry.Context.MULTI_SIG)
.orElse(null))
long sum = lockedTrades.map(trade -> btcWalletService.getAddressEntry(trade.getId(),
AddressEntry.Context.MULTI_SIG)
.orElse(null))
.filter(Objects::nonNull)
.mapToLong(AddressEntry::getCoinLockedInMultiSig)
.sum();
Expand Down
27 changes: 21 additions & 6 deletions core/src/main/java/bisq/core/btc/model/AddressEntryList.java
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ public void onWalletReady(Wallet wallet) {
wallet.getIssuedReceiveAddresses().stream()
.filter(this::isAddressNotInEntries)
.forEach(address -> {
DeterministicKey key = (DeterministicKey) wallet.findKeyFromAddress(address);
DeterministicKey key = (DeterministicKey) wallet.findKeyFromAddress(address);
if (key != null) {
// Address will be derived from key in getAddress method
log.info("Create AddressEntry for IssuedReceiveAddress. address={}", address.toString());
Expand Down Expand Up @@ -209,11 +209,26 @@ public void swapToAvailable(AddressEntry addressEntry) {
}

log.info("swapToAvailable addressEntry to swap={}", addressEntry);
boolean setChangedByRemove = entrySet.remove(addressEntry);
boolean setChangedByAdd = entrySet.add(new AddressEntry(addressEntry.getKeyPair(),
AddressEntry.Context.AVAILABLE,
addressEntry.isSegwit()));
if (setChangedByRemove || setChangedByAdd) {
if (entrySet.remove(addressEntry)) {
requestPersistence();
}
// If we have an address entry which shared the address with another one (shared maker fee offers use case)
// then we do not swap to available as we need to protect the address of the remaining entry.
boolean entryWithSameContextStillExists = entrySet.stream().anyMatch(entry -> {
if (addressEntry.getAddressString() != null) {
return addressEntry.getAddressString().equals(entry.getAddressString()) &&
addressEntry.getContext() == entry.getContext();
}
return false;
});
if (entryWithSameContextStillExists) {
return;
}
// no other uses of the address context remain, so make it available
if (entrySet.add(
new AddressEntry(addressEntry.getKeyPair(),
AddressEntry.Context.AVAILABLE,
addressEntry.isSegwit()))) {
requestPersistence();
}
}
Expand Down
19 changes: 19 additions & 0 deletions core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,25 @@ public Optional<AddressEntry> getAddressEntry(String offerId,
.findAny();
}

// For cloned offers with shared maker fee we create a new address entry based on the source entry
// and set the new offerId.
public AddressEntry getOrCloneAddressEntryWithOfferId(AddressEntry sourceAddressEntry, String offerId) {
Optional<AddressEntry> addressEntry = getAddressEntryListAsImmutableList().stream()
.filter(entry -> offerId.equals(entry.getOfferId()))
.filter(entry -> sourceAddressEntry.getContext() == entry.getContext())
.findAny();
if (addressEntry.isPresent()) {
return addressEntry.get();
} else {
AddressEntry cloneWithNewOfferId = new AddressEntry(sourceAddressEntry.getKeyPair(),
sourceAddressEntry.getContext(),
offerId,
sourceAddressEntry.isSegwit());
addressEntryList.addAddressEntry(cloneWithNewOfferId);
return cloneWithNewOfferId;
}
}

public AddressEntry getOrCreateAddressEntry(String offerId, AddressEntry.Context context) {
Optional<AddressEntry> addressEntry = getAddressEntryListAsImmutableList().stream()
.filter(e -> offerId.equals(e.getOfferId()))
Expand Down
8 changes: 6 additions & 2 deletions core/src/main/java/bisq/core/btc/wallet/WalletService.java
Original file line number Diff line number Diff line change
Expand Up @@ -461,8 +461,8 @@ public TransactionConfidence getConfidenceForAddressFromBlockHeight(Address addr
.map(tx -> getTransactionConfidence(tx, address))
.filter(Objects::nonNull)
.filter(con -> con.getConfidenceType() == TransactionConfidence.ConfidenceType.PENDING ||
(con.getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING &&
con.getAppearedAtChainHeight() > targetHeight))
(con.getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING &&
con.getAppearedAtChainHeight() > targetHeight))
.collect(Collectors.toList()));
}
return getMostRecentConfidence(transactionConfidenceList);
Expand Down Expand Up @@ -751,6 +751,10 @@ public boolean isEncrypted() {
return wallet.isEncrypted();
}

public List<Transaction> getAllRecentTransactions(boolean includeDead) {
return getRecentTransactions(Integer.MAX_VALUE, includeDead);
}

public List<Transaction> getRecentTransactions(int numTransactions, boolean includeDead) {
// Returns a list ordered by tx.getUpdateTime() desc
return wallet.getRecentTransactions(numTransactions, includeDead);
Expand Down
1 change: 1 addition & 0 deletions core/src/main/java/bisq/core/offer/Offer.java
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,7 @@ public String getMakerPaymentAccountId() {
return offerPayloadBase.getMakerPaymentAccountId();
}

@Nullable
public String getOfferFeePaymentTxId() {
return getOfferPayload().map(OfferPayload::getOfferFeePaymentTxId).orElse(null);
}
Expand Down
115 changes: 100 additions & 15 deletions core/src/main/java/bisq/core/offer/OpenOfferManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,10 @@ public void onUpdatedDataReceived() {
}

private void cleanUpAddressEntries() {
Set<String> openOffersIdSet = openOffers.getList().stream().map(OpenOffer::getId).collect(Collectors.toSet());
Set<String> openOffersIdSet = openOffers.getList().stream()
.map(OpenOffer::getId)
.collect(Collectors.toSet());
// We reset all AddressEntriesForOpenOffer which do not have a corresponding openOffer
btcWalletService.getAddressEntriesForOpenOffer().stream()
.filter(e -> !openOffersIdSet.contains(e.getOfferId()))
.forEach(e -> {
Expand Down Expand Up @@ -381,12 +384,19 @@ public void onAwakeFromStandby() {
public void placeOffer(Offer offer,
double buyerSecurityDeposit,
boolean useSavingsWallet,
boolean isSharedMakerFee,
long triggerPrice,
TransactionResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) {
checkNotNull(offer.getMakerFee(), "makerFee must not be null");
checkArgument(!offer.isBsqSwapOffer());

int numClones = getOpenOffersByMakerFeeTxId(offer.getOfferFeePaymentTxId()).size();
if (numClones >= 10) {
errorMessageHandler.handleErrorMessage("Cannot create offer because maximum number of 10 cloned offers with shared maker fee is reached.");
return;
}

Coin reservedFundsForOffer = createOfferService.getReservedFundsForOffer(offer.getDirection(),
offer.getAmount(),
buyerSecurityDeposit,
Expand All @@ -395,6 +405,7 @@ public void placeOffer(Offer offer,
PlaceOfferModel model = new PlaceOfferModel(offer,
reservedFundsForOffer,
useSavingsWallet,
isSharedMakerFee,
btcWalletService,
tradeWalletService,
bsqWalletService,
Expand All @@ -409,6 +420,19 @@ public void placeOffer(Offer offer,
model,
transaction -> {
OpenOffer openOffer = new OpenOffer(offer, triggerPrice);
if (isSharedMakerFee) {
if (cannotActivateOffer(offer)) {
openOffer.setState(OpenOffer.State.DEACTIVATED);
} else {
// We did not use the AddToOfferBook task for publishing because we
// do not have created the openOffer during the protocol and we need that to determine if the offer can be activated.
// So in case we have an activated cloned offer we do the publishing here.
model.getOfferBookService().addOffer(model.getOffer(),
() -> model.setOfferAddedToOfferBook(true),
errorMessage -> model.getOffer().setErrorMessage("Could not add offer to offerbook.\n" +
"Please check your network connection and try again."));
}
}
addOpenOfferToList(openOffer);
if (!stopped) {
startPeriodicRepublishOffersTimer();
Expand Down Expand Up @@ -452,7 +476,12 @@ public void activateOpenOffer(OpenOffer openOffer,
ResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) {
if (offersToBeEdited.containsKey(openOffer.getId())) {
errorMessageHandler.handleErrorMessage("You can't activate an offer that is currently edited.");
errorMessageHandler.handleErrorMessage(Res.get("offerbook.cannotActivateEditedOffer.warning"));
return;
}

if (cannotActivateOffer(openOffer.getOffer())) {
errorMessageHandler.handleErrorMessage(Res.get("offerbook.cannotActivate.warning"));
return;
}

Expand Down Expand Up @@ -576,39 +605,85 @@ public void editOpenOfferCancel(OpenOffer openOffer,
} else {
resultHandler.handleResult();
}
} else {
errorMessageHandler.handleErrorMessage("Editing of offer can't be canceled as it is not edited.");
}
}

private void onRemoved(OpenOffer openOffer, ResultHandler resultHandler, Offer offer) {
offer.setState(Offer.State.REMOVED);
openOffer.setState(OpenOffer.State.CANCELED);
removeOpenOfferFromList(openOffer);

if (!openOffer.getOffer().isBsqSwapOffer()) {
closedTradableManager.add(openOffer);
btcWalletService.resetAddressEntriesForOpenOffer(offer.getId());
// In case of an offer which has its maker fee shared with other offers, we do not add the openOffer
// to history. Only when the last offer with that maker fee txId got removed we add it.
// Only canceled offers which have lost maker fees are shown in history.
// For that reason we also do not add BSQ offers.
if (getOpenOffersByMakerFeeTxId(offer.getOfferFeePaymentTxId()).isEmpty()) {
closedTradableManager.add(openOffer);

// We only reset if there are no other offers with the shared maker fee as otherwise the
// address in the addressEntry would become available while it's still RESERVED_FOR_TRADE
// for the remaining offers.
btcWalletService.resetAddressEntriesForOpenOffer(offer.getId());
}
}
log.info("onRemoved offerId={}", offer.getId());
resultHandler.handleResult();
}

// Close openOffer after deposit published
public void closeOpenOffer(Offer offer) {
getOpenOfferById(offer.getId()).ifPresent(openOffer -> {
removeOpenOfferFromList(openOffer);
openOffer.setState(OpenOffer.State.CLOSED);
offerBookService.removeOffer(openOffer.getOffer().getOfferPayloadBase(),
() -> log.trace("Successful removed offer"),
log::error);
});
if (offer.isBsqSwapOffer()) {
getOpenOfferById(offer.getId()).ifPresent(openOffer -> {
removeOpenOfferFromList(openOffer);
openOffer.setState(OpenOffer.State.CLOSED);
offerBookService.removeOffer(openOffer.getOffer().getOfferPayloadBase(),
() -> log.trace("Successfully removed offer"),
log::error);
});
} else {
getOpenOffersByMakerFeeTxId(offer.getOfferFeePaymentTxId()).forEach(openOffer -> {
removeOpenOfferFromList(openOffer);

if (offer.getId().equals(openOffer.getId())) {
openOffer.setState(OpenOffer.State.CLOSED);
} else {
// We use CANCELED for the offers which have shared maker fee but have not been taken for the trade.
openOffer.setState(OpenOffer.State.CANCELED);
// We need to reset now those entries as well
btcWalletService.resetAddressEntriesForOpenOffer(openOffer.getId());
}
offerBookService.removeOffer(openOffer.getOffer().getOfferPayloadBase(),
() -> log.trace("Successfully removed offer"),
log::error);
});
}
}

public void reserveOpenOffer(OpenOffer openOffer) {
openOffer.setState(OpenOffer.State.RESERVED);
requestPersistence();
}

public boolean cannotActivateOffer(Offer offer) {
return openOffers.stream()
.filter(openOffer -> !openOffer.getOffer().isBsqSwapOffer()) // We only handle non-BSQ offers
.filter(openOffer -> !openOffer.getId().equals(offer.getId())) // our own offer gets skipped
.filter(openOffer -> !openOffer.isDeactivated()) // we only check with activated offers
.anyMatch(openOffer ->
// Offers which share our maker fee will get checked if they have the same payment method
// and currency.
openOffer.getOffer().getOfferFeePaymentTxId() != null &&
openOffer.getOffer().getOfferFeePaymentTxId().equals(offer.getOfferFeePaymentTxId()) &&
openOffer.getOffer().getPaymentMethodId().equalsIgnoreCase(offer.getPaymentMethodId()) &&
openOffer.getOffer().getCounterCurrencyCode().equalsIgnoreCase(offer.getCounterCurrencyCode()) &&
openOffer.getOffer().getBaseCurrencyCode().equalsIgnoreCase(offer.getBaseCurrencyCode()));
}

public boolean hasOfferSharedMakerFee(OpenOffer openOffer) {
return getOpenOffersByMakerFeeTxId(openOffer.getOffer().getOfferFeePaymentTxId()).size() > 1;
}


///////////////////////////////////////////////////////////////////////////////////////////
// Getters
Expand Down Expand Up @@ -818,7 +893,7 @@ private void sendAckMessage(OfferAvailabilityRequest message,
result,
errorMessage);

final NodeAddress takersNodeAddress = sender;
NodeAddress takersNodeAddress = sender;
PubKeyRing takersPubKeyRing = message.getPubKeyRing();
log.info("Send AckMessage for OfferAvailabilityRequest to peer {} with offerId {} and sourceUid {}",
takersNodeAddress, offerId, ackMessage.getSourceUid());
Expand Down Expand Up @@ -1144,6 +1219,16 @@ private boolean isBsqSwapOfferLackingFunds(OpenOffer openOffer) {
}

private boolean preventedFromPublishing(OpenOffer openOffer) {
return openOffer.isDeactivated() || openOffer.isBsqSwapOfferHasMissingFunds();
return openOffer.isDeactivated() ||
openOffer.isBsqSwapOfferHasMissingFunds() ||
cannotActivateOffer(openOffer.getOffer());
}

private Set<OpenOffer> getOpenOffersByMakerFeeTxId(String makerFeeTxId) {
return openOffers.stream()
.filter(openOffer -> !openOffer.getOffer().isBsqSwapOffer() &&
makerFeeTxId != null &&
makerFeeTxId.equals(openOffer.getOffer().getOfferFeePaymentTxId()))
.collect(Collectors.toSet());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public class PlaceOfferModel implements Model {
private final Offer offer;
private final Coin reservedFundsForOffer;
private final boolean useSavingsWallet;
private final boolean isSharedMakerFee;
private final BtcWalletService walletService;
private final TradeWalletService tradeWalletService;
private final BsqWalletService bsqWalletService;
Expand All @@ -66,6 +67,7 @@ public class PlaceOfferModel implements Model {
public PlaceOfferModel(Offer offer,
Coin reservedFundsForOffer,
boolean useSavingsWallet,
boolean isSharedMakerFee,
BtcWalletService walletService,
TradeWalletService tradeWalletService,
BsqWalletService bsqWalletService,
Expand All @@ -79,6 +81,7 @@ public PlaceOfferModel(Offer offer,
this.offer = offer;
this.reservedFundsForOffer = reservedFundsForOffer;
this.useSavingsWallet = useSavingsWallet;
this.isSharedMakerFee = isSharedMakerFee;
this.walletService = walletService;
this.tradeWalletService = tradeWalletService;
this.bsqWalletService = bsqWalletService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import bisq.core.offer.placeoffer.bisq_v1.tasks.AddToOfferBook;
import bisq.core.offer.placeoffer.bisq_v1.tasks.CheckNumberOfUnconfirmedTransactions;
import bisq.core.offer.placeoffer.bisq_v1.tasks.CloneAddressEntryForSharedMakerFee;
import bisq.core.offer.placeoffer.bisq_v1.tasks.CreateMakerFeeTx;
import bisq.core.offer.placeoffer.bisq_v1.tasks.ValidateOffer;
import bisq.core.trade.bisq_v1.TransactionResultHandler;
Expand Down Expand Up @@ -76,12 +77,20 @@ public void placeOffer() {
errorMessageHandler.handleErrorMessage(errorMessage);
}
);
taskRunner.addTasks(
ValidateOffer.class,
CheckNumberOfUnconfirmedTransactions.class,
CreateMakerFeeTx.class,
AddToOfferBook.class
);

if (model.isSharedMakerFee()) {
taskRunner.addTasks(
ValidateOffer.class,
CloneAddressEntryForSharedMakerFee.class
);
} else {
taskRunner.addTasks(
ValidateOffer.class,
CheckNumberOfUnconfirmedTransactions.class,
CreateMakerFeeTx.class,
AddToOfferBook.class
);
}

taskRunner.run();
}
Expand Down
Loading