diff --git a/src/main/java/org/qortal/api/resource/CrossChainBitcoinResource.java b/src/main/java/org/qortal/api/resource/CrossChainBitcoinResource.java index ff4aee02..9fc1cafe 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainBitcoinResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainBitcoinResource.java @@ -18,7 +18,11 @@ import org.qortal.api.model.crosschain.AddressRequest; import org.qortal.api.model.crosschain.BitcoinSendRequest; import org.qortal.crosschain.AddressInfo; import org.qortal.crosschain.Bitcoin; +import org.qortal.crosschain.ChainableServer; +import org.qortal.crosschain.ElectrumX; import org.qortal.crosschain.ForeignBlockchainException; +import org.qortal.crosschain.ServerConnectionInfo; +import org.qortal.crosschain.ServerInfo; import org.qortal.crosschain.SimpleTransaction; import org.qortal.crosschain.ServerConfigurationInfo; @@ -267,6 +271,181 @@ public class CrossChainBitcoinResource { return CrossChainUtils.buildServerConfigurationInfo(Bitcoin.getInstance()); } + @GET + @Path("/serverconnectionhistory") + @Operation( + summary = "Returns Bitcoin server connection history", + description = "Returns Bitcoin server connection history", + responses = { + @ApiResponse( + content = @Content(array = @ArraySchema( schema = @Schema( implementation = ServerConnectionInfo.class ) ) ) + ) + } + ) + public List getServerConnectionHistory() { + + return CrossChainUtils.buildServerConnectionHistory(Bitcoin.getInstance()); + } + + @POST + @Path("/addserver") + @Operation( + summary = "Add server to list of Bitcoin servers", + description = "Add server to list of Bitcoin servers", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerInfo.class + ) + ) + ), + responses = { + @ApiResponse( + description = "true if removed, false if not found", + content = @Content( + schema = @Schema( + type = "string" + ) + ) + ) + } + + ) + @ApiErrors({ApiError.INVALID_DATA}) + @SecurityRequirement(name = "apiKey") + public String addServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) { + Security.checkApiCallAllowed(request); + + try { + ElectrumX.Server server = new ElectrumX.Server( + serverInfo.getHostName(), + ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()), + serverInfo.getPort() + ); + + if( CrossChainUtils.addServer( Bitcoin.getInstance(), server )) { + return "true"; + } + else { + return "false"; + } + } + catch (IllegalArgumentException | NullPointerException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + } + catch (Exception e) { + return "false"; + } + } + + @POST + @Path("/removeserver") + @Operation( + summary = "Remove server from list of Bitcoin servers", + description = "Remove server from list of Bitcoin servers", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerInfo.class + ) + ) + ), + responses = { + @ApiResponse( + description = "true if removed, otherwise", + content = @Content( + schema = @Schema( + type = "string" + ) + ) + ) + } + + ) + @ApiErrors({ApiError.INVALID_DATA}) + @SecurityRequirement(name = "apiKey") + public String removeServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) { + Security.checkApiCallAllowed(request); + + try { + ElectrumX.Server server = new ElectrumX.Server( + serverInfo.getHostName(), + ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()), + serverInfo.getPort() + ); + + if( CrossChainUtils.removeServer( Bitcoin.getInstance(), server ) ) { + + return "true"; + } + else { + return "false"; + } + } + catch (IllegalArgumentException | NullPointerException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + } + catch (Exception e) { + return "false"; + } + } + + @POST + @Path("/setcurrentserver") + @Operation( + summary = "Set current Bitcoin server", + description = "Set current Bitcoin server", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerInfo.class + ) + ) + ), + responses = { + @ApiResponse( + description = "connection info", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerConnectionInfo.class + ) + ) + ) + } + + ) + @ApiErrors({ApiError.INVALID_DATA}) + @SecurityRequirement(name = "apiKey") + public ServerConnectionInfo setCurrentServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) { + Security.checkApiCallAllowed(request); + + if( serverInfo.getConnectionType() == null || + serverInfo.getHostName() == null) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + try { + return CrossChainUtils.setCurrentServer( Bitcoin.getInstance(), serverInfo ); + } + catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + } + catch (Exception e) { + return new ServerConnectionInfo( + serverInfo, + CrossChainUtils.CORE_API_CALL, + true, + false, + System.currentTimeMillis(), + CrossChainUtils.getNotes(e)); + } + } + + @GET @Path("/feekb") @Operation( diff --git a/src/main/java/org/qortal/api/resource/CrossChainDigibyteResource.java b/src/main/java/org/qortal/api/resource/CrossChainDigibyteResource.java index d78a4ed9..f69007f9 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainDigibyteResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainDigibyteResource.java @@ -16,8 +16,12 @@ import org.qortal.api.Security; import org.qortal.api.model.crosschain.AddressRequest; import org.qortal.api.model.crosschain.DigibyteSendRequest; import org.qortal.crosschain.AddressInfo; -import org.qortal.crosschain.Digibyte; +import org.qortal.crosschain.ChainableServer; +import org.qortal.crosschain.ElectrumX; import org.qortal.crosschain.ForeignBlockchainException; +import org.qortal.crosschain.Digibyte; +import org.qortal.crosschain.ServerConnectionInfo; +import org.qortal.crosschain.ServerInfo; import org.qortal.crosschain.SimpleTransaction; import org.qortal.crosschain.ServerConfigurationInfo; @@ -266,6 +270,181 @@ public class CrossChainDigibyteResource { return CrossChainUtils.buildServerConfigurationInfo(Digibyte.getInstance()); } + @GET + @Path("/serverconnectionhistory") + @Operation( + summary = "Returns Digibyte server connection history", + description = "Returns Digibyte server connection history", + responses = { + @ApiResponse( + content = @Content(array = @ArraySchema( schema = @Schema( implementation = ServerConnectionInfo.class ) ) ) + ) + } + ) + public List getServerConnectionHistory() { + + return CrossChainUtils.buildServerConnectionHistory(Digibyte.getInstance()); + } + + @POST + @Path("/addserver") + @Operation( + summary = "Add server to list of Digibyte servers", + description = "Add server to list of Digibyte servers", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerInfo.class + ) + ) + ), + responses = { + @ApiResponse( + description = "true if removed, false if not found", + content = @Content( + schema = @Schema( + type = "string" + ) + ) + ) + } + + ) + @ApiErrors({ApiError.INVALID_DATA}) + @SecurityRequirement(name = "apiKey") + public String addServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) { + Security.checkApiCallAllowed(request); + + try { + ElectrumX.Server server = new ElectrumX.Server( + serverInfo.getHostName(), + ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()), + serverInfo.getPort() + ); + + if( CrossChainUtils.addServer( Digibyte.getInstance(), server )) { + return "true"; + } + else { + return "false"; + } + } + catch (IllegalArgumentException | NullPointerException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + } + catch (Exception e) { + return "false"; + } + } + + @POST + @Path("/removeserver") + @Operation( + summary = "Remove server from list of Digibyte servers", + description = "Remove server from list of Digibyte servers", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerInfo.class + ) + ) + ), + responses = { + @ApiResponse( + description = "true if removed, otherwise", + content = @Content( + schema = @Schema( + type = "string" + ) + ) + ) + } + + ) + @ApiErrors({ApiError.INVALID_DATA}) + @SecurityRequirement(name = "apiKey") + public String removeServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) { + Security.checkApiCallAllowed(request); + + try { + ElectrumX.Server server = new ElectrumX.Server( + serverInfo.getHostName(), + ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()), + serverInfo.getPort() + ); + + if( CrossChainUtils.removeServer( Digibyte.getInstance(), server ) ) { + + return "true"; + } + else { + return "false"; + } + } + catch (IllegalArgumentException | NullPointerException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + } + catch (Exception e) { + return "false"; + } + } + + @POST + @Path("/setcurrentserver") + @Operation( + summary = "Set current Digibyte server", + description = "Set current Digibyte server", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerInfo.class + ) + ) + ), + responses = { + @ApiResponse( + description = "connection info", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerConnectionInfo.class + ) + ) + ) + } + + ) + @ApiErrors({ApiError.INVALID_DATA}) + @SecurityRequirement(name = "apiKey") + public ServerConnectionInfo setCurrentServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) { + Security.checkApiCallAllowed(request); + + if( serverInfo.getConnectionType() == null || + serverInfo.getHostName() == null) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + try { + return CrossChainUtils.setCurrentServer( Digibyte.getInstance(), serverInfo ); + } + catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + } + catch (Exception e) { + return new ServerConnectionInfo( + serverInfo, + CrossChainUtils.CORE_API_CALL, + true, + false, + System.currentTimeMillis(), + CrossChainUtils.getNotes(e)); + } + } + + @GET @Path("/feekb") @Operation( diff --git a/src/main/java/org/qortal/api/resource/CrossChainDogecoinResource.java b/src/main/java/org/qortal/api/resource/CrossChainDogecoinResource.java index 8575a28d..4bb6e102 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainDogecoinResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainDogecoinResource.java @@ -16,8 +16,12 @@ import org.qortal.api.Security; import org.qortal.api.model.crosschain.AddressRequest; import org.qortal.api.model.crosschain.DogecoinSendRequest; import org.qortal.crosschain.AddressInfo; -import org.qortal.crosschain.Dogecoin; +import org.qortal.crosschain.ChainableServer; +import org.qortal.crosschain.ElectrumX; import org.qortal.crosschain.ForeignBlockchainException; +import org.qortal.crosschain.Dogecoin; +import org.qortal.crosschain.ServerConnectionInfo; +import org.qortal.crosschain.ServerInfo; import org.qortal.crosschain.SimpleTransaction; import org.qortal.crosschain.ServerConfigurationInfo; @@ -266,6 +270,181 @@ public class CrossChainDogecoinResource { return CrossChainUtils.buildServerConfigurationInfo(Dogecoin.getInstance()); } + @GET + @Path("/serverconnectionhistory") + @Operation( + summary = "Returns Dogecoin server connection history", + description = "Returns Dogecoin server connection history", + responses = { + @ApiResponse( + content = @Content(array = @ArraySchema( schema = @Schema( implementation = ServerConnectionInfo.class ) ) ) + ) + } + ) + public List getServerConnectionHistory() { + + return CrossChainUtils.buildServerConnectionHistory(Dogecoin.getInstance()); + } + + @POST + @Path("/addserver") + @Operation( + summary = "Add server to list of Dogecoin servers", + description = "Add server to list of Dogecoin servers", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerInfo.class + ) + ) + ), + responses = { + @ApiResponse( + description = "true if removed, false if not found", + content = @Content( + schema = @Schema( + type = "string" + ) + ) + ) + } + + ) + @ApiErrors({ApiError.INVALID_DATA}) + @SecurityRequirement(name = "apiKey") + public String addServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) { + Security.checkApiCallAllowed(request); + + try { + ElectrumX.Server server = new ElectrumX.Server( + serverInfo.getHostName(), + ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()), + serverInfo.getPort() + ); + + if( CrossChainUtils.addServer( Dogecoin.getInstance(), server )) { + return "true"; + } + else { + return "false"; + } + } + catch (IllegalArgumentException | NullPointerException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + } + catch (Exception e) { + return "false"; + } + } + + @POST + @Path("/removeserver") + @Operation( + summary = "Remove server from list of Dogecoin servers", + description = "Remove server from list of Dogecoin servers", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerInfo.class + ) + ) + ), + responses = { + @ApiResponse( + description = "true if removed, otherwise", + content = @Content( + schema = @Schema( + type = "string" + ) + ) + ) + } + + ) + @ApiErrors({ApiError.INVALID_DATA}) + @SecurityRequirement(name = "apiKey") + public String removeServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) { + Security.checkApiCallAllowed(request); + + try { + ElectrumX.Server server = new ElectrumX.Server( + serverInfo.getHostName(), + ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()), + serverInfo.getPort() + ); + + if( CrossChainUtils.removeServer( Dogecoin.getInstance(), server ) ) { + + return "true"; + } + else { + return "false"; + } + } + catch (IllegalArgumentException | NullPointerException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + } + catch (Exception e) { + return "false"; + } + } + + @POST + @Path("/setcurrentserver") + @Operation( + summary = "Set current Dogecoin server", + description = "Set current Dogecoin server", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerInfo.class + ) + ) + ), + responses = { + @ApiResponse( + description = "connection info", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerConnectionInfo.class + ) + ) + ) + } + + ) + @ApiErrors({ApiError.INVALID_DATA}) + @SecurityRequirement(name = "apiKey") + public ServerConnectionInfo setCurrentServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) { + Security.checkApiCallAllowed(request); + + if( serverInfo.getConnectionType() == null || + serverInfo.getHostName() == null) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + try { + return CrossChainUtils.setCurrentServer( Dogecoin.getInstance(), serverInfo ); + } + catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + } + catch (Exception e) { + return new ServerConnectionInfo( + serverInfo, + CrossChainUtils.CORE_API_CALL, + true, + false, + System.currentTimeMillis(), + CrossChainUtils.getNotes(e)); + } + } + + @GET @Path("/feekb") @Operation( diff --git a/src/main/java/org/qortal/api/resource/CrossChainLitecoinResource.java b/src/main/java/org/qortal/api/resource/CrossChainLitecoinResource.java index 7667eea1..92519f03 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainLitecoinResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainLitecoinResource.java @@ -16,8 +16,12 @@ import org.qortal.api.Security; import org.qortal.api.model.crosschain.AddressRequest; import org.qortal.api.model.crosschain.LitecoinSendRequest; import org.qortal.crosschain.AddressInfo; +import org.qortal.crosschain.ChainableServer; +import org.qortal.crosschain.ElectrumX; import org.qortal.crosschain.ForeignBlockchainException; import org.qortal.crosschain.Litecoin; +import org.qortal.crosschain.ServerConnectionInfo; +import org.qortal.crosschain.ServerInfo; import org.qortal.crosschain.SimpleTransaction; import org.qortal.crosschain.ServerConfigurationInfo; @@ -266,6 +270,180 @@ public class CrossChainLitecoinResource { return CrossChainUtils.buildServerConfigurationInfo(Litecoin.getInstance()); } + @GET + @Path("/serverconnectionhistory") + @Operation( + summary = "Returns Litecoin server connection history", + description = "Returns Litecoin server connection history", + responses = { + @ApiResponse( + content = @Content(array = @ArraySchema( schema = @Schema( implementation = ServerConnectionInfo.class ) ) ) + ) + } + ) + public List getServerConnectionHistory() { + + return CrossChainUtils.buildServerConnectionHistory(Litecoin.getInstance()); + } + + @POST + @Path("/addserver") + @Operation( + summary = "Add server to list of Litecoin servers", + description = "Add server to list of Litecoin servers", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerInfo.class + ) + ) + ), + responses = { + @ApiResponse( + description = "true if removed, false if not found", + content = @Content( + schema = @Schema( + type = "string" + ) + ) + ) + } + + ) + @ApiErrors({ApiError.INVALID_DATA}) + @SecurityRequirement(name = "apiKey") + public String addServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) { + Security.checkApiCallAllowed(request); + + try { + ElectrumX.Server server = new ElectrumX.Server( + serverInfo.getHostName(), + ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()), + serverInfo.getPort() + ); + + if( CrossChainUtils.addServer( Litecoin.getInstance(), server )) { + return "true"; + } + else { + return "false"; + } + } + catch (IllegalArgumentException | NullPointerException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + } + catch (Exception e) { + return "false"; + } + } + + @POST + @Path("/removeserver") + @Operation( + summary = "Remove server from list of Litecoin servers", + description = "Remove server from list of Litecoin servers", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerInfo.class + ) + ) + ), + responses = { + @ApiResponse( + description = "true if removed, otherwise", + content = @Content( + schema = @Schema( + type = "string" + ) + ) + ) + } + + ) + @ApiErrors({ApiError.INVALID_DATA}) + @SecurityRequirement(name = "apiKey") + public String removeServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) { + Security.checkApiCallAllowed(request); + + try { + ElectrumX.Server server = new ElectrumX.Server( + serverInfo.getHostName(), + ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()), + serverInfo.getPort() + ); + + if( CrossChainUtils.removeServer( Litecoin.getInstance(), server ) ) { + + return "true"; + } + else { + return "false"; + } + } + catch (IllegalArgumentException | NullPointerException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + } + catch (Exception e) { + return "false"; + } + } + + @POST + @Path("/setcurrentserver") + @Operation( + summary = "Set current Litecoin server", + description = "Set current Litecoin server", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerInfo.class + ) + ) + ), + responses = { + @ApiResponse( + description = "connection info", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerConnectionInfo.class + ) + ) + ) + } + + ) + @ApiErrors({ApiError.INVALID_DATA}) + @SecurityRequirement(name = "apiKey") + public ServerConnectionInfo setCurrentServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) { + Security.checkApiCallAllowed(request); + + if( serverInfo.getConnectionType() == null || + serverInfo.getHostName() == null) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + try { + return CrossChainUtils.setCurrentServer( Litecoin.getInstance(), serverInfo ); + } + catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + } + catch (Exception e) { + return new ServerConnectionInfo( + serverInfo, + CrossChainUtils.CORE_API_CALL, + true, + false, + System.currentTimeMillis(), + CrossChainUtils.getNotes(e)); + } + } + @POST @Path("/repair") @Operation( diff --git a/src/main/java/org/qortal/api/resource/CrossChainPirateChainResource.java b/src/main/java/org/qortal/api/resource/CrossChainPirateChainResource.java index 03ff43b8..be25b9a3 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainPirateChainResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainPirateChainResource.java @@ -13,8 +13,12 @@ import org.qortal.api.ApiErrors; import org.qortal.api.ApiExceptionFactory; import org.qortal.api.Security; import org.qortal.api.model.crosschain.PirateChainSendRequest; +import org.qortal.crosschain.ChainableServer; import org.qortal.crosschain.ForeignBlockchainException; import org.qortal.crosschain.PirateChain; +import org.qortal.crosschain.PirateLightClient; +import org.qortal.crosschain.ServerConnectionInfo; +import org.qortal.crosschain.ServerInfo; import org.qortal.crosschain.SimpleTransaction; import org.qortal.crosschain.ServerConfigurationInfo; @@ -352,6 +356,180 @@ public class CrossChainPirateChainResource { return CrossChainUtils.buildServerConfigurationInfo(PirateChain.getInstance()); } + @GET + @Path("/serverconnectionhistory") + @Operation( + summary = "Returns Pirate Chain server connection history", + description = "Returns Pirate Chain server connection history", + responses = { + @ApiResponse( + content = @Content(array = @ArraySchema( schema = @Schema( implementation = ServerConnectionInfo.class ) ) ) + ) + } + ) + public List getServerConnectionHistory() { + + return CrossChainUtils.buildServerConnectionHistory(PirateChain.getInstance()); + } + + @POST + @Path("/addserver") + @Operation( + summary = "Add server to list of Pirate Chain servers", + description = "Add server to list of Pirate Chain servers", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerInfo.class + ) + ) + ), + responses = { + @ApiResponse( + description = "true if removed, false if not found", + content = @Content( + schema = @Schema( + type = "string" + ) + ) + ) + } + + ) + @ApiErrors({ApiError.INVALID_DATA}) + @SecurityRequirement(name = "apiKey") + public String addServerInfo(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) { + Security.checkApiCallAllowed(request); + + try { + PirateLightClient.Server server = new PirateLightClient.Server( + serverInfo.getHostName(), + ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()), + serverInfo.getPort() + ); + + if( CrossChainUtils.addServer( PirateChain.getInstance(), server )) { + return "true"; + } + else { + return "false"; + } + } + catch (IllegalArgumentException | NullPointerException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + } + catch (Exception e) { + return "false"; + } + } + + @POST + @Path("/removeserver") + @Operation( + summary = "Remove server from list of Pirate Chain servers", + description = "Remove server from list of Pirate Chain servers", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerInfo.class + ) + ) + ), + responses = { + @ApiResponse( + description = "true if removed, otherwise", + content = @Content( + schema = @Schema( + type = "string" + ) + ) + ) + } + + ) + @ApiErrors({ApiError.INVALID_DATA}) + @SecurityRequirement(name = "apiKey") + public String removeServerInfo(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) { + Security.checkApiCallAllowed(request); + + try { + PirateLightClient.Server server = new PirateLightClient.Server( + serverInfo.getHostName(), + ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()), + serverInfo.getPort() + ); + + if( CrossChainUtils.removeServer( PirateChain.getInstance(), server ) ) { + + return "true"; + } + else { + return "false"; + } + } + catch (IllegalArgumentException | NullPointerException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + } + catch (Exception e) { + return "false"; + } + } + + @POST + @Path("/setcurrentserver") + @Operation( + summary = "Set current Pirate Chain server", + description = "Set current Pirate Chain server", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerInfo.class + ) + ) + ), + responses = { + @ApiResponse( + description = "connection info", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerConnectionInfo.class + ) + ) + ) + } + + ) + @ApiErrors({ApiError.INVALID_DATA}) + @SecurityRequirement(name = "apiKey") + public ServerConnectionInfo setCurrentServerInfo(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) { + Security.checkApiCallAllowed(request); + + if( serverInfo.getConnectionType() == null || + serverInfo.getHostName() == null) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + try { + return CrossChainUtils.setCurrentServer( PirateChain.getInstance(), serverInfo ); + } + catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + } + catch (Exception e) { + return new ServerConnectionInfo( + serverInfo, + CrossChainUtils.CORE_API_CALL, + true, + false, + System.currentTimeMillis(), + CrossChainUtils.getNotes(e)); + } + } + @GET @Path("/feekb") @Operation( diff --git a/src/main/java/org/qortal/api/resource/CrossChainRavencoinResource.java b/src/main/java/org/qortal/api/resource/CrossChainRavencoinResource.java index ce5cd668..978345c9 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainRavencoinResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainRavencoinResource.java @@ -16,8 +16,12 @@ import org.qortal.api.Security; import org.qortal.api.model.crosschain.AddressRequest; import org.qortal.api.model.crosschain.RavencoinSendRequest; import org.qortal.crosschain.AddressInfo; +import org.qortal.crosschain.ChainableServer; +import org.qortal.crosschain.ElectrumX; import org.qortal.crosschain.ForeignBlockchainException; import org.qortal.crosschain.Ravencoin; +import org.qortal.crosschain.ServerConnectionInfo; +import org.qortal.crosschain.ServerInfo; import org.qortal.crosschain.SimpleTransaction; import org.qortal.crosschain.ServerConfigurationInfo; @@ -266,6 +270,181 @@ public class CrossChainRavencoinResource { return CrossChainUtils.buildServerConfigurationInfo(Ravencoin.getInstance()); } + @GET + @Path("/serverconnectionhistory") + @Operation( + summary = "Returns Ravencoin server connection history", + description = "Returns Ravencoin server connection history", + responses = { + @ApiResponse( + content = @Content(array = @ArraySchema( schema = @Schema( implementation = ServerConnectionInfo.class ) ) ) + ) + } + ) + public List getServerConnectionHistory() { + + return CrossChainUtils.buildServerConnectionHistory(Ravencoin.getInstance()); + } + + @POST + @Path("/addserver") + @Operation( + summary = "Add server to list of Ravencoin servers", + description = "Add server to list of Ravencoin servers", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerInfo.class + ) + ) + ), + responses = { + @ApiResponse( + description = "true if removed, false if not found", + content = @Content( + schema = @Schema( + type = "string" + ) + ) + ) + } + + ) + @ApiErrors({ApiError.INVALID_DATA}) + @SecurityRequirement(name = "apiKey") + public String addServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) { + Security.checkApiCallAllowed(request); + + try { + ElectrumX.Server server = new ElectrumX.Server( + serverInfo.getHostName(), + ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()), + serverInfo.getPort() + ); + + if( CrossChainUtils.addServer( Ravencoin.getInstance(), server )) { + return "true"; + } + else { + return "false"; + } + } + catch (IllegalArgumentException | NullPointerException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + } + catch (Exception e) { + return "false"; + } + } + + @POST + @Path("/removeserver") + @Operation( + summary = "Remove server from list of Ravencoin servers", + description = "Remove server from list of Ravencoin servers", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerInfo.class + ) + ) + ), + responses = { + @ApiResponse( + description = "true if removed, otherwise", + content = @Content( + schema = @Schema( + type = "string" + ) + ) + ) + } + + ) + @ApiErrors({ApiError.INVALID_DATA}) + @SecurityRequirement(name = "apiKey") + public String removeServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) { + Security.checkApiCallAllowed(request); + + try { + ElectrumX.Server server = new ElectrumX.Server( + serverInfo.getHostName(), + ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()), + serverInfo.getPort() + ); + + if( CrossChainUtils.removeServer( Ravencoin.getInstance(), server ) ) { + + return "true"; + } + else { + return "false"; + } + } + catch (IllegalArgumentException | NullPointerException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + } + catch (Exception e) { + return "false"; + } + } + + @POST + @Path("/setcurrentserver") + @Operation( + summary = "Set current Ravencoin server", + description = "Set current Ravencoin server", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerInfo.class + ) + ) + ), + responses = { + @ApiResponse( + description = "connection info", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerConnectionInfo.class + ) + ) + ) + } + + ) + @ApiErrors({ApiError.INVALID_DATA}) + @SecurityRequirement(name = "apiKey") + public ServerConnectionInfo setCurrentServer(@HeaderParam(Security.API_KEY_HEADER) String apiKey, ServerInfo serverInfo) { + Security.checkApiCallAllowed(request); + + if( serverInfo.getConnectionType() == null || + serverInfo.getHostName() == null) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + try { + return CrossChainUtils.setCurrentServer( Ravencoin.getInstance(), serverInfo ); + } + catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + } + catch (Exception e) { + return new ServerConnectionInfo( + serverInfo, + CrossChainUtils.CORE_API_CALL, + true, + false, + System.currentTimeMillis(), + CrossChainUtils.getNotes(e)); + } + } + + @GET @Path("/feekb") @Operation( diff --git a/src/main/java/org/qortal/api/resource/CrossChainUtils.java b/src/main/java/org/qortal/api/resource/CrossChainUtils.java index 2c1fabc2..d1453bda 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainUtils.java +++ b/src/main/java/org/qortal/api/resource/CrossChainUtils.java @@ -9,10 +9,7 @@ import org.bitcoinj.script.ScriptBuilder; import org.qortal.crosschain.*; import org.qortal.data.at.ATData; -import org.qortal.data.crosschain.AtomicTransactionData; -import org.qortal.data.crosschain.CrossChainTradeData; -import org.qortal.data.crosschain.TradeBotData; -import org.qortal.data.crosschain.TransactionSummary; +import org.qortal.data.crosschain.*; import org.qortal.repository.DataException; import org.qortal.repository.Repository; @@ -22,6 +19,7 @@ import java.util.stream.Collectors; public class CrossChainUtils { private static final Logger LOGGER = LogManager.getLogger(CrossChainUtils.class); + public static final String CORE_API_CALL = "Core API Call"; public static ServerConfigurationInfo buildServerConfigurationInfo(Bitcoiny blockchain) { @@ -184,6 +182,74 @@ public class CrossChainUtils { return summaries; } + /** + * Add Server + * + * Add foreign blockchain server to list of candidates. + * + * @param bitcoiny the foreign blockchain + * @param server the server + * + * @return true if the add was successful, otherwise false + */ + public static boolean addServer(Bitcoiny bitcoiny, ChainableServer server) { + + return bitcoiny.getBlockchainProvider().addServer(server); + } + + /** + * Remove Server + * + * Remove foreign blockchain server from list of candidates. + * + * @param bitcoiny the foreign blockchain + * @param server the server + * + * @return true if the removal was successful, otherwise false + */ + public static boolean removeServer(Bitcoiny bitcoiny, ChainableServer server){ + + return bitcoiny.getBlockchainProvider().removeServer(server); + } + + /** + * Set Current Server + * + * Set the server to use the intended foreign blockchain. + * + * @param bitcoiny the foreign blockchain + * @param serverInfo the server configuration information + * + * @return the server connection information + */ + public static ServerConnectionInfo setCurrentServer(Bitcoiny bitcoiny, ServerInfo serverInfo) throws ForeignBlockchainException { + + final BitcoinyBlockchainProvider blockchainProvider = bitcoiny.getBlockchainProvider(); + + ChainableServer server = blockchainProvider.getServer( + serverInfo.getHostName(), + ChainableServer.ConnectionType.valueOf(serverInfo.getConnectionType()), + serverInfo.getPort() + ); + + ChainableServerConnection connection = blockchainProvider.setCurrentServer(server, CORE_API_CALL).get(); + + return new ServerConnectionInfo( + new ServerInfo( + 0, + serverInfo.getHostName(), + serverInfo.getPort(), + serverInfo.getConnectionType(), + connection.isSuccess() + ), + CORE_API_CALL, + true, + connection.isSuccess() , + System.currentTimeMillis(), + connection.getNotes() + ); + } + /** * Get P2Sh From Trade Bot * @@ -423,4 +489,60 @@ public class CrossChainUtils { } return totalInputOut; } + + /** + * Get Notes + * + * Build notes from an exception thrown. + * + * @param e the exception + * + * @return the exception message or the exception class name + */ + public static String getNotes(Exception e) { + return e.getMessage() + " (" + e.getClass().getSimpleName() + ")"; + } + + /** + * Build Server Connection History + * + * @param bitcoiny the foreign blockchain + * + * @return the history of connections from latest to first + */ + public static List buildServerConnectionHistory(Bitcoiny bitcoiny) { + + return bitcoiny.getBlockchainProvider().getServerConnections().stream() + .sorted(Comparator.comparing(ChainableServerConnection::getCurrentTimeMillis).reversed()) + .map( + connection -> new ServerConnectionInfo( + serverToServerInfo( connection.getServer()), + connection.getRequestedBy(), + connection.isOpen(), + connection.isSuccess(), + connection.getCurrentTimeMillis(), + connection.getNotes() + ) + ) + .collect(Collectors.toList()); + } + + /** + * Server To Server Info + * + * Make a server info object from a server object. + * + * @param server the server + * + * @return the server info + */ + private static ServerInfo serverToServerInfo(ChainableServer server) { + + return new ServerInfo( + 0, + server.getHostName(), + server.getPort(), + server.getConnectionType().toString(), + false); + } } \ No newline at end of file diff --git a/src/main/java/org/qortal/crosschain/BitcoinyBlockchainProvider.java b/src/main/java/org/qortal/crosschain/BitcoinyBlockchainProvider.java index 238eff38..1b14a474 100644 --- a/src/main/java/org/qortal/crosschain/BitcoinyBlockchainProvider.java +++ b/src/main/java/org/qortal/crosschain/BitcoinyBlockchainProvider.java @@ -3,12 +3,14 @@ package org.qortal.crosschain; import cash.z.wallet.sdk.rpc.CompactFormats.CompactBlock; import java.util.List; +import java.util.Optional; import java.util.Set; public abstract class BitcoinyBlockchainProvider { public static final boolean INCLUDE_UNCONFIRMED = true; public static final boolean EXCLUDE_UNCONFIRMED = false; + public static final String EMPTY = ""; /** Sets the blockchain using this provider instance */ public abstract void setBlockchain(Bitcoiny blockchain); @@ -67,4 +69,62 @@ public abstract class BitcoinyBlockchainProvider { public abstract Set getUselessServers(); public abstract ChainableServer getCurrentServer(); + + /** + * Add Server + * + * Add server to list of candidate servers. + * + * @param server the server + * + * @return true if added, otherwise false + */ + public abstract boolean addServer( ChainableServer server ); + + /** + * Remove Server + * + * Remove server from list of candidate servers. + * + * @param server the server + * + * @return true if removed, otherwise false + */ + public abstract boolean removeServer( ChainableServer server ); + + /** + * Set Current Server + * + * Set server to be used for this foreign blockchain. + * + * @param server the server + * @param requestedBy who requested this setting + * + * @return the connection that was made + * + * @throws ForeignBlockchainException + */ + public abstract Optional setCurrentServer(ChainableServer server, String requestedBy) throws ForeignBlockchainException; + + /** + * Get Server Connections + * + * Get the server connections made to this foreign blockchain, + * + * @return the server connections + */ + public abstract List getServerConnections(); + + /** + * Get Server + * + * Get a server for this foreign blockchain. + * + * @param hostName the host URL + * @param type the type of connection (TCP, SSL) + * @param port the port + * + * @return the server + */ + public abstract ChainableServer getServer(String hostName, ChainableServer.ConnectionType type, int port); } diff --git a/src/main/java/org/qortal/crosschain/ChainableServerConnection.java b/src/main/java/org/qortal/crosschain/ChainableServerConnection.java new file mode 100644 index 00000000..e680061a --- /dev/null +++ b/src/main/java/org/qortal/crosschain/ChainableServerConnection.java @@ -0,0 +1,71 @@ +package org.qortal.crosschain; + +import java.util.Objects; + +public class ChainableServerConnection { + + private ChainableServer server; + private String requestedBy; + private boolean open; + private boolean success; + private long currentTimeMillis; + private String notes; + + public ChainableServerConnection(ChainableServer server, String requestedBy, boolean open, boolean success, long currentTimeMillis, String notes) { + this.server = server; + this.requestedBy = requestedBy; + this.open = open; + this.success = success; + this.currentTimeMillis = currentTimeMillis; + this.notes = notes; + } + + public ChainableServer getServer() { + return server; + } + + public String getRequestedBy() { + return requestedBy; + } + + public boolean isOpen() { + return open; + } + + public boolean isSuccess() { + return success; + } + + public long getCurrentTimeMillis() { + return currentTimeMillis; + } + + public String getNotes() { + return notes; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ChainableServerConnection that = (ChainableServerConnection) o; + return currentTimeMillis == that.currentTimeMillis && Objects.equals(server, that.server); + } + + @Override + public int hashCode() { + return Objects.hash(server, currentTimeMillis); + } + + @Override + public String toString() { + return "ChainableServerConnection{" + + "server=" + server + + ", requestedBy='" + requestedBy + '\'' + + ", open=" + open + + ", success=" + success + + ", currentTimeMillis=" + currentTimeMillis + + ", notes='" + notes + '\'' + + '}'; + } +} diff --git a/src/main/java/org/qortal/crosschain/ChainableServerConnectionRecorder.java b/src/main/java/org/qortal/crosschain/ChainableServerConnectionRecorder.java new file mode 100644 index 00000000..aa26338f --- /dev/null +++ b/src/main/java/org/qortal/crosschain/ChainableServerConnectionRecorder.java @@ -0,0 +1,45 @@ +package org.qortal.crosschain; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public class ChainableServerConnectionRecorder { + + private List connections; + private int limit; + + public ChainableServerConnectionRecorder(int limit) { + this.connections = new ArrayList<>(limit); + this.limit = limit; + } + + public ChainableServerConnection recordConnection( + ChainableServer server, String requestedBy, boolean open, boolean success, String notes) { + + ChainableServerConnection connection + = new ChainableServerConnection(server, requestedBy, open, success, System.currentTimeMillis(), notes); + + connections.add(connection); + + if( connections.size() > limit) { + ChainableServerConnection firstConnection + = connections.stream().sorted(Comparator.comparing(ChainableServerConnection::getCurrentTimeMillis)) + .findFirst().get(); + connections.remove(firstConnection); + } + return connection; + } + + public int getLimit() { + return limit; + } + + public void setLimit(int limit) { + this.limit = limit; + } + + public List getConnections() { + return this.connections; + } +} diff --git a/src/main/java/org/qortal/crosschain/ElectrumX.java b/src/main/java/org/qortal/crosschain/ElectrumX.java index 27e140e2..ad211c03 100644 --- a/src/main/java/org/qortal/crosschain/ElectrumX.java +++ b/src/main/java/org/qortal/crosschain/ElectrumX.java @@ -8,6 +8,7 @@ import org.apache.logging.log4j.Logger; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.JSONValue; +import org.qortal.api.resource.CrossChainUtils; import org.qortal.crypto.Crypto; import org.qortal.crypto.TrustlessSSLSocketFactory; import org.qortal.utils.BitTwiddling; @@ -26,6 +27,7 @@ import java.util.regex.Pattern; /** ElectrumX network support for querying Bitcoiny-related info like block headers, transaction outputs, etc. */ public class ElectrumX extends BitcoinyBlockchainProvider { + public static final String NULL_RESPONSE_FROM_ELECTRUM_X_SERVER = "Null response from ElectrumX server"; private static final Logger LOGGER = LogManager.getLogger(ElectrumX.class); private static final Random RANDOM = new Random(); @@ -44,6 +46,10 @@ public class ElectrumX extends BitcoinyBlockchainProvider { private static final int RESPONSE_TIME_READINGS = 5; private static final long MAX_AVG_RESPONSE_TIME = 2000L; // ms + public static final String MINIMUM_VERSION_ERROR = "MINIMUM VERSION ERROR"; + public static final String EXPECTED_GENESIS_ERROR = "EXPECTED GENESIS ERROR"; + + private ChainableServerConnectionRecorder recorder = new ChainableServerConnectionRecorder(100); public static class Server implements ChainableServer { String hostname; @@ -421,7 +427,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider { Server uselessServer = (Server) e.getServer(); LOGGER.trace(() -> String.format("Server %s doesn't support verbose transactions - barring use of that server", uselessServer)); this.uselessServers.add(uselessServer); - this.closeServer(uselessServer); + this.closeServer(uselessServer, this.getClass().getSimpleName(), CrossChainUtils.getNotes(e)); continue; } @@ -495,12 +501,13 @@ public class ElectrumX extends BitcoinyBlockchainProvider { // Update: it turns out that they were just using a different key - "address" instead of "addresses" // The code below can remain in place, just in case a peer returns a missing address in the future if (addresses == null || addresses.isEmpty()) { + final String message = String.format("No output addresses returned for transaction %s", txHash); if (this.currentServer != null) { this.uselessServers.add(this.currentServer); - this.closeServer(this.currentServer); + this.closeServer(this.currentServer, this.getClass().getSimpleName(), message); } LOGGER.info("No output addresses returned for transaction {}", txHash); - throw new ForeignBlockchainException(String.format("No output addresses returned for transaction %s", txHash)); + throw new ForeignBlockchainException(message); } outputs.add(new BitcoinyTransaction.Output(scriptPubKey, value, addresses)); @@ -654,8 +661,9 @@ public class ElectrumX extends BitcoinyBlockchainProvider { if (!this.remainingServers.isEmpty()) { long averageResponseTime = this.currentServer.averageResponseTime(); if (averageResponseTime > MAX_AVG_RESPONSE_TIME) { - LOGGER.info("Slow average response time {}ms from {} - trying another server...", averageResponseTime, this.currentServer.getHostName()); - this.closeServer(); + String message = String.format("Slow average response time %dms from %s - trying another server...", averageResponseTime, this.currentServer.getHostName()); + LOGGER.info(message); + this.closeServer(this.getClass().getSimpleName(), message); break; } } @@ -663,8 +671,9 @@ public class ElectrumX extends BitcoinyBlockchainProvider { if (response != null) return response; + LOGGER.info(NULL_RESPONSE_FROM_ELECTRUM_X_SERVER); // Didn't work, try another server... - this.closeServer(); + this.closeServer(this.getClass().getSimpleName(), NULL_RESPONSE_FROM_ELECTRUM_X_SERVER); } // Failed to perform RPC - maybe lack of servers? @@ -680,56 +689,61 @@ public class ElectrumX extends BitcoinyBlockchainProvider { while (!this.remainingServers.isEmpty()) { ChainableServer server = this.remainingServers.remove(RANDOM.nextInt(this.remainingServers.size())); - LOGGER.trace(() -> String.format("Connecting to %s", server)); - - try { - SocketAddress endpoint = new InetSocketAddress(server.getHostName(), server.getPort()); - int timeout = 5000; // ms - - this.socket = new Socket(); - this.socket.connect(endpoint, timeout); - this.socket.setTcpNoDelay(true); - - if (server.getConnectionType() == Server.ConnectionType.SSL) { - SSLSocketFactory factory = TrustlessSSLSocketFactory.getSocketFactory(); - this.socket = factory.createSocket(this.socket, server.getHostName(), server.getPort(), true); - } - - this.scanner = new Scanner(this.socket.getInputStream()); - this.scanner.useDelimiter("\n"); - - // All connections need to start with a version negotiation - this.connectedRpc("server.version"); - - // Check connection is suitable by asking for server features, including genesis block hash - JSONObject featuresJson = (JSONObject) this.connectedRpc("server.features"); - - if (featuresJson == null || Double.valueOf((String) featuresJson.get("protocol_min")) < MIN_PROTOCOL_VERSION) - continue; - - if (this.expectedGenesisHash != null && !((String) featuresJson.get("genesis_hash")).equals(this.expectedGenesisHash)) - continue; - - // Ask for more servers - Set moreServers = serverPeersSubscribe(); - // Discard duplicate servers we already know - moreServers.removeAll(this.servers); - // Add to both lists - this.remainingServers.addAll(moreServers); - this.servers.addAll(moreServers); - - LOGGER.debug(() -> String.format("Connected to %s", server)); - this.currentServer = server; - return true; - } catch (IOException | ForeignBlockchainException | ClassCastException | NullPointerException e) { - // Didn't work, try another server... - closeServer(); - } + Optional chainableServerConnection = makeConnection(server, this.getClass().getSimpleName()); + if(chainableServerConnection.isPresent() && chainableServerConnection.get().isSuccess() ) return true; } return false; } + private Optional makeConnection(ChainableServer server, String requestedBy) { + LOGGER.info(() -> String.format("Connecting to %s", server)); + + try { + SocketAddress endpoint = new InetSocketAddress(server.getHostName(), server.getPort()); + int timeout = 5000; // ms + + this.socket = new Socket(); + this.socket.connect(endpoint, timeout); + this.socket.setTcpNoDelay(true); + + if (server.getConnectionType() == Server.ConnectionType.SSL) { + SSLSocketFactory factory = TrustlessSSLSocketFactory.getSocketFactory(); + this.socket = factory.createSocket(this.socket, server.getHostName(), server.getPort(), true); + } + + this.scanner = new Scanner(this.socket.getInputStream()); + this.scanner.useDelimiter("\n"); + + // All connections need to start with a version negotiation + this.connectedRpc("server.version"); + + // Check connection is suitable by asking for server features, including genesis block hash + JSONObject featuresJson = (JSONObject) this.connectedRpc("server.features"); + + if (featuresJson == null || Double.valueOf((String) featuresJson.get("protocol_min")) < MIN_PROTOCOL_VERSION) + return Optional.of( recorder.recordConnection(server, requestedBy, true, false, MINIMUM_VERSION_ERROR) ); + + if (this.expectedGenesisHash != null && !((String) featuresJson.get("genesis_hash")).equals(this.expectedGenesisHash)) + return Optional.of( recorder.recordConnection(server, requestedBy, true, false, EXPECTED_GENESIS_ERROR) ); + + // Ask for more servers + Set moreServers = serverPeersSubscribe(); + // Discard duplicate servers we already know + moreServers.removeAll(this.servers); + // Add to both lists + this.remainingServers.addAll(moreServers); + this.servers.addAll(moreServers); + + LOGGER.info(() -> String.format("Connected to %s", server)); + this.currentServer = server; + return Optional.of( this.recorder.recordConnection( server, requestedBy, true, true, EMPTY) ); + } catch (IOException | ForeignBlockchainException | ClassCastException | NullPointerException e) { + // Didn't work, try another server... + return Optional.of( this.recorder.recordConnection( server, requestedBy, true, false, CrossChainUtils.getNotes(e))); + } + } + /** * Perform RPC using currently connected server. *

@@ -846,12 +860,19 @@ public class ElectrumX extends BitcoinyBlockchainProvider { /** * Closes connection to server if it is currently connected server. + * * @param server + * @param notes */ - private void closeServer(ChainableServer server) { + private Optional closeServer(ChainableServer server, String requestedBy, String notes) { + + ChainableServerConnection chainableServerConnection; + synchronized (this.serverLock) { if (this.currentServer == null || !this.currentServer.equals(server)) - return; + return Optional.empty(); + + chainableServerConnection = this.recorder.recordConnection(server, requestedBy, false, true, notes); if (this.socket != null && !this.socket.isClosed()) try { @@ -864,12 +885,14 @@ public class ElectrumX extends BitcoinyBlockchainProvider { this.scanner = null; this.currentServer = null; } + + return Optional.of( chainableServerConnection ); } /** Closes connection to currently connected server (if any). */ - private void closeServer() { + private Optional closeServer(String requestedBy, String notes) { synchronized (this.serverLock) { - this.closeServer(this.currentServer); + return this.closeServer(this.currentServer, requestedBy, notes); } } @@ -893,4 +916,32 @@ public class ElectrumX extends BitcoinyBlockchainProvider { public ChainableServer getCurrentServer() { return currentServer; } + + @Override + public boolean addServer(ChainableServer server) { + return this.servers.add(server); + } + + @Override + public boolean removeServer(ChainableServer server) { + boolean removedServer = this.servers.remove(server); + boolean removedRemaining = this.remainingServers.remove(server); + + return removedServer || removedRemaining; + } + + @Override + public Optional setCurrentServer(ChainableServer server, String requestedBy) { + return this.makeConnection(server, requestedBy); + } + + @Override + public List getServerConnections() { + return this.recorder.getConnections(); + } + + @Override + public ChainableServer getServer(String hostName, ChainableServer.ConnectionType type, int port) { + return new ElectrumX.Server(hostName, type, port); + } } diff --git a/src/main/java/org/qortal/crosschain/PirateLightClient.java b/src/main/java/org/qortal/crosschain/PirateLightClient.java index ae7c3cc1..4bb94ecb 100644 --- a/src/main/java/org/qortal/crosschain/PirateLightClient.java +++ b/src/main/java/org/qortal/crosschain/PirateLightClient.java @@ -14,6 +14,7 @@ import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import org.json.simple.parser.ParseException; +import org.qortal.api.resource.CrossChainUtils; import org.qortal.settings.Settings; import org.qortal.transform.TransformationException; @@ -127,6 +128,8 @@ public class PirateLightClient extends BitcoinyBlockchainProvider { } }); + private ChainableServerConnectionRecorder recorder = new ChainableServerConnectionRecorder(100); + // Constructors public PirateLightClient(String netId, String genesisHash, Collection initialServerList, Map defaultPorts) { @@ -443,12 +446,13 @@ public class PirateLightClient extends BitcoinyBlockchainProvider { // Update: it turns out that they were just using a different key - "address" instead of "addresses" // The code below can remain in place, just in case a peer returns a missing address in the future if (addresses == null || addresses.isEmpty()) { + final String message = String.format("No output addresses returned for transaction %s", txHash); if (this.currentServer != null) { this.uselessServers.add(this.currentServer); - this.closeServer(this.currentServer); + this.closeServer(this.currentServer, message, this.getClass().getSimpleName()); } - LOGGER.info("No output addresses returned for transaction {}", txHash); - throw new ForeignBlockchainException(String.format("No output addresses returned for transaction %s", txHash)); + LOGGER.info(message); + throw new ForeignBlockchainException(message); } outputs.add(new BitcoinyTransaction.Output(scriptPubKey, value, addresses)); @@ -557,6 +561,42 @@ public class PirateLightClient extends BitcoinyBlockchainProvider { @Override public ChainableServer getCurrentServer() { return this.currentServer; } + @Override + public boolean addServer(ChainableServer server) { + return this.servers.add(server); + } + + @Override + public boolean removeServer(ChainableServer server) { + boolean removedServer = this.servers.remove(server); + boolean removedRemaining = this.remainingServers.remove(server); + + return removedServer || removedRemaining; + } + + @Override + public Optional setCurrentServer(ChainableServer server, String requestedBy) throws ForeignBlockchainException { + + closeServer( requestedBy, "Connecting to different server by request." ); + Optional connection = makeConnection(server, requestedBy); + + if( !connection.isPresent() || !connection.get().isSuccess() ) { + haveConnection(); + } + + return connection; + } + + @Override + public List getServerConnections() { + return this.recorder.getConnections(); + } + + @Override + public ChainableServer getServer(String hostName, ChainableServer.ConnectionType type, int port) { + return new PirateLightClient.Server(hostName, type, port); + } + // Class-private utility methods @@ -576,8 +616,9 @@ public class PirateLightClient extends BitcoinyBlockchainProvider { if (!this.remainingServers.isEmpty()) { long averageResponseTime = this.currentServer.averageResponseTime(); if (averageResponseTime > MAX_AVG_RESPONSE_TIME) { - LOGGER.info("Slow average response time {}ms from {} - trying another server...", averageResponseTime, this.currentServer.getHostName()); - this.closeServer(); + String message = String.format("Slow average response time %dms from %s - trying another server...", averageResponseTime, this.currentServer.getHostName()); + LOGGER.info(message); + this.closeServer(this.getClass().getSimpleName(), message); continue; } } @@ -601,18 +642,27 @@ public class PirateLightClient extends BitcoinyBlockchainProvider { while (!this.remainingServers.isEmpty()) { ChainableServer server = this.remainingServers.remove(RANDOM.nextInt(this.remainingServers.size())); - LOGGER.trace(() -> String.format("Connecting to %s", server)); - try { - this.channel = ManagedChannelBuilder.forAddress(server.getHostName(), server.getPort()).build(); + Optional chainableServerConnection = makeConnection(server, this.getClass().getSimpleName()); + if( chainableServerConnection.isPresent() && chainableServerConnection.get().isSuccess() ) return true; + } - CompactTxStreamerGrpc.CompactTxStreamerBlockingStub stub = CompactTxStreamerGrpc.newBlockingStub(this.channel); - LightdInfo lightdInfo = stub.getLightdInfo(Empty.newBuilder().build()); + return false; + } - if (lightdInfo == null || lightdInfo.getBlockHeight() <= 0) - continue; + private Optional makeConnection(ChainableServer server, String requestedBy) { + LOGGER.info(() -> String.format("Connecting to %s", server)); - // TODO: find a way to verify that the server is using the expected chain + try { + this.channel = ManagedChannelBuilder.forAddress(server.getHostName(), server.getPort()).build(); + + CompactTxStreamerGrpc.CompactTxStreamerBlockingStub stub = CompactTxStreamerGrpc.newBlockingStub(this.channel); + LightdInfo lightdInfo = stub.getLightdInfo(Empty.newBuilder().build()); + + if (lightdInfo == null || lightdInfo.getBlockHeight() <= 0) + return Optional.of( this.recorder.recordConnection(server, requestedBy,true, false, "lightd info issues") ); + + // TODO: find a way to verify that the server is using the expected chain // if (featuresJson == null || Double.valueOf((String) featuresJson.get("protocol_min")) < MIN_PROTOCOL_VERSION) // continue; @@ -620,28 +670,31 @@ public class PirateLightClient extends BitcoinyBlockchainProvider { // if (this.expectedGenesisHash != null && !((String) featuresJson.get("genesis_hash")).equals(this.expectedGenesisHash)) // continue; - LOGGER.debug(() -> String.format("Connected to %s", server)); - this.currentServer = server; - return true; - } catch (Exception e) { - // Didn't work, try another server... - closeServer(); - } + LOGGER.info(() -> String.format("Connected to %s", server)); + this.currentServer = server; + return Optional.of( this.recorder.recordConnection(server, requestedBy,true, true, EMPTY) ); + } catch (Exception e) { + // Didn't work, try another server... + return Optional.of( this.recorder.recordConnection( server, requestedBy, true, false, CrossChainUtils.getNotes(e))); } - - return false; } - /** * Closes connection to server if it is currently connected server. + * * @param server + * @param requestedBy */ - private void closeServer(ChainableServer server) { + private Optional closeServer(ChainableServer server, String notes, String requestedBy) { + + final ChainableServerConnection connection; + synchronized (this.serverLock) { if (this.currentServer == null || !this.currentServer.equals(server) || this.channel == null) { - return; + return Optional.empty(); } + connection = this.recorder.recordConnection(server, requestedBy, false, true, notes); + // Close the gRPC managed-channel if not shut down already. if (!this.channel.isShutdown()) { try { @@ -669,12 +722,14 @@ public class PirateLightClient extends BitcoinyBlockchainProvider { this.channel = null; this.currentServer = null; } + + return Optional.of( connection ); } /** Closes connection to currently connected server (if any). */ - private void closeServer() { + private Optional closeServer(String requestedBy, String notes) { synchronized (this.serverLock) { - this.closeServer(this.currentServer); + return this.closeServer(this.currentServer, notes, requestedBy); } } diff --git a/src/main/java/org/qortal/crosschain/ServerConnectionInfo.java b/src/main/java/org/qortal/crosschain/ServerConnectionInfo.java new file mode 100644 index 00000000..c4829399 --- /dev/null +++ b/src/main/java/org/qortal/crosschain/ServerConnectionInfo.java @@ -0,0 +1,82 @@ +package org.qortal.crosschain; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import java.util.Objects; + +@XmlAccessorType(XmlAccessType.FIELD) +public class ServerConnectionInfo { + + private ServerInfo serverInfo; + + private String requestedBy; + + private boolean open; + + private boolean success; + + private long timeInMillis; + + private String notes; + + public ServerConnectionInfo() { + } + + public ServerConnectionInfo(ServerInfo serverInfo, String requestedBy, boolean open, boolean success, long timeInMillis, String notes) { + this.serverInfo = serverInfo; + this.requestedBy = requestedBy; + this.open = open; + this.success = success; + this.timeInMillis = timeInMillis; + this.notes = notes; + } + + public ServerInfo getServerInfo() { + return serverInfo; + } + + public String getRequestedBy() { + return requestedBy; + } + + public boolean isOpen() { + return open; + } + + public boolean isSuccess() { + return success; + } + + public long getTimeInMillis() { + return timeInMillis; + } + + public String getNotes() { + return notes; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ServerConnectionInfo that = (ServerConnectionInfo) o; + return timeInMillis == that.timeInMillis && Objects.equals(serverInfo, that.serverInfo); + } + + @Override + public int hashCode() { + return Objects.hash(serverInfo, timeInMillis); + } + + @Override + public String toString() { + return "ServerConnectionInfo{" + + "serverInfo=" + serverInfo + + ", requestedBy='" + requestedBy + '\'' + + ", open=" + open + + ", success=" + success + + ", timeInMillis=" + timeInMillis + + ", notes='" + notes + '\'' + + '}'; + } +}