47: Add explicit format versions to configuration files

This commit is contained in:
2026-08-05 10:12:39 +02:00
parent 4cc14ceef5
commit 12843e1501
16 changed files with 416 additions and 109 deletions
@@ -3,17 +3,19 @@ package com.fanitas.evelyn.core.impl.ref;
import com.fanitas.evelyn.core.State;
import com.fanitas.evelyn.raydium.RaydiumLiquidityPoolPositionAccounting;
import com.fanitas.evelyn.raydium.RaydiumLiquidityPoolPositionConcentrated;
import com.r35157.libs.configuration.ConfigurationFormatVersionValidator;
import com.r35157.libs.raydium.*;
import com.r35157.libs.valuetypes.basic.WellKnownCurrencyTypes;
import com.r35157.libs.valuetypes.basic.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -72,14 +74,26 @@ public class StateImpl implements State {
}
private void updateStateFromPersistence() throws IOException {
try (BufferedReader reader = Files.newBufferedReader(PATH_EVELYN_STATE, StandardCharsets.UTF_8)) {
iterationInterval = readIterationInterval(reader);
syrupOwnedByEvelyn = readSyrupOwnedByEvelyn(reader);
solanaAddressForEvelynIOU = readSolanaAddress(reader);
solanaAddressForEvelynIOUBurner = readSolanaAddress(reader);
raydiumPoolId = readRaydiumPoolId(reader);
curveWidth = readCurveWidth(reader);
}
List<String> lines = Files.readAllLines(
PATH_EVELYN_STATE,
StandardCharsets.UTF_8
);
int formatVersionLineIndex = ConfigurationFormatVersionValidator.validate(
PATH_EVELYN_STATE,
lines,
SUPPORTED_EVELYN_FORMAT_VERSION
);
Iterator<String> lineIterator = lines.subList(
formatVersionLineIndex + 1,
lines.size()
).iterator();
iterationInterval = readIterationInterval(lineIterator);
syrupOwnedByEvelyn = readSyrupOwnedByEvelyn(lineIterator);
solanaAddressForEvelynIOU = readSolanaAddress(lineIterator);
solanaAddressForEvelynIOUBurner = readSolanaAddress(lineIterator);
raydiumPoolId = readRaydiumPoolId(lineIterator);
curveWidth = readCurveWidth(lineIterator);
}
private void updateStateFromRaydium() throws IOException, InterruptedException {
@@ -176,33 +190,37 @@ public class StateImpl implements State {
private Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionAccounting> readPositionAccounting()
throws IOException {
Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionAccounting> result = new HashMap<>();
List<String> lines = Files.readAllLines(PATH_POSITIONS, StandardCharsets.UTF_8);
int formatVersionLineIndex = ConfigurationFormatVersionValidator.validate(
PATH_POSITIONS,
lines,
SUPPORTED_POSITIONS_FORMAT_VERSION
);
try (BufferedReader reader = Files.newBufferedReader(PATH_POSITIONS, StandardCharsets.UTF_8)) {
String line;
for (int lineIndex = formatVersionLineIndex + 1;
lineIndex < lines.size();
lineIndex++) {
String line = lines.get(lineIndex).trim();
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
// Remove comments at end of line
int index = line.indexOf('#');
if(index > -1) {
line = line.substring(0, index).trim();
}
// Remove comments at end of line
int index = line.indexOf('#');
if(index > -1) {
line = line.substring(0, index).trim();
}
RaydiumLiquidityPoolPositionAccounting accounting = mapPositionAccounting(line);
RaydiumLiquidityPoolPositionAccounting accounting = mapPositionAccounting(line);
RaydiumLiquidityPoolPositionAccounting previous = result.put(
accounting.nftId(),
accounting
);
RaydiumLiquidityPoolPositionAccounting previous = result.put(
accounting.nftId(),
accounting
);
if (previous != null) {
throw new IOException("Duplicate position accounting for NFT id: " + accounting.nftId());
}
if (previous != null) {
throw new IOException("Duplicate position accounting for NFT id: " + accounting.nftId());
}
}
@@ -234,47 +252,46 @@ public class StateImpl implements State {
);
}
private ΩSolanaAddressΩ readSolanaAddress(BufferedReader reader) throws IOException {
ΩSolanaAddressΩ sa = readLineFromFile(reader);
private ΩSolanaAddressΩ readSolanaAddress(Iterator<String> lineIterator) throws IOException {
ΩSolanaAddressΩ sa = readLineFromFile(lineIterator);
return sa;
}
private ΩRaydiumLiquidityPoolIdΩ readRaydiumPoolId(BufferedReader reader) throws IOException {
ΩRaydiumLiquidityPoolIdΩ rlpid = readLineFromFile(reader);
private ΩRaydiumLiquidityPoolIdΩ readRaydiumPoolId(Iterator<String> lineIterator) throws IOException {
ΩRaydiumLiquidityPoolIdΩ rlpid = readLineFromFile(lineIterator);
return rlpid;
}
private ΩCurveWidthΩ readCurveWidth(BufferedReader reader) throws IOException {
String cwStr = readLineFromFile(reader);
private ΩCurveWidthΩ readCurveWidth(Iterator<String> lineIterator) throws IOException {
String cwStr = readLineFromFile(lineIterator);
ΩCurveWidthΩ cw = new ΩCurveWidthΩ(cwStr);
return cw;
}
private ΩmilliSecondsΩ readIterationInterval(BufferedReader reader) throws IOException {
String iterationIntervalStr = readLineFromFile(reader);
private ΩmilliSecondsΩ readIterationInterval(Iterator<String> lineIterator) throws IOException {
String iterationIntervalStr = readLineFromFile(lineIterator);
ΩmilliSecondsΩ millis = Long.parseLong(iterationIntervalStr) * 1000;
return millis;
}
private ΩSyrupAmountΩ readSyrupOwnedByEvelyn(BufferedReader reader) throws IOException {
String syrupOwnedByEvelynStr = readLineFromFile(reader);
private ΩSyrupAmountΩ readSyrupOwnedByEvelyn(Iterator<String> lineIterator) throws IOException {
String syrupOwnedByEvelynStr = readLineFromFile(lineIterator);
CurrencyType ct = WellKnownCurrencyTypes.SYRUPUSDC.getCurrencyType();
ΩSyrupAmountΩ ma = stringToMoneyAmount(syrupOwnedByEvelynStr, ct);
return ma;
}
private static String readLineFromFile(BufferedReader reader) throws IOException {
String line;
while ((line = reader.readLine()) != null) {
private static String readLineFromFile(Iterator<String> lineIterator) {
while (lineIterator.hasNext()) {
String line = lineIterator.next();
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
break;
return line;
}
return line;
return null;
}
private MoneyAmount bigDecimalToMoneyAmount(BigDecimal bd, CurrencyType ct) {
@@ -290,6 +307,8 @@ public class StateImpl implements State {
private static final Path PATH_EVELYN_STATE = Path.of("conf/evelyn.conf");
private static final Path PATH_POSITIONS = Path.of("conf/positions.conf");
private static final int SUPPORTED_EVELYN_FORMAT_VERSION = 1;
private static final int SUPPORTED_POSITIONS_FORMAT_VERSION = 1;
private final Raydium raydium;
@@ -1,5 +1,7 @@
package com.r35157.jupiterperpsalarm.impl.ref;
import com.r35157.libs.configuration.ConfigurationFormatVersionValidator;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -11,10 +13,19 @@ public final class AlarmConfigurationParser {
public static AlarmConfiguration parse(Path path) throws IOException {
List<String> lines = Files.readAllLines(path);
int formatVersionLineIndex = ConfigurationFormatVersionValidator.validate(
path,
lines,
SUPPORTED_FORMAT_VERSION
);
List<PriceAlarmDefinition> alarms = new ArrayList<>();
Map<String, String> constants = new LinkedHashMap<>();
for (int lineNumber = 1; lineNumber <= lines.size(); lineNumber++) {
if (lineNumber - 1 == formatVersionLineIndex) {
continue;
}
String line = lines.get(lineNumber - 1);
String trimmed = line.trim();
@@ -313,6 +324,7 @@ public final class AlarmConfigurationParser {
private static final Pattern PERSISTENT_GRACE_PERIOD_PATTERN =
Pattern.compile("(\\d+)(ms|s|m|h|d|w|M|y)");
private static final int SUPPORTED_FORMAT_VERSION = 1;
private AlarmConfigurationParser() {
}
@@ -1,5 +1,7 @@
package com.r35157.jupiterperpsalarm.impl.ref;
import com.r35157.libs.configuration.ConfigurationFormatVersionValidator;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -49,9 +51,18 @@ public final class ConsoleAlarmAction implements ConfiguredAlarmAction {
private static Set<Integer> parseAlarmIds(Path path) throws IOException {
List<String> lines = Files.readAllLines(path);
int formatVersionLineIndex = ConfigurationFormatVersionValidator.validate(
path,
lines,
SUPPORTED_FORMAT_VERSION
);
Set<Integer> result = new LinkedHashSet<>();
for (int lineNumber = 1; lineNumber <= lines.size(); lineNumber++) {
if (lineNumber - 1 == formatVersionLineIndex) {
continue;
}
String line = removeComment(lines.get(lineNumber - 1)).trim();
if (line.isEmpty()) {
continue;
@@ -86,5 +97,7 @@ public final class ConsoleAlarmAction implements ConfiguredAlarmAction {
return commentStart < 0 ? line : line.substring(0, commentStart);
}
private static final int SUPPORTED_FORMAT_VERSION = 1;
private final Set<Integer> alarmIds;
}
@@ -1,5 +1,7 @@
package com.r35157.jupiterperpsalarm.impl.ref;
import com.r35157.libs.configuration.ConfigurationFormatVersionValidator;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Files;
@@ -17,6 +19,11 @@ public final class JupiterPerpsPositionDecreaseAlarmActionConfigurationParser {
) throws IOException {
Objects.requireNonNull(variableResolver, "variableResolver");
List<String> lines = Files.readAllLines(path);
int formatVersionLineIndex = ConfigurationFormatVersionValidator.validate(
path,
lines,
SUPPORTED_FORMAT_VERSION
);
Map<Integer, JupiterPerpsPositionDecreaseAlarmActionConfiguration.PositionDecrease>
positionDecreases = new LinkedHashMap<>();
ΩSolanaWalletIdΩ walletId = null;
@@ -24,6 +31,10 @@ public final class JupiterPerpsPositionDecreaseAlarmActionConfigurationParser {
ΩUSDCAmountΩ reservePositionSizeUsdLimit = null;
for (int lineNumber = 1; lineNumber <= lines.size(); lineNumber++) {
if (lineNumber - 1 == formatVersionLineIndex) {
continue;
}
String line = removeComment(lines.get(lineNumber - 1)).trim();
if (line.isEmpty()) {
continue;
@@ -162,6 +173,8 @@ public final class JupiterPerpsPositionDecreaseAlarmActionConfigurationParser {
: line.substring(0, commentStart);
}
private static final int SUPPORTED_FORMAT_VERSION = 1;
private JupiterPerpsPositionDecreaseAlarmActionConfigurationParser() {
}
}
}
@@ -1,5 +1,7 @@
package com.r35157.jupiterperpsalarm.impl.ref;
import com.r35157.libs.configuration.ConfigurationFormatVersionValidator;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Files;
@@ -17,6 +19,11 @@ public final class JupiterPerpsPositionIncreaseAlarmActionConfigurationParser {
) throws IOException {
Objects.requireNonNull(variableResolver, "variableResolver");
List<String> lines = Files.readAllLines(path);
int formatVersionLineIndex = ConfigurationFormatVersionValidator.validate(
path,
lines,
SUPPORTED_FORMAT_VERSION
);
Map<Integer, JupiterPerpsPositionIncreaseAlarmActionConfiguration.PositionIncrease>
positionIncreases = new LinkedHashMap<>();
ΩSolanaWalletIdΩ walletId = null;
@@ -24,6 +31,10 @@ public final class JupiterPerpsPositionIncreaseAlarmActionConfigurationParser {
ΩUSDCAmountΩ reserveUsdcLimit = null;
for (int lineNumber = 1; lineNumber <= lines.size(); lineNumber++) {
if (lineNumber - 1 == formatVersionLineIndex) {
continue;
}
String line = removeComment(lines.get(lineNumber - 1)).trim();
if (line.isEmpty()) {
continue;
@@ -139,6 +150,8 @@ public final class JupiterPerpsPositionIncreaseAlarmActionConfigurationParser {
return commentStart < 0 ? line : line.substring(0, commentStart);
}
private static final int SUPPORTED_FORMAT_VERSION = 1;
private JupiterPerpsPositionIncreaseAlarmActionConfigurationParser() {
}
}
@@ -1,6 +1,7 @@
package com.r35157.jupiterperpsalarm.impl.ref;
import com.r35157.jupiterperpsalarm.AlarmSeverity;
import com.r35157.libs.configuration.ConfigurationFormatVersionValidator;
import java.io.IOException;
import java.nio.file.Files;
@@ -18,12 +19,21 @@ public final class PushoverAlarmActionConfigurationParser {
) throws IOException {
Objects.requireNonNull(variableResolver, "variableResolver");
List<String> lines = Files.readAllLines(path);
int formatVersionLineIndex = ConfigurationFormatVersionValidator.validate(
path,
lines,
SUPPORTED_FORMAT_VERSION
);
Map<Integer, PushoverAlarmActionConfiguration.Notification> notifications =
new LinkedHashMap<>();
String applicationToken = null;
String userKey = null;
for (int lineNumber = 1; lineNumber <= lines.size(); lineNumber++) {
if (lineNumber - 1 == formatVersionLineIndex) {
continue;
}
String line = lines.get(lineNumber - 1);
String trimmed = line.trim();
@@ -205,6 +215,8 @@ public final class PushoverAlarmActionConfigurationParser {
private int position;
}
private static final int SUPPORTED_FORMAT_VERSION = 1;
private PushoverAlarmActionConfigurationParser() {
}
}
@@ -0,0 +1,152 @@
package com.r35157.libs.configuration;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class ConfigurationFormatVersionValidator {
public static int validate(
Path path,
List<String> lines,
int supportedVersion
) {
Objects.requireNonNull(path, "path");
Objects.requireNonNull(lines, "lines");
if (supportedVersion <= 0) {
throw new IllegalArgumentException(
"Supported configuration format version must be a positive integer"
);
}
int firstConfigurationEntryIndex = -1;
List<FormatVersionDeclaration> declarations = new ArrayList<>();
for (int lineIndex = 0; lineIndex < lines.size(); lineIndex++) {
String trimmedLine = lines.get(lineIndex).trim();
if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) {
continue;
}
if (firstConfigurationEntryIndex < 0) {
firstConfigurationEntryIndex = lineIndex;
}
if (trimmedLine.startsWith(FORMAT_VERSION_KEY)) {
declarations.add(parseDeclaration(path, trimmedLine, lineIndex));
}
}
if (declarations.isEmpty()) {
throw new IllegalArgumentException(
path + ": Missing " + FORMAT_VERSION_KEY + " declaration. "
+ "It must be the first actual configuration entry."
);
}
if (declarations.size() > 1) {
String lineNumbers = declarations.stream()
.map(declaration -> Integer.toString(declaration.lineIndex() + 1))
.reduce((left, right) -> left + ", " + right)
.orElseThrow();
throw new IllegalArgumentException(
path + ": Duplicate " + FORMAT_VERSION_KEY
+ " declarations on lines " + lineNumbers
);
}
FormatVersionDeclaration declaration = declarations.get(0);
if (declaration.lineIndex() != firstConfigurationEntryIndex) {
throw new IllegalArgumentException(
path + ":" + (declaration.lineIndex() + 1) + ": "
+ FORMAT_VERSION_KEY
+ " must be the first actual configuration entry"
);
}
if (declaration.version() != supportedVersion) {
throw versionMismatch(
path,
declaration.version(),
supportedVersion
);
}
return declaration.lineIndex();
}
private static FormatVersionDeclaration parseDeclaration(
Path path,
String trimmedLine,
int lineIndex
) {
Matcher matcher = FORMAT_VERSION_PATTERN.matcher(trimmedLine);
if (!matcher.matches()) {
throw malformedVersion(path, lineIndex, trimmedLine, null);
}
try {
int version = Integer.parseInt(matcher.group(1));
if (version <= 0) {
throw malformedVersion(path, lineIndex, trimmedLine, null);
}
return new FormatVersionDeclaration(lineIndex, version);
} catch (NumberFormatException exception) {
throw malformedVersion(path, lineIndex, trimmedLine, exception);
}
}
private static IllegalArgumentException malformedVersion(
Path path,
int lineIndex,
String line,
Exception cause
) {
return new IllegalArgumentException(
path + ":" + (lineIndex + 1) + ": Invalid "
+ FORMAT_VERSION_KEY + " declaration '" + line
+ "'. Expected " + FORMAT_VERSION_KEY
+ "=<positive integer>",
cause
);
}
private static IllegalArgumentException versionMismatch(
Path path,
int fileVersion,
int supportedVersion
) {
return new IllegalArgumentException(
"Configuration format version mismatch in " + path + ":\n"
+ "File version: " + fileVersion + "\n"
+ "Supported version: " + supportedVersion + "\n\n"
+ "The configuration file and the running application "
+ "are not compatible.\n"
+ "Update the configuration file or compile and deploy "
+ "the correct application version."
);
}
private record FormatVersionDeclaration(
int lineIndex,
int version
) {
}
private static final String FORMAT_VERSION_KEY = "FORMAT_VERSION";
private static final Pattern FORMAT_VERSION_PATTERN =
Pattern.compile(FORMAT_VERSION_KEY + "=(\\d+)");
private ConfigurationFormatVersionValidator() {
}
}