foreign fees manager implementation, feeCeiling -> feeRequired name change, thread-safety measures for fee values, fee backup file implementation, unsigned fees socket implementation

This commit is contained in:
kennycud 2025-04-25 17:51:01 -07:00
parent bcf3538d18
commit 144d6cc5c7
34 changed files with 2428 additions and 155 deletions

View File

@ -197,6 +197,7 @@ public class ApiService {
context.addServlet(DataMonitorSocket.class, "/websockets/datamonitor");
context.addServlet(ActiveChatsWebSocket.class, "/websockets/chat/active/*");
context.addServlet(ChatMessagesWebSocket.class, "/websockets/chat/messages");
context.addServlet(UnsignedFeesSocket.class, "/websockets/crosschain/unsignedfees");
context.addServlet(TradeOffersWebSocket.class, "/websockets/crosschain/tradeoffers");
context.addServlet(TradeBotWebSocket.class, "/websockets/crosschain/tradebot");
context.addServlet(TradePresenceWebSocket.class, "/websockets/crosschain/tradepresence");

View File

@ -304,11 +304,11 @@ public class BitcoinyTBDRequest {
private String networkName;
/**
* Fee Ceiling
* Fee Required
*
* web search, LTC fee ceiling = 1000L
* web search, LTC fee required = 1000L
*/
private long feeCeiling;
private long feeRequired;
/**
* Extended Public Key
@ -570,8 +570,8 @@ public class BitcoinyTBDRequest {
return this.networkName;
}
public long getFeeCeiling() {
return this.feeCeiling;
public long getFeeRequired() {
return this.feeRequired;
}
public String getExtendedPublicKey() {
@ -671,7 +671,7 @@ public class BitcoinyTBDRequest {
", minimumOrderAmount=" + minimumOrderAmount +
", feePerKb=" + feePerKb +
", networkName='" + networkName + '\'' +
", feeCeiling=" + feeCeiling +
", feeRequired=" + feeRequired +
", extendedPublicKey='" + extendedPublicKey + '\'' +
", sendAmount=" + sendAmount +
", sendingFeePerByte=" + sendingFeePerByte +

View File

@ -502,10 +502,10 @@ public class CrossChainBitcoinResource {
}
@GET
@Path("/feeceiling")
@Path("/feerequired")
@Operation(
summary = "Returns Bitcoin fee per Kb.",
description = "Returns Bitcoin fee per Kb.",
summary = "The total fee required for unlocking BTC to the trade offer creator.",
description = "This is in sats for a transaction that is approximately 300 kB in size.",
responses = {
@ApiResponse(
content = @Content(
@ -516,17 +516,17 @@ public class CrossChainBitcoinResource {
)
}
)
public String getBitcoinFeeCeiling() {
public String getBitcoinFeeRequired() {
Bitcoin bitcoin = Bitcoin.getInstance();
return String.valueOf(bitcoin.getFeeCeiling());
return String.valueOf(bitcoin.getFeeRequired());
}
@POST
@Path("/updatefeeceiling")
@Path("/updatefeerequired")
@Operation(
summary = "Sets Bitcoin fee ceiling.",
description = "Sets Bitcoin fee ceiling.",
summary = "The total fee required for unlocking BTC to the trade offer creator.",
description = "This is in sats for a transaction that is approximately 300 kB in size.",
requestBody = @RequestBody(
required = true,
content = @Content(
@ -545,13 +545,13 @@ public class CrossChainBitcoinResource {
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
public String setBitcoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
public String setBitcoinFeeRequired(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
Security.checkApiCallAllowed(request);
Bitcoin bitcoin = Bitcoin.getInstance();
try {
return CrossChainUtils.setFeeCeiling(bitcoin, fee);
return CrossChainUtils.setFeeRequired(bitcoin, fee);
}
catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);

View File

@ -502,10 +502,10 @@ public class CrossChainDigibyteResource {
}
@GET
@Path("/feeceiling")
@Path("/feerequired")
@Operation(
summary = "Returns Digibyte fee per Kb.",
description = "Returns Digibyte fee per Kb.",
summary = "The total fee required for unlocking DGB to the trade offer creator.",
description = "This is in sats for a transaction that is approximately 300 kB in size.",
responses = {
@ApiResponse(
content = @Content(
@ -516,17 +516,17 @@ public class CrossChainDigibyteResource {
)
}
)
public String getDigibyteFeeCeiling() {
public String getDigibyteFeeRequired() {
Digibyte digibyte = Digibyte.getInstance();
return String.valueOf(digibyte.getFeeCeiling());
return String.valueOf(digibyte.getFeeRequired());
}
@POST
@Path("/updatefeeceiling")
@Path("/updatefeerequired")
@Operation(
summary = "Sets Digibyte fee ceiling.",
description = "Sets Digibyte fee ceiling.",
summary = "The total fee required for unlocking DGB to the trade offer creator.",
description = "This is in sats for a transaction that is approximately 300 kB in size.",
requestBody = @RequestBody(
required = true,
content = @Content(
@ -545,13 +545,13 @@ public class CrossChainDigibyteResource {
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
public String setDigibyteFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
public String setDigibyteFeeRequired(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
Security.checkApiCallAllowed(request);
Digibyte digibyte = Digibyte.getInstance();
try {
return CrossChainUtils.setFeeCeiling(digibyte, fee);
return CrossChainUtils.setFeeRequired(digibyte, fee);
}
catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);

View File

@ -502,10 +502,10 @@ public class CrossChainDogecoinResource {
}
@GET
@Path("/feeceiling")
@Path("/feerequired")
@Operation(
summary = "Returns Dogecoin fee per Kb.",
description = "Returns Dogecoin fee per Kb.",
summary = "The total fee required for unlocking DOGE to the trade offer creator.",
description = "This is in sats for a transaction that is approximately 300 kB in size.",
responses = {
@ApiResponse(
content = @Content(
@ -516,17 +516,17 @@ public class CrossChainDogecoinResource {
)
}
)
public String getDogecoinFeeCeiling() {
public String getDogecoinFeeRequired() {
Dogecoin dogecoin = Dogecoin.getInstance();
return String.valueOf(dogecoin.getFeeCeiling());
return String.valueOf(dogecoin.getFeeRequired());
}
@POST
@Path("/updatefeeceiling")
@Path("/updatefeerequired")
@Operation(
summary = "Sets Dogecoin fee ceiling.",
description = "Sets Dogecoin fee ceiling.",
summary = "The total fee required for unlocking DOGE to the trade offer creator.",
description = "This is in sats for a transaction that is approximately 300 kB in size.",
requestBody = @RequestBody(
required = true,
content = @Content(
@ -545,13 +545,13 @@ public class CrossChainDogecoinResource {
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
public String setDogecoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
public String setDogecoinFeeRequired(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
Security.checkApiCallAllowed(request);
Dogecoin dogecoin = Dogecoin.getInstance();
try {
return CrossChainUtils.setFeeCeiling(dogecoin, fee);
return CrossChainUtils.setFeeRequired(dogecoin, fee);
}
catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);

View File

@ -540,10 +540,10 @@ public class CrossChainLitecoinResource {
}
@GET
@Path("/feeceiling")
@Path("/feerequired")
@Operation(
summary = "Returns Litecoin fee per Kb.",
description = "Returns Litecoin fee per Kb.",
summary = "The total fee required for unlocking LTC to the trade offer creator.",
description = "This is in sats for a transaction that is approximately 300 kB in size.",
responses = {
@ApiResponse(
content = @Content(
@ -554,17 +554,17 @@ public class CrossChainLitecoinResource {
)
}
)
public String getLitecoinFeeCeiling() {
public String getLitecoinFeeRequired() {
Litecoin litecoin = Litecoin.getInstance();
return String.valueOf(litecoin.getFeeCeiling());
return String.valueOf(litecoin.getFeeRequired());
}
@POST
@Path("/updatefeeceiling")
@Path("/updatefeerequired")
@Operation(
summary = "Sets Litecoin fee ceiling.",
description = "Sets Litecoin fee ceiling.",
summary = "The total fee required for unlocking LTC to the trade offer creator.",
description = "This is in sats for a transaction that is approximately 300 kB in size.",
requestBody = @RequestBody(
required = true,
content = @Content(
@ -583,13 +583,13 @@ public class CrossChainLitecoinResource {
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
public String setLitecoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
public String setLitecoinFeeRequired(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
Security.checkApiCallAllowed(request);
Litecoin litecoin = Litecoin.getInstance();
try {
return CrossChainUtils.setFeeCeiling(litecoin, fee);
return CrossChainUtils.setFeeRequired(litecoin, fee);
}
catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);

View File

@ -587,10 +587,10 @@ public class CrossChainPirateChainResource {
}
@GET
@Path("/feeceiling")
@Path("/feerequired")
@Operation(
summary = "Returns PirateChain fee per Kb.",
description = "Returns PirateChain fee per Kb.",
summary = "The total fee required for unlocking ARRR to the trade offer creator.",
description = "The total fee required for unlocking ARRR to the trade offer creator.",
responses = {
@ApiResponse(
content = @Content(
@ -601,17 +601,17 @@ public class CrossChainPirateChainResource {
)
}
)
public String getPirateChainFeeCeiling() {
public String getPirateChainFeeRequired() {
PirateChain pirateChain = PirateChain.getInstance();
return String.valueOf(pirateChain.getFeeCeiling());
return String.valueOf(pirateChain.getFeeRequired());
}
@POST
@Path("/updatefeeceiling")
@Path("/updatefeerequired")
@Operation(
summary = "Sets PirateChain fee ceiling.",
description = "Sets PirateChain fee ceiling.",
summary = "The total fee required for unlocking ARRR to the trade offer creator.",
description = "This is in sats for a transaction that is approximately 300 kB in size.",
requestBody = @RequestBody(
required = true,
content = @Content(
@ -630,13 +630,13 @@ public class CrossChainPirateChainResource {
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
public String setPirateChainFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
public String setPirateChainFeeRequired(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
Security.checkApiCallAllowed(request);
PirateChain pirateChain = PirateChain.getInstance();
try {
return CrossChainUtils.setFeeCeiling(pirateChain, fee);
return CrossChainUtils.setFeeRequired(pirateChain, fee);
}
catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);

View File

@ -502,10 +502,10 @@ public class CrossChainRavencoinResource {
}
@GET
@Path("/feeceiling")
@Path("/feerequired")
@Operation(
summary = "Returns Ravencoin fee per Kb.",
description = "Returns Ravencoin fee per Kb.",
summary = "The total fee required for unlocking RVN to the trade offer creator.",
description = "This is in sats for a transaction that is approximately 300 kB in size.",
responses = {
@ApiResponse(
content = @Content(
@ -516,17 +516,17 @@ public class CrossChainRavencoinResource {
)
}
)
public String getRavencoinFeeCeiling() {
public String getRavencoinFeeRequired() {
Ravencoin ravencoin = Ravencoin.getInstance();
return String.valueOf(ravencoin.getFeeCeiling());
return String.valueOf(ravencoin.getFeeRequired());
}
@POST
@Path("/updatefeeceiling")
@Path("/updatefeerequired")
@Operation(
summary = "Sets Ravencoin fee ceiling.",
description = "Sets Ravencoin fee ceiling.",
summary = "The total fee required for unlocking RVN to the trade offer creator.",
description = "This is in sats for a transaction that is approximately 300 kB in size.",
requestBody = @RequestBody(
required = true,
content = @Content(
@ -545,13 +545,13 @@ public class CrossChainRavencoinResource {
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA})
public String setRavencoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
public String setRavencoinFeeRequired(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) {
Security.checkApiCallAllowed(request);
Ravencoin ravencoin = Ravencoin.getInstance();
try {
return CrossChainUtils.setFeeCeiling(ravencoin, fee);
return CrossChainUtils.setFeeRequired(ravencoin, fee);
}
catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);

View File

@ -10,6 +10,8 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
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.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.glassfish.jersey.media.multipart.ContentDisposition;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
@ -18,6 +20,7 @@ import org.qortal.api.Security;
import org.qortal.api.model.CrossChainCancelRequest;
import org.qortal.api.model.CrossChainTradeLedgerEntry;
import org.qortal.api.model.CrossChainTradeSummary;
import org.qortal.controller.ForeignFeesManager;
import org.qortal.controller.tradebot.TradeBot;
import org.qortal.crosschain.ACCT;
import org.qortal.crosschain.AcctMode;
@ -29,6 +32,8 @@ 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.crosschain.ForeignFeeDecodedData;
import org.qortal.data.crosschain.ForeignFeeEncodedData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
@ -64,6 +69,8 @@ import java.util.stream.Collectors;
@Tag(name = "Cross-Chain")
public class CrossChainResource {
private static final Logger LOGGER = LogManager.getLogger(CrossChainResource.class);
@Context
HttpServletRequest request;
@ -360,6 +367,97 @@ public class CrossChainResource {
}
}
@POST
@Path("/signedfees")
@Operation(
summary = "",
description = "",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
array = @ArraySchema(
schema = @Schema(
implementation = ForeignFeeEncodedData.class
)
)
)
),
responses = {
@ApiResponse(
description = "true on success",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "boolean"
)
)
)
}
)
public String postSignedForeignFees(List<ForeignFeeEncodedData> signedFees) {
LOGGER.info("signedFees = " + signedFees);
try {
ForeignFeesManager.getInstance().addSignedFees(signedFees);
return "true";
}
catch( Exception e ) {
LOGGER.error(e.getMessage(), e);
return "false";
}
}
@GET
@Path("/unsignedfees/{address}")
@Operation(
summary = "",
description = "",
responses = {
@ApiResponse(
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = ForeignFeeEncodedData.class
)
)
)
)
}
)
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
public List<ForeignFeeEncodedData> getUnsignedFees(@PathParam("address") String address) {
return ForeignFeesManager.getInstance().getUnsignedFeesForAddress(address);
}
@GET
@Path("/signedfees")
@Operation(
summary = "",
description = "",
responses = {
@ApiResponse(
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = ForeignFeeDecodedData.class
)
)
)
)
}
)
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
public List<ForeignFeeDecodedData> getSignedFees() {
return ForeignFeesManager.getInstance().getSignedFees();
}
/**
* Decode Public Key
*

View File

@ -16,6 +16,9 @@ import org.qortal.crosschain.*;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.data.crosschain.*;
import org.qortal.event.EventBus;
import org.qortal.event.LockingFeeUpdateEvent;
import org.qortal.event.RequiredFeeUpdateEvent;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.utils.Amounts;
@ -23,14 +26,9 @@ import org.qortal.utils.BitTwiddling;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Writer;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.stream.Collectors;
@ -103,11 +101,13 @@ public class CrossChainUtils {
bitcoiny.setFeePerKb(Coin.valueOf(satoshis) );
EventBus.INSTANCE.notify(new LockingFeeUpdateEvent());
return String.valueOf(bitcoiny.getFeePerKb().value);
}
/**
* Set Fee Ceiling
* Set Fee Required
*
* @param bitcoiny the blockchain support
* @param fee the fee in satoshis
@ -116,14 +116,16 @@ public class CrossChainUtils {
*
* @throws IllegalArgumentException if invalid
*/
public static String setFeeCeiling(Bitcoiny bitcoiny, String fee) throws IllegalArgumentException{
public static String setFeeRequired(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));
bitcoiny.setFeeRequired( Long.parseLong(fee));
return String.valueOf(bitcoiny.getFeeCeiling());
EventBus.INSTANCE.notify(new RequiredFeeUpdateEvent(bitcoiny));
return String.valueOf(bitcoiny.getFeeRequired());
}
/**

View File

@ -0,0 +1,81 @@
package org.qortal.api.websocket;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.data.crosschain.UnsignedFeeEvent;
import org.qortal.event.Event;
import org.qortal.event.EventBus;
import org.qortal.event.FeeWaitingEvent;
import org.qortal.event.Listener;
import java.io.IOException;
import java.io.StringWriter;
@WebSocket
@SuppressWarnings("serial")
public class UnsignedFeesSocket extends ApiWebSocket implements Listener {
private static final Logger LOGGER = LogManager.getLogger(UnsignedFeesSocket.class);
@Override
public void configure(WebSocketServletFactory factory) {
LOGGER.info("configure");
factory.register(UnsignedFeesSocket.class);
EventBus.INSTANCE.addListener(this);
}
@Override
public void listen(Event event) {
if (!(event instanceof FeeWaitingEvent))
return;
for (Session session : getSessions())
sendUnsignedFeeEvent(session, new UnsignedFeeEvent());
}
@OnWebSocketConnect
@Override
public void onWebSocketConnect(Session session) {
super.onWebSocketConnect(session);
}
@OnWebSocketClose
@Override
public void onWebSocketClose(Session session, int statusCode, String reason) {
super.onWebSocketClose(session, statusCode, reason);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
/* We ignore errors for now, but method here to silence log spam */
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
LOGGER.info("onWebSocketMessage: message = " + message);
}
private void sendUnsignedFeeEvent(Session session, UnsignedFeeEvent unsignedFeeEvent) {
StringWriter stringWriter = new StringWriter();
try {
marshall(stringWriter, unsignedFeeEvent);
session.getRemote().sendStringByFuture(stringWriter.toString());
} catch (IOException | WebSocketException e) {
// No output this time
}
}
}

View File

@ -560,6 +560,9 @@ public class Controller extends Thread {
LOGGER.info("Starting online accounts manager");
OnlineAccountsManager.getInstance().start();
LOGGER.info("Starting foreign fees manager");
ForeignFeesManager.getInstance().start();
LOGGER.info("Starting transaction importer");
TransactionImporter.getInstance().start();
@ -1130,6 +1133,9 @@ public class Controller extends Thread {
LOGGER.info("Shutting down online accounts manager");
OnlineAccountsManager.getInstance().shutdown();
LOGGER.info("Shutting down foreign fees manager");
ForeignFeesManager.getInstance().shutdown();
LOGGER.info("Shutting down transaction importer");
TransactionImporter.getInstance().shutdown();
@ -1474,6 +1480,14 @@ public class Controller extends Thread {
OnlineAccountsManager.getInstance().onNetworkOnlineAccountsV3Message(peer, message);
break;
case GET_FOREIGN_FEES:
ForeignFeesManager.getInstance().onNetworkGetForeignFeesMessage(peer, message);
break;
case FOREIGN_FEES:
ForeignFeesManager.getInstance().onNetworkForeignFeesMessage(peer, message);
break;
case GET_ARBITRARY_DATA:
// Not currently supported
break;

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,6 @@
package org.qortal.crosschain;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Context;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Transaction;
@ -14,15 +15,21 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.EnumMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
public class Bitcoin extends Bitcoiny {
public static final String CURRENCY_CODE = "BTC";
private static final long MINIMUM_ORDER_AMOUNT = 100000; // 0.001 BTC minimum order, due to high fees
// Locking fee to lock in a QORT for BTC. This is the default value that the user should reset to
// a value inline with the BTC fee market. This is 5 sats per kB.
private static final Coin DEFAULT_FEE_PER_KB = Coin.valueOf(5_000); // 0.00005 BTC per 1000 bytes
// Temporary values until a dynamic fee system is written.
private static final long NEW_FEE_AMOUNT = 6_000L;
private static final long MINIMUM_ORDER_AMOUNT = 100_000; // 0.001 BTC minimum order, due to high fees
// Default value until user resets fee to compete with the current market. This is a total value for a
// p2sh transaction, size 300 kB, 5 sats per kB
private static final long NEW_FEE_AMOUNT = 1_500L;
private static final long NON_MAINNET_FEE = 1000L; // enough for TESTNET3 and should be OK for REGTEST
@ -111,7 +118,7 @@ public class Bitcoin extends Bitcoiny {
@Override
public long getP2shFee(Long timestamp) {
return this.getFeeCeiling();
return this.getFeeRequired();
}
},
TEST3 {
@ -173,14 +180,14 @@ public class Bitcoin extends Bitcoiny {
}
};
private long feeCeiling = NEW_FEE_AMOUNT;
private AtomicLong feeRequired = new AtomicLong(NEW_FEE_AMOUNT);
public long getFeeCeiling() {
return feeCeiling;
public long getFeeRequired() {
return feeRequired.get();
}
public void setFeeCeiling(long feeCeiling) {
this.feeCeiling = feeCeiling;
public void setFeeRequired(long feeRequired) {
this.feeRequired.set(feeRequired);
}
public abstract NetworkParameters getParams();
@ -196,7 +203,7 @@ public class Bitcoin extends Bitcoiny {
// Constructors and instance
private Bitcoin(BitcoinNet bitcoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
super(blockchain, bitcoinjContext, currencyCode, bitcoinjContext.getFeePerKb());
super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB);
this.bitcoinNet = bitcoinNet;
LOGGER.info(() -> String.format("Starting Bitcoin support using %s", this.bitcoinNet.name()));
@ -242,14 +249,14 @@ public class Bitcoin extends Bitcoiny {
}
@Override
public long getFeeCeiling() {
return this.bitcoinNet.getFeeCeiling();
public long getFeeRequired() {
return this.bitcoinNet.getFeeRequired();
}
@Override
public void setFeeCeiling(long fee) {
public void setFeeRequired(long fee) {
this.bitcoinNet.setFeeCeiling( fee );
this.bitcoinNet.setFeeRequired( fee );
}
/**
* Returns bitcoinj transaction sending <tt>amount</tt> to <tt>recipient</tt> using 20 sat/byte fee.

View File

@ -840,9 +840,9 @@ public abstract class Bitcoiny implements ForeignBlockchain {
} while (true);
}
public abstract long getFeeCeiling();
public abstract long getFeeRequired();
public abstract void setFeeCeiling(long fee);
public abstract void setFeeRequired(long fee);
// UTXOProvider support

View File

@ -89,7 +89,7 @@ public class BitcoinyTBD extends Bitcoiny {
NetTBD netTBD
= new NetTBD(
bitcoinyTBDRequest.getNetworkName(),
bitcoinyTBDRequest.getFeeCeiling(),
bitcoinyTBDRequest.getFeeRequired(),
networkParams,
Collections.emptyList(),
bitcoinyTBDRequest.getExpectedGenesisHash()
@ -134,18 +134,18 @@ public class BitcoinyTBD extends Bitcoiny {
@Override
public long getP2shFee(Long timestamp) throws ForeignBlockchainException {
return this.netTBD.getFeeCeiling();
return this.netTBD.getFeeRequired();
}
@Override
public long getFeeCeiling() {
public long getFeeRequired() {
return this.netTBD.getFeeCeiling();
return this.netTBD.getFeeRequired();
}
@Override
public void setFeeCeiling(long fee) {
public void setFeeRequired(long fee) {
this.netTBD.setFeeCeiling( fee );
this.netTBD.setFeeRequired( fee );
}
}

View File

@ -14,6 +14,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.EnumMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
public class Digibyte extends Bitcoiny {
@ -59,7 +60,7 @@ public class Digibyte extends Bitcoiny {
@Override
public long getP2shFee(Long timestamp) {
return this.getFeeCeiling();
return this.getFeeRequired();
}
},
TEST3 {
@ -109,14 +110,14 @@ public class Digibyte extends Bitcoiny {
}
};
private long feeCeiling = MAINNET_FEE;
private AtomicLong feeRequired = new AtomicLong(MAINNET_FEE);
public long getFeeCeiling() {
return feeCeiling;
public long getFeeRequired() {
return feeRequired.get();
}
public void setFeeCeiling(long feeCeiling) {
this.feeCeiling = feeCeiling;
public void setFeeRequired(long feeRequired) {
this.feeRequired.set(feeRequired);
}
public abstract NetworkParameters getParams();
@ -178,13 +179,13 @@ public class Digibyte extends Bitcoiny {
}
@Override
public long getFeeCeiling() {
return this.digibyteNet.getFeeCeiling();
public long getFeeRequired() {
return this.digibyteNet.getFeeRequired();
}
@Override
public void setFeeCeiling(long fee) {
public void setFeeRequired(long fee) {
this.digibyteNet.setFeeCeiling( fee );
this.digibyteNet.setFeeRequired( fee );
}
}

View File

@ -13,6 +13,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.EnumMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
public class Dogecoin extends Bitcoiny {
@ -60,7 +61,7 @@ public class Dogecoin extends Bitcoiny {
@Override
public long getP2shFee(Long timestamp) {
return this.getFeeCeiling();
return this.getFeeRequired();
}
},
TEST3 {
@ -110,14 +111,14 @@ public class Dogecoin extends Bitcoiny {
}
};
private long feeCeiling = MAINNET_FEE;
private AtomicLong feeRequired = new AtomicLong(MAINNET_FEE);
public long getFeeCeiling() {
return feeCeiling;
public long getFeeRequired() {
return feeRequired.get();
}
public void setFeeCeiling(long feeCeiling) {
this.feeCeiling = feeCeiling;
public void setFeeRequired(long feeRequired) {
this.feeRequired.set(feeRequired);
}
public abstract NetworkParameters getParams();
@ -179,13 +180,13 @@ public class Dogecoin extends Bitcoiny {
}
@Override
public long getFeeCeiling() {
return this.dogecoinNet.getFeeCeiling();
public long getFeeRequired() {
return this.dogecoinNet.getFeeRequired();
}
@Override
public void setFeeCeiling(long fee) {
public void setFeeRequired(long fee) {
this.dogecoinNet.setFeeCeiling( fee );
this.dogecoinNet.setFeeRequired( fee );
}
}

View File

@ -14,6 +14,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.EnumMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
public class Litecoin extends Bitcoiny {
@ -63,7 +64,7 @@ public class Litecoin extends Bitcoiny {
@Override
public long getP2shFee(Long timestamp) {
return this.getFeeCeiling();
return this.getFeeRequired();
}
},
TEST3 {
@ -116,14 +117,14 @@ public class Litecoin extends Bitcoiny {
}
};
private long feeCeiling = MAINNET_FEE;
private AtomicLong feeRequired = new AtomicLong(MAINNET_FEE);
public long getFeeCeiling() {
return feeCeiling;
public long getFeeRequired() {
return feeRequired.get();
}
public void setFeeCeiling(long feeCeiling) {
this.feeCeiling = feeCeiling;
public void setFeeRequired(long feeRequired) {
this.feeRequired.set(feeRequired);
}
public abstract NetworkParameters getParams();
@ -185,13 +186,13 @@ public class Litecoin extends Bitcoiny {
}
@Override
public long getFeeCeiling() {
return this.litecoinNet.getFeeCeiling();
public long getFeeRequired() {
return this.litecoinNet.getFeeRequired();
}
@Override
public void setFeeCeiling(long fee) {
public void setFeeRequired(long fee) {
this.litecoinNet.setFeeCeiling( fee );
this.litecoinNet.setFeeRequired( fee );
}
}

View File

@ -3,18 +3,19 @@ package org.qortal.crosschain;
import org.bitcoinj.core.NetworkParameters;
import java.util.Collection;
import java.util.concurrent.atomic.AtomicLong;
public class NetTBD {
private String name;
private long feeCeiling;
private AtomicLong feeRequired;
private NetworkParameters params;
private Collection<ElectrumX.Server> servers;
private String genesisHash;
public NetTBD(String name, long feeCeiling, NetworkParameters params, Collection<ElectrumX.Server> servers, String genesisHash) {
public NetTBD(String name, long feeRequired, NetworkParameters params, Collection<ElectrumX.Server> servers, String genesisHash) {
this.name = name;
this.feeCeiling = feeCeiling;
this.feeRequired = new AtomicLong(feeRequired);
this.params = params;
this.servers = servers;
this.genesisHash = genesisHash;
@ -25,14 +26,14 @@ public class NetTBD {
return this.name;
}
public long getFeeCeiling() {
public long getFeeRequired() {
return feeCeiling;
return feeRequired.get();
}
public void setFeeCeiling(long feeCeiling) {
public void setFeeRequired(long feeRequired) {
this.feeCeiling = feeCeiling;
this.feeRequired.set(feeRequired);
}
public NetworkParameters getParams() {

View File

@ -21,6 +21,7 @@ import org.qortal.utils.BitTwiddling;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
public class PirateChain extends Bitcoiny {
@ -67,7 +68,7 @@ public class PirateChain extends Bitcoiny {
@Override
public long getP2shFee(Long timestamp) {
return this.getFeeCeiling();
return this.getFeeRequired();
}
},
TEST3 {
@ -117,14 +118,14 @@ public class PirateChain extends Bitcoiny {
}
};
private long feeCeiling = MAINNET_FEE;
private AtomicLong feeRequired = new AtomicLong(MAINNET_FEE);
public long getFeeCeiling() {
return feeCeiling;
public long getFeeRequired() {
return feeRequired.get();
}
public void setFeeCeiling(long feeCeiling) {
this.feeCeiling = feeCeiling;
public void setFeeRequired(long feeRequired) {
this.feeRequired.set(feeRequired);
}
public abstract NetworkParameters getParams();
@ -186,14 +187,14 @@ public class PirateChain extends Bitcoiny {
}
@Override
public long getFeeCeiling() {
return this.pirateChainNet.getFeeCeiling();
public long getFeeRequired() {
return this.pirateChainNet.getFeeRequired();
}
@Override
public void setFeeCeiling(long fee) {
public void setFeeRequired(long fee) {
this.pirateChainNet.setFeeCeiling( fee );
this.pirateChainNet.setFeeRequired( fee );
}
/**
* Returns confirmed balance, based on passed payment script.

View File

@ -14,6 +14,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.EnumMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
public class Ravencoin extends Bitcoiny {
@ -61,7 +62,7 @@ public class Ravencoin extends Bitcoiny {
@Override
public long getP2shFee(Long timestamp) {
return this.getFeeCeiling();
return this.getFeeRequired();
}
},
TEST3 {
@ -111,14 +112,14 @@ public class Ravencoin extends Bitcoiny {
}
};
private long feeCeiling = MAINNET_FEE;
private AtomicLong feeRequired = new AtomicLong( MAINNET_FEE );
public long getFeeCeiling() {
return feeCeiling;
public long getFeeRequired() {
return feeRequired.get();
}
public void setFeeCeiling(long feeCeiling) {
this.feeCeiling = feeCeiling;
public void setFeeRequired(long feeRequired) {
this.feeRequired.set(feeRequired);
}
public abstract NetworkParameters getParams();
@ -180,13 +181,13 @@ public class Ravencoin extends Bitcoiny {
}
@Override
public long getFeeCeiling() {
return this.ravencoinNet.getFeeCeiling();
public long getFeeRequired() {
return this.ravencoinNet.getFeeRequired();
}
@Override
public void setFeeCeiling(long fee) {
public void setFeeRequired(long fee) {
this.ravencoinNet.setFeeCeiling( fee );
this.ravencoinNet.setFeeRequired( fee );
}
}

View File

@ -0,0 +1,57 @@
package org.qortal.data.crosschain;
import org.json.JSONObject;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
public class ForeignFeeData {
private String blockchain;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long fee;
protected ForeignFeeData() {
/* JAXB */
}
public ForeignFeeData(String blockchain,
long fee) {
this.blockchain = blockchain;
this.fee = fee;
}
public String getBlockchain() {
return this.blockchain;
}
public long getFee() {
return this.fee;
}
public JSONObject toJson() {
JSONObject jsonObject = new JSONObject();
jsonObject.put("blockchain", this.getBlockchain());
jsonObject.put("fee", this.getFee());
return jsonObject;
}
public static ForeignFeeData fromJson(JSONObject json) {
return new ForeignFeeData(
json.isNull("blockchain") ? null : json.getString("blockchain"),
json.isNull("fee") ? null : json.getLong("fee")
);
}
@Override
public String toString() {
return "ForeignFeeData{" +
"blockchain='" + blockchain + '\'' +
", fee=" + fee +
'}';
}
}

View File

@ -0,0 +1,90 @@
package org.qortal.data.crosschain;
import org.json.JSONObject;
import org.qortal.data.account.MintingAccountData;
import org.qortal.utils.Base58;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import java.util.Objects;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
public class ForeignFeeDecodedData {
protected long timestamp;
protected byte[] data;
protected String atAddress;
protected Integer fee;
// Constructors
// necessary for JAXB serialization
protected ForeignFeeDecodedData() {
}
public ForeignFeeDecodedData(long timestamp, byte[] data, String atAddress, Integer fee) {
this.timestamp = timestamp;
this.data = data;
this.atAddress = atAddress;
this.fee = fee;
}
public long getTimestamp() {
return this.timestamp;
}
public byte[] getData() {
return this.data;
}
public String getAtAddress() {
return atAddress;
}
public Integer getFee() {
return this.fee;
}
// Comparison
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ForeignFeeDecodedData that = (ForeignFeeDecodedData) o;
return timestamp == that.timestamp && Objects.equals(atAddress, that.atAddress) && Objects.equals(fee, that.fee);
}
@Override
public int hashCode() {
return Objects.hash(timestamp, atAddress, fee);
}
@Override
public String toString() {
return "ForeignFeeDecodedData{" +
"timestamp=" + timestamp +
", atAddress='" + atAddress + '\'' +
", fee=" + fee +
'}';
}
public JSONObject toJson() {
JSONObject jsonObject = new JSONObject();
jsonObject.put("data", Base58.encode(this.data));
jsonObject.put("atAddress", this.atAddress);
jsonObject.put("timestamp", this.timestamp);
jsonObject.put("fee", this.fee);
return jsonObject;
}
public static ForeignFeeDecodedData fromJson(JSONObject json) {
return new ForeignFeeDecodedData(
json.isNull("timestamp") ? null : json.getLong("timestamp"),
json.isNull("data") ? null : Base58.decode(json.getString("data")),
json.isNull("atAddress") ? null : json.getString("atAddress"),
json.isNull("fee") ? null : json.getInt("fee"));
}
}

View File

@ -0,0 +1,69 @@
package org.qortal.data.crosschain;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import java.util.Objects;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
public class ForeignFeeEncodedData {
protected long timestamp;
protected String data;
protected String atAddress;
protected Integer fee;
// Constructors
// necessary for JAXB serialization
protected ForeignFeeEncodedData() {
}
public ForeignFeeEncodedData(long timestamp, String data, String atAddress, Integer fee) {
this.timestamp = timestamp;
this.data = data;
this.atAddress = atAddress;
this.fee = fee;
}
public long getTimestamp() {
return this.timestamp;
}
public String getData() {
return this.data;
}
public String getAtAddress() {
return atAddress;
}
public Integer getFee() {
return this.fee;
}
// Comparison
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ForeignFeeEncodedData that = (ForeignFeeEncodedData) o;
return timestamp == that.timestamp && Objects.equals(atAddress, that.atAddress) && Objects.equals(fee, that.fee);
}
@Override
public int hashCode() {
return Objects.hash(timestamp, atAddress, fee);
}
@Override
public String toString() {
return "ForeignFeeDecodedData{" +
"timestamp=" + timestamp +
", atAddress='" + atAddress + '\'' +
", fee=" + fee +
'}';
}
}

View File

@ -0,0 +1,8 @@
package org.qortal.data.crosschain;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
@XmlAccessorType(XmlAccessType.FIELD)
public class UnsignedFeeEvent {
}

View File

@ -0,0 +1,26 @@
package org.qortal.event;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
@XmlAccessorType(XmlAccessType.FIELD)
public class FeeWaitingEvent implements Event{
private long timestamp;
private String address;
public FeeWaitingEvent() {
}
public FeeWaitingEvent(long timestamp, String address) {
this.timestamp = timestamp;
this.address = address;
}
public long getTimestamp() {
return timestamp;
}
public String getAddress() {
return address;
}
}

View File

@ -0,0 +1,4 @@
package org.qortal.event;
public class LockingFeeUpdateEvent implements Event{
}

View File

@ -0,0 +1,15 @@
package org.qortal.event;
import org.qortal.crosschain.Bitcoiny;
public class RequiredFeeUpdateEvent implements Event{
private final Bitcoiny bitcoiny;
public RequiredFeeUpdateEvent(Bitcoiny bitcoiny) {
this.bitcoiny = bitcoiny;
}
public Bitcoiny getBitcoiny() {
return bitcoiny;
}
}

View File

@ -0,0 +1,43 @@
package org.qortal.network.message;
import org.qortal.data.crosschain.ForeignFeeDecodedData;
import org.qortal.utils.ForeignFeesMessageUtils;
import java.nio.ByteBuffer;
import java.util.List;
/**
* For sending online accounts info to remote peer.
*
* Same format as V2, but with added support for a mempow nonce.
*/
public class ForeignFeesMessage extends Message {
public static final long MIN_PEER_VERSION = 0x300060000L; // 3.6.0
private List<ForeignFeeDecodedData> foreignFees;
public ForeignFeesMessage(List<ForeignFeeDecodedData> foreignFeeDecodedData) {
super(MessageType.FOREIGN_FEES);
this.dataBytes = ForeignFeesMessageUtils.fromDataToSendBytes(foreignFeeDecodedData);
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private ForeignFeesMessage(int id, List<ForeignFeeDecodedData> foreignFees) {
super(id, MessageType.ONLINE_ACCOUNTS_V3);
this.foreignFees = foreignFees;
}
public List<ForeignFeeDecodedData> getForeignFees() {
return this.foreignFees;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws MessageException {
List<ForeignFeeDecodedData> foreignFeeDecodedData = ForeignFeesMessageUtils.fromSendBytesToData(bytes);
return new ForeignFeesMessage(id, foreignFeeDecodedData);
}
}

View File

@ -0,0 +1,46 @@
package org.qortal.network.message;
import org.qortal.data.crosschain.ForeignFeeDecodedData;
import org.qortal.utils.ForeignFeesMessageUtils;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class GetForeignFeesMessage extends Message {
private static final Map<Long, Map<Byte, byte[]>> EMPTY_ONLINE_ACCOUNTS = Collections.emptyMap();
private final List<ForeignFeeDecodedData> foreignFeeDecodedData;
public GetForeignFeesMessage(List<ForeignFeeDecodedData> foreignFeeDecodedData) {
super(MessageType.GET_ONLINE_ACCOUNTS_V3);
this.foreignFeeDecodedData = foreignFeeDecodedData;
// If we don't have ANY online accounts then it's an easier construction...
if (foreignFeeDecodedData.isEmpty()) {
this.dataBytes = EMPTY_DATA_BYTES;
return;
}
this.dataBytes = ForeignFeesMessageUtils.fromDataToGetBytes(foreignFeeDecodedData);
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetForeignFeesMessage(int id, List<ForeignFeeDecodedData> foreignFeeDecodedData) {
super(id, MessageType.GET_FOREIGN_FEES);
this.foreignFeeDecodedData = foreignFeeDecodedData;
}
public List<ForeignFeeDecodedData> getForeignFeeData() {
return foreignFeeDecodedData;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
return new GetForeignFeesMessage(id, ForeignFeesMessageUtils.fromGetBytesToData(bytes));
}
}

View File

@ -79,7 +79,10 @@ public enum MessageType {
GET_NAME(182, GetNameMessage::fromByteBuffer),
TRANSACTIONS(190, TransactionsMessage::fromByteBuffer),
GET_ACCOUNT_TRANSACTIONS(191, GetAccountTransactionsMessage::fromByteBuffer);
GET_ACCOUNT_TRANSACTIONS(191, GetAccountTransactionsMessage::fromByteBuffer),
FOREIGN_FEES( 200, ForeignFeesMessage::fromByteBuffer),
GET_FOREIGN_FEES( 201, GetForeignFeesMessage::fromByteBuffer);
public final int value;
public final MessageProducer fromByteBufferMethod;

View File

@ -0,0 +1,187 @@
package org.qortal.utils;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.data.crosschain.ForeignFeeDecodedData;
import org.qortal.transform.Transformer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import static org.qortal.transform.Transformer.ADDRESS_LENGTH;
/**
* Class ForeignFeesMessageUtils
*/
public class ForeignFeesMessageUtils {
private static final Logger LOGGER = LogManager.getLogger(ForeignFeesMessageUtils.class);
/**
* From Data To Send Bytes
*
* Convert foreign fee data into bytes for send messages.
*
* @param foreignFees the data
*
* @return the bytes
*/
public static byte[] fromDataToSendBytes(List<ForeignFeeDecodedData> foreignFees) {
return fromDataToBytes(foreignFees, true);
}
/**
* From Data To Bytes
*
* @param foreignFees
* @param includeSignature
* @return
*/
private static byte[] fromDataToBytes(List<ForeignFeeDecodedData> foreignFees, boolean includeSignature) {
try {
if (foreignFees.isEmpty()) {
return new byte[0];
}
else {
// allocate size for each data item for timestamp, AT address, fee and signature
int byteSize
= foreignFees.size()
*
(Transformer.TIMESTAMP_LENGTH + Transformer.ADDRESS_LENGTH + Transformer.INT_LENGTH + Transformer.SIGNATURE_LENGTH);
if( includeSignature ) byteSize += foreignFees.size() * Transformer.SIGNATURE_LENGTH;
ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
// for each foreign fee data item, convert to bytes and fill the array
for( ForeignFeeDecodedData feeData : foreignFees) {
bytes.write(Longs.toByteArray(feeData.getTimestamp()));
bytes.write(Base58.decode(feeData.getAtAddress()));
bytes.write(Ints.toByteArray(feeData.getFee()));
if( includeSignature ) bytes.write(feeData.getData());
}
return bytes.toByteArray();
}
} catch (Exception e) {
LOGGER.warn(e.getMessage());
return new byte[0];
}
}
/**
* From Send Bytes to Data
*
* @param bytes the bytes to convert to data
*
* @return the data
*/
public static List<ForeignFeeDecodedData> fromSendBytesToData(ByteBuffer bytes) {
return fromBytesToData(bytes, true);
}
/**
* From Bytes To Data
*
* @param bytes the bytes
* @param includeSignature true if the bytes include signatures
*
* @return the foreign fee data with signatures (data member)
*/
private static List<ForeignFeeDecodedData> fromBytesToData(ByteBuffer bytes, boolean includeSignature) {
if( !bytes.hasRemaining() ) return new ArrayList<>(0);
List<ForeignFeeDecodedData> foreignFees = new ArrayList<>();
try {
while (bytes.hasRemaining()) {
// read in the timestamp as a long
long timestamp = bytes.getLong();
// read in the address as a byte array with a predetermined length
byte[] atAddressBytes = new byte[ADDRESS_LENGTH];
bytes.get(atAddressBytes);
String atAddress = Base58.encode(atAddressBytes);
// rwad in the fee as an integer
int fee = bytes.getInt();
byte[] signature;
if( includeSignature ) {
signature = new byte[Transformer.SIGNATURE_LENGTH];
bytes.get(signature);
}
else {
signature = null;
}
foreignFees.add(new ForeignFeeDecodedData(timestamp, signature, atAddress, fee));
}
}
// if there are any exception, log the error as a warning and clear the list before returning it
catch (Exception e) {
LOGGER.warn(e.getMessage());
foreignFees.clear();
}
return foreignFees;
}
/**
* From Data To Get Bytes
*
* Convert foreign fees data objects into get foreign fees messages. Get messages do not include signatures.
*
* @param foreignFees the foreign fees objects
*
* @return the messages
*/
public static byte[] fromDataToGetBytes(List<ForeignFeeDecodedData> foreignFees) {
return fromDataToBytes(foreignFees, false);
}
/**
* From Get Bytes to Data
*
* Convert bytes from get foreign fees messages to foreign fees objects. Get messages do not include signatures.
*
* @param bytes the bytes to convert
*
* @return the foreign fees data objects
*/
public static List<ForeignFeeDecodedData> fromGetBytesToData(ByteBuffer bytes) {
return fromBytesToData(bytes, false);
}
/**
* Build Foreign Fees Data Message
*
* Build the unsigned message for the foreign fees data objects.
*
* @param timestamp the timestamp in milliseconds
* @param atAddress the AT address
* @param fee the fee
* @return
* @throws IOException
*/
public static byte[] buildForeignFeesDataMessage(Long timestamp, String atAddress, int fee) throws IOException {
int byteSize = Transformer.TIMESTAMP_LENGTH + Transformer.ADDRESS_LENGTH + Transformer.INT_LENGTH;
ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
bytes.write(Longs.toByteArray(timestamp));
bytes.write(Base58.decode(atAddress));
bytes.write(Ints.toByteArray(fee));
return bytes.toByteArray();
}
}

View File

@ -0,0 +1,334 @@
package org.qortal.test.network.message;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.Assert;
import org.junit.Test;
import org.qortal.crypto.Crypto;
import org.qortal.data.crosschain.ForeignFeeDecodedData;
import org.qortal.test.utils.TestUtils;
import org.qortal.transform.Transformer;
import org.qortal.utils.Base58;
import org.qortal.utils.ForeignFeesMessageUtils;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.KeyPair;
import java.security.Security;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* Class ForeignFeesMessageTests
*/
public class ForeignFeesMessageTests {
/**
* Random
*
* Random input generator for seeding keys/addresses.
*/
private static final Random RANDOM = new Random();
static {
// add the Bouncy Castle provider for keys/addresses
Security.addProvider(new BouncyCastleProvider());
}
@Test
public void testDataToSendBytesToDataEmpty() {
byte[] bytes = ForeignFeesMessageUtils.fromDataToSendBytes(new ArrayList<>(0));
List<ForeignFeeDecodedData> list = ForeignFeesMessageUtils.fromSendBytesToData(ByteBuffer.wrap(bytes));
Assert.assertNotNull(list);
Assert.assertEquals(0, list.size());
}
@Test
public void testDataToGetBytesToDataEmpty() {
byte[] bytes = ForeignFeesMessageUtils.fromDataToGetBytes(new ArrayList<>(0));
List<ForeignFeeDecodedData> list = ForeignFeesMessageUtils.fromGetBytesToData(ByteBuffer.wrap(bytes));
Assert.assertNotNull(list);
Assert.assertEquals(0, list.size());
}
@Test
public void testSignature() {
boolean exceptionThrown = false;
try {
KeyPair keyPair = TestUtils.generateKeyPair();
long timestamp = 1_000_000L;
String atAddress = generateAtAddress();
int fee = 1;
assertSignature(keyPair, timestamp, atAddress, fee);
} catch (Exception e) {
exceptionThrown = true;
}
Assert.assertFalse(exceptionThrown);
}
/**
* Assert Signature
*
* @param keyPair the key pair that is signing
* @param timestamp the timestamp for the data
* @param atAddress the AT address
* @param fee the fee
*
* @return the signature bytes
*
* @throws IOException
*/
private static byte[] assertSignature(KeyPair keyPair, long timestamp, String atAddress, int fee) throws IOException {
// build the message and sign it
byte[] message = ForeignFeesMessageUtils.buildForeignFeesDataMessage(timestamp, atAddress, fee);
byte[] signature = Crypto.sign( keyPair.getPrivate().getEncoded(), message );
// assert signaute length
Assert.assertEquals(Transformer.SIGNATURE_LENGTH, signature.length);
// assert verification
boolean verified = Crypto.verify(Crypto.toPublicKey(keyPair.getPrivate().getEncoded()), signature, message);
Assert.assertTrue(verified);
return signature;
}
@Test
public void testDataToSendBytesToDataSingle() {
Long timestamp = 1_000_000L;
String atAddress = generateAtAddress();
int fee = 1;
boolean exceptionThrown = false;
try {
// random key generation for signing data
KeyPair keyPair = TestUtils.generateKeyPair();
// data to send, a list of 1 foreign fee data
List<ForeignFeeDecodedData> sendData
= List.of(
new ForeignFeeDecodedData(timestamp, assertSignature(keyPair,timestamp,atAddress, fee), atAddress, fee)
);
// from data to bytes
byte[] sendBytes = ForeignFeesMessageUtils.fromDataToSendBytes(sendData);
// from bytes to data
List<ForeignFeeDecodedData> returnData = ForeignFeesMessageUtils.fromSendBytesToData(ByteBuffer.wrap(sendBytes));
assertListedForeignFees(sendData, returnData, true);
} catch (Exception e) {
exceptionThrown = true;
}
Assert.assertFalse(exceptionThrown);
}
@Test
public void testDataToGetBytesToDataSingle() {
Long timestamp = 1_000_000L;
String atAddress = generateAtAddress();
int fee = 1;
boolean exceptionThrown = false;
try {
// random key generation for signing data
KeyPair keyPair = TestUtils.generateKeyPair();
// data to send, a list of 1 foreign fee data
List<ForeignFeeDecodedData> sendData
= List.of(
new ForeignFeeDecodedData(timestamp, assertSignature(keyPair,timestamp,atAddress, fee), atAddress, fee)
);
// from data to bytes
byte[] sendBytes = ForeignFeesMessageUtils.fromDataToGetBytes(sendData);
// from bytes to data
List<ForeignFeeDecodedData> returnData = ForeignFeesMessageUtils.fromGetBytesToData(ByteBuffer.wrap(sendBytes));
assertListedForeignFees(sendData, returnData, false);
} catch (Exception e) {
exceptionThrown = true;
}
Assert.assertFalse(exceptionThrown);
}
@Test
public void testDataToSendBytesToDataTriple() {
Long timestamp1 = 1_000_000L;
String atAddress1 = generateAtAddress();
int fee1 = 1;
Long timestamp2 = 2_000_000L;
String atAddress2 = generateAtAddress();
int fee2 = 2;
Long timestamp3 = 5_000_000L;
String atAddress3 = generateAtAddress();
int fee3 = 3;
boolean exceptionThrown = false;
try {
// random key generation for signing data
KeyPair keyPair1 = TestUtils.generateKeyPair();
KeyPair keyPair2 = TestUtils.generateKeyPair();
// data to send, a list of 3 foreign fee data
List<ForeignFeeDecodedData> sendData
= List.of(
new ForeignFeeDecodedData(timestamp1, assertSignature(keyPair1,timestamp1,atAddress1, fee1), atAddress1, fee1),
new ForeignFeeDecodedData(timestamp2, assertSignature(keyPair1,timestamp2,atAddress2, fee2), atAddress2, fee2),
new ForeignFeeDecodedData(timestamp3, assertSignature(keyPair2,timestamp3,atAddress3, fee3), atAddress3, fee3)
);
// from data to bytes
byte[] sendBytes = ForeignFeesMessageUtils.fromDataToSendBytes(sendData);
// from bytes to data
List<ForeignFeeDecodedData> returnData = ForeignFeesMessageUtils.fromSendBytesToData(ByteBuffer.wrap(sendBytes));
assertListedForeignFees(sendData, returnData, true);
} catch (Exception e) {
exceptionThrown = true;
}
Assert.assertFalse(exceptionThrown);
}
@Test
public void testDataToGetBytesToDataTriple() {
Long timestamp1 = 1_000_000L;
String atAddress1 = generateAtAddress();
int fee1 = 1;
Long timestamp2 = 2_000_000L;
String atAddress2 = generateAtAddress();
int fee2 = 2;
Long timestamp3 = 5_000_000L;
String atAddress3 = generateAtAddress();
int fee3 = 3;
boolean exceptionThrown = false;
try {
// random key generation for signing data
KeyPair keyPair1 = TestUtils.generateKeyPair();
KeyPair keyPair2 = TestUtils.generateKeyPair();
// data to send, a list of 3 foreign fee data
List<ForeignFeeDecodedData> sendData
= List.of(
new ForeignFeeDecodedData(timestamp1, assertSignature(keyPair1,timestamp1,atAddress1, fee1), atAddress1, fee1),
new ForeignFeeDecodedData(timestamp2, assertSignature(keyPair1,timestamp2,atAddress2, fee2), atAddress2, fee2),
new ForeignFeeDecodedData(timestamp3, assertSignature(keyPair2,timestamp3,atAddress3, fee3), atAddress3, fee3)
);
// from data to bytes
byte[] sendBytes = ForeignFeesMessageUtils.fromDataToGetBytes(sendData);
// from bytes to data
List<ForeignFeeDecodedData> returnData = ForeignFeesMessageUtils.fromGetBytesToData(ByteBuffer.wrap(sendBytes));
assertListedForeignFees(sendData, returnData, false);
} catch (Exception e) {
exceptionThrown = true;
}
Assert.assertFalse(exceptionThrown);
}
/**
* Assert Listed Foreign Fees
*
* @param expectedList
* @param actualList
* @param includeSignature
*/
private static void assertListedForeignFees(List<ForeignFeeDecodedData> expectedList, List<ForeignFeeDecodedData> actualList, boolean includeSignature) {
int expectedSize = expectedList.size();
// basic assertions on return data
Assert.assertNotNull(actualList);
Assert.assertEquals(expectedSize, actualList.size());
for( int index = 0; index < expectedSize; index++ ) {
// expected and actual fee data
ForeignFeeDecodedData expected = expectedList.get(index);
ForeignFeeDecodedData actual = actualList.get(index);
assertForeignFeeEquality(expected, actual, includeSignature);
}
}
/**
* Assert Foreign Fee Equality
*
* @param expected the expected data, for comparison
* @param actual the actual data, the response to evaluate
* @param includeSignature
*/
private static void assertForeignFeeEquality(ForeignFeeDecodedData expected, ForeignFeeDecodedData actual, boolean includeSignature) {
// assert
Assert.assertEquals(expected, actual);
if( includeSignature ) {
// get the data members of each, since the data members are not part of the object comparison above
byte[] expectedData = expected.getData();
byte[] actualData = actual.getData();
// assert data members, must encode them to strings for comparisons
Assert.assertNotNull(actualData);
Assert.assertEquals(Base58.encode(expectedData), Base58.encode(actualData));
}
}
/**
* Generate AT Address
*
* Generate AT address using a random inpute seed.
*
* @return the AT address
*/
private static String generateAtAddress() {
byte[] signature = new byte[64];
RANDOM.nextBytes(signature);
String atAddress = Crypto.toATAddress(signature);
return atAddress;
}
}