From 21c45535be484b64c7a1f844b3d029b16ff01ebf Mon Sep 17 00:00:00 2001 From: kennycud Date: Wed, 17 Jan 2024 18:19:09 -0800 Subject: [PATCH 1/2] Enabled fee updates for foreign coin trade transactions. --- .../resource/CrossChainBitcoinResource.java | 114 +++++++++++++++++ .../resource/CrossChainDigibyteResource.java | 115 +++++++++++++++++- .../resource/CrossChainDogecoinResource.java | 115 +++++++++++++++++- .../resource/CrossChainLitecoinResource.java | 113 +++++++++++++++++ .../CrossChainPirateChainResource.java | 113 +++++++++++++++++ .../resource/CrossChainRavencoinResource.java | 113 +++++++++++++++++ .../qortal/api/resource/CrossChainUtils.java | 43 ++++++- .../java/org/qortal/crosschain/Bitcoin.java | 30 +++-- .../java/org/qortal/crosschain/Bitcoiny.java | 15 ++- .../java/org/qortal/crosschain/Digibyte.java | 30 +++-- .../java/org/qortal/crosschain/Dogecoin.java | 30 +++-- .../java/org/qortal/crosschain/Litecoin.java | 31 +++-- .../org/qortal/crosschain/PirateChain.java | 31 +++-- .../java/org/qortal/crosschain/Ravencoin.java | 30 +++-- 14 files changed, 868 insertions(+), 55 deletions(-) diff --git a/src/main/java/org/qortal/api/resource/CrossChainBitcoinResource.java b/src/main/java/org/qortal/api/resource/CrossChainBitcoinResource.java index 6f3b9f506..ff4aee023 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainBitcoinResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainBitcoinResource.java @@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; +import org.bitcoinj.core.Coin; import org.bitcoinj.core.Transaction; import org.qortal.api.ApiError; import org.qortal.api.ApiErrors; @@ -265,4 +266,117 @@ public ServerConfigurationInfo getServerConfiguration() { return CrossChainUtils.buildServerConfigurationInfo(Bitcoin.getInstance()); } + + @GET + @Path("/feekb") + @Operation( + summary = "Returns Bitcoin fee per Kb.", + description = "Returns Bitcoin fee per Kb.", + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "number" + ) + ) + ) + } + ) + public String getBitcoinFeePerKb() { + Bitcoin bitcoin = Bitcoin.getInstance(); + + return String.valueOf(bitcoin.getFeePerKb().value); + } + + @POST + @Path("/updatefeekb") + @Operation( + summary = "Sets Bitcoin fee per Kb.", + description = "Sets Bitcoin fee per Kb.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "number", + description = "the fee per Kb", + example = "100" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA}) + public String setBitcoinFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) { + Security.checkApiCallAllowed(request); + + Bitcoin bitcoin = Bitcoin.getInstance(); + + try { + return CrossChainUtils.setFeePerKb(bitcoin, fee); + } catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + } + } + + @GET + @Path("/feeceiling") + @Operation( + summary = "Returns Bitcoin fee per Kb.", + description = "Returns Bitcoin fee per Kb.", + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "number" + ) + ) + ) + } + ) + public String getBitcoinFeeCeiling() { + Bitcoin bitcoin = Bitcoin.getInstance(); + + return String.valueOf(bitcoin.getFeeCeiling()); + } + + @POST + @Path("/updatefeeceiling") + @Operation( + summary = "Sets Bitcoin fee ceiling.", + description = "Sets Bitcoin fee ceiling.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "number", + description = "the fee", + example = "100" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA}) + public String setBitcoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) { + Security.checkApiCallAllowed(request); + + Bitcoin bitcoin = Bitcoin.getInstance(); + + try { + return CrossChainUtils.setFeeCeiling(bitcoin, fee); + } + catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + } + } } diff --git a/src/main/java/org/qortal/api/resource/CrossChainDigibyteResource.java b/src/main/java/org/qortal/api/resource/CrossChainDigibyteResource.java index 898754d6b..d78a4ed95 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainDigibyteResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainDigibyteResource.java @@ -265,4 +265,117 @@ public ServerConfigurationInfo getServerConfiguration() { return CrossChainUtils.buildServerConfigurationInfo(Digibyte.getInstance()); } -} + + @GET + @Path("/feekb") + @Operation( + summary = "Returns Digibyte fee per Kb.", + description = "Returns Digibyte fee per Kb.", + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "number" + ) + ) + ) + } + ) + public String getDigibyteFeePerKb() { + Digibyte digibyte = Digibyte.getInstance(); + + return String.valueOf(digibyte.getFeePerKb().value); + } + + @POST + @Path("/updatefeekb") + @Operation( + summary = "Sets Digibyte fee per Kb.", + description = "Sets Digibyte fee per Kb.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "number", + description = "the fee per Kb", + example = "100" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA}) + public String setDigibyteFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) { + Security.checkApiCallAllowed(request); + + Digibyte digibyte = Digibyte.getInstance(); + + try { + return CrossChainUtils.setFeePerKb(digibyte, fee); + } catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + } + } + + @GET + @Path("/feeceiling") + @Operation( + summary = "Returns Digibyte fee per Kb.", + description = "Returns Digibyte fee per Kb.", + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "number" + ) + ) + ) + } + ) + public String getDigibyteFeeCeiling() { + Digibyte digibyte = Digibyte.getInstance(); + + return String.valueOf(digibyte.getFeeCeiling()); + } + + @POST + @Path("/updatefeeceiling") + @Operation( + summary = "Sets Digibyte fee ceiling.", + description = "Sets Digibyte fee ceiling.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "number", + description = "the fee", + example = "100" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA}) + public String setDigibyteFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) { + Security.checkApiCallAllowed(request); + + Digibyte digibyte = Digibyte.getInstance(); + + try { + return CrossChainUtils.setFeeCeiling(digibyte, fee); + } + catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/api/resource/CrossChainDogecoinResource.java b/src/main/java/org/qortal/api/resource/CrossChainDogecoinResource.java index 1608c90ec..8575a28d5 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainDogecoinResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainDogecoinResource.java @@ -265,4 +265,117 @@ public ServerConfigurationInfo getServerConfiguration() { return CrossChainUtils.buildServerConfigurationInfo(Dogecoin.getInstance()); } -} + + @GET + @Path("/feekb") + @Operation( + summary = "Returns Dogecoin fee per Kb.", + description = "Returns Dogecoin fee per Kb.", + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "number" + ) + ) + ) + } + ) + public String getDogecoinFeePerKb() { + Dogecoin dogecoin = Dogecoin.getInstance(); + + return String.valueOf(dogecoin.getFeePerKb().value); + } + + @POST + @Path("/updatefeekb") + @Operation( + summary = "Sets Dogecoin fee per Kb.", + description = "Sets Dogecoin fee per Kb.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "number", + description = "the fee per Kb", + example = "100" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA}) + public String setDogecoinFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) { + Security.checkApiCallAllowed(request); + + Dogecoin dogecoin = Dogecoin.getInstance(); + + try { + return CrossChainUtils.setFeePerKb(dogecoin, fee); + } catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + } + } + + @GET + @Path("/feeceiling") + @Operation( + summary = "Returns Dogecoin fee per Kb.", + description = "Returns Dogecoin fee per Kb.", + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "number" + ) + ) + ) + } + ) + public String getDogecoinFeeCeiling() { + Dogecoin dogecoin = Dogecoin.getInstance(); + + return String.valueOf(dogecoin.getFeeCeiling()); + } + + @POST + @Path("/updatefeeceiling") + @Operation( + summary = "Sets Dogecoin fee ceiling.", + description = "Sets Dogecoin fee ceiling.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "number", + description = "the fee", + example = "100" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA}) + public String setDogecoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) { + Security.checkApiCallAllowed(request); + + Dogecoin dogecoin = Dogecoin.getInstance(); + + try { + return CrossChainUtils.setFeeCeiling(dogecoin, fee); + } + catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/api/resource/CrossChainLitecoinResource.java b/src/main/java/org/qortal/api/resource/CrossChainLitecoinResource.java index 3296c3ca8..7667eea13 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainLitecoinResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainLitecoinResource.java @@ -304,4 +304,117 @@ public String repairOldWallet(@HeaderParam(Security.API_KEY_HEADER) String apiKe throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE); } } + + @GET + @Path("/feekb") + @Operation( + summary = "Returns Litecoin fee per Kb.", + description = "Returns Litecoin fee per Kb.", + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "number" + ) + ) + ) + } + ) + public String getLitecoinFeePerKb() { + Litecoin litecoin = Litecoin.getInstance(); + + return String.valueOf(litecoin.getFeePerKb().value); + } + + @POST + @Path("/updatefeekb") + @Operation( + summary = "Sets Litecoin fee per Kb.", + description = "Sets Litecoin fee per Kb.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "number", + description = "the fee per Kb", + example = "100" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA}) + public String setLitecoinFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) { + Security.checkApiCallAllowed(request); + + Litecoin litecoin = Litecoin.getInstance(); + + try { + return CrossChainUtils.setFeePerKb(litecoin, fee); + } catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + } + } + + @GET + @Path("/feeceiling") + @Operation( + summary = "Returns Litecoin fee per Kb.", + description = "Returns Litecoin fee per Kb.", + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "number" + ) + ) + ) + } + ) + public String getLitecoinFeeCeiling() { + Litecoin litecoin = Litecoin.getInstance(); + + return String.valueOf(litecoin.getFeeCeiling()); + } + + @POST + @Path("/updatefeeceiling") + @Operation( + summary = "Sets Litecoin fee ceiling.", + description = "Sets Litecoin fee ceiling.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "number", + description = "the fee", + example = "100" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA}) + public String setLitecoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) { + Security.checkApiCallAllowed(request); + + Litecoin litecoin = Litecoin.getInstance(); + + try { + return CrossChainUtils.setFeeCeiling(litecoin, fee); + } + catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + } + } } \ No newline at end of file diff --git a/src/main/java/org/qortal/api/resource/CrossChainPirateChainResource.java b/src/main/java/org/qortal/api/resource/CrossChainPirateChainResource.java index 0e29e8926..03ff43b8b 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainPirateChainResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainPirateChainResource.java @@ -351,4 +351,117 @@ public ServerConfigurationInfo getServerConfiguration() { return CrossChainUtils.buildServerConfigurationInfo(PirateChain.getInstance()); } + + @GET + @Path("/feekb") + @Operation( + summary = "Returns PirateChain fee per Kb.", + description = "Returns PirateChain fee per Kb.", + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "number" + ) + ) + ) + } + ) + public String getPirateChainFeePerKb() { + PirateChain pirateChain = PirateChain.getInstance(); + + return String.valueOf(pirateChain.getFeePerKb().value); + } + + @POST + @Path("/updatefeekb") + @Operation( + summary = "Sets PirateChain fee per Kb.", + description = "Sets PirateChain fee per Kb.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "number", + description = "the fee per Kb", + example = "100" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA}) + public String setPirateChainFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) { + Security.checkApiCallAllowed(request); + + PirateChain pirateChain = PirateChain.getInstance(); + + try { + return CrossChainUtils.setFeePerKb(pirateChain, fee); + } catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + } + } + + @GET + @Path("/feeceiling") + @Operation( + summary = "Returns PirateChain fee per Kb.", + description = "Returns PirateChain fee per Kb.", + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "number" + ) + ) + ) + } + ) + public String getPirateChainFeeCeiling() { + PirateChain pirateChain = PirateChain.getInstance(); + + return String.valueOf(pirateChain.getFeeCeiling()); + } + + @POST + @Path("/updatefeeceiling") + @Operation( + summary = "Sets PirateChain fee ceiling.", + description = "Sets PirateChain fee ceiling.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "number", + description = "the fee", + example = "100" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA}) + public String setPirateChainFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) { + Security.checkApiCallAllowed(request); + + PirateChain pirateChain = PirateChain.getInstance(); + + try { + return CrossChainUtils.setFeeCeiling(pirateChain, fee); + } + catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + } + } } diff --git a/src/main/java/org/qortal/api/resource/CrossChainRavencoinResource.java b/src/main/java/org/qortal/api/resource/CrossChainRavencoinResource.java index 9b702b25c..ce5cd6688 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainRavencoinResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainRavencoinResource.java @@ -265,4 +265,117 @@ public ServerConfigurationInfo getServerConfiguration() { return CrossChainUtils.buildServerConfigurationInfo(Ravencoin.getInstance()); } + + @GET + @Path("/feekb") + @Operation( + summary = "Returns Ravencoin fee per Kb.", + description = "Returns Ravencoin fee per Kb.", + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "number" + ) + ) + ) + } + ) + public String getRavencoinFeePerKb() { + Ravencoin ravencoin = Ravencoin.getInstance(); + + return String.valueOf(ravencoin.getFeePerKb().value); + } + + @POST + @Path("/updatefeekb") + @Operation( + summary = "Sets Ravencoin fee per Kb.", + description = "Sets Ravencoin fee per Kb.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "number", + description = "the fee per Kb", + example = "100" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA}) + public String setRavencoinFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) { + Security.checkApiCallAllowed(request); + + Ravencoin ravencoin = Ravencoin.getInstance(); + + try { + return CrossChainUtils.setFeePerKb(ravencoin, fee); + } catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + } + } + + @GET + @Path("/feeceiling") + @Operation( + summary = "Returns Ravencoin fee per Kb.", + description = "Returns Ravencoin fee per Kb.", + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "number" + ) + ) + ) + } + ) + public String getRavencoinFeeCeiling() { + Ravencoin ravencoin = Ravencoin.getInstance(); + + return String.valueOf(ravencoin.getFeeCeiling()); + } + + @POST + @Path("/updatefeeceiling") + @Operation( + summary = "Sets Ravencoin fee ceiling.", + description = "Sets Ravencoin fee ceiling.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "number", + description = "the fee", + example = "100" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA}) + public String setRavencoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) { + Security.checkApiCallAllowed(request); + + Ravencoin ravencoin = Ravencoin.getInstance(); + + try { + return CrossChainUtils.setFeeCeiling(ravencoin, fee); + } + catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + } + } } diff --git a/src/main/java/org/qortal/api/resource/CrossChainUtils.java b/src/main/java/org/qortal/api/resource/CrossChainUtils.java index d45ba2571..6e631b7a5 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainUtils.java +++ b/src/main/java/org/qortal/api/resource/CrossChainUtils.java @@ -2,6 +2,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.bitcoinj.core.Coin; import org.qortal.crosschain.*; import java.util.ArrayList; @@ -44,4 +45,44 @@ public static List buildInfos(Collection servers, C return infos; } -} + + /** + * Set Fee Per Kb + * + * @param bitcoiny the blockchain support + * @param fee the fee in satoshis + * + * @return the fee if valid + * + * @throws IllegalArgumentException if invalid + */ + public static String setFeePerKb(Bitcoiny bitcoiny, String fee) throws IllegalArgumentException { + + long satoshis = Long.parseLong(fee); + if( satoshis < 0 ) throw new IllegalArgumentException("can't set fee to negative number"); + + bitcoiny.setFeePerKb(Coin.valueOf(satoshis) ); + + return String.valueOf(bitcoiny.getFeePerKb().value); + } + + /** + * Set Fee Ceiling + * + * @param bitcoiny the blockchain support + * @param fee the fee in satoshis + * + * @return the fee if valid + * + * @throws IllegalArgumentException if invalid + */ + public static String setFeeCeiling(Bitcoiny bitcoiny, String fee) throws IllegalArgumentException{ + + long satoshis = Long.parseLong(fee); + if( satoshis < 0 ) throw new IllegalArgumentException("can't set fee to negative number"); + + bitcoiny.setFeeCeiling( Long.parseLong(fee)); + + return String.valueOf(bitcoiny.getFeeCeiling()); + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/crosschain/Bitcoin.java b/src/main/java/org/qortal/crosschain/Bitcoin.java index b2a016e74..e11dde0bd 100644 --- a/src/main/java/org/qortal/crosschain/Bitcoin.java +++ b/src/main/java/org/qortal/crosschain/Bitcoin.java @@ -22,8 +22,6 @@ public class Bitcoin extends Bitcoiny { private static final long MINIMUM_ORDER_AMOUNT = 100000; // 0.001 BTC minimum order, due to high fees // Temporary values until a dynamic fee system is written. - private static final long OLD_FEE_AMOUNT = 4_000L; // Not 5000 so that existing P2SH-B can output 1000, avoiding dust issue, leaving 4000 for fees. - private static final long NEW_FEE_TIMESTAMP = 1598280000000L; // milliseconds since epoch private static final long NEW_FEE_AMOUNT = 6_000L; private static final long NON_MAINNET_FEE = 1000L; // enough for TESTNET3 and should be OK for REGTEST @@ -125,11 +123,7 @@ public String getGenesisHash() { @Override public long getP2shFee(Long timestamp) { - // TODO: This will need to be replaced with something better in the near future! - if (timestamp != null && timestamp < NEW_FEE_TIMESTAMP) - return OLD_FEE_AMOUNT; - - return NEW_FEE_AMOUNT; + return this.getFeeCeiling(); } }, TEST3 { @@ -186,6 +180,16 @@ public long getP2shFee(Long timestamp) { } }; + private long feeCeiling = NEW_FEE_AMOUNT; + + public long getFeeCeiling() { + return feeCeiling; + } + + public void setFeeCeiling(long feeCeiling) { + this.feeCeiling = feeCeiling; + } + public abstract NetworkParameters getParams(); public abstract Collection getServers(); public abstract String getGenesisHash(); @@ -199,7 +203,7 @@ public long getP2shFee(Long timestamp) { // Constructors and instance private Bitcoin(BitcoinNet bitcoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) { - super(blockchain, bitcoinjContext, currencyCode); + super(blockchain, bitcoinjContext, currencyCode, bitcoinjContext.getFeePerKb()); this.bitcoinNet = bitcoinNet; LOGGER.info(() -> String.format("Starting Bitcoin support using %s", this.bitcoinNet.name())); @@ -244,6 +248,16 @@ public long getP2shFee(Long timestamp) throws ForeignBlockchainException { return this.bitcoinNet.getP2shFee(timestamp); } + @Override + public long getFeeCeiling() { + return this.bitcoinNet.getFeeCeiling(); + } + + @Override + public void setFeeCeiling(long fee) { + + this.bitcoinNet.setFeeCeiling( fee ); + } /** * Returns bitcoinj transaction sending amount to recipient using 20 sat/byte fee. * diff --git a/src/main/java/org/qortal/crosschain/Bitcoiny.java b/src/main/java/org/qortal/crosschain/Bitcoiny.java index b7614d1e8..1ae70fe96 100644 --- a/src/main/java/org/qortal/crosschain/Bitcoiny.java +++ b/src/main/java/org/qortal/crosschain/Bitcoiny.java @@ -53,12 +53,15 @@ public abstract class Bitcoiny implements ForeignBlockchain { /** Byte offset into raw block headers to block timestamp. */ private static final int TIMESTAMP_OFFSET = 4 + 32 + 32; + protected Coin feePerKb; + // Constructors and instance - protected Bitcoiny(BitcoinyBlockchainProvider blockchainProvider, Context bitcoinjContext, String currencyCode) { + protected Bitcoiny(BitcoinyBlockchainProvider blockchainProvider, Context bitcoinjContext, String currencyCode, Coin feePerKb) { this.blockchainProvider = blockchainProvider; this.bitcoinjContext = bitcoinjContext; this.currencyCode = currencyCode; + this.feePerKb = feePerKb; this.params = this.bitcoinjContext.getParams(); } @@ -167,7 +170,11 @@ public int getBlockchainHeight() throws ForeignBlockchainException { /** Returns fee per transaction KB. To be overridden for testnet/regtest. */ public Coin getFeePerKb() { - return this.bitcoinjContext.getFeePerKb(); + return this.feePerKb; + } + + public void setFeePerKb(Coin feePerKb) { + this.feePerKb = feePerKb; } /** Returns minimum order size in sats. To be overridden for coins that need to restrict order size. */ @@ -757,6 +764,10 @@ public String getUnusedReceiveAddress(String key58) throws ForeignBlockchainExce } while (true); } + public abstract long getFeeCeiling(); + + public abstract void setFeeCeiling(long fee); + // UTXOProvider support static class WalletAwareUTXOProvider implements UTXOProvider { diff --git a/src/main/java/org/qortal/crosschain/Digibyte.java b/src/main/java/org/qortal/crosschain/Digibyte.java index bda6c18df..e8e8d7d3f 100644 --- a/src/main/java/org/qortal/crosschain/Digibyte.java +++ b/src/main/java/org/qortal/crosschain/Digibyte.java @@ -63,8 +63,7 @@ public String getGenesisHash() { @Override public long getP2shFee(Long timestamp) { - // TODO: This will need to be replaced with something better in the near future! - return MAINNET_FEE; + return this.getFeeCeiling(); } }, TEST3 { @@ -114,6 +113,16 @@ public long getP2shFee(Long timestamp) { } }; + private long feeCeiling = MAINNET_FEE; + + public long getFeeCeiling() { + return feeCeiling; + } + + public void setFeeCeiling(long feeCeiling) { + this.feeCeiling = feeCeiling; + } + public abstract NetworkParameters getParams(); public abstract Collection getServers(); public abstract String getGenesisHash(); @@ -127,7 +136,7 @@ public long getP2shFee(Long timestamp) { // Constructors and instance private Digibyte(DigibyteNet digibyteNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) { - super(blockchain, bitcoinjContext, currencyCode); + super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB); this.digibyteNet = digibyteNet; LOGGER.info(() -> String.format("Starting Digibyte support using %s", this.digibyteNet.name())); @@ -156,11 +165,6 @@ public static synchronized void resetForTesting() { // Actual useful methods for use by other classes - @Override - public Coin getFeePerKb() { - return DEFAULT_FEE_PER_KB; - } - @Override public long getMinimumOrderAmount() { return MINIMUM_ORDER_AMOUNT; @@ -177,4 +181,14 @@ public long getP2shFee(Long timestamp) throws ForeignBlockchainException { return this.digibyteNet.getP2shFee(timestamp); } + @Override + public long getFeeCeiling() { + return this.digibyteNet.getFeeCeiling(); + } + + @Override + public void setFeeCeiling(long fee) { + + this.digibyteNet.setFeeCeiling( fee ); + } } diff --git a/src/main/java/org/qortal/crosschain/Dogecoin.java b/src/main/java/org/qortal/crosschain/Dogecoin.java index c687ca609..ca96ef7c9 100644 --- a/src/main/java/org/qortal/crosschain/Dogecoin.java +++ b/src/main/java/org/qortal/crosschain/Dogecoin.java @@ -63,8 +63,7 @@ public String getGenesisHash() { @Override public long getP2shFee(Long timestamp) { - // TODO: This will need to be replaced with something better in the near future! - return MAINNET_FEE; + return this.getFeeCeiling(); } }, TEST3 { @@ -114,6 +113,16 @@ public long getP2shFee(Long timestamp) { } }; + private long feeCeiling = MAINNET_FEE; + + public long getFeeCeiling() { + return feeCeiling; + } + + public void setFeeCeiling(long feeCeiling) { + this.feeCeiling = feeCeiling; + } + public abstract NetworkParameters getParams(); public abstract Collection getServers(); public abstract String getGenesisHash(); @@ -127,7 +136,7 @@ public long getP2shFee(Long timestamp) { // Constructors and instance private Dogecoin(DogecoinNet dogecoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) { - super(blockchain, bitcoinjContext, currencyCode); + super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB); this.dogecoinNet = dogecoinNet; LOGGER.info(() -> String.format("Starting Dogecoin support using %s", this.dogecoinNet.name())); @@ -156,11 +165,6 @@ public static synchronized void resetForTesting() { // Actual useful methods for use by other classes - @Override - public Coin getFeePerKb() { - return DEFAULT_FEE_PER_KB; - } - @Override public long getMinimumOrderAmount() { return MINIMUM_ORDER_AMOUNT; @@ -177,4 +181,14 @@ public long getP2shFee(Long timestamp) throws ForeignBlockchainException { return this.dogecoinNet.getP2shFee(timestamp); } + @Override + public long getFeeCeiling() { + return this.dogecoinNet.getFeeCeiling(); + } + + @Override + public void setFeeCeiling(long fee) { + + this.dogecoinNet.setFeeCeiling( fee ); + } } diff --git a/src/main/java/org/qortal/crosschain/Litecoin.java b/src/main/java/org/qortal/crosschain/Litecoin.java index d87cd1a1a..96c13532a 100644 --- a/src/main/java/org/qortal/crosschain/Litecoin.java +++ b/src/main/java/org/qortal/crosschain/Litecoin.java @@ -67,8 +67,7 @@ public String getGenesisHash() { @Override public long getP2shFee(Long timestamp) { - // TODO: This will need to be replaced with something better in the near future! - return MAINNET_FEE; + return this.getFeeCeiling(); } }, TEST3 { @@ -123,6 +122,16 @@ public long getP2shFee(Long timestamp) { } }; + private long feeCeiling = MAINNET_FEE; + + public long getFeeCeiling() { + return feeCeiling; + } + + public void setFeeCeiling(long feeCeiling) { + this.feeCeiling = feeCeiling; + } + public abstract NetworkParameters getParams(); public abstract Collection getServers(); public abstract String getGenesisHash(); @@ -136,7 +145,7 @@ public long getP2shFee(Long timestamp) { // Constructors and instance private Litecoin(LitecoinNet litecoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) { - super(blockchain, bitcoinjContext, currencyCode); + super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB); this.litecoinNet = litecoinNet; LOGGER.info(() -> String.format("Starting Litecoin support using %s", this.litecoinNet.name())); @@ -165,12 +174,6 @@ public static synchronized void resetForTesting() { // Actual useful methods for use by other classes - /** Default Litecoin fee is lower than Bitcoin: only 10sats/byte. */ - @Override - public Coin getFeePerKb() { - return DEFAULT_FEE_PER_KB; - } - @Override public long getMinimumOrderAmount() { return MINIMUM_ORDER_AMOUNT; @@ -187,4 +190,14 @@ public long getP2shFee(Long timestamp) throws ForeignBlockchainException { return this.litecoinNet.getP2shFee(timestamp); } + @Override + public long getFeeCeiling() { + return this.litecoinNet.getFeeCeiling(); + } + + @Override + public void setFeeCeiling(long fee) { + + this.litecoinNet.setFeeCeiling( fee ); + } } diff --git a/src/main/java/org/qortal/crosschain/PirateChain.java b/src/main/java/org/qortal/crosschain/PirateChain.java index 3f3cba478..d0f9317d2 100644 --- a/src/main/java/org/qortal/crosschain/PirateChain.java +++ b/src/main/java/org/qortal/crosschain/PirateChain.java @@ -67,8 +67,7 @@ public String getGenesisHash() { @Override public long getP2shFee(Long timestamp) { - // TODO: This will need to be replaced with something better in the near future! - return MAINNET_FEE; + return this.getFeeCeiling(); } }, TEST3 { @@ -118,6 +117,16 @@ public long getP2shFee(Long timestamp) { } }; + private long feeCeiling = MAINNET_FEE; + + public long getFeeCeiling() { + return feeCeiling; + } + + public void setFeeCeiling(long feeCeiling) { + this.feeCeiling = feeCeiling; + } + public abstract NetworkParameters getParams(); public abstract Collection getServers(); public abstract String getGenesisHash(); @@ -131,7 +140,7 @@ public long getP2shFee(Long timestamp) { // Constructors and instance private PirateChain(PirateChainNet pirateChainNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) { - super(blockchain, bitcoinjContext, currencyCode); + super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB); this.pirateChainNet = pirateChainNet; LOGGER.info(() -> String.format("Starting Pirate Chain support using %s", this.pirateChainNet.name())); @@ -160,12 +169,6 @@ public static synchronized void resetForTesting() { // Actual useful methods for use by other classes - /** Default Litecoin fee is lower than Bitcoin: only 10sats/byte. */ - @Override - public Coin getFeePerKb() { - return DEFAULT_FEE_PER_KB; - } - @Override public long getMinimumOrderAmount() { return MINIMUM_ORDER_AMOUNT; @@ -182,6 +185,16 @@ public long getP2shFee(Long timestamp) throws ForeignBlockchainException { return this.pirateChainNet.getP2shFee(timestamp); } + @Override + public long getFeeCeiling() { + return this.pirateChainNet.getFeeCeiling(); + } + + @Override + public void setFeeCeiling(long fee) { + + this.pirateChainNet.setFeeCeiling( fee ); + } /** * Returns confirmed balance, based on passed payment script. *

diff --git a/src/main/java/org/qortal/crosschain/Ravencoin.java b/src/main/java/org/qortal/crosschain/Ravencoin.java index d9301f48e..b4ed1a134 100644 --- a/src/main/java/org/qortal/crosschain/Ravencoin.java +++ b/src/main/java/org/qortal/crosschain/Ravencoin.java @@ -65,8 +65,7 @@ public String getGenesisHash() { @Override public long getP2shFee(Long timestamp) { - // TODO: This will need to be replaced with something better in the near future! - return MAINNET_FEE; + return this.getFeeCeiling(); } }, TEST3 { @@ -116,6 +115,16 @@ public long getP2shFee(Long timestamp) { } }; + private long feeCeiling = MAINNET_FEE; + + public long getFeeCeiling() { + return feeCeiling; + } + + public void setFeeCeiling(long feeCeiling) { + this.feeCeiling = feeCeiling; + } + public abstract NetworkParameters getParams(); public abstract Collection getServers(); public abstract String getGenesisHash(); @@ -129,7 +138,7 @@ public long getP2shFee(Long timestamp) { // Constructors and instance private Ravencoin(RavencoinNet ravencoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) { - super(blockchain, bitcoinjContext, currencyCode); + super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB); this.ravencoinNet = ravencoinNet; LOGGER.info(() -> String.format("Starting Ravencoin support using %s", this.ravencoinNet.name())); @@ -158,11 +167,6 @@ public static synchronized void resetForTesting() { // Actual useful methods for use by other classes - @Override - public Coin getFeePerKb() { - return DEFAULT_FEE_PER_KB; - } - @Override public long getMinimumOrderAmount() { return MINIMUM_ORDER_AMOUNT; @@ -179,4 +183,14 @@ public long getP2shFee(Long timestamp) throws ForeignBlockchainException { return this.ravencoinNet.getP2shFee(timestamp); } + @Override + public long getFeeCeiling() { + return this.ravencoinNet.getFeeCeiling(); + } + + @Override + public void setFeeCeiling(long fee) { + + this.ravencoinNet.setFeeCeiling( fee ); + } } From 3f29116b47b51516315de997957288dfb83462d1 Mon Sep 17 00:00:00 2001 From: kennycud Date: Mon, 12 Feb 2024 06:31:17 -0800 Subject: [PATCH 2/2] Foreign coin trade transaction summaries --- .../api/resource/CrossChainResource.java | 109 ++++++ .../qortal/api/resource/CrossChainUtils.java | 332 ++++++++++++++++++ .../crosschain/AtomicTransactionData.java | 32 ++ .../data/crosschain/TransactionSummary.java | 106 ++++++ 4 files changed, 579 insertions(+) create mode 100644 src/main/java/org/qortal/data/crosschain/AtomicTransactionData.java create mode 100644 src/main/java/org/qortal/data/crosschain/TransactionSummary.java diff --git a/src/main/java/org/qortal/api/resource/CrossChainResource.java b/src/main/java/org/qortal/api/resource/CrossChainResource.java index 1161dc63e..d3919d9bf 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainResource.java @@ -19,11 +19,14 @@ import org.qortal.controller.tradebot.TradeBot; import org.qortal.crosschain.ACCT; import org.qortal.crosschain.AcctMode; +import org.qortal.crosschain.Bitcoiny; +import org.qortal.crosschain.ForeignBlockchainException; import org.qortal.crosschain.SupportedBlockchain; import org.qortal.crypto.Crypto; import org.qortal.data.at.ATData; import org.qortal.data.at.ATStateData; import org.qortal.data.crosschain.CrossChainTradeData; +import org.qortal.data.crosschain.TransactionSummary; import org.qortal.data.transaction.BaseTransactionData; import org.qortal.data.transaction.MessageTransactionData; import org.qortal.data.transaction.TransactionData; @@ -47,6 +50,7 @@ import javax.ws.rs.core.MediaType; import java.util.*; import java.util.function.Supplier; +import java.util.stream.Collectors; @Path("/crosschain") @Tag(name = "Cross-Chain") @@ -497,6 +501,111 @@ public String cancelTrade(@HeaderParam(Security.API_KEY_HEADER) String apiKey, C } } + @POST + @Path("/p2sh") + @Operation( + summary = "Returns P2SH Address", + description = "Get the P2SH address to lock foreign coin in a cross chain trade for QORT", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "string", + description = "the AT address", + example = "AKFnu9yBp7tUAc5HAphhfCxRZTYoeKXgUy" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "address")) + ) + } + ) + @ApiErrors({ApiError.ADDRESS_UNKNOWN, ApiError.INVALID_CRITERIA}) + @SecurityRequirement(name = "apiKey") + public String getForeignP2SH(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String atAddress) { + Security.checkApiCallAllowed(request); + + try (final Repository repository = RepositoryManager.getRepository()) { + ATData atData = repository.getATRepository().fromATAddress(atAddress); + if (atData == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN); + + ACCT acct = SupportedBlockchain.getAcctByCodeHash(atData.getCodeHash()); + + if( acct == null || !(acct.getBlockchain() instanceof Bitcoiny) ) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + + Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain(); + + CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atData); + + Optional p2sh + = CrossChainUtils.getP2ShAddressForAT(atAddress, repository, bitcoiny, crossChainTradeData); + + if(p2sh.isPresent()){ + return p2sh.get(); + } + else{ + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN); + } + } + catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage()); + } + } + + @POST + @Path("/txactivity") + @Operation( + summary = "Returns Foreign Transaction Activity", + description = "Get the activity related to foreign coin trading", + responses = { + @ApiResponse( + content = @Content( + array = @ArraySchema( + schema = @Schema( + implementation = TransactionSummary.class + ) + ) + ) + ) + } + ) + @ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE}) + @SecurityRequirement(name = "apiKey") + public List getForeignTransactionActivity(@HeaderParam(Security.API_KEY_HEADER) String apiKey, @Parameter( + description = "Limit to specific blockchain", + example = "LITECOIN", + schema = @Schema(implementation = SupportedBlockchain.class) + ) @QueryParam("foreignBlockchain") SupportedBlockchain foreignBlockchain) { + Security.checkApiCallAllowed(request); + + if (!(foreignBlockchain.getInstance() instanceof Bitcoiny)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + + Bitcoiny bitcoiny = (Bitcoiny) foreignBlockchain.getInstance() ; + + org.bitcoinj.core.Context.propagate( bitcoiny.getBitcoinjContext() ); + + try (final Repository repository = RepositoryManager.getRepository()) { + + // sort from last lock to first lock + return CrossChainUtils + .getForeignTradeSummaries(foreignBlockchain, repository, bitcoiny).stream() + .sorted(Comparator.comparing(TransactionSummary::getLockingTimestamp).reversed()) + .collect(Collectors.toList()); + } + catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage()); + } + catch (ForeignBlockchainException e) { + throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE, e.getMessage()); + } + } + private ATData fetchAtDataWithChecking(Repository repository, String atAddress) throws DataException { ATData atData = repository.getATRepository().fromATAddress(atAddress); if (atData == null) diff --git a/src/main/java/org/qortal/api/resource/CrossChainUtils.java b/src/main/java/org/qortal/api/resource/CrossChainUtils.java index 6e631b7a5..b07a9d6c4 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainUtils.java +++ b/src/main/java/org/qortal/api/resource/CrossChainUtils.java @@ -2,12 +2,28 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; +import org.bitcoinj.script.Script; +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.repository.DataException; +import org.qortal.repository.Repository; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Optional; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + public class CrossChainUtils { private static final Logger LOGGER = LogManager.getLogger(CrossChainUtils.class); @@ -85,4 +101,320 @@ public static String setFeeCeiling(Bitcoiny bitcoiny, String fee) throws Illega return String.valueOf(bitcoiny.getFeeCeiling()); } + + /** + * Get P2Sh Address For AT + * + * @param atAddress the AT address + * @param repository the repository + * @param bitcoiny the blockchain data + * @param crossChainTradeData the trade data + * + * @return the p2sh address for the trade, if there is one + * + * @throws DataException + */ + public static Optional getP2ShAddressForAT( + String atAddress, + Repository repository, + Bitcoiny bitcoiny, + CrossChainTradeData crossChainTradeData) throws DataException { + + // get the trade bot data for the AT address + Optional tradeBotDataOptional + = repository.getCrossChainRepository() + .getAllTradeBotData().stream() + .filter(data -> data.getAtAddress().equals(atAddress)) + .findFirst(); + + if( tradeBotDataOptional.isEmpty() ) + return Optional.empty(); + + TradeBotData tradeBotData = tradeBotDataOptional.get(); + + // return the p2sh address from the trade bot + return getP2ShFromTradeBot(bitcoiny, crossChainTradeData, tradeBotData); + } + + /** + * Get Foreign Trade Summaries + * + * @param foreignBlockchain the blockchain traded on + * @param repository the repository + * @param bitcoiny data for the blockchain trade on + * @return + * @throws DataException + * @throws ForeignBlockchainException + */ + public static List getForeignTradeSummaries( + SupportedBlockchain foreignBlockchain, + Repository repository, + Bitcoiny bitcoiny) throws DataException, ForeignBlockchainException { + + // get all the AT address for the given blockchain + List atAddresses + = repository.getCrossChainRepository().getAllTradeBotData().stream() + .filter(data -> foreignBlockchain.name().toLowerCase().equals(data.getForeignBlockchain().toLowerCase())) + //.filter( data -> data.getForeignKey().equals( xpriv )) // TODO + .map(data -> data.getAtAddress()) + .collect(Collectors.toList()); + + List summaries = new ArrayList<>( atAddresses.size() * 2 ); + + // for each AT address, gather the data and get foreign trade summary + for( String atAddress: atAddresses) { + + ATData atData = repository.getATRepository().fromATAddress(atAddress); + + CrossChainTradeData crossChainTradeData = foreignBlockchain.getLatestAcct().populateTradeData(repository, atData); + + Optional address = getP2ShAddressForAT(atAddress,repository, bitcoiny, crossChainTradeData); + + if( address.isPresent()){ + summaries.add( getForeignTradeSummary( bitcoiny, address.get(), atAddress ) ); + } + } + + return summaries; + } + + /** + * Get P2Sh From Trade Bot + * + * Get P2Sh address from the trade bot + * + * @param bitcoiny the blockchain for the trade + * @param crossChainTradeData the cross cahin data for the trade + * @param tradeBotData the data from the trade bot + * + * @return the address, original format + */ + private static Optional getP2ShFromTradeBot( + Bitcoiny bitcoiny, + CrossChainTradeData crossChainTradeData, + TradeBotData tradeBotData) { + + // Pirate Chain does not support this + if( SupportedBlockchain.PIRATECHAIN.name().equals(tradeBotData.getForeignBlockchain())) return Optional.empty(); + + // need to get the trade PKH from the trade bot + if( tradeBotData.getTradeForeignPublicKeyHash() == null ) return Optional.empty(); + + // need to get the lock time from the trade bot + if( tradeBotData.getLockTimeA() == null ) return Optional.empty(); + + // need to get the creator PKH from the trade bot + if( crossChainTradeData.creatorForeignPKH == null ) return Optional.empty(); + + // need to get the secret from the trade bot + if( tradeBotData.getHashOfSecret() == null ) return Optional.empty(); + + // if we have the necessary data from the trade bot, + // then build the redeem script necessary to facilitate the trade + byte[] redeemScriptBytes + = BitcoinyHTLC.buildScript( + tradeBotData.getTradeForeignPublicKeyHash(), + tradeBotData.getLockTimeA(), + crossChainTradeData.creatorForeignPKH, + tradeBotData.getHashOfSecret() + ); + + + String p2shAddress = bitcoiny.deriveP2shAddress(redeemScriptBytes); + + return Optional.of(p2shAddress); + } + + /** + * Get Foreign Trade Summary + * + * @param bitcoiny the blockchain the trade occurred on + * @param p2shAddress the p2sh address + * @param atAddress the AT address the p2sh address is derived from + * + * @return the summary + * + * @throws ForeignBlockchainException + */ + public static TransactionSummary getForeignTradeSummary(Bitcoiny bitcoiny, String p2shAddress, String atAddress) + throws ForeignBlockchainException { + Script outputScript = ScriptBuilder.createOutputScript( + Address.fromString(bitcoiny.getNetworkParameters(), p2shAddress)); + + List hashes + = bitcoiny.getAddressTransactions( outputScript.getProgram(), true); + + TransactionSummary summary; + + if(hashes.isEmpty()){ + summary + = new TransactionSummary( + atAddress, + p2shAddress, + "N/A", + "N/A", + 0, + 0, + 0, + 0, + "N/A", + 0, + 0, + 0, + 0); + } + else if( hashes.size() == 1) { + AtomicTransactionData data = buildTransactionData(bitcoiny, hashes.get(0)); + summary = new TransactionSummary( + atAddress, + p2shAddress, + "N/A", + data.hash.txHash, + data.timestamp, + data.totalAmount, + getTotalInput(bitcoiny, data.inputs) - data.totalAmount, + data.size, + "N/A", + 0, + 0, + 0, + 0); + } + // otherwise assuming there is 2 and only 2 hashes + else { + List atomicTransactionDataList = new ArrayList<>(2); + + // hashes -> data + for( TransactionHash hash : hashes){ + atomicTransactionDataList.add(buildTransactionData(bitcoiny,hash)); + } + + // sort the transaction data by time + List sorted + = atomicTransactionDataList.stream() + .sorted((data1, data2) -> data1.timestamp.compareTo(data2.timestamp)) + .collect(Collectors.toList()); + + // build the summary using the first 2 transactions + summary = buildForeignTradeSummary(atAddress, p2shAddress, sorted.get(0), sorted.get(1), bitcoiny); + } + return summary; + } + + /** + * Build Foreign Trade Summary + * + * @param p2shValue the p2sh address, original format + * @param lockingTransaction the transaction lock the foreighn coin + * @param unlockingTransaction the transaction to unlock the foreign coin + * @param bitcoiny the blockchain the trade occurred on + * + * @return + * + * @throws ForeignBlockchainException + */ + private static TransactionSummary buildForeignTradeSummary( + String atAddress, + String p2shValue, + AtomicTransactionData lockingTransaction, + AtomicTransactionData unlockingTransaction, + Bitcoiny bitcoiny) throws ForeignBlockchainException { + + // get sum of the relevant inputs for each transaction + long lockingTotalInput = getTotalInput(bitcoiny, lockingTransaction.inputs); + long unlockingTotalInput = getTotalInput(bitcoiny, unlockingTransaction.inputs); + + // find the address that has output that matches the total input + Optional, Long>> addressValue + = lockingTransaction.valueByAddress.entrySet().stream() + .filter(entry -> entry.getValue() == unlockingTotalInput).findFirst(); + + // set that matching address, if found + String p2shAddress; + if( addressValue.isPresent() && addressValue.get().getKey().size() == 1 ){ + p2shAddress = addressValue.get().getKey().get(0); + } + else { + p2shAddress = "N/A"; + } + + // build summaries with prepared values + // the fees are the total amount subtracted by the total transaction input + return new TransactionSummary( + atAddress, + p2shValue, + p2shAddress, + lockingTransaction.hash.txHash, + lockingTransaction.timestamp, + lockingTransaction.totalAmount, + lockingTotalInput - lockingTransaction.totalAmount, + lockingTransaction.size, + unlockingTransaction.hash.txHash, + unlockingTransaction.timestamp, + unlockingTransaction.totalAmount, + unlockingTotalInput - unlockingTransaction.totalAmount, + unlockingTransaction.size + ); + + } + + /** + * Build Transaction Data + * + * @param bitcoiny the coin for the transaction + * @param hash the hash for the transaction + * + * @return the data for the transaction + * + * @throws ForeignBlockchainException + */ + private static AtomicTransactionData buildTransactionData( Bitcoiny bitcoiny, TransactionHash hash) + throws ForeignBlockchainException { + + BitcoinyTransaction transaction = bitcoiny.getTransaction(hash.txHash); + + // destination address list -> value + Map, Long> valueByAddress = new HashMap<>(); + + // for each output in the transaction, index by address list + for( BitcoinyTransaction.Output output : transaction.outputs) { + valueByAddress.put(output.addresses, output.value); + } + + return new AtomicTransactionData( + hash, + transaction.timestamp, + transaction.inputs, + valueByAddress, + transaction.totalAmount, + transaction.size); + } + + /** + * Get Total Input + * + * Get the sum of all the inputs used in a list of inputs. + * + * @param bitcoiny the coin the inputs belong to + * @param inputs the inputs + * + * @return the sum + * + * @throws ForeignBlockchainException + */ + private static long getTotalInput(Bitcoiny bitcoiny, List inputs) + throws ForeignBlockchainException { + + long totalInputOut = 0; + + // for each input, add to total input, + // get the indexed transaction output value and add to total value + for( BitcoinyTransaction.Input input : inputs){ + + BitcoinyTransaction inputOut = bitcoiny.getTransaction(input.outputTxHash); + BitcoinyTransaction.Output output = inputOut.outputs.get(input.outputVout); + totalInputOut += output.value; + } + return totalInputOut; + } } \ No newline at end of file diff --git a/src/main/java/org/qortal/data/crosschain/AtomicTransactionData.java b/src/main/java/org/qortal/data/crosschain/AtomicTransactionData.java new file mode 100644 index 000000000..04c7a2a9b --- /dev/null +++ b/src/main/java/org/qortal/data/crosschain/AtomicTransactionData.java @@ -0,0 +1,32 @@ +package org.qortal.data.crosschain; + +import org.qortal.crosschain.BitcoinyTransaction; +import org.qortal.crosschain.TransactionHash; + +import java.util.List; +import java.util.Map; + +public class AtomicTransactionData { + public final TransactionHash hash; + public final Integer timestamp; + public final List inputs; + public final Map, Long> valueByAddress; + public final long totalAmount; + public final int size; + + public AtomicTransactionData( + TransactionHash hash, + Integer timestamp, + List inputs, + Map, Long> valueByAddress, + long totalAmount, + int size) { + + this.hash = hash; + this.timestamp = timestamp; + this.inputs = inputs; + this.valueByAddress = valueByAddress; + this.totalAmount = totalAmount; + this.size = size; + } +} diff --git a/src/main/java/org/qortal/data/crosschain/TransactionSummary.java b/src/main/java/org/qortal/data/crosschain/TransactionSummary.java new file mode 100644 index 000000000..ac67a2f6b --- /dev/null +++ b/src/main/java/org/qortal/data/crosschain/TransactionSummary.java @@ -0,0 +1,106 @@ +package org.qortal.data.crosschain; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +@XmlAccessorType(XmlAccessType.FIELD) +public class TransactionSummary { + + private String atAddress; + private String p2shValue; + private String p2shAddress; + private String lockingHash; + private Integer lockingTimestamp; + private long lockingTotalAmount; + private long lockingFee; + private int lockingSize; + private String unlockingHash; + private Integer unlockingTimestamp; + private long unlockingTotalAmount; + private long unlockingFee; + private int unlockingSize; + + public TransactionSummary(){} + + public TransactionSummary( + String atAddress, + String p2shValue, + String p2shAddress, + String lockingHash, + Integer lockingTimestamp, + long lockingTotalAmount, + long lockingFee, + int lockingSize, + String unlockingHash, + Integer unlockingTimestamp, + long unlockingTotalAmount, + long unlockingFee, + int unlockingSize) { + + this.atAddress = atAddress; + this.p2shValue = p2shValue; + this.p2shAddress = p2shAddress; + this.lockingHash = lockingHash; + this.lockingTimestamp = lockingTimestamp; + this.lockingTotalAmount = lockingTotalAmount; + this.lockingFee = lockingFee; + this.lockingSize = lockingSize; + this.unlockingHash = unlockingHash; + this.unlockingTimestamp = unlockingTimestamp; + this.unlockingTotalAmount = unlockingTotalAmount; + this.unlockingFee = unlockingFee; + this.unlockingSize = unlockingSize; + } + + public String getAtAddress() { + return atAddress; + } + + public String getP2shValue() { + return p2shValue; + } + + public String getP2shAddress() { + return p2shAddress; + } + + public String getLockingHash() { + return lockingHash; + } + + public Integer getLockingTimestamp() { + return lockingTimestamp; + } + + public long getLockingTotalAmount() { + return lockingTotalAmount; + } + + public long getLockingFee() { + return lockingFee; + } + + public int getLockingSize() { + return lockingSize; + } + + public String getUnlockingHash() { + return unlockingHash; + } + + public Integer getUnlockingTimestamp() { + return unlockingTimestamp; + } + + public long getUnlockingTotalAmount() { + return unlockingTotalAmount; + } + + public long getUnlockingFee() { + return unlockingFee; + } + + public int getUnlockingSize() { + return unlockingSize; + } +}