Compare commits

...

13 commits

Author SHA1 Message Date
woodser
f053a274a4 bump version to 1.0.17
Some checks are pending
CI / build (macos-13) (push) Waiting to run
CI / build (ubuntu-22.04) (push) Waiting to run
CI / build (windows-latest) (push) Waiting to run
Codacy Coverage Reporter / Publish coverage (push) Waiting to run
CodeQL / Analyze (push) Waiting to run
2024-12-21 09:19:18 -05:00
woodser
fdee044023 fix occasional miscolored buttons to remove or edit my offer 2024-12-21 09:17:06 -05:00
woodser
42ede83ca2 'show all' resets default currency to create new offer 2024-12-21 09:16:57 -05:00
woodser
5444d96832 reverse order of funds > confirmations and memo columns 2024-12-21 09:16:48 -05:00
woodser
7340ca9c21 allow scheduling funds from split output tx 2024-12-21 09:16:40 -05:00
woodser
542441d9d2 increase contrast of filter toggles and remove bottom highlight 2024-12-21 09:16:28 -05:00
woodser
c5ef60ce5c fix ui to set security deposit pct w/o deposit 2024-12-21 09:16:28 -05:00
woodser
389c5dddac fix no deposit filter applied to sell tab 2024-12-21 09:16:28 -05:00
woodser
7240b5f222 document changing download url for network deployment 2024-12-21 08:45:16 -05:00
woodser
34e0c4b71f remove bitcoin donation address from readme 2024-12-20 09:36:57 -05:00
woodser
aab4d0207e update links to typescript client and tests 2024-12-20 06:43:14 -05:00
woodser
1a51b171a0 bump version to 1.0.16 2024-12-19 16:21:44 -05:00
woodser
a557d90e5d fix password prompt on startup by referencing lock@2x.png 2024-12-19 16:16:22 -05:00
17 changed files with 114 additions and 92 deletions

View file

@ -73,18 +73,9 @@ To incentivize development and reward contributors, we adopt a simple bounty sys
To bring Haveno to life, we need resources. If you have the possibility, please consider [becoming a sponsor](https://haveno.exchange/sponsors/) or donating to the project:
### Monero
<p>
<img src="https://raw.githubusercontent.com/haveno-dex/haveno/master/media/donate_monero.png" alt="Donate Monero" width="115" height="115"><br>
<code>42sjokkT9FmiWPqVzrWPFE5NCJXwt96bkBozHf4vgLR9hXyJDqKHEHKVscAARuD7in5wV1meEcSTJTanCTDzidTe2cFXS1F</code>
</p>
If you are using a wallet that supports OpenAlias (like the 'official' CLI and GUI wallets), you can simply put `fund@haveno.exchange` as the "receiver" address.
### Bitcoin
<p>
<img src="https://raw.githubusercontent.com/haveno-dex/haveno/master/media/donate_bitcoin.png" alt="Donate Bitcoin" width="115" height="115"><br>
<code>1AKq3CE1yBAnxGmHXbNFfNYStcByNDc5gQ</code>
</p>

View file

@ -610,7 +610,7 @@ configure(project(':desktop')) {
apply plugin: 'com.github.johnrengelman.shadow'
apply from: 'package/package.gradle'
version = '1.0.15-SNAPSHOT'
version = '1.0.17-SNAPSHOT'
jar.manifest.attributes(
"Implementation-Title": project.name,

View file

@ -28,7 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument;
public class Version {
// The application versions
// We use semantic versioning with major, minor and patch
public static final String VERSION = "1.0.15";
public static final String VERSION = "1.0.17";
/**
* Holds a list of the tagged resource files for optimizing the getData requests.

View file

@ -115,7 +115,6 @@ import lombok.Getter;
import monero.common.MoneroRpcConnection;
import monero.daemon.model.MoneroKeyImageSpentStatus;
import monero.daemon.model.MoneroTx;
import monero.wallet.model.MoneroIncomingTransfer;
import monero.wallet.model.MoneroOutputQuery;
import monero.wallet.model.MoneroOutputWallet;
import monero.wallet.model.MoneroTransferQuery;
@ -1159,23 +1158,17 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
private void scheduleWithEarliestTxs(List<OpenOffer> openOffers, OpenOffer openOffer) {
// check for sufficient balance - scheduled offers amount
BigInteger offerReserveAmount = openOffer.getOffer().getAmountNeeded();
if (xmrWalletService.getBalance().subtract(getScheduledAmount(openOffers)).compareTo(offerReserveAmount) < 0) {
throw new RuntimeException("Not enough money in Haveno wallet");
}
// get earliest available or pending txs with sufficient spendable amount
BigInteger offerReserveAmount = openOffer.getOffer().getAmountNeeded();
BigInteger scheduledAmount = BigInteger.ZERO;
Set<MoneroTxWallet> scheduledTxs = new HashSet<MoneroTxWallet>();
for (MoneroTxWallet tx : xmrWalletService.getTxs()) {
// get spendable amount
BigInteger spendableAmount = getSpendableAmount(tx);
// get unscheduled spendable amount
BigInteger spendableAmount = getUnscheduledSpendableAmount(tx, openOffers);
// skip if no spendable amount or already scheduled
// skip if no spendable amount
if (spendableAmount.equals(BigInteger.ZERO)) continue;
if (isTxScheduledByOtherOffer(openOffers, openOffer, tx.getHash())) continue;
// schedule tx
scheduledAmount = scheduledAmount.add(spendableAmount);
@ -1184,7 +1177,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// break if sufficient funds
if (scheduledAmount.compareTo(offerReserveAmount) >= 0) break;
}
if (scheduledAmount.compareTo(offerReserveAmount) < 0) throw new RuntimeException("Not enough funds to schedule offer");
if (scheduledAmount.compareTo(offerReserveAmount) < 0) throw new RuntimeException("Not enough funds to create offer");
// schedule txs
openOffer.setScheduledTxHashes(scheduledTxs.stream().map(tx -> tx.getHash()).collect(Collectors.toList()));
@ -1192,6 +1185,30 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
openOffer.setState(OpenOffer.State.PENDING);
}
private BigInteger getUnscheduledSpendableAmount(MoneroTxWallet tx, List<OpenOffer> openOffers) {
if (isScheduledWithUnknownAmount(tx, openOffers)) return BigInteger.ZERO;
return getSpendableAmount(tx).subtract(getSplitAmount(tx, openOffers)).max(BigInteger.ZERO);
}
private boolean isScheduledWithUnknownAmount(MoneroTxWallet tx, List<OpenOffer> openOffers) {
for (OpenOffer openOffer : openOffers) {
if (openOffer.getScheduledTxHashes() == null) continue;
if (openOffer.getScheduledTxHashes().contains(tx.getHash()) && !tx.getHash().equals(openOffer.getSplitOutputTxHash())) {
return true;
}
}
return false;
}
private BigInteger getSplitAmount(MoneroTxWallet tx, List<OpenOffer> openOffers) {
for (OpenOffer openOffer : openOffers) {
if (openOffer.getSplitOutputTxHash() == null) continue;
if (!openOffer.getSplitOutputTxHash().equals(tx.getHash())) continue;
return openOffer.getOffer().getAmountNeeded();
}
return BigInteger.ZERO;
}
private BigInteger getSpendableAmount(MoneroTxWallet tx) {
// compute spendable amount from outputs if confirmed
@ -1220,23 +1237,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
return getSpendableAmount(tx).compareTo(BigInteger.ZERO) > 0;
}
private BigInteger getScheduledAmount(List<OpenOffer> openOffers) {
BigInteger scheduledAmount = BigInteger.ZERO;
for (OpenOffer openOffer : openOffers) {
if (openOffer.getState() != OpenOffer.State.PENDING) continue;
if (openOffer.getScheduledTxHashes() == null) continue;
List<MoneroTxWallet> fundingTxs = xmrWalletService.getTxs(openOffer.getScheduledTxHashes());
for (MoneroTxWallet fundingTx : fundingTxs) {
if (fundingTx.getIncomingTransfers() != null) {
for (MoneroIncomingTransfer transfer : fundingTx.getIncomingTransfers()) {
if (transfer.getAccountIndex() == 0) scheduledAmount = scheduledAmount.add(transfer.getAmount());
}
}
}
}
return scheduledAmount;
}
private boolean isTxScheduledByOtherOffer(List<OpenOffer> openOffers, OpenOffer openOffer, String txHash) {
for (OpenOffer otherOffer : openOffers) {
if (otherOffer == openOffer) continue;

View file

@ -60,6 +60,6 @@
</content_rating>
<releases>
<release version="1.0.15" date="2024-12-19"/>
<release version="1.0.17" date="2024-12-21"/>
</releases>
</component>

View file

@ -5,10 +5,10 @@
<!-- See: https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -->
<key>CFBundleVersion</key>
<string>1.0.15</string>
<string>1.0.17</string>
<key>CFBundleShortVersionString</key>
<string>1.0.15</string>
<string>1.0.17</string>
<key>CFBundleExecutable</key>
<string>Haveno</string>

View file

@ -200,7 +200,7 @@ public class HavenoAppMain extends HavenoExecutable {
// Add an icon to the dialog
Stage stage = (Stage) getDialogPane().getScene().getWindow();
stage.getIcons().add(ImageUtil.getImageByPath("lock.png"));
stage.getIcons().add(ImageUtil.getImageByPath("lock@2x.png"));
// Create the password field
PasswordField passwordField = new PasswordField();

View file

@ -38,8 +38,8 @@
<TableColumn fx:id="transactionColumn" minWidth="180"/>
<TableColumn fx:id="amountColumn" minWidth="110" maxWidth="110"/>
<TableColumn fx:id="txFeeColumn" minWidth="110" maxWidth="110"/>
<TableColumn fx:id="confidenceColumn" minWidth="60" maxWidth="130"/>
<TableColumn fx:id="memoColumn" minWidth="40"/>
<TableColumn fx:id="confidenceColumn" minWidth="120" maxWidth="130"/>
<TableColumn fx:id="revertTxColumn" sortable="false" minWidth="110" maxWidth="110" visible="false"/>
</columns>
</TableView>

View file

@ -70,7 +70,7 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
@FXML
TableView<TransactionsListItem> tableView;
@FXML
TableColumn<TransactionsListItem, TransactionsListItem> dateColumn, detailsColumn, addressColumn, transactionColumn, amountColumn, txFeeColumn, memoColumn, confidenceColumn, revertTxColumn;
TableColumn<TransactionsListItem, TransactionsListItem> dateColumn, detailsColumn, addressColumn, transactionColumn, amountColumn, txFeeColumn, confidenceColumn, memoColumn, revertTxColumn;
@FXML
Label numItems;
@FXML
@ -133,8 +133,8 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
transactionColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.txId", Res.getBaseCurrencyCode())));
amountColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amountWithCur", Res.getBaseCurrencyCode())));
txFeeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.txFee", Res.getBaseCurrencyCode())));
memoColumn.setGraphic(new AutoTooltipLabel(Res.get("funds.tx.memo")));
confidenceColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.confirmations", Res.getBaseCurrencyCode())));
memoColumn.setGraphic(new AutoTooltipLabel(Res.get("funds.tx.memo")));
revertTxColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.revert", Res.getBaseCurrencyCode())));
tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN);
@ -146,8 +146,8 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
setTransactionColumnCellFactory();
setAmountColumnCellFactory();
setTxFeeColumnCellFactory();
setMemoColumnCellFactory();
setConfidenceColumnCellFactory();
setMemoColumnCellFactory();
setRevertTxColumnCellFactory();
dateColumn.setComparator(Comparator.comparing(TransactionsListItem::getDate));
@ -221,8 +221,8 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
columns[3] = item.getTxId();
columns[4] = item.getAmountStr();
columns[5] = item.getTxFeeStr();
columns[6] = item.getMemo() == null ? "" : item.getMemo();
columns[7] = String.valueOf(item.getNumConfirmations());
columns[6] = String.valueOf(item.getNumConfirmations());
columns[7] = item.getMemo() == null ? "" : item.getMemo();
return columns;
};

View file

@ -56,6 +56,7 @@ import haveno.core.util.coin.CoinUtil;
import haveno.core.util.validation.AmountValidator4Decimals;
import haveno.core.util.validation.AmountValidator8Decimals;
import haveno.core.util.validation.InputValidator;
import haveno.core.util.validation.InputValidator.ValidationResult;
import haveno.core.util.validation.MonetaryValidator;
import haveno.core.xmr.wallet.Restrictions;
import haveno.desktop.Navigation;
@ -490,6 +491,8 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
xmrValidator.setMaxTradeLimit(BigInteger.valueOf(dataModel.getMaxTradeLimit()));
if (amount.get() != null) amountValidationResult.set(isXmrInputValid(amount.get()));
updateSecurityDeposit();
setSecurityDepositToModel();
onFocusOutSecurityDepositTextField(true, false); // refresh security deposit field
applyMakerFee();
dataModel.calculateTotalToPay();
updateButtonDisableState();
@ -769,7 +772,8 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
}
}
}
// We want to trigger a recalculation of the volume
// trigger recalculation of the volume
UserThread.execute(() -> {
onFocusOutVolumeTextField(true, false);
onFocusOutMinAmountTextField(true, false);
@ -815,6 +819,11 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
}
maybeShowMakeOfferToUnsignedAccountWarning();
// trigger recalculation of the security deposit
UserThread.execute(() -> {
onFocusOutSecurityDepositTextField(true, false);
});
}
}
@ -944,11 +953,16 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
if (marketPriceMargin.get() == null && amount.get() != null && volume.get() != null) {
updateMarketPriceToManual();
}
// trigger recalculation of security deposit
UserThread.execute(() -> {
onFocusOutSecurityDepositTextField(true, false);
});
}
}
void onFocusOutSecurityDepositTextField(boolean oldValue, boolean newValue) {
if (oldValue && !newValue) {
if (oldValue && !newValue && !isMinSecurityDeposit.get()) {
InputValidator.ValidationResult result = securityDepositValidator.validate(securityDeposit.get());
securityDepositValidationResult.set(result);
if (result.isValid) {
@ -1040,6 +1054,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
public String getSecurityDepositLabel() {
return dataModel.buyerAsTakerWithoutDeposit.get() && dataModel.isSellOffer() ? Res.get("createOffer.myDeposit") :
dataModel.isMinSecurityDeposit() ? Res.get("createOffer.minSecurityDepositUsed") :
Preferences.USE_SYMMETRIC_SECURITY_DEPOSIT ? Res.get("createOffer.setDepositForBothTraders") :
dataModel.isBuyOffer() ? Res.get("createOffer.setDepositAsBuyer") : Res.get("createOffer.setDeposit");
}
@ -1211,7 +1226,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
}
private void setSecurityDepositToModel() {
if (!(dataModel.buyerAsTakerWithoutDeposit.get() && dataModel.isSellOffer()) && securityDeposit.get() != null && !securityDeposit.get().isEmpty()) {
if (securityDeposit.get() != null && !securityDeposit.get().isEmpty() && !isMinSecurityDeposit.get()) {
dataModel.setSecurityDepositPct(ParsingUtils.parsePercentStringToDouble(securityDeposit.get()));
} else {
dataModel.setSecurityDepositPct(Restrictions.getDefaultSecurityDepositAsPercent());
@ -1282,11 +1297,11 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
private void updateSecurityDeposit() {
isMinSecurityDeposit.set(dataModel.isMinSecurityDeposit());
if (dataModel.isMinSecurityDeposit()) {
securityDepositLabel.set(Res.get("createOffer.minSecurityDepositUsed"));
securityDeposit.set(HavenoUtils.formatXmr(Restrictions.getMinSecurityDeposit()));
} else {
securityDepositLabel.set(getSecurityDepositLabel());
if (dataModel.isMinSecurityDeposit()) {
securityDeposit.set(HavenoUtils.formatXmr(Restrictions.getMinSecurityDeposit()));
securityDepositValidationResult.set(new ValidationResult(true));
} else {
boolean hasBuyerAsTakerWithoutDeposit = dataModel.buyerAsTakerWithoutDeposit.get() && dataModel.isSellOffer();
securityDeposit.set(FormattingUtils.formatToPercent(hasBuyerAsTakerWithoutDeposit ?
Restrictions.getDefaultSecurityDepositAsPercent() : // use default percent if no deposit from buyer

View file

@ -185,7 +185,7 @@ abstract public class OfferBookView<R extends GridPane, M extends OfferBookViewM
paymentMethodComboBox.setCellFactory(GUIUtil.getPaymentMethodCellFactory());
paymentMethodComboBox.setPrefWidth(250);
matchingOffersToggleButton = AwesomeDude.createIconToggleButton(AwesomeIcon.USER, null, "1.3em", null);
matchingOffersToggleButton = AwesomeDude.createIconToggleButton(AwesomeIcon.USER, null, "1.5em", null);
matchingOffersToggleButton.getStyleClass().add("toggle-button-no-slider");
matchingOffersToggleButton.setPrefHeight(27);
Tooltip matchingOffersTooltip = new Tooltip(Res.get("offerbook.matchingOffers"));
@ -1069,6 +1069,10 @@ abstract public class OfferBookView<R extends GridPane, M extends OfferBookViewM
return new TableCell<>() {
OfferFilterService.Result canTakeOfferResult = null;
@Override
public void updateItem(final OfferBookListItem item, boolean empty) {
super.updateItem(item, empty);
final ImageView iconView = new ImageView();
final AutoTooltipButton button = new AutoTooltipButton();
@ -1098,10 +1102,6 @@ abstract public class OfferBookView<R extends GridPane, M extends OfferBookViewM
HBox.setHgrow(button2, Priority.ALWAYS);
}
@Override
public void updateItem(final OfferBookListItem item, boolean empty) {
super.updateItem(item, empty);
TableRow<OfferBookListItem> tableRow = getTableRow();
if (item != null && !empty) {
Offer offer = item.getOffer();

View file

@ -260,7 +260,10 @@ abstract class OfferBookViewModel extends ActivatableViewModel {
showAllTradeCurrenciesProperty.set(showAllEntry);
if (isEditEntry(code))
navigation.navigateTo(MainView.class, SettingsView.class, PreferencesView.class);
else if (!showAllEntry) {
else if (showAllEntry) {
this.selectedTradeCurrency = getDefaultTradeCurrency();
tradeCurrencyCode.set(selectedTradeCurrency.getCode());
} else {
this.selectedTradeCurrency = tradeCurrency;
tradeCurrencyCode.set(code);
}
@ -579,7 +582,10 @@ abstract class OfferBookViewModel extends ActivatableViewModel {
getCurrencyAndMethodPredicate(direction, selectedTradeCurrency).and(getOffersMatchingMyAccountsPredicate()) :
getCurrencyAndMethodPredicate(direction, selectedTradeCurrency);
// filter private offers
if (direction == OfferDirection.BUY) {
predicate = predicate.and(offerBookListItem -> offerBookListItem.getOffer().isPrivateOffer() == showPrivateOffers);
}
if (!filterText.isEmpty()) {

View file

@ -561,8 +561,10 @@
.toggle-button-no-slider {
-fx-focus-color: transparent;
-fx-faint-focus-color: transparent;
-fx-background-radius: 3;
-fx-background-insets: 0, 1;
}
.toggle-button-no-slider:selected {
-fx-background-color: -bs-color-gray-ddd;
-fx-background-color: -bs-color-gray-bbb;
}

View file

@ -118,6 +118,10 @@ The price node is separated from Haveno and is run as a standalone service. To d
After the price node is built and deployed, add the price node to `DEFAULT_NODES` in [ProvidersRepository.java](https://github.com/haveno-dex/haveno/blob/3cdd88b56915c7f8afd4f1a39e6c1197c2665d63/core/src/main/java/haveno/core/provider/ProvidersRepository.java#L50).
### Update the download URL
Change every instance of `https://haveno.exchange/downloads` to your download URL. For example, `https://havenoexample.com/downloads`.
## Review all local changes
For comparison, placeholders to run on mainnet are marked [here on this branch](https://github.com/haveno-dex/haveno/tree/mainnet_placeholders).

View file

@ -243,6 +243,10 @@ Set `ARBITRATOR_ASSIGNS_TRADE_FEE_ADDRESS` to `true` for the arbitrator to assig
Otherwise set `ARBITRATOR_ASSIGNS_TRADE_FEE_ADDRESS` to `false` and set the XMR address in `getGlobalTradeFeeAddress()` to collect all trade fees to a single address (e.g. a multisig wallet shared among network administrators).
## Update the download URL
Change every instance of `https://haveno.exchange/downloads` to your download URL. For example, `https://havenoexample.com/downloads`.
## Start users for testing
Start user1 on Monero's mainnet using `make user1-desktop-mainnet` or Monero's stagenet using `make user1-desktop-stagenet`.
@ -266,7 +270,7 @@ Then follow these instructions: https://github.com/haveno-dex/haveno/blob/master
<b>Set the mandatory minimum version for trading (optional)</b>
If applicable, update the mandatory minimum version for trading, by entering `ctrl + f` to open the Filter window, enter a private key with developer privileges, and enter the minimum version (e.g. 1.0.16) in the field labeled "Min. version required for trading".
If applicable, update the mandatory minimum version for trading, by entering `ctrl + f` to open the Filter window, enter a private key with developer privileges, and enter the minimum version (e.g. 1.0.17) in the field labeled "Min. version required for trading".
<b>Send update alert</b>

View file

@ -31,8 +31,8 @@ Follow [instructions](https://github.com/haveno-dex/haveno-ts#run-tests) to run
For example, the gRPC function to get offers is implemented by [`GrpcServer`](https://github.com/haveno-dex/haveno/blob/master/daemon/src/main/java/haveno/daemon/grpc/GrpcServer.java) > [`GrpcOffersService.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/b761dbfd378faf49d95090c126318b419af7926b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java#L104) > [`CoreApi.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/b761dbfd378faf49d95090c126318b419af7926b/core/src/main/java/haveno/core/api/CoreApi.java#L128) > [`CoreOffersService.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/b761dbfd378faf49d95090c126318b419af7926b/core/src/main/java/haveno/core/api/CoreOffersService.java#L126) > [`OfferBookService.getOffers()`](https://github.com/haveno-dex/haveno/blob/b761dbfd378faf49d95090c126318b419af7926b/core/src/main/java/haveno/core/offer/OfferBookService.java#L193).
5. Build Haveno: `make`
6. Update the gRPC client in haveno-ts: `npm install`
7. Add the corresponding typescript method(s) to [haveno.ts](https://github.com/haveno-dex/haveno-ts/blob/master/src/haveno.ts) with clear and concise documentation.
8. Add clean and comprehensive tests to [haveno.test.ts](https://github.com/haveno-dex/haveno-ts/blob/master/src/haveno.test.ts), following existing patterns.
7. Add the corresponding typescript method(s) to [HavenoClient.ts](https://github.com/haveno-dex/haveno-ts/blob/master/src/HavenoClient.ts) with clear and concise documentation.
8. Add clean and comprehensive tests to [HavenoClient.test.ts](https://github.com/haveno-dex/haveno-ts/blob/master/src/HavenoClient.test.ts), following existing patterns.
9. Run the tests with `npm run test -- -t 'my test'` to run tests by name and `npm test` to run all tests together. Ensure all tests pass and there are no exception stacktraces in the terminals of Alice, Bob, or the arbitrator.
10. Open pull requests to the haveno and haveno-ts projects for the backend and frontend implementations.

View file

@ -41,7 +41,7 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SeedNodeMain extends ExecutableForAppWithP2p {
private static final long CHECK_CONNECTION_LOSS_SEC = 30;
private static final String VERSION = "1.0.15";
private static final String VERSION = "1.0.17";
private SeedNode seedNode;
private Timer checkConnectionLossTime;