diff --git a/apitest/scripts/mainnet-test.sh b/apitest/scripts/mainnet-test.sh index ae3afd73d2d..9c5889ff3a0 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 5197a35634c..b0ce2c548e6 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.OptionSet; import joptsimple.OptionSpec; +import java.net.InetAddress; + import java.nio.file.Paths; import java.io.File; @@ -169,7 +171,7 @@ public ApiTestConfig(String... args) { 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 f9100bee96c..30e7472e2ac 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 @@ */ 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 2cf4e8ae1cd..a77fe633ea6 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.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 static void setUp() { 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 22413cf9d3c..ed6083c8d3d 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 static void setUp() { @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 694aa6806e3..3437ac59c94 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 @@ 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 @@ protected final GetFundingAddressesRequest createGetFundingAddressesRequest() { 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 00000000000..1ad3a36f16a --- /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 450fb58e010..f74c8e705c3 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 static void setUp() { @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 void testGetBalanceOnEncryptedWalletShouldThrowException() { @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 void testUnlockWalletFor4Seconds() { @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 void testLockWalletBeforeUnlockTimeoutExpiry() { @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 void testSetNewWalletPasswordWithIncorrectNewPasswordShouldThrowException @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 e95e310eb58..0b30d72c1cf 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.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 static void setUp() { @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/cli/src/main/java/bisq/cli/GrpcStubs.java b/cli/src/main/java/bisq/cli/GrpcStubs.java index e12a6efa7c7..2ef5efb75b7 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 @@ 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 GrpcStubs(String apiHost, int apiPort, String apiPassword) { } })); + 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/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 30cedeb3b93..35af589f3d5 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 @@ @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 00000000000..a1071ab3b2b --- /dev/null +++ b/core/src/main/java/bisq/core/api/CoreDisputeAgentsService.java @@ -0,0 +1,145 @@ +/* + * 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.net.InetAddress; + +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; + +@Slf4j +class CoreDisputeAgentsService { + + private final Config config; + private final KeyRing keyRing; + private final MediatorManager mediatorManager; + private final RefundAgentManager refundAgentManager; + private final P2PService p2PService; + + @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; + } + + 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"); + + switch (disputeAgentType) { + case "arbitrator": + throw new IllegalArgumentException("arbitrators must be registered in a Bisq UI"); + case "mediator": + case "refundagent": + NodeAddress nodeAddress = new NodeAddress( + InetAddress.getLoopbackAddress().getHostAddress(), config.nodePort); + List languageCodes = Arrays.asList("de", "en", "es", "fr"); + ECKey ecKey = mediatorManager.getRegistrationKey(registrationKey); + String signature = mediatorManager.signStorageSignaturePubKey(Objects.requireNonNull(ecKey)); + + if (disputeAgentType.equals("mediator")) + registerMediator(nodeAddress, languageCodes, ecKey, signature); + else + 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/daemon/src/main/java/bisq/daemon/grpc/GrpcDisputeAgentsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcDisputeAgentsService.java new file mode 100644 index 00000000000..24fd192fea8 --- /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 a1293dfa0ac..0c0e2f5cb3f 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/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 730920b0622..ed2cb4fd61b 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; +} +