diff --git a/apitest/scripts/mainnet-test.sh b/apitest/scripts/mainnet-test.sh index ae3afd73d2..9c5889ff3a 100755 --- a/apitest/scripts/mainnet-test.sh +++ b/apitest/scripts/mainnet-test.sh @@ -48,14 +48,14 @@ run ./bisq-cli --password="xyz" getversion [ "$status" -eq 0 ] echo "actual output: $output" >&2 - [ "$output" = "1.3.7" ] + [ "$output" = "1.3.8" ] } @test "test getversion" { run ./bisq-cli --password=xyz getversion [ "$status" -eq 0 ] echo "actual output: $output" >&2 - [ "$output" = "1.3.7" ] + [ "$output" = "1.3.8" ] } @test "test setwalletpassword \"a b c\"" { diff --git a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java index 5197a35634..b0ce2c548e 100644 --- a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java +++ b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java @@ -27,6 +27,8 @@ import joptsimple.OptionParser; import joptsimple.OptionSet; import joptsimple.OptionSpec; +import java.net.InetAddress; + import java.nio.file.Paths; import java.io.File; @@ -169,7 +171,7 @@ public class ApiTestConfig { ArgumentAcceptingOptionSpec bitcoinRegtestHostOpt = parser.accepts(BITCOIN_REGTEST_HOST, "Bitcoin Core regtest host") .withRequiredArg() - .ofType(String.class).defaultsTo("localhost"); + .ofType(String.class).defaultsTo(InetAddress.getLoopbackAddress().getHostAddress()); ArgumentAcceptingOptionSpec bitcoinRpcPortOpt = parser.accepts(BITCOIN_RPC_PORT, "Bitcoin Core rpc port (non-default)") diff --git a/apitest/src/test/java/bisq/apitest/ApiTestCase.java b/apitest/src/test/java/bisq/apitest/ApiTestCase.java index f9100bee96..30e7472e2a 100644 --- a/apitest/src/test/java/bisq/apitest/ApiTestCase.java +++ b/apitest/src/test/java/bisq/apitest/ApiTestCase.java @@ -17,16 +17,20 @@ package bisq.apitest; +import java.net.InetAddress; + import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.ExecutionException; -import static bisq.apitest.config.BisqAppConfig.alicedaemon; import static java.util.concurrent.TimeUnit.MILLISECONDS; import bisq.apitest.config.ApiTestConfig; +import bisq.apitest.config.BisqAppConfig; import bisq.apitest.method.BitcoinCliHelper; import bisq.cli.GrpcStubs; @@ -57,34 +61,41 @@ import bisq.cli.GrpcStubs; */ public class ApiTestCase { - // The gRPC service stubs are used by method & scenario tests, but not e2e tests. - protected static GrpcStubs grpcStubs; - protected static Scaffold scaffold; protected static ApiTestConfig config; protected static BitcoinCliHelper bitcoinCli; + // gRPC service stubs are used by method & scenario tests, but not e2e tests. + private static final Map grpcStubsCache = new HashMap<>(); + public static void setUpScaffold(String supportingApps) throws InterruptedException, ExecutionException, IOException { scaffold = new Scaffold(supportingApps).setUp(); config = scaffold.config; bitcoinCli = new BitcoinCliHelper((config)); - // For now, all grpc requests are sent to the alicedaemon, but this will need to - // be made configurable for new test cases that call arb or bob node daemons. - grpcStubs = new GrpcStubs("localhost", alicedaemon.apiPort, config.apiPassword); } public static void setUpScaffold(String[] params) throws InterruptedException, ExecutionException, IOException { scaffold = new Scaffold(params).setUp(); config = scaffold.config; - grpcStubs = new GrpcStubs("localhost", alicedaemon.apiPort, config.apiPassword); } public static void tearDownScaffold() { scaffold.tearDown(); } + protected static GrpcStubs grpcStubs(BisqAppConfig bisqAppConfig) { + if (grpcStubsCache.containsKey(bisqAppConfig)) { + return grpcStubsCache.get(bisqAppConfig); + } else { + GrpcStubs stubs = new GrpcStubs(InetAddress.getLoopbackAddress().getHostAddress(), + bisqAppConfig.apiPort, config.apiPassword); + grpcStubsCache.put(bisqAppConfig, stubs); + return stubs; + } + } + protected void sleep(long ms) { try { MILLISECONDS.sleep(ms); diff --git a/apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java b/apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java index 2cf4e8ae1c..a77fe633ea 100644 --- a/apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java +++ b/apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java @@ -27,6 +27,7 @@ import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; +import static bisq.apitest.config.BisqAppConfig.alicedaemon; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; @@ -57,7 +58,8 @@ public class GetBalanceTest extends MethodTest { public void testGetBalance() { // All tests depend on the DAO / regtest environment, and Alice's wallet is // initialized with 10 BTC during the scaffolding setup. - var balance = grpcStubs.walletsService.getBalance(GetBalanceRequest.newBuilder().build()).getBalance(); + var balance = grpcStubs(alicedaemon).walletsService + .getBalance(GetBalanceRequest.newBuilder().build()).getBalance(); assertEquals(1000000000, balance); } diff --git a/apitest/src/test/java/bisq/apitest/method/GetVersionTest.java b/apitest/src/test/java/bisq/apitest/method/GetVersionTest.java index 22413cf9d3..ed6083c8d3 100644 --- a/apitest/src/test/java/bisq/apitest/method/GetVersionTest.java +++ b/apitest/src/test/java/bisq/apitest/method/GetVersionTest.java @@ -50,7 +50,8 @@ public class GetVersionTest extends MethodTest { @Test @Order(1) public void testGetVersion() { - var version = grpcStubs.versionService.getVersion(GetVersionRequest.newBuilder().build()).getVersion(); + var version = grpcStubs(alicedaemon).versionService + .getVersion(GetVersionRequest.newBuilder().build()).getVersion(); assertEquals(VERSION, version); } diff --git a/apitest/src/test/java/bisq/apitest/method/MethodTest.java b/apitest/src/test/java/bisq/apitest/method/MethodTest.java index 694aa6806e..3437ac59c9 100644 --- a/apitest/src/test/java/bisq/apitest/method/MethodTest.java +++ b/apitest/src/test/java/bisq/apitest/method/MethodTest.java @@ -20,13 +20,17 @@ package bisq.apitest.method; import bisq.proto.grpc.GetBalanceRequest; import bisq.proto.grpc.GetFundingAddressesRequest; import bisq.proto.grpc.LockWalletRequest; +import bisq.proto.grpc.RegisterDisputeAgentRequest; import bisq.proto.grpc.RemoveWalletPasswordRequest; import bisq.proto.grpc.SetWalletPasswordRequest; import bisq.proto.grpc.UnlockWalletRequest; +import static bisq.common.app.DevEnv.DEV_PRIVILEGE_PRIV_KEY; + import bisq.apitest.ApiTestCase; +import bisq.apitest.config.BisqAppConfig; public class MethodTest extends ApiTestCase { @@ -60,24 +64,31 @@ public class MethodTest extends ApiTestCase { return GetFundingAddressesRequest.newBuilder().build(); } + protected final RegisterDisputeAgentRequest createRegisterDisputeAgentRequest(String disputeAgentType) { + return RegisterDisputeAgentRequest.newBuilder() + .setDisputeAgentType(disputeAgentType) + .setRegistrationKey(DEV_PRIVILEGE_PRIV_KEY).build(); + } + // Convenience methods for calling frequently used & thoroughly tested gRPC services. - protected final long getBalance() { - return grpcStubs.walletsService.getBalance(createBalanceRequest()).getBalance(); + protected final long getBalance(BisqAppConfig bisqAppConfig) { + return grpcStubs(bisqAppConfig).walletsService.getBalance(createBalanceRequest()).getBalance(); } - protected final void unlockWallet(String password, long timeout) { + protected final void unlockWallet(BisqAppConfig bisqAppConfig, String password, long timeout) { //noinspection ResultOfMethodCallIgnored - grpcStubs.walletsService.unlockWallet(createUnlockWalletRequest(password, timeout)); + grpcStubs(bisqAppConfig).walletsService.unlockWallet(createUnlockWalletRequest(password, timeout)); } - protected final void lockWallet() { + protected final void lockWallet(BisqAppConfig bisqAppConfig) { //noinspection ResultOfMethodCallIgnored - grpcStubs.walletsService.lockWallet(createLockWalletRequest()); + grpcStubs(bisqAppConfig).walletsService.lockWallet(createLockWalletRequest()); } - protected final String getUnusedBtcAddress() { - return grpcStubs.walletsService.getFundingAddresses(createGetFundingAddressesRequest()) + protected final String getUnusedBtcAddress(BisqAppConfig bisqAppConfig) { + //noinspection OptionalGetWithoutIsPresent + return grpcStubs(bisqAppConfig).walletsService.getFundingAddresses(createGetFundingAddressesRequest()) .getAddressBalanceInfoList() .stream() .filter(a -> a.getBalance() == 0 && a.getNumConfirmations() == 0) diff --git a/apitest/src/test/java/bisq/apitest/method/RegisterDisputeAgentsTest.java b/apitest/src/test/java/bisq/apitest/method/RegisterDisputeAgentsTest.java new file mode 100644 index 0000000000..1ad3a36f16 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/RegisterDisputeAgentsTest.java @@ -0,0 +1,108 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.apitest.method; + +import bisq.proto.grpc.RegisterDisputeAgentRequest; + +import io.grpc.StatusRuntimeException; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.config.BisqAppConfig.arbdaemon; +import static bisq.common.app.DevEnv.DEV_PRIVILEGE_PRIV_KEY; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; + + +@SuppressWarnings("ResultOfMethodCallIgnored") +@Slf4j +@TestMethodOrder(OrderAnnotation.class) +public class RegisterDisputeAgentsTest extends MethodTest { + + @BeforeAll + public static void setUp() { + try { + setUpScaffold("bitcoind,seednode,arbdaemon"); + } catch (Exception ex) { + fail(ex); + } + } + + @Test + @Order(1) + public void testRegisterArbitratorShouldThrowException() { + var req = + createRegisterDisputeAgentRequest("arbitrator"); + Throwable exception = assertThrows(StatusRuntimeException.class, () -> + grpcStubs(arbdaemon).disputeAgentsService.registerDisputeAgent(req)); + assertEquals("INVALID_ARGUMENT: arbitrators must be registered in a Bisq UI", + exception.getMessage()); + } + + @Test + @Order(2) + public void testInvalidDisputeAgentTypeArgShouldThrowException() { + var req = + createRegisterDisputeAgentRequest("badagent"); + Throwable exception = assertThrows(StatusRuntimeException.class, () -> + grpcStubs(arbdaemon).disputeAgentsService.registerDisputeAgent(req)); + assertEquals("INVALID_ARGUMENT: unknown dispute agent type badagent", + exception.getMessage()); + } + + @Test + @Order(3) + public void testInvalidRegistrationKeyArgShouldThrowException() { + var req = RegisterDisputeAgentRequest.newBuilder() + .setDisputeAgentType("refundagent") + .setRegistrationKey("invalid" + DEV_PRIVILEGE_PRIV_KEY).build(); + Throwable exception = assertThrows(StatusRuntimeException.class, () -> + grpcStubs(arbdaemon).disputeAgentsService.registerDisputeAgent(req)); + assertEquals("INVALID_ARGUMENT: invalid registration key", + exception.getMessage()); + } + + @Test + @Order(4) + public void testRegisterMediator() { + var req = + createRegisterDisputeAgentRequest("mediator"); + grpcStubs(arbdaemon).disputeAgentsService.registerDisputeAgent(req); + } + + @Test + @Order(5) + public void testRegisterRefundAgent() { + var req = + createRegisterDisputeAgentRequest("refundagent"); + grpcStubs(arbdaemon).disputeAgentsService.registerDisputeAgent(req); + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java b/apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java index 450fb58e01..f74c8e705c 100644 --- a/apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java +++ b/apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java @@ -36,13 +36,13 @@ public class WalletProtectionTest extends MethodTest { @Order(1) public void testSetWalletPassword() { var request = createSetWalletPasswordRequest("first-password"); - grpcStubs.walletsService.setWalletPassword(request); + grpcStubs(alicedaemon).walletsService.setWalletPassword(request); } @Test @Order(2) public void testGetBalanceOnEncryptedWalletShouldThrowException() { - Throwable exception = assertThrows(StatusRuntimeException.class, this::getBalance); + Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBalance(alicedaemon)); assertEquals("UNKNOWN: wallet is locked", exception.getMessage()); } @@ -50,11 +50,10 @@ public class WalletProtectionTest extends MethodTest { @Order(3) public void testUnlockWalletFor4Seconds() { var request = createUnlockWalletRequest("first-password", 4); - grpcStubs.walletsService.unlockWallet(request); - getBalance(); // should not throw 'wallet locked' exception - + grpcStubs(alicedaemon).walletsService.unlockWallet(request); + getBalance(alicedaemon); // should not throw 'wallet locked' exception sleep(4500); // let unlock timeout expire - Throwable exception = assertThrows(StatusRuntimeException.class, this::getBalance); + Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBalance(alicedaemon)); assertEquals("UNKNOWN: wallet is locked", exception.getMessage()); } @@ -62,20 +61,19 @@ public class WalletProtectionTest extends MethodTest { @Order(4) public void testGetBalanceAfterUnlockTimeExpiryShouldThrowException() { var request = createUnlockWalletRequest("first-password", 3); - grpcStubs.walletsService.unlockWallet(request); + grpcStubs(alicedaemon).walletsService.unlockWallet(request); sleep(4000); // let unlock timeout expire - Throwable exception = assertThrows(StatusRuntimeException.class, this::getBalance); + Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBalance(alicedaemon)); assertEquals("UNKNOWN: wallet is locked", exception.getMessage()); } @Test @Order(5) public void testLockWalletBeforeUnlockTimeoutExpiry() { - unlockWallet("first-password", 60); + unlockWallet(alicedaemon, "first-password", 60); var request = createLockWalletRequest(); - grpcStubs.walletsService.lockWallet(request); - - Throwable exception = assertThrows(StatusRuntimeException.class, this::getBalance); + grpcStubs(alicedaemon).walletsService.lockWallet(request); + Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBalance(alicedaemon)); assertEquals("UNKNOWN: wallet is locked", exception.getMessage()); } @@ -83,40 +81,39 @@ public class WalletProtectionTest extends MethodTest { @Order(6) public void testLockWalletWhenWalletAlreadyLockedShouldThrowException() { var request = createLockWalletRequest(); - Throwable exception = assertThrows(StatusRuntimeException.class, () -> - grpcStubs.walletsService.lockWallet(request)); + grpcStubs(alicedaemon).walletsService.lockWallet(request)); assertEquals("UNKNOWN: wallet is already locked", exception.getMessage()); } @Test @Order(7) public void testUnlockWalletTimeoutOverride() { - unlockWallet("first-password", 2); + unlockWallet(alicedaemon, "first-password", 2); sleep(500); // override unlock timeout after 0.5s - unlockWallet("first-password", 6); + unlockWallet(alicedaemon, "first-password", 6); sleep(5000); - getBalance(); // getbalance 5s after resetting unlock timeout to 6s + getBalance(alicedaemon); // getbalance 5s after resetting unlock timeout to 6s } @Test @Order(8) public void testSetNewWalletPassword() { - var request = createSetWalletPasswordRequest("first-password", "second-password"); - grpcStubs.walletsService.setWalletPassword(request); - - unlockWallet("second-password", 2); - getBalance(); + var request = createSetWalletPasswordRequest( + "first-password", "second-password"); + grpcStubs(alicedaemon).walletsService.setWalletPassword(request); + unlockWallet(alicedaemon, "second-password", 2); + getBalance(alicedaemon); sleep(2500); // allow time for wallet save } @Test @Order(9) public void testSetNewWalletPasswordWithIncorrectNewPasswordShouldThrowException() { - var request = createSetWalletPasswordRequest("bad old password", "irrelevant"); - + var request = createSetWalletPasswordRequest( + "bad old password", "irrelevant"); Throwable exception = assertThrows(StatusRuntimeException.class, () -> - grpcStubs.walletsService.setWalletPassword(request)); + grpcStubs(alicedaemon).walletsService.setWalletPassword(request)); assertEquals("UNKNOWN: incorrect old password", exception.getMessage()); } @@ -124,8 +121,8 @@ public class WalletProtectionTest extends MethodTest { @Order(10) public void testRemoveNewWalletPassword() { var request = createRemoveWalletPasswordRequest("second-password"); - grpcStubs.walletsService.removeWalletPassword(request); - getBalance(); // should not throw 'wallet locked' exception + grpcStubs(alicedaemon).walletsService.removeWalletPassword(request); + getBalance(alicedaemon); // should not throw 'wallet locked' exception } @AfterAll diff --git a/apitest/src/test/java/bisq/apitest/scenario/FundWalletScenarioTest.java b/apitest/src/test/java/bisq/apitest/scenario/FundWalletScenarioTest.java index e95e310eb5..0b30d72c1c 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/FundWalletScenarioTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/FundWalletScenarioTest.java @@ -26,6 +26,7 @@ import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; +import static bisq.apitest.config.BisqAppConfig.alicedaemon; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; @@ -48,16 +49,17 @@ public class FundWalletScenarioTest extends ScenarioTest { @Test @Order(1) public void testFundWallet() { - long balance = getBalance(); // bisq wallet was initialized with 10 btc + // bisq wallet was initialized with 10 btc + long balance = getBalance(alicedaemon); assertEquals(1000000000, balance); - String unusedAddress = getUnusedBtcAddress(); + String unusedAddress = getUnusedBtcAddress(alicedaemon); bitcoinCli.sendToAddress(unusedAddress, "2.5"); bitcoinCli.generateBlocks(1); sleep(1500); - balance = getBalance(); + balance = getBalance(alicedaemon); assertEquals(1250000000L, balance); // new balance is 12.5 btc } diff --git a/build.gradle b/build.gradle index 532a111f90..13e2c3c172 100644 --- a/build.gradle +++ b/build.gradle @@ -63,7 +63,7 @@ configure(subprojects) { loggingVersion = '1.2' lombokVersion = '1.18.2' mockitoVersion = '3.0.0' - netlayerVersion = '0.6.7' + netlayerVersion = '0.6.8' protobufVersion = '3.10.0' protocVersion = protobufVersion pushyVersion = '0.13.2' @@ -497,7 +497,7 @@ configure(project(':pricenode')) { test { useJUnitPlatform() - + // Disabled by default, since spot provider tests include connections to external API endpoints // Can be enabled by adding -Dtest.pricenode.includeSpotProviderTests=true to the gradle command: // ./gradlew test -Dtest.pricenode.includeSpotProviderTests=true diff --git a/cli/src/main/java/bisq/cli/GrpcStubs.java b/cli/src/main/java/bisq/cli/GrpcStubs.java index e12a6efa7c..2ef5efb75b 100644 --- a/cli/src/main/java/bisq/cli/GrpcStubs.java +++ b/cli/src/main/java/bisq/cli/GrpcStubs.java @@ -17,6 +17,7 @@ package bisq.cli; +import bisq.proto.grpc.DisputeAgentsGrpc; import bisq.proto.grpc.GetVersionGrpc; import bisq.proto.grpc.OffersGrpc; import bisq.proto.grpc.PaymentAccountsGrpc; @@ -29,6 +30,7 @@ import static java.util.concurrent.TimeUnit.SECONDS; public class GrpcStubs { + public final DisputeAgentsGrpc.DisputeAgentsBlockingStub disputeAgentsService; public final GetVersionGrpc.GetVersionBlockingStub versionService; public final OffersGrpc.OffersBlockingStub offersService; public final PaymentAccountsGrpc.PaymentAccountsBlockingStub paymentAccountsService; @@ -46,6 +48,7 @@ public class GrpcStubs { } })); + this.disputeAgentsService = DisputeAgentsGrpc.newBlockingStub(channel).withCallCredentials(credentials); this.versionService = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials); this.offersService = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); this.paymentAccountsService = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials); diff --git a/common/src/main/java/bisq/common/storage/FileUtil.java b/common/src/main/java/bisq/common/storage/FileUtil.java index 85862a06de..66c572e787 100644 --- a/common/src/main/java/bisq/common/storage/FileUtil.java +++ b/common/src/main/java/bisq/common/storage/FileUtil.java @@ -102,7 +102,9 @@ public class FileUtil { deleteDirectory(file, null, true); } - public static void deleteDirectory(File file, @Nullable File exclude, boolean ignoreLockedFiles) throws IOException { + public static void deleteDirectory(File file, + @Nullable File exclude, + boolean ignoreLockedFiles) throws IOException { boolean excludeFileFound = false; if (file.isDirectory()) { File[] files = file.listFiles(); @@ -156,7 +158,8 @@ public class FileUtil { return !file.canWrite(); } - public static void resourceToFile(String resourcePath, File destinationFile) throws ResourceNotFoundException, IOException { + public static void resourceToFile(String resourcePath, + File destinationFile) throws ResourceNotFoundException, IOException { try (InputStream inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(resourcePath)) { if (inputStream == null) { throw new ResourceNotFoundException(resourcePath); @@ -182,6 +185,20 @@ public class FileUtil { } } + public static void copyFile(File origin, File target) throws IOException { + if (!origin.exists()) { + return; + } + + try { + Files.copy(origin, target); + } catch (IOException e) { + log.error("Copy file failed", e); + throw new IOException("Failed to copy " + origin + " to " + target); + } + + } + public static void copyDirectory(File source, File destination) throws IOException { FileUtils.copyDirectory(source, destination); } diff --git a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java index 4747e3feb4..c1a55e2bb0 100644 --- a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java +++ b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java @@ -20,7 +20,6 @@ package bisq.core.account.witness; import bisq.core.account.sign.SignedWitness; import bisq.core.account.sign.SignedWitnessService; import bisq.core.filter.FilterManager; -import bisq.core.filter.PaymentAccountFilter; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.offer.Offer; @@ -718,10 +717,8 @@ public class AccountAgeWitnessService { filterManager.isCurrencyBanned(dispute.getContract().getOfferPayload().getCurrencyCode()) || filterManager.isPaymentMethodBanned( PaymentMethod.getPaymentMethodById(dispute.getContract().getPaymentMethodId())) || - filterManager.arePeersPaymentAccountDataBanned(dispute.getContract().getBuyerPaymentAccountPayload(), - new PaymentAccountFilter[1]) || - filterManager.arePeersPaymentAccountDataBanned(dispute.getContract().getSellerPaymentAccountPayload(), - new PaymentAccountFilter[1]) || + filterManager.arePeersPaymentAccountDataBanned(dispute.getContract().getBuyerPaymentAccountPayload()) || + filterManager.arePeersPaymentAccountDataBanned(dispute.getContract().getSellerPaymentAccountPayload()) || filterManager.isWitnessSignerPubKeyBanned( Utils.HEX.encode(dispute.getContract().getBuyerPubKeyRing().getSignaturePubKeyBytes())) || filterManager.isWitnessSignerPubKeyBanned( diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 30cedeb3b9..35af589f3d 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -47,26 +47,38 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class CoreApi { + private final CoreDisputeAgentsService coreDisputeAgentsService; private final CoreOffersService coreOffersService; private final CorePaymentAccountsService paymentAccountsService; private final CoreWalletsService walletsService; private final TradeStatisticsManager tradeStatisticsManager; @Inject - public CoreApi(CoreOffersService coreOffersService, + public CoreApi(CoreDisputeAgentsService coreDisputeAgentsService, + CoreOffersService coreOffersService, CorePaymentAccountsService paymentAccountsService, CoreWalletsService walletsService, TradeStatisticsManager tradeStatisticsManager) { + this.coreDisputeAgentsService = coreDisputeAgentsService; this.coreOffersService = coreOffersService; this.paymentAccountsService = paymentAccountsService; this.walletsService = walletsService; this.tradeStatisticsManager = tradeStatisticsManager; } + @SuppressWarnings("SameReturnValue") public String getVersion() { return Version.VERSION; } + /////////////////////////////////////////////////////////////////////////////////////////// + // Dispute Agents + /////////////////////////////////////////////////////////////////////////////////////////// + + public void registerDisputeAgent(String disputeAgentType, String registrationKey) { + coreDisputeAgentsService.registerDisputeAgent(disputeAgentType, registrationKey); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Offers /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/api/CoreDisputeAgentsService.java b/core/src/main/java/bisq/core/api/CoreDisputeAgentsService.java new file mode 100644 index 0000000000..29e51b8853 --- /dev/null +++ b/core/src/main/java/bisq/core/api/CoreDisputeAgentsService.java @@ -0,0 +1,144 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.api; + +import bisq.core.support.dispute.mediation.mediator.Mediator; +import bisq.core.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.support.dispute.refund.refundagent.RefundAgent; +import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; + +import bisq.common.config.Config; +import bisq.common.crypto.KeyRing; + +import org.bitcoinj.core.ECKey; + +import javax.inject.Inject; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Objects; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.common.app.DevEnv.DEV_PRIVILEGE_PRIV_KEY; +import static java.net.InetAddress.getLoopbackAddress; + +@Slf4j +class CoreDisputeAgentsService { + + private final Config config; + private final KeyRing keyRing; + private final MediatorManager mediatorManager; + private final RefundAgentManager refundAgentManager; + private final P2PService p2PService; + private final NodeAddress nodeAddress; + private final List languageCodes; + + @Inject + public CoreDisputeAgentsService(Config config, + KeyRing keyRing, + MediatorManager mediatorManager, + RefundAgentManager refundAgentManager, + P2PService p2PService) { + this.config = config; + this.keyRing = keyRing; + this.mediatorManager = mediatorManager; + this.refundAgentManager = refundAgentManager; + this.p2PService = p2PService; + this.nodeAddress = new NodeAddress(getLoopbackAddress().getHostAddress(), config.nodePort); + this.languageCodes = Arrays.asList("de", "en", "es", "fr"); + } + + public void registerDisputeAgent(String disputeAgentType, String registrationKey) { + if (!p2PService.isBootstrapped()) + throw new IllegalStateException("p2p service is not bootstrapped yet"); + + if (config.baseCurrencyNetwork.isMainnet() + || config.baseCurrencyNetwork.isDaoBetaNet() + || !config.useLocalhostForP2P) + throw new IllegalStateException("dispute agents must be registered in a Bisq UI"); + + if (!registrationKey.equals(DEV_PRIVILEGE_PRIV_KEY)) + throw new IllegalArgumentException("invalid registration key"); + + ECKey ecKey; + String signature; + switch (disputeAgentType) { + case "arbitrator": + throw new IllegalArgumentException("arbitrators must be registered in a Bisq UI"); + case "mediator": + ecKey = mediatorManager.getRegistrationKey(registrationKey); + signature = mediatorManager.signStorageSignaturePubKey(Objects.requireNonNull(ecKey)); + registerMediator(nodeAddress, languageCodes, ecKey, signature); + return; + case "refundagent": + ecKey = refundAgentManager.getRegistrationKey(registrationKey); + signature = refundAgentManager.signStorageSignaturePubKey(Objects.requireNonNull(ecKey)); + registerRefundAgent(nodeAddress, languageCodes, ecKey, signature); + return; + default: + throw new IllegalArgumentException("unknown dispute agent type " + disputeAgentType); + } + } + + private void registerMediator(NodeAddress nodeAddress, + List languageCodes, + ECKey ecKey, + String signature) { + Mediator mediator = new Mediator(nodeAddress, + keyRing.getPubKeyRing(), + languageCodes, + new Date().getTime(), + ecKey.getPubKey(), + signature, + null, + null, + null + ); + mediatorManager.addDisputeAgent(mediator, () -> { + }, errorMessage -> { + }); + mediatorManager.getDisputeAgentByNodeAddress(nodeAddress).orElseThrow(() -> + new IllegalStateException("could not register mediator")); + } + + private void registerRefundAgent(NodeAddress nodeAddress, + List languageCodes, + ECKey ecKey, + String signature) { + RefundAgent refundAgent = new RefundAgent(nodeAddress, + keyRing.getPubKeyRing(), + languageCodes, + new Date().getTime(), + ecKey.getPubKey(), + signature, + null, + null, + null + ); + refundAgentManager.addDisputeAgent(refundAgent, () -> { + }, errorMessage -> { + }); + refundAgentManager.getDisputeAgentByNodeAddress(nodeAddress).orElseThrow(() -> + new IllegalStateException("could not register refund agent")); + } +} diff --git a/core/src/main/java/bisq/core/btc/model/AddressEntry.java b/core/src/main/java/bisq/core/btc/model/AddressEntry.java index e37a84fa86..26c2c50956 100644 --- a/core/src/main/java/bisq/core/btc/model/AddressEntry.java +++ b/core/src/main/java/bisq/core/btc/model/AddressEntry.java @@ -186,10 +186,6 @@ public final class AddressEntry implements PersistablePayload { return context == Context.MULTI_SIG || context == Context.TRADE_PAYOUT; } - public boolean isTradable() { - return isOpenOffer() || isTrade(); - } - public Coin getCoinLockedInMultiSig() { return Coin.valueOf(coinLockedInMultiSig); } @@ -197,9 +193,10 @@ public final class AddressEntry implements PersistablePayload { @Override public String toString() { return "AddressEntry{" + - "offerId='" + getOfferId() + '\'' + + "address=" + address + ", context=" + context + - ", address=" + getAddressString() + - '}'; + ", offerId='" + offerId + '\'' + + ", coinLockedInMultiSig=" + coinLockedInMultiSig + + "}"; } } diff --git a/core/src/main/java/bisq/core/btc/model/AddressEntryList.java b/core/src/main/java/bisq/core/btc/model/AddressEntryList.java index becb46dfbe..a3b2ae7856 100644 --- a/core/src/main/java/bisq/core/btc/model/AddressEntryList.java +++ b/core/src/main/java/bisq/core/btc/model/AddressEntryList.java @@ -17,38 +17,41 @@ package bisq.core.btc.model; +import bisq.common.config.Config; import bisq.common.proto.persistable.PersistedDataHost; import bisq.common.proto.persistable.UserThreadMappedPersistableEnvelope; import bisq.common.storage.Storage; import com.google.protobuf.Message; +import org.bitcoinj.core.Address; import org.bitcoinj.core.Transaction; import org.bitcoinj.crypto.DeterministicKey; import org.bitcoinj.wallet.Wallet; import com.google.inject.Inject; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import com.google.common.collect.ImmutableList; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.stream.Collectors; -import lombok.Getter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; /** - * The List supporting our persistence solution. + * The AddressEntries was previously stored as list, now as hashSet. We still keep the old name to reflect the + * associated protobuf message. */ @ToString @Slf4j public final class AddressEntryList implements UserThreadMappedPersistableEnvelope, PersistedDataHost { transient private Storage storage; transient private Wallet wallet; - @Getter - private List list; + private final Set entrySet = new CopyOnWriteArraySet<>(); @Inject public AddressEntryList(Storage storage) { @@ -58,8 +61,10 @@ public final class AddressEntryList implements UserThreadMappedPersistableEnvelo @Override public void readPersisted() { AddressEntryList persisted = storage.initAndGetPersisted(this, 50); - if (persisted != null) - list = new ArrayList<>(persisted.getList()); + if (persisted != null) { + entrySet.clear(); + entrySet.addAll(persisted.entrySet); + } } @@ -67,22 +72,22 @@ public final class AddressEntryList implements UserThreadMappedPersistableEnvelo // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// - private AddressEntryList(List list) { - this.list = list; + private AddressEntryList(Set entrySet) { + this.entrySet.addAll(entrySet); } public static AddressEntryList fromProto(protobuf.AddressEntryList proto) { - return new AddressEntryList(new ArrayList<>(proto.getAddressEntryList().stream().map(AddressEntry::fromProto).collect(Collectors.toList()))); + Set entrySet = proto.getAddressEntryList().stream() + .map(AddressEntry::fromProto) + .collect(Collectors.toSet()); + return new AddressEntryList(entrySet); } @Override public Message toProtoMessage() { - // We clone list as we got ConcurrentModificationExceptions - List clone = new ArrayList<>(list); - List addressEntries = clone.stream() + Set addressEntries = entrySet.stream() .map(AddressEntry::toProtoMessage) - .collect(Collectors.toList()); - + .collect(Collectors.toSet()); return protobuf.PersistableEnvelope.newBuilder() .setAddressEntryList(protobuf.AddressEntryList.newBuilder() .addAllAddressEntry(addressEntries)) @@ -97,29 +102,50 @@ public final class AddressEntryList implements UserThreadMappedPersistableEnvelo public void onWalletReady(Wallet wallet) { this.wallet = wallet; - if (list != null) { - list.forEach(addressEntry -> { + if (!entrySet.isEmpty()) { + Set toBeRemoved = new HashSet<>(); + entrySet.forEach(addressEntry -> { DeterministicKey keyFromPubHash = (DeterministicKey) wallet.findKeyFromPubHash(addressEntry.getPubKeyHash()); if (keyFromPubHash != null) { - addressEntry.setDeterministicKey(keyFromPubHash); + Address addressFromKey = keyFromPubHash.toAddress(Config.baseCurrencyNetworkParameters()); + // We want to ensure key and address matches in case we have address in entry available already + if (addressEntry.getAddress() == null || addressFromKey.equals(addressEntry.getAddress())) { + addressEntry.setDeterministicKey(keyFromPubHash); + } else { + log.error("We found an address entry without key but cannot apply the key as the address " + + "is not matching. " + + "We remove that entry as it seems it is not compatible with our wallet. " + + "addressFromKey={}, addressEntry.getAddress()={}", + addressFromKey, addressEntry.getAddress()); + toBeRemoved.add(addressEntry); + } } else { - log.error("Key from addressEntry not found in that wallet " + addressEntry.toString()); + log.error("Key from addressEntry {} not found in that wallet. We remove that entry. " + + "This is expected at restore from seeds.", addressEntry.toString()); + toBeRemoved.add(addressEntry); } }); - } else { - list = new ArrayList<>(); - add(new AddressEntry(wallet.freshReceiveKey(), AddressEntry.Context.ARBITRATOR)); - // In case we restore from seed words and have balance we need to add the relevant addresses to our list. - // IssuedReceiveAddresses does not contain all addresses where we expect balance so we need to listen to - // incoming txs at blockchain sync to add the rest. - if (wallet.getBalance().isPositive()) { - wallet.getIssuedReceiveAddresses().forEach(address -> { - log.info("Create AddressEntry for IssuedReceiveAddress. address={}", address.toString()); - add(new AddressEntry((DeterministicKey) wallet.findKeyFromPubHash(address.getHash160()), AddressEntry.Context.AVAILABLE)); - }); - } - persist(); + toBeRemoved.forEach(entrySet::remove); + } else { + // As long the old arbitration domain is not removed from the code base we still support it here. + entrySet.add(new AddressEntry(wallet.freshReceiveKey(), AddressEntry.Context.ARBITRATOR)); + } + + // In case we restore from seed words and have balance we need to add the relevant addresses to our list. + // IssuedReceiveAddresses does not contain all addresses where we expect balance so we need to listen to + // incoming txs at blockchain sync to add the rest. + if (wallet.getBalance().isPositive()) { + wallet.getIssuedReceiveAddresses().stream() + .filter(this::isAddressNotInEntries) + .forEach(address -> { + log.info("Create AddressEntry for IssuedReceiveAddress. address={}", address.toString()); + DeterministicKey key = (DeterministicKey) wallet.findKeyFromPubHash(address.getHash160()); + if (key != null) { + // Address will be derived from key in getAddress method + entrySet.add(new AddressEntry(key, AddressEntry.Context.AVAILABLE)); + } + }); } // We add those listeners to get notified about potential new transactions and @@ -127,62 +153,41 @@ public final class AddressEntryList implements UserThreadMappedPersistableEnvelo // but can help as well in case the addressEntry list would miss an address where the wallet was received // funds (e.g. if the user sends funds to an address which has not been provided in the main UI - like from the // wallet details window). - wallet.addCoinsReceivedEventListener((w, tx, prevBalance, newBalance) -> { - updateList(tx); + wallet.addCoinsReceivedEventListener((wallet1, tx, prevBalance, newBalance) -> { + maybeAddNewAddressEntry(tx); }); - wallet.addCoinsSentEventListener((w, tx, prevBalance, newBalance) -> { - updateList(tx); + wallet.addCoinsSentEventListener((wallet1, tx, prevBalance, newBalance) -> { + maybeAddNewAddressEntry(tx); }); + + persist(); } - private void updateList(Transaction tx) { - tx.getOutputs().stream() - .filter(output -> output.isMine(wallet)) - .map(output -> output.getAddressFromP2PKHScript(wallet.getNetworkParameters())) - .filter(Objects::nonNull) - .filter(address -> !listContainsEntryWithAddress(address.toBase58())) - .map(address -> (DeterministicKey) wallet.findKeyFromPubHash(address.getHash160())) - .filter(Objects::nonNull) - .map(deterministicKey -> new AddressEntry(deterministicKey, AddressEntry.Context.AVAILABLE)) - .forEach(addressEntry -> list.add(addressEntry)); + public ImmutableList getAddressEntriesAsListImmutable() { + return ImmutableList.copyOf(entrySet); } - private boolean listContainsEntryWithAddress(String addressString) { - return list.stream().anyMatch(addressEntry -> Objects.equals(addressEntry.getAddressString(), addressString)); - } - - private boolean add(AddressEntry addressEntry) { - return list.add(addressEntry); - } - - private boolean remove(AddressEntry addressEntry) { - return list.remove(addressEntry); - } - - public AddressEntry addAddressEntry(AddressEntry addressEntry) { - boolean changed = add(addressEntry); - if (changed) + public void addAddressEntry(AddressEntry addressEntry) { + boolean setChangedByAdd = entrySet.add(addressEntry); + if (setChangedByAdd) persist(); - return addressEntry; - } - - public void swapTradeToSavings(String offerId) { - list.stream().filter(addressEntry -> offerId.equals(addressEntry.getOfferId())) - .findAny().ifPresent(this::swapToAvailable); } public void swapToAvailable(AddressEntry addressEntry) { - boolean changed1 = remove(addressEntry); - boolean changed2 = add(new AddressEntry(addressEntry.getKeyPair(), AddressEntry.Context.AVAILABLE)); - if (changed1 || changed2) + boolean setChangedByRemove = entrySet.remove(addressEntry); + boolean setChangedByAdd = entrySet.add(new AddressEntry(addressEntry.getKeyPair(), AddressEntry.Context.AVAILABLE)); + if (setChangedByRemove || setChangedByAdd) { persist(); + } } - public AddressEntry swapAvailableToAddressEntryWithOfferId(AddressEntry addressEntry, AddressEntry.Context context, String offerId) { - boolean changed1 = remove(addressEntry); + public AddressEntry swapAvailableToAddressEntryWithOfferId(AddressEntry addressEntry, + AddressEntry.Context context, + String offerId) { + boolean setChangedByRemove = entrySet.remove(addressEntry); final AddressEntry newAddressEntry = new AddressEntry(addressEntry.getKeyPair(), context, offerId); - boolean changed2 = add(newAddressEntry); - if (changed1 || changed2) + boolean setChangedByAdd = entrySet.add(newAddressEntry); + if (setChangedByRemove || setChangedByAdd) persist(); return newAddressEntry; @@ -192,7 +197,24 @@ public final class AddressEntryList implements UserThreadMappedPersistableEnvelo storage.queueUpForSave(50); } - public Stream stream() { - return list.stream(); + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void maybeAddNewAddressEntry(Transaction tx) { + tx.getOutputs().stream() + .filter(output -> output.isMine(wallet)) + .map(output -> output.getAddressFromP2PKHScript(wallet.getNetworkParameters())) + .filter(Objects::nonNull) + .filter(this::isAddressNotInEntries) + .map(address -> (DeterministicKey) wallet.findKeyFromPubHash(address.getHash160())) + .filter(Objects::nonNull) + .map(deterministicKey -> new AddressEntry(deterministicKey, AddressEntry.Context.AVAILABLE)) + .forEach(this::addAddressEntry); + } + + private boolean isAddressNotInEntries(Address address) { + return entrySet.stream().noneMatch(e -> address.equals(e.getAddress())); } } diff --git a/core/src/main/java/bisq/core/btc/setup/WalletsSetup.java b/core/src/main/java/bisq/core/btc/setup/WalletsSetup.java index a2a94dcab9..d569448457 100644 --- a/core/src/main/java/bisq/core/btc/setup/WalletsSetup.java +++ b/core/src/main/java/bisq/core/btc/setup/WalletsSetup.java @@ -59,7 +59,6 @@ import javax.inject.Inject; import javax.inject.Named; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Service; import org.apache.commons.lang3.StringUtils; @@ -498,7 +497,7 @@ public class WalletsSetup { } public Set
getAddressesByContext(@SuppressWarnings("SameParameterValue") AddressEntry.Context context) { - return ImmutableList.copyOf(addressEntryList.getList()).stream() + return addressEntryList.getAddressEntriesAsListImmutable().stream() .filter(addressEntry -> addressEntry.getContext() == context) .map(AddressEntry::getAddress) .collect(Collectors.toSet()); diff --git a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java index 3bb4721b17..d31ece5294 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java @@ -47,7 +47,6 @@ import org.bitcoinj.wallet.Wallet; import javax.inject.Inject; import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; @@ -108,8 +107,8 @@ public class BtcWalletService extends WalletService { void decryptWallet(@NotNull KeyParameter key) { super.decryptWallet(key); - addressEntryList.stream().forEach(e -> { - final DeterministicKey keyPair = e.getKeyPair(); + addressEntryList.getAddressEntriesAsListImmutable().stream().forEach(e -> { + DeterministicKey keyPair = e.getKeyPair(); if (keyPair.isEncrypted()) e.setDeterministicKey(keyPair.decrypt(key)); }); @@ -119,8 +118,8 @@ public class BtcWalletService extends WalletService { @Override void encryptWallet(KeyCrypterScrypt keyCrypterScrypt, KeyParameter key) { super.encryptWallet(keyCrypterScrypt, key); - addressEntryList.stream().forEach(e -> { - final DeterministicKey keyPair = e.getKeyPair(); + addressEntryList.getAddressEntriesAsListImmutable().stream().forEach(e -> { + DeterministicKey keyPair = e.getKeyPair(); if (keyPair.isEncrypted()) e.setDeterministicKey(keyPair.encrypt(keyCrypterScrypt, key)); }); @@ -157,12 +156,18 @@ public class BtcWalletService extends WalletService { // Proposal txs /////////////////////////////////////////////////////////////////////////////////////////// - public Transaction completePreparedReimbursementRequestTx(Coin issuanceAmount, Address issuanceAddress, Transaction feeTx, byte[] opReturnData) + public Transaction completePreparedReimbursementRequestTx(Coin issuanceAmount, + Address issuanceAddress, + Transaction feeTx, + byte[] opReturnData) throws TransactionVerificationException, WalletException, InsufficientMoneyException { return completePreparedProposalTx(feeTx, opReturnData, issuanceAmount, issuanceAddress); } - public Transaction completePreparedCompensationRequestTx(Coin issuanceAmount, Address issuanceAddress, Transaction feeTx, byte[] opReturnData) + public Transaction completePreparedCompensationRequestTx(Coin issuanceAmount, + Address issuanceAddress, + Transaction feeTx, + byte[] opReturnData) throws TransactionVerificationException, WalletException, InsufficientMoneyException { return completePreparedProposalTx(feeTx, opReturnData, issuanceAmount, issuanceAddress); } @@ -292,7 +297,8 @@ public class BtcWalletService extends WalletService { return completePreparedBsqTxWithBtcFee(preparedTx, opReturnData); } - private Transaction completePreparedBsqTxWithBtcFee(Transaction preparedTx, byte[] opReturnData) throws InsufficientMoneyException, TransactionVerificationException, WalletException { + private Transaction completePreparedBsqTxWithBtcFee(Transaction preparedTx, + byte[] opReturnData) throws InsufficientMoneyException, TransactionVerificationException, WalletException { // Remember index for first BTC input int indexOfBtcFirstInput = preparedTx.getInputs().size(); @@ -306,7 +312,8 @@ public class BtcWalletService extends WalletService { return tx; } - private Transaction addInputsForMinerFee(Transaction preparedTx, byte[] opReturnData) throws InsufficientMoneyException { + private Transaction addInputsForMinerFee(Transaction preparedTx, + byte[] opReturnData) throws InsufficientMoneyException { // safety check counter to avoid endless loops int counter = 0; // estimated size of input sig @@ -421,7 +428,9 @@ public class BtcWalletService extends WalletService { return completePreparedBsqTx(preparedBsqTx, isSendTx, null); } - public Transaction completePreparedBsqTx(Transaction preparedBsqTx, boolean useCustomTxFee, @Nullable byte[] opReturnData) throws + public Transaction completePreparedBsqTx(Transaction preparedBsqTx, + boolean useCustomTxFee, + @Nullable byte[] opReturnData) throws TransactionVerificationException, WalletException, InsufficientMoneyException { // preparedBsqTx has following structure: @@ -545,7 +554,8 @@ public class BtcWalletService extends WalletService { // AddressEntry /////////////////////////////////////////////////////////////////////////////////////////// - public Optional getAddressEntry(String offerId, @SuppressWarnings("SameParameterValue") AddressEntry.Context context) { + public Optional getAddressEntry(String offerId, + @SuppressWarnings("SameParameterValue") AddressEntry.Context context) { return getAddressEntryListAsImmutableList().stream() .filter(e -> offerId.equals(e.getOfferId())) .filter(e -> context == e.getContext()) @@ -592,17 +602,14 @@ public class BtcWalletService extends WalletService { return getOrCreateAddressEntry(context, addressEntry); } - public AddressEntry getNewAddressEntry(String offerId, AddressEntry.Context context) { + public void getNewAddressEntry(String offerId, AddressEntry.Context context) { AddressEntry entry = new AddressEntry(wallet.freshReceiveKey(), context, offerId); addressEntryList.addAddressEntry(entry); - return entry; } - public AddressEntry recoverAddressEntry(String offerId, String address, AddressEntry.Context context) { - var available = findAddressEntry(address, AddressEntry.Context.AVAILABLE); - if (!available.isPresent()) - return null; - return addressEntryList.swapAvailableToAddressEntryWithOfferId(available.get(), context, offerId); + public void recoverAddressEntry(String offerId, String address, AddressEntry.Context context) { + findAddressEntry(address, AddressEntry.Context.AVAILABLE).ifPresent(addressEntry -> + addressEntryList.swapAvailableToAddressEntryWithOfferId(addressEntry, context, offerId)); } private AddressEntry getOrCreateAddressEntry(AddressEntry.Context context, Optional addressEntry) { @@ -655,20 +662,18 @@ public class BtcWalletService extends WalletService { } public List getAddressEntryListAsImmutableList() { - return ImmutableList.copyOf(addressEntryList.getList()); + return addressEntryList.getAddressEntriesAsListImmutable(); } public void swapTradeEntryToAvailableEntry(String offerId, AddressEntry.Context context) { - Optional addressEntryOptional = getAddressEntryListAsImmutableList().stream() + getAddressEntryListAsImmutableList().stream() .filter(e -> offerId.equals(e.getOfferId())) .filter(e -> context == e.getContext()) - .findAny(); - addressEntryOptional.ifPresent(e -> { - log.info("swap addressEntry with address {} and offerId {} from context {} to available", - e.getAddressString(), e.getOfferId(), context); - addressEntryList.swapToAvailable(e); - saveAddressEntryList(); - }); + .forEach(e -> { + log.info("swap addressEntry with address {} and offerId {} from context {} to available", + e.getAddressString(), e.getOfferId(), context); + addressEntryList.swapToAvailable(e); + }); } public void resetAddressEntriesForOpenOffer(String offerId) { diff --git a/core/src/main/java/bisq/core/filter/FilterManager.java b/core/src/main/java/bisq/core/filter/FilterManager.java index 0302fc24f7..cd5bb84cf0 100644 --- a/core/src/main/java/bisq/core/filter/FilterManager.java +++ b/core/src/main/java/bisq/core/filter/FilterManager.java @@ -371,24 +371,19 @@ public class FilterManager { return requireUpdateToNewVersion; } - public boolean arePeersPaymentAccountDataBanned(PaymentAccountPayload paymentAccountPayload, - PaymentAccountFilter[] appliedPaymentAccountFilter) { + public boolean arePeersPaymentAccountDataBanned(PaymentAccountPayload paymentAccountPayload) { return getFilter() != null && getFilter().getBannedPaymentAccounts().stream() + .filter(paymentAccountFilter -> paymentAccountFilter.getPaymentMethodId().equals( + paymentAccountPayload.getPaymentMethodId())) .anyMatch(paymentAccountFilter -> { - final boolean samePaymentMethodId = paymentAccountFilter.getPaymentMethodId().equals( - paymentAccountPayload.getPaymentMethodId()); - if (samePaymentMethodId) { - try { - Method method = paymentAccountPayload.getClass().getMethod(paymentAccountFilter.getGetMethodName()); - String result = (String) method.invoke(paymentAccountPayload); - appliedPaymentAccountFilter[0] = paymentAccountFilter; - return result.toLowerCase().equals(paymentAccountFilter.getValue().toLowerCase()); - } catch (Throwable e) { - log.error(e.getMessage()); - return false; - } - } else { + try { + Method method = paymentAccountPayload.getClass().getMethod(paymentAccountFilter.getGetMethodName()); + // We invoke getter methods (no args), e.g. getHolderName + String valueFromInvoke = (String) method.invoke(paymentAccountPayload); + return valueFromInvoke.equalsIgnoreCase(paymentAccountFilter.getValue()); + } catch (Throwable e) { + log.error(e.getMessage()); return false; } }); diff --git a/core/src/main/java/bisq/core/support/dispute/agent/MultipleHolderNameDetection.java b/core/src/main/java/bisq/core/support/dispute/agent/MultipleHolderNameDetection.java index 48025ae266..80a64808c6 100644 --- a/core/src/main/java/bisq/core/support/dispute/agent/MultipleHolderNameDetection.java +++ b/core/src/main/java/bisq/core/support/dispute/agent/MultipleHolderNameDetection.java @@ -200,18 +200,19 @@ public class MultipleHolderNameDetection { private Map> getAllDisputesByTraderMap() { Map> allDisputesByTraderMap = new HashMap<>(); - disputeManager.getDisputesAsObservableList() - .forEach(dispute -> { + disputeManager.getDisputesAsObservableList().stream() + .filter(dispute -> { Contract contract = dispute.getContract(); PaymentAccountPayload paymentAccountPayload = isBuyer(dispute) ? contract.getBuyerPaymentAccountPayload() : contract.getSellerPaymentAccountPayload(); - if (paymentAccountPayload instanceof PayloadWithHolderName) { - String traderPubKeyHash = getSigPubKeyHashAsHex(dispute); - allDisputesByTraderMap.putIfAbsent(traderPubKeyHash, new ArrayList<>()); - List disputes = allDisputesByTraderMap.get(traderPubKeyHash); - disputes.add(dispute); - } + return paymentAccountPayload instanceof PayloadWithHolderName; + }) + .forEach(dispute -> { + String traderPubKeyHash = getSigPubKeyHashAsHex(dispute); + allDisputesByTraderMap.putIfAbsent(traderPubKeyHash, new ArrayList<>()); + List disputes = allDisputesByTraderMap.get(traderPubKeyHash); + disputes.add(dispute); }); return allDisputesByTraderMap; } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ApplyFilter.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ApplyFilter.java index 55e40c120f..cbcb0ae8c2 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ApplyFilter.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ApplyFilter.java @@ -18,7 +18,6 @@ package bisq.core.trade.protocol.tasks; import bisq.core.filter.FilterManager; -import bisq.core.filter.PaymentAccountFilter; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.trade.Trade; @@ -42,9 +41,8 @@ public class ApplyFilter extends TradeTask { try { runInterceptHook(); - final NodeAddress nodeAddress = processModel.getTempTradingPeerNodeAddress(); + NodeAddress nodeAddress = processModel.getTempTradingPeerNodeAddress(); PaymentAccountPayload paymentAccountPayload = checkNotNull(processModel.getTradingPeer().getPaymentAccountPayload()); - final PaymentAccountFilter[] appliedPaymentAccountFilter = new PaymentAccountFilter[1]; FilterManager filterManager = processModel.getFilterManager(); if (nodeAddress != null && filterManager.isNodeAddressBanned(nodeAddress)) { @@ -56,13 +54,12 @@ public class ApplyFilter extends TradeTask { } else if (trade.getOffer() != null && filterManager.isCurrencyBanned(trade.getOffer().getCurrencyCode())) { failed("Currency is banned.\n" + "Currency code=" + trade.getOffer().getCurrencyCode()); - } else if (filterManager.isPaymentMethodBanned(trade.getOffer().getPaymentMethod())) { + } else if (filterManager.isPaymentMethodBanned(checkNotNull(trade.getOffer()).getPaymentMethod())) { failed("Payment method is banned.\n" + "Payment method=" + trade.getOffer().getPaymentMethod().getId()); - } else if (filterManager.arePeersPaymentAccountDataBanned(paymentAccountPayload, appliedPaymentAccountFilter)) { + } else if (filterManager.arePeersPaymentAccountDataBanned(paymentAccountPayload)) { failed("Other trader is banned by their trading account data.\n" + - "paymentAccountPayload=" + paymentAccountPayload.getPaymentDetails() + "\n" + - "banFilter=" + appliedPaymentAccountFilter[0].toString()); + "paymentAccountPayload=" + paymentAccountPayload.getPaymentDetails()); } else if (filterManager.requireUpdateToNewVersionForTrading()) { failed("Your version of Bisq is not compatible for trading anymore. " + "Please update to the latest Bisq version at https://bisq.network/downloads."); diff --git a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java index c3ec7de284..5c83364cab 100644 --- a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java +++ b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java @@ -166,6 +166,7 @@ public class XmrTxProofService implements AssetTxProofService { if (!preferences.findAutoConfirmSettings("XMR").isPresent()) { log.error("AutoConfirmSettings is not present"); + return; } autoConfirmSettings = preferences.findAutoConfirmSettings("XMR").get(); diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index b44aa54d5c..a36cef1a56 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -35,7 +35,7 @@ shared.no=No shared.iUnderstand=I understand shared.na=N/A shared.shutDown=Shut down -shared.reportBug=Report bug at GitHub issues +shared.reportBug=Report bug on GitHub shared.buyBitcoin=Buy bitcoin shared.sellBitcoin=Sell bitcoin shared.buyCurrency=Buy {0} @@ -94,21 +94,21 @@ shared.BTCMinMax=BTC (min - max) shared.removeOffer=Remove offer shared.dontRemoveOffer=Don't remove offer shared.editOffer=Edit offer -shared.openLargeQRWindow=Open large QR-Code window +shared.openLargeQRWindow=Open large QR code window shared.tradingAccount=Trading account -shared.faq=Visit FAQ web page +shared.faq=Visit FAQ page shared.yesCancel=Yes, cancel shared.nextStep=Next step shared.selectTradingAccount=Select trading account shared.fundFromSavingsWalletButton=Transfer funds from Bisq wallet shared.fundFromExternalWalletButton=Open your external wallet for funding -shared.openDefaultWalletFailed=Opening a default Bitcoin wallet application has failed. Perhaps you don't have one installed? +shared.openDefaultWalletFailed=Failed to open a Bitcoin wallet application. Are you sure you have one installed? shared.distanceInPercent=Distance in % from market price shared.belowInPercent=Below % from market price shared.aboveInPercent=Above % from market price shared.enterPercentageValue=Enter % value shared.OR=OR -shared.notEnoughFunds=You don''t have enough funds in your Bisq wallet.\nYou need {0} but you have only {1} in your Bisq wallet.\n\nPlease fund the trade from an external Bitcoin wallet or fund your Bisq wallet at \"Funds/Receive funds\". +shared.notEnoughFunds=You don''t have enough funds in your Bisq wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Bisq wallet at Funds > Receive Funds. shared.waitingForFunds=Waiting for funds... shared.depositTransactionId=Deposit transaction ID shared.TheBTCBuyer=The BTC buyer @@ -116,22 +116,22 @@ shared.You=You shared.reasonForPayment=Reason for payment shared.sendingConfirmation=Sending confirmation... shared.sendingConfirmationAgain=Please send confirmation again -shared.exportCSV=Export to csv +shared.exportCSV=Export to CSV shared.exportJSON=Export to JSON shared.noDateAvailable=No date available shared.noDetailsAvailable=No details available shared.notUsedYet=Not used yet shared.date=Date shared.sendFundsDetailsWithFee=Sending: {0}\nFrom address: {1}\nTo receiving address: {2}.\nRequired mining fee is: {3} ({4} satoshis/byte)\nTransaction size: {5} Kb\n\nThe recipient will receive: {6}\n\nAre you sure you want to withdraw this amount? -shared.sendFundsDetailsDust=Bisq detected that this transaction would create a change output which is below the minimum dust threshold (and not allowed by Bitcoin consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n +shared.sendFundsDetailsDust=Bisq detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Bitcoin consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Copy to clipboard shared.language=Language shared.country=Country shared.applyAndShutDown=Apply and shut down shared.selectPaymentMethod=Select payment method -shared.accountNameAlreadyUsed=That account name is already used in a saved account.\nPlease use another name. +shared.accountNameAlreadyUsed=That account name is already used for another saved account.\nPlease choose another name. shared.askConfirmDeleteAccount=Do you really want to delete the selected account? -shared.cannotDeleteAccount=You cannot delete that account because it is used in an open offer or in a trade. +shared.cannotDeleteAccount=You cannot delete that account because it is being used in an open offer (or in an open trade). shared.noAccountsSetupYet=There are no accounts set up yet shared.manageAccounts=Manage accounts shared.addNewAccount=Add new account @@ -375,10 +375,10 @@ offerbook.deactivateOffer.failed=Deactivating of offer failed:\n{0} offerbook.activateOffer.failed=Publishing of offer failed:\n{0} offerbook.withdrawFundsHint=You can withdraw the funds you paid in from the {0} screen. -offerbook.warning.noTradingAccountForCurrency.headline=No trading account for selected currency -offerbook.warning.noTradingAccountForCurrency.msg=You don't have a trading account for the selected currency.\nDo you want to create an offer with one of your existing trading accounts? -offerbook.warning.noMatchingAccount.headline=No matching trading account. -offerbook.warning.noMatchingAccount.msg=To take this offer, you will need to set up a payment account using this payment method.\n\nWould you like to do this now? +offerbook.warning.noTradingAccountForCurrency.headline=No payment account for selected currency +offerbook.warning.noTradingAccountForCurrency.msg=You don't have a payment account set up for the selected currency.\n\nWould you like to create an offer for another currency instead? +offerbook.warning.noMatchingAccount.headline=No matching payment account. +offerbook.warning.noMatchingAccount.msg=This offer uses a payment method you haven't set up yet. \n\nWould you like to set up a new payment account now? offerbook.warning.counterpartyTradeRestrictions=This offer cannot be taken due to counterparty trade restrictions @@ -1931,9 +1931,9 @@ dao.bond.bondedRoleType.DATA_RELAY_NODE_OPERATOR=Price node operator # suppress inspection "UnusedProperty" dao.bond.bondedRoleType.BTC_NODE_OPERATOR=Bitcoin node operator # suppress inspection "UnusedProperty" -dao.bond.bondedRoleType.MARKETS_OPERATOR=Markets API operator +dao.bond.bondedRoleType.MARKETS_OPERATOR=Markets operator # suppress inspection "UnusedProperty" -dao.bond.bondedRoleType.BSQ_EXPLORER_OPERATOR=BSQ explorer operator +dao.bond.bondedRoleType.BSQ_EXPLORER_OPERATOR=Explorer operator # suppress inspection "UnusedProperty" dao.bond.bondedRoleType.MOBILE_NOTIFICATIONS_RELAY_OPERATOR=Mobile notifications relay operator # suppress inspection "UnusedProperty" @@ -2512,8 +2512,8 @@ offerDetailsWindow.confirm.taker=Confirm: Take offer to {0} bitcoin offerDetailsWindow.creationDate=Creation date offerDetailsWindow.makersOnion=Maker's onion address -qRCodeWindow.headline=QR-Code -qRCodeWindow.msg=Please use that QR-Code for funding your Bisq wallet from your external wallet. +qRCodeWindow.headline=QR Code +qRCodeWindow.msg=Please use this QR code for funding your Bisq wallet from your external wallet. qRCodeWindow.request=Payment request:\n{0} selectDepositTxWindow.headline=Select deposit transaction for dispute @@ -2859,8 +2859,8 @@ systemTray.tooltip=Bisq: A decentralized bitcoin exchange network # GUI Util #################################################################### -guiUtil.miningFeeInfo=Please be sure that the mining fee used at your external wallet is \ -at least {0} satoshis/byte. Otherwise the trade transactions cannot be confirmed and a trade would end up in a dispute. +guiUtil.miningFeeInfo=Please be sure that the mining fee used by your external wallet is \ +at least {0} satoshis/byte. Otherwise the trade transactions may not be confirmed in time and the trade will end up in a dispute. guiUtil.accountExport.savedToPath=Trading accounts saved to path:\n{0} guiUtil.accountExport.noAccountSetup=You don't have trading accounts set up for exporting. @@ -3036,6 +3036,8 @@ Ideally you should specify the date your wallet seed was created.\n\n\n\ Are you sure you want to go ahead without specifying a wallet date? seed.restore.success=Wallets restored successfully with the new seed words.\n\nYou need to shut down and restart the application. seed.restore.error=An error occurred when restoring the wallets with seed words.{0} +seed.restore.openOffers.warn=You have open offers which will be removed if you restore from seed words.\n\ + Are you sure that you want to continue? #################################################################### diff --git a/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java b/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java index b37a47d057..2b2d11e562 100644 --- a/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java +++ b/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java @@ -218,7 +218,7 @@ public class AccountAgeWitnessServiceTest { when(filterManager.isNodeAddressBanned(any())).thenReturn(false); when(filterManager.isCurrencyBanned(any())).thenReturn(false); when(filterManager.isPaymentMethodBanned(any())).thenReturn(false); - when(filterManager.arePeersPaymentAccountDataBanned(any(), any())).thenReturn(false); + when(filterManager.arePeersPaymentAccountDataBanned(any())).thenReturn(false); when(filterManager.isWitnessSignerPubKeyBanned(any())).thenReturn(false); when(chargeBackRisk.hasChargebackRisk(any(), any())).thenReturn(true); diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcDisputeAgentsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcDisputeAgentsService.java new file mode 100644 index 0000000000..24fd192fea --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcDisputeAgentsService.java @@ -0,0 +1,45 @@ +package bisq.daemon.grpc; + +import bisq.core.api.CoreApi; + +import bisq.proto.grpc.DisputeAgentsGrpc; +import bisq.proto.grpc.RegisterDisputeAgentReply; +import bisq.proto.grpc.RegisterDisputeAgentRequest; + +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.stub.StreamObserver; + +import javax.inject.Inject; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +class GrpcDisputeAgentsService extends DisputeAgentsGrpc.DisputeAgentsImplBase { + + private final CoreApi coreApi; + + @Inject + public GrpcDisputeAgentsService(CoreApi coreApi) { + this.coreApi = coreApi; + } + + @Override + public void registerDisputeAgent(RegisterDisputeAgentRequest req, + StreamObserver responseObserver) { + try { + coreApi.registerDisputeAgent(req.getDisputeAgentType(), req.getRegistrationKey()); + var reply = RegisterDisputeAgentReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (IllegalArgumentException cause) { + var ex = new StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription(cause.getMessage())); + responseObserver.onError(ex); + throw ex; + } catch (IllegalStateException cause) { + var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage())); + responseObserver.onError(ex); + throw ex; + } + } +} diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java index a1293dfa0a..0c0e2f5cb3 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java @@ -51,11 +51,13 @@ public class GrpcServer { @Inject public GrpcServer(Config config, CoreApi coreApi, + GrpcDisputeAgentsService disputeAgentsService, GrpcOffersService offersService, GrpcPaymentAccountsService paymentAccountsService, GrpcWalletsService walletsService) { this.coreApi = coreApi; this.server = ServerBuilder.forPort(config.apiPort) + .addService(disputeAgentsService) .addService(new GetVersionService()) .addService(new GetTradeStatisticsService()) .addService(offersService) diff --git a/desktop/src/main/java/bisq/desktop/app/BisqApp.java b/desktop/src/main/java/bisq/desktop/app/BisqApp.java index 1202a1eb1f..c595daf138 100644 --- a/desktop/src/main/java/bisq/desktop/app/BisqApp.java +++ b/desktop/src/main/java/bisq/desktop/app/BisqApp.java @@ -54,6 +54,8 @@ import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.name.Names; +import com.google.common.base.Joiner; + import javafx.application.Application; import javafx.stage.Modality; @@ -69,6 +71,8 @@ import javafx.scene.layout.StackPane; import java.awt.GraphicsEnvironment; import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -241,8 +245,19 @@ public class BisqApp extends Application implements UncaughtExceptionHandler { // configure the primary stage String appName = injector.getInstance(Key.get(String.class, Names.named(Config.APP_NAME))); - if (!Config.baseCurrencyNetwork().isMainnet()) - appName += " [" + Res.get(Config.baseCurrencyNetwork().name()) + "]"; + List postFixes = new ArrayList<>(); + if (!Config.baseCurrencyNetwork().isMainnet()) { + postFixes.add(Config.baseCurrencyNetwork().name()); + } + if (injector.getInstance(Config.class).useLocalhostForP2P) { + postFixes.add("LOCALHOST"); + } + if (injector.getInstance(Config.class).useDevMode) { + postFixes.add("DEV MODE"); + } + if (!postFixes.isEmpty()) { + appName += " [" + Joiner.on(", ").join(postFixes) + " ]"; + } stage.setTitle(appName); stage.setScene(scene); diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/AssetsForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AssetsForm.java index 125f685a2b..2933099ee1 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/AssetsForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AssetsForm.java @@ -38,6 +38,7 @@ import bisq.core.payment.validation.AltCoinAddressValidator; import bisq.core.util.coin.CoinFormatter; import bisq.core.util.validation.InputValidator; +import bisq.common.UserThread; import bisq.common.util.Tuple3; import org.apache.commons.lang3.StringUtils; @@ -123,6 +124,13 @@ public class AssetsForm extends PaymentMethodForm { addressInputTextField.setValidator(altCoinAddressValidator); addressInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + if (newValue.startsWith("monero:")) { + UserThread.execute(() -> { + String addressWithoutPrefix = newValue.replace("monero:", ""); + addressInputTextField.setText(addressWithoutPrefix); + }); + return; + } assetAccount.setAddress(newValue); updateFromInputs(); }); diff --git a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java index 5504947bce..1fd34cb7bd 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java @@ -685,10 +685,20 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener { return marketPricePresentation.getPriceFeedComboBoxItems(); } + // We keep daoPresentation and accountPresentation support even it is not used atm. But if we add a new feature and + // add a badge again it will be needed. + @SuppressWarnings({"unused"}) public BooleanProperty getShowDaoUpdatesNotification() { return daoPresentation.getShowDaoUpdatesNotification(); } + // We keep daoPresentation and accountPresentation support even it is not used atm. But if we add a new feature and + // add a badge again it will be needed. + @SuppressWarnings("unused") + public BooleanProperty getShowAccountUpdatesNotification() { + return accountPresentation.getShowAccountUpdatesNotification(); + } + public BooleanProperty getShowSettingsUpdatesNotification() { return settingsPresentation.getShowSettingsUpdatesNotification(); } diff --git a/desktop/src/main/java/bisq/desktop/main/SharedPresentation.java b/desktop/src/main/java/bisq/desktop/main/SharedPresentation.java new file mode 100644 index 0000000000..913529779d --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/SharedPresentation.java @@ -0,0 +1,87 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.desktop.main; + +import bisq.desktop.app.BisqApp; +import bisq.desktop.main.overlays.popups.Popup; + +import bisq.core.btc.wallet.WalletsManager; +import bisq.core.locale.Res; +import bisq.core.offer.OpenOfferManager; + +import bisq.common.UserThread; +import bisq.common.storage.FileUtil; + +import org.bitcoinj.wallet.DeterministicSeed; + +import java.io.File; + +import java.util.concurrent.TimeUnit; + +import lombok.extern.slf4j.Slf4j; + +/** + * This serves as shared space for static methods used from different views where no common parent view would fit as + * owner of that code. We keep it strictly static. It should replace GUIUtil for those methods which are not utility + * methods. + */ +@Slf4j +public class SharedPresentation { + public static void restoreSeedWords(WalletsManager walletsManager, + OpenOfferManager openOfferManager, + DeterministicSeed seed, + File storageDir) { + if (!openOfferManager.getObservableList().isEmpty()) { + UserThread.runAfter(() -> + new Popup().warning(Res.get("seed.restore.openOffers.warn")) + .actionButtonText(Res.get("shared.yes")) + .onAction(() -> { + openOfferManager.removeAllOpenOffers(() -> { + doRestoreSeedWords(walletsManager, seed, storageDir); + }); + }) + .show(), 100, TimeUnit.MILLISECONDS); + } else { + doRestoreSeedWords(walletsManager, seed, storageDir); + } + } + + private static void doRestoreSeedWords(WalletsManager walletsManager, + DeterministicSeed seed, + File storageDir) { + try { + File backup = new File(storageDir, "AddressEntryList_backup_pre_wallet_restore_" + System.currentTimeMillis()); + FileUtil.copyFile(new File(storageDir, "AddressEntryList"), backup); + } catch (Throwable t) { + new Popup().error(Res.get("error.deleteAddressEntryListFailed", t)).show(); + } + + walletsManager.restoreSeedWords( + seed, + () -> UserThread.execute(() -> { + log.info("Wallets restored with seed words"); + new Popup().feedback(Res.get("seed.restore.success")).hideCloseButton().show(); + BisqApp.getShutDownHandler().run(); + }), + throwable -> UserThread.execute(() -> { + log.error(throwable.toString()); + new Popup().error(Res.get("seed.restore.error", Res.get("shared.errorMessageInline", throwable))) + .show(); + })); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/seedwords/SeedWordsView.java b/desktop/src/main/java/bisq/desktop/main/account/content/seedwords/SeedWordsView.java index a299b5434b..ea3186a003 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/content/seedwords/SeedWordsView.java +++ b/desktop/src/main/java/bisq/desktop/main/account/content/seedwords/SeedWordsView.java @@ -19,14 +19,15 @@ package bisq.desktop.main.account.content.seedwords; import bisq.desktop.common.view.ActivatableView; import bisq.desktop.common.view.FxmlView; +import bisq.desktop.main.SharedPresentation; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.WalletPasswordWindow; -import bisq.desktop.util.GUIUtil; import bisq.desktop.util.Layout; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.WalletsManager; import bisq.core.locale.Res; +import bisq.core.offer.OpenOfferManager; import bisq.core.user.DontShowAgainLookup; import bisq.common.config.Config; @@ -68,6 +69,7 @@ import static javafx.beans.binding.Bindings.createBooleanBinding; @FxmlView public class SeedWordsView extends ActivatableView { private final WalletsManager walletsManager; + private final OpenOfferManager openOfferManager; private final BtcWalletService btcWalletService; private final WalletPasswordWindow walletPasswordWindow; private final File storageDir; @@ -91,10 +93,12 @@ public class SeedWordsView extends ActivatableView { @Inject private SeedWordsView(WalletsManager walletsManager, + OpenOfferManager openOfferManager, BtcWalletService btcWalletService, WalletPasswordWindow walletPasswordWindow, @Named(Config.STORAGE_DIR) File storageDir) { this.walletsManager = walletsManager; + this.openOfferManager = openOfferManager; this.btcWalletService = btcWalletService; this.walletPasswordWindow = walletPasswordWindow; this.storageDir = storageDir; @@ -166,20 +170,18 @@ public class SeedWordsView extends ActivatableView { String key = "showBackupWarningAtSeedPhrase"; if (DontShowAgainLookup.showAgain(key)) { new Popup().warning(Res.get("account.seed.backup.warning")) - .onAction(() -> { - showSeedPhrase(); - }) - .actionButtonText(Res.get("shared.iUnderstand")) - .useIUnderstandButton() - .dontShowAgainId(key) - .hideCloseButton() - .show(); + .onAction(this::showSeedPhrase) + .actionButtonText(Res.get("shared.iUnderstand")) + .useIUnderstandButton() + .dontShowAgainId(key) + .hideCloseButton() + .show(); } else { showSeedPhrase(); } } - public void showSeedPhrase() { + private void showSeedPhrase() { DeterministicSeed keyChainSeed = btcWalletService.getKeyChainSeed(); // wallet creation date is not encrypted walletCreationDate = Instant.ofEpochSecond(walletsManager.getChainSeedCreationTimeSeconds()).atZone(ZoneId.systemDefault()).toLocalDate(); @@ -305,6 +307,6 @@ public class SeedWordsView extends ActivatableView { long date = localDateTime.toEpochSecond(ZoneOffset.UTC); DeterministicSeed seed = new DeterministicSeed(Splitter.on(" ").splitToList(seedWordsTextArea.getText()), null, "", date); - GUIUtil.restoreSeedWords(seed, walletsManager, storageDir); + SharedPresentation.restoreSeedWords(walletsManager, openOfferManager, seed, storageDir); } } diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/WalletPasswordWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/WalletPasswordWindow.java index b5ba8539ff..640ee6fc62 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/WalletPasswordWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/WalletPasswordWindow.java @@ -21,15 +21,16 @@ import bisq.desktop.components.AutoTooltipButton; import bisq.desktop.components.AutoTooltipLabel; import bisq.desktop.components.BusyAnimation; import bisq.desktop.components.PasswordTextField; +import bisq.desktop.main.SharedPresentation; import bisq.desktop.main.overlays.Overlay; import bisq.desktop.main.overlays.popups.Popup; -import bisq.desktop.util.GUIUtil; import bisq.desktop.util.Layout; import bisq.desktop.util.Transitions; import bisq.core.btc.wallet.WalletsManager; import bisq.core.crypto.ScryptUtil; import bisq.core.locale.Res; +import bisq.core.offer.OpenOfferManager; import bisq.common.UserThread; import bisq.common.config.Config; @@ -88,6 +89,7 @@ import static javafx.beans.binding.Bindings.createBooleanBinding; @Slf4j public class WalletPasswordWindow extends Overlay { private final WalletsManager walletsManager; + private final OpenOfferManager openOfferManager; private File storageDir; private Button unlockButton; @@ -115,8 +117,10 @@ public class WalletPasswordWindow extends Overlay { @Inject private WalletPasswordWindow(WalletsManager walletsManager, + OpenOfferManager openOfferManager, @Named(Config.STORAGE_DIR) File storageDir) { this.walletsManager = walletsManager; + this.openOfferManager = openOfferManager; this.storageDir = storageDir; type = Type.Attention; width = 900; @@ -277,7 +281,6 @@ public class WalletPasswordWindow extends Overlay { gridPane.getChildren().add(headLine2Label); seedWordsTextArea = addTextArea(gridPane, ++rowIndex, Res.get("seed.enterSeedWords"), 5); - ; seedWordsTextArea.setPrefHeight(60); Tuple2 labelDatePickerTuple2 = addTopLabelDatePicker(gridPane, ++rowIndex, @@ -356,6 +359,6 @@ public class WalletPasswordWindow extends Overlay { //TODO Is ZoneOffset correct? long date = value != null ? value.atStartOfDay().toEpochSecond(ZoneOffset.UTC) : 0; DeterministicSeed seed = new DeterministicSeed(Splitter.on(" ").splitToList(seedWordsTextArea.getText()), null, "", date); - GUIUtil.restoreSeedWords(seed, walletsManager, storageDir); + SharedPresentation.restoreSeedWords(walletsManager, openOfferManager, seed, storageDir); } } diff --git a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java index 5fabd2c94a..0d26e80a96 100644 --- a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java +++ b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java @@ -32,7 +32,6 @@ import bisq.desktop.util.validation.RegexValidator; import bisq.core.account.witness.AccountAgeWitness; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.btc.setup.WalletsSetup; -import bisq.core.btc.wallet.WalletsManager; import bisq.core.locale.Country; import bisq.core.locale.CountryUtil; import bisq.core.locale.CurrencyUtil; @@ -63,7 +62,6 @@ import bisq.common.config.Config; import bisq.common.proto.persistable.PersistableList; import bisq.common.proto.persistable.PersistenceProtoResolver; import bisq.common.storage.CorruptedDatabaseFilesHandler; -import bisq.common.storage.FileUtil; import bisq.common.storage.Storage; import bisq.common.util.MathUtils; import bisq.common.util.Tuple2; @@ -75,7 +73,6 @@ import org.bitcoinj.core.Coin; import org.bitcoinj.core.TransactionConfidence; import org.bitcoinj.uri.BitcoinURI; import org.bitcoinj.utils.Fiat; -import org.bitcoinj.wallet.DeterministicSeed; import com.googlecode.jcsv.CSVStrategy; import com.googlecode.jcsv.writer.CSVEntryConverter; @@ -803,26 +800,6 @@ public class GUIUtil { } } - public static void restoreSeedWords(DeterministicSeed seed, WalletsManager walletsManager, File storageDir) { - try { - FileUtil.renameFile(new File(storageDir, "AddressEntryList"), new File(storageDir, "AddressEntryList_wallet_restore_" + System.currentTimeMillis())); - } catch (Throwable t) { - new Popup().error(Res.get("error.deleteAddressEntryListFailed", t)).show(); - } - walletsManager.restoreSeedWords( - seed, - () -> UserThread.execute(() -> { - log.info("Wallets restored with seed words"); - new Popup().feedback(Res.get("seed.restore.success")).hideCloseButton().show(); - BisqApp.getShutDownHandler().run(); - }), - throwable -> UserThread.execute(() -> { - log.error(throwable.toString()); - new Popup().error(Res.get("seed.restore.error", Res.get("shared.errorMessageInline", throwable))) - .show(); - })); - } - public static void showSelectableTextModal(String title, String text) { TextArea textArea = new BisqTextArea(); textArea.setText(text); diff --git a/docs/build.md b/docs/build.md index ca3ab06457..ea054c7936 100644 --- a/docs/build.md +++ b/docs/build.md @@ -7,7 +7,7 @@ Bisq uses Git LFS to track certain large binary files. Follow the instructions a $ git lfs version git-lfs/2.10.0 (GitHub; darwin amd64; go 1.13.6) - + ## Clone @@ -17,8 +17,9 @@ Bisq uses Git LFS to track certain large binary files. Follow the instructions a ## Build -You do _not_ need to install Gradle to complete the following command. The `gradlew` shell script will install it for you if necessary. +You do _not_ need to install Gradle to complete the following command. The `gradlew` shell script will install it for you if necessary. Pull the lfs data first. + git lfs pull ./gradlew build If on Windows run `gradlew.bat build` instead. diff --git a/gradle/witness/gradle-witness.gradle b/gradle/witness/gradle-witness.gradle index 08f74f9f51..1e6db53567 100644 --- a/gradle/witness/gradle-witness.gradle +++ b/gradle/witness/gradle-witness.gradle @@ -19,14 +19,14 @@ dependencyVerification { 'com.fasterxml.jackson.core:jackson-annotations:2566b3a6662afa3c6af4f5b25006cb46be2efc68f1b5116291d6998a8cdf7ed3', 'com.fasterxml.jackson.core:jackson-core:39a74610521d7fb9eb3f437bb8739bbf47f6435be12d17bf954c731a0c6352bb', 'com.fasterxml.jackson.core:jackson-databind:fcf3c2b0c332f5f54604f7e27fa7ee502378a2cc5df6a944bbfae391872c32ff', - 'com.github.JesusMcCloud.netlayer:tor.external:fb080d812aa88f5fb1ec74273ae24ae13980b7902c75e0ffc3ac462b4cf6b242', - 'com.github.JesusMcCloud.netlayer:tor.native:1227275377ac73d9d25dec1d05bc2f7a2ae3b1f1afde77f39049637dcb8fc407', - 'com.github.JesusMcCloud.netlayer:tor:6ff47a97dc0dc97079d83c1f2c66ebcc956f25dd3bacfa6e24206cb2c742f52b', - 'com.github.JesusMcCloud.tor-binary:tor-binary-geoip:13a3c6e02f37f9def172ce1d231ea1a45aa1ddbc36b1d9e81b7a91632bc474ed', - 'com.github.JesusMcCloud.tor-binary:tor-binary-linux32:2022da3a398527d366bd954bea92069cf209218c649aeb3342f85f7434aa0786', - 'com.github.JesusMcCloud.tor-binary:tor-binary-linux64:972d5a946964176fe849eb872baa64a591d9e6fe0467c62ae71986b946101836', - 'com.github.JesusMcCloud.tor-binary:tor-binary-macos:95a4d093e2d48099623015e70add1bd7dcc3e392f9e87ed4d691286c868516c9', - 'com.github.JesusMcCloud.tor-binary:tor-binary-windows:43a07290443a1c55211eaa6001681839f9cb7453574089a939d0da3dd20e2cf2', + 'com.github.JesusMcCloud.netlayer:tor.external:1facb63d5f4107a1d4a176a03c149b60eed19a76371b58d1b6aa62ad5cacde3f', + 'com.github.JesusMcCloud.netlayer:tor.native:88267f1a3b2d1433a77a2fd9dc3a8f1d5cd28fbbefa2e078d83b176cbe91d477', + 'com.github.JesusMcCloud.netlayer:tor:0eaae7bcea11ef25e4f041e77c237ebc96f120462e64a7818073a88dadf9b805', + 'com.github.JesusMcCloud.tor-binary:tor-binary-geoip:08a461608990aed2c272121dc835a38687017d3bb172bc181ea0793df7ebb5aa', + 'com.github.JesusMcCloud.tor-binary:tor-binary-linux32:81fb1e80d191ed9d0a509e514311d21d59c87c6edee6a490975061a18390b10c', + 'com.github.JesusMcCloud.tor-binary:tor-binary-linux64:7b90b5e8f1cc32d86ed8e2e5b1f57d774bc27bebc679ececdbf1ff0699bd1f4c', + 'com.github.JesusMcCloud.tor-binary:tor-binary-macos:db5aef1082fdd8afb4f7f21cf0956f46a76e93aad75bed6ed149dd3e3767c0a7', + 'com.github.JesusMcCloud.tor-binary:tor-binary-windows:1bb83786a71449d8edb152e40ce5efc129eaca6a0900f4912d7555b6df2f52a2', 'com.github.JesusMcCloud:jtorctl:389d61b1b5a85eb2f23c582c3913ede49f80c9f2b553e4762382c836270e57e5', 'com.github.bisq-network.bitcoinj:bitcoinj-core:f979c2187e61ee3b08dd4cbfc49a149734cff64c045d29ed112f2e12f34068a3', 'com.github.ravn:jsocks:3c71600af027b2b6d4244e4ad14d98ff2352a379410daebefff5d8cd48d742a4', diff --git a/p2p/src/main/java/bisq/network/p2p/network/Connection.java b/p2p/src/main/java/bisq/network/p2p/network/Connection.java index d6908d80ed..e7e8aeb8ae 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/Connection.java +++ b/p2p/src/main/java/bisq/network/p2p/network/Connection.java @@ -48,6 +48,8 @@ import bisq.common.proto.network.NetworkEnvelope; import bisq.common.proto.network.NetworkProtoResolver; import bisq.common.util.Utilities; +import com.google.protobuf.InvalidProtocolBufferException; + import javax.inject.Inject; import com.google.common.util.concurrent.MoreExecutors; @@ -283,12 +285,20 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { bundleSender.schedule(() -> { if (!stopped) { synchronized (lock) { - BundleOfEnvelopes current = queueOfBundles.poll(); - if (current != null && !stopped) { - if (current.getEnvelopes().size() == 1) { - protoOutputStream.writeEnvelope(current.getEnvelopes().get(0)); - } else { - protoOutputStream.writeEnvelope(current); + BundleOfEnvelopes bundle = queueOfBundles.poll(); + if (bundle != null && !stopped) { + NetworkEnvelope envelope = bundle.getEnvelopes().size() == 1 ? + bundle.getEnvelopes().get(0) : + bundle; + try { + protoOutputStream.writeEnvelope(envelope); + } catch (Throwable t) { + log.error("Sending envelope of class {} to address {} " + + "failed due {}", + envelope.getClass().getSimpleName(), + this.getPeersNodeAddressOptional(), + t.toString()); + log.error("envelope: {}", envelope); } } } @@ -876,7 +886,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { log.error(e.getMessage()); e.printStackTrace(); reportInvalidRequest(RuleViolation.INVALID_CLASS); - } catch (ProtobufferException | NoClassDefFoundError e) { + } catch (ProtobufferException | NoClassDefFoundError | InvalidProtocolBufferException e) { log.error(e.getMessage()); e.printStackTrace(); reportInvalidRequest(RuleViolation.INVALID_DATA_TYPE); diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 730920b062..ed2cb4fd61 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -24,19 +24,20 @@ option java_package = "bisq.proto.grpc"; option java_multiple_files = true; /////////////////////////////////////////////////////////////////////////////////////////// -// Version +// DisputeAgents /////////////////////////////////////////////////////////////////////////////////////////// -service GetVersion { - rpc GetVersion (GetVersionRequest) returns (GetVersionReply) { +service DisputeAgents { + rpc RegisterDisputeAgent (RegisterDisputeAgentRequest) returns (RegisterDisputeAgentReply) { } } -message GetVersionRequest { +message RegisterDisputeAgentRequest { + string disputeAgentType = 1; + string registrationKey = 2; } -message GetVersionReply { - string version = 1; +message RegisterDisputeAgentReply { } /////////////////////////////////////////////////////////////////////////////////////////// @@ -214,3 +215,20 @@ message AddressBalanceInfo { int64 balance = 2; int64 numConfirmations = 3; } + +/////////////////////////////////////////////////////////////////////////////////////////// +// Version +/////////////////////////////////////////////////////////////////////////////////////////// + +service GetVersion { + rpc GetVersion (GetVersionRequest) returns (GetVersionReply) { + } +} + +message GetVersionRequest { +} + +message GetVersionReply { + string version = 1; +} +