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
+33
View File
@@ -11,6 +11,39 @@ This module was initially created with the following command:
## Usage ## Usage
Explain how to use this module Explain how to use this module
## Configuration format versions
Every application-owned, human-maintained runtime configuration file starts
with an explicit format version:
```text
FORMAT_VERSION=1
```
Blank lines and full-line comments may precede the declaration, but it must be
the first actual configuration entry. Loading fails before any other content
is parsed when the declaration is missing, duplicated, malformed, misplaced,
or different from the version supported by the corresponding loader.
Versions are independent for each configuration format. A breaking change to
one format, such as `alarms.conf`, does not change any other format's version.
When a breaking format change is made, update both the loader's hardcoded
`SUPPORTED_FORMAT_VERSION` and the declaration in the runtime and example
configuration files. Compatible additions do not require a version increment.
This policy currently applies to:
- `alarms.conf`
- `alarmaction_Console.conf`
- `alarmaction_Pushover.conf`
- `alarmaction_JupiterPerpsPositionIncreaseAlarmAction.conf`
- `alarmaction_JupiterPerpsPositionDecreaseAlarmAction.conf`
- `evelyn.conf`
- `positions.conf`
Third-party configuration formats such as Log4j's XML configuration are
versioned by their owning library and do not use this declaration.
## Release log: ## Release log:
| Date | Version | Description | | Date | Version | Description |
|:-----------------|:--------------:|----------------------| |:-----------------|:--------------:|----------------------|
+32 -5
View File
@@ -1,10 +1,10 @@
# Jupiter Perps Price Alarm # Jupiter Perps Price Alarm
The Jupiter Perps Price Alarm monitors the Jupiter Perps aggregated oracle accounts for SOL, ETH, and BTC. It evaluates configurable price conditions and dispatches matching alarms to independently configured console, Pushover, and Jupiter position-increase actions. The Jupiter Perps Price Alarm monitors the Jupiter Perps aggregated oracle accounts for SOL, ETH, and BTC. It evaluates configurable price conditions and dispatches matching alarms to independently configured console, Pushover, and Jupiter position-increase or position-decrease actions.
Oracle prices are received through Solana WebSocket `accountSubscribe` using `processed` commitment. The application does not poll prices once per second. It also fetches the current oracle account state whenever a WebSocket connection opens, reconnects automatically, and supports multiple RPC endpoints for redundancy. Oracle prices are received through Solana WebSocket `accountSubscribe` using `processed` commitment. The application does not poll prices once per second. It also fetches the current oracle account state whenever a WebSocket connection opens, reconnects automatically, and supports multiple RPC endpoints for redundancy.
> **Important:** This application is not necessarily notification-only. Every active entry in `alarmaction_JupiterPerpsPositionIncreaseAlarmAction.conf` can sign and submit a real Jupiter Perps transaction. See [Automatic position increase](#automatic-position-increase) before running it. > **Important:** This application is not necessarily notification-only. Every active entry in a Jupiter Perps position action file can sign and submit a real transaction. Review both transaction action files before running it.
## Startup flow ## Startup flow
@@ -15,7 +15,7 @@ The application performs the following steps before it starts monitoring prices:
3. Populate entry-price and liquidation-price variables. 3. Populate entry-price and liquidation-price variables.
4. Resolve and validate every alarm condition. 4. Resolve and validate every alarm condition.
5. Start the periodic variable refresher and file-trigger watcher. 5. Start the periodic variable refresher and file-trigger watcher.
6. Parse and validate the three action configuration files. 6. Parse and validate the four action configuration files.
7. Open one oracle WebSocket connection per configured asset and RPC endpoint. 7. Open one oracle WebSocket connection per configured asset and RPC endpoint.
Startup fails before WebSockets are opened if an alarm condition or action configuration contains an unresolved variable or invalid value. Startup fails before WebSockets are opened if an alarm condition or action configuration contains an unresolved variable or invalid value.
@@ -75,7 +75,7 @@ The variable-refresh file watcher uses the configuration file's parent directory
## Configuration files ## Configuration files
The application uses four configuration files: The application uses five configuration files:
| File | Purpose | | File | Purpose |
|---|---| |---|---|
@@ -83,8 +83,9 @@ The application uses four configuration files:
| `alarmaction_Console.conf` | Alarm IDs written to the console | | `alarmaction_Console.conf` | Alarm IDs written to the console |
| `alarmaction_Pushover.conf` | Pushover credentials and notifications | | `alarmaction_Pushover.conf` | Pushover credentials and notifications |
| `alarmaction_JupiterPerpsPositionIncreaseAlarmAction.conf` | Jupiter wallet, signer, and position increases | | `alarmaction_JupiterPerpsPositionIncreaseAlarmAction.conf` | Jupiter wallet, signer, and position increases |
| `alarmaction_JupiterPerpsPositionDecreaseAlarmAction.conf` | Jupiter wallet, signer, and position decreases |
The three action files are resolved in the same directory as the file supplied through `--config`. All four files are required at startup, although an action can contain no alarm IDs. Blank lines and lines beginning with `#` are ignored. The four action files are resolved in the same directory as the file supplied through `--config`. All five files are required at startup, although an action can contain no alarm IDs. Blank lines and lines beginning with `#` are ignored.
Example files: Example files:
@@ -92,6 +93,26 @@ Example files:
- [alarmaction_Console.conf.example](conf/alarmaction_Console.conf.example) - [alarmaction_Console.conf.example](conf/alarmaction_Console.conf.example)
- [alarmaction_Pushover.conf.example](conf/alarmaction_Pushover.conf.example) - [alarmaction_Pushover.conf.example](conf/alarmaction_Pushover.conf.example)
- [alarmaction_JupiterPerpsPositionIncreaseAlarmAction.conf.example](conf/alarmaction_JupiterPerpsPositionIncreaseAlarmAction.conf.example) - [alarmaction_JupiterPerpsPositionIncreaseAlarmAction.conf.example](conf/alarmaction_JupiterPerpsPositionIncreaseAlarmAction.conf.example)
- [alarmaction_JupiterPerpsPositionDecreaseAlarmAction.conf.example](conf/alarmaction_JupiterPerpsPositionDecreaseAlarmAction.conf.example)
### Format version
Every configuration file must declare its format version as its first actual
configuration entry:
```text
FORMAT_VERSION=1
```
Blank lines and full-line comments may appear before it. The application
rejects a missing, duplicate, malformed, misplaced, older, or newer version
before parsing the remaining configuration.
Each file format has an independent version. Increment only the affected
format when making an incompatible change, and update both its parser constant
and its runtime and example files. See
[Configuration format versions](README.md#configuration-format-versions) for
the project-wide policy.
## Shared variables ## Shared variables
@@ -302,6 +323,8 @@ Actions are executed sequentially through `CompositeAlarmAction`. A runtime fail
`alarmaction_Console.conf` contains one alarm ID per line: `alarmaction_Console.conf` contains one alarm ID per line:
```text ```text
FORMAT_VERSION=1
1 1
4 4
5 5
@@ -314,6 +337,8 @@ Only listed alarms are written to the console. The output includes the asset, cu
`alarmaction_Pushover.conf` contains the credentials followed by zero or more notification definitions: `alarmaction_Pushover.conf` contains the credentials followed by zero or more notification definitions:
```text ```text
FORMAT_VERSION=1
APPLICATION_TOKEN <pushover-application-token> APPLICATION_TOKEN <pushover-application-token>
USER_KEY <pushover-user-key> USER_KEY <pushover-user-key>
@@ -340,6 +365,8 @@ Credentials, severity, and notes may reference shared variables. Credentials and
`alarmaction_JupiterPerpsPositionIncreaseAlarmAction.conf` contains the wallet and signer followed by zero or more position increases: `alarmaction_JupiterPerpsPositionIncreaseAlarmAction.conf` contains the wallet and signer followed by zero or more position increases:
```text ```text
FORMAT_VERSION=1
WALLET_ID {{JUPITER_PERPS_WALLET}} WALLET_ID {{JUPITER_PERPS_WALLET}}
SIGNER_KEY_NAME <jup-key-name> SIGNER_KEY_NAME <jup-key-name>
+20 -19
View File
@@ -1,21 +1,22 @@
FORMAT_VERSION=1
# Alarm ids handled by ConsoleAlarmAction # Alarm ids handled by ConsoleAlarmAction
########################################## ##########################################
#1
1 #2
2 #3
3 #4
4 #5
5 #6
6 #7
7 #8
8 #9
9 #10
10 #11
11 #12
12 #13
13 #14
14 #15
15 #16
16 #17
17 #18
18
@@ -1,3 +1,5 @@
FORMAT_VERSION=1
# Wallet and signer used for position-decrease transactions # Wallet and signer used for position-decrease transactions
################################################################ ################################################################
WALLET_ID {{JUPITER_PERPS_WALLET}} WALLET_ID {{JUPITER_PERPS_WALLET}}
@@ -1,3 +1,5 @@
FORMAT_VERSION=1
# Wallet and signer used for position-increase transactions # Wallet and signer used for position-increase transactions
################################################################ ################################################################
WALLET_ID {{JUPITER_PERPS_WALLET}} WALLET_ID {{JUPITER_PERPS_WALLET}}
+20 -18
View File
@@ -1,3 +1,5 @@
FORMAT_VERSION=1
# Pushover credentials # Pushover credentials
########################################## ##########################################
# Values and notes may reference {{VARIABLES}} defined in alarms.conf. # Values and notes may reference {{VARIABLES}} defined in alarms.conf.
@@ -9,31 +11,31 @@ USER_KEY <pushover-user-key>
########################################## ##########################################
# SOL SHORT # SOL SHORT
1 CRITICAL "🚨 SOL Short add 1.1x" #1 CRITICAL "🚨 SOL Short add 1.1x"
2 INFO "🌱 SOL Short add 3x" #2 INFO "🌱 SOL Short add 3x"
3 INFO "💵 SOL Short close" #3 INFO "💵 SOL Short close"
# SOL LONG # SOL LONG
4 CRITICAL "🚨 SOL Long add 1.1x" #4 CRITICAL "🚨 SOL Long add 1.1x"
5 INFO "🌱 SOL Long add 3x" #5 INFO "🌱 SOL Long add 3x"
6 INFO "💵 SOL Long close" #6 INFO "💵 SOL Long close"
# ETH SHORT # ETH SHORT
7 CRITICAL "🚨 ETH Short add 1.1x" #7 CRITICAL "🚨 ETH Short add 1.1x"
8 INFO "🌱 ETH Short add 3x" #8 INFO "🌱 ETH Short add 3x"
9 INFO "💵 ETH Short close" #9 INFO "💵 ETH Short close"
# ETH LONG # ETH LONG
10 CRITICAL "🚨 ETH Long add 1.1x" #10 CRITICAL "🚨 ETH Long add 1.1x"
11 INFO "🌱 ETH Long add 3x" #11 INFO "🌱 ETH Long add 3x"
12 INFO "💵 ETH Long close" #12 INFO "💵 ETH Long close"
# BTC SHORT # BTC SHORT
13 CRITICAL "🚨 BTC Short add 1.1x" #13 CRITICAL "🚨 BTC Short add 1.1x"
14 INFO "🌱 BTC Short add 3x" #14 INFO "🌱 BTC Short add 3x"
15 INFO "💵 BTC Short close" #15 INFO "💵 BTC Short close"
# BTC LONG # BTC LONG
16 CRITICAL "🚨 BTC Long add 1.1x" #16 CRITICAL "🚨 BTC Long add 1.1x"
17 INFO "🌱 BTC Long add 3x" #17 INFO "🌱 BTC Long add 3x"
18 INFO "💵 BTC Long close" #18 INFO "💵 BTC Long close"
+20 -18
View File
@@ -1,3 +1,5 @@
FORMAT_VERSION=1
# General configurations # General configurations
##################################################### #####################################################
{{JUPITER_PERPS_WALLET}} vj98roDZ7744EBfxyuDFkKpEGCsKQLr7K8UFRumJNHf {{JUPITER_PERPS_WALLET}} vj98roDZ7744EBfxyuDFkKpEGCsKQLr7K8UFRumJNHf
@@ -20,34 +22,34 @@
################################################################################################ ################################################################################################
# SOL SHORT # SOL SHORT
1 SOL >={{SOL_SHORT_LIQ_PRICE}}-1% PERSISTENT:1m #1 SOL >={{SOL_SHORT_LIQ_PRICE}}-1% PERSISTENT:1m
2 SOL [{{SOL_SHORT_LIQ_PRICE}}-3%-->{{SOL_SHORT_LIQ_PRICE}}-1%) PERSISTENT:1h #2 SOL [{{SOL_SHORT_LIQ_PRICE}}-3%-->{{SOL_SHORT_LIQ_PRICE}}-1%) PERSISTENT:1h
3 SOL <={{SOL_SHORT_ENTRY_PRICE}}-5% PERSISTENT:1h #3 SOL <={{SOL_SHORT_ENTRY_PRICE}}-5% PERSISTENT:1h
# SOL LONG # SOL LONG
4 SOL <={{SOL_LONG_LIQ_PRICE}}+1% PERSISTENT:1m #4 SOL <={{SOL_LONG_LIQ_PRICE}}+1% PERSISTENT:1m
5 SOL ({{SOL_LONG_LIQ_PRICE}}+1%-->{{SOL_LONG_LIQ_PRICE}}+3%] PERSISTENT:1h #5 SOL ({{SOL_LONG_LIQ_PRICE}}+1%-->{{SOL_LONG_LIQ_PRICE}}+3%] PERSISTENT:1h
6 SOL >={{SOL_LONG_ENTRY_PRICE}}+5% PERSISTENT:1h #6 SOL >={{SOL_LONG_ENTRY_PRICE}}+5% PERSISTENT:1h
# ETH SHORT # ETH SHORT
7 ETH >={{ETH_SHORT_LIQ_PRICE}}-1% PERSISTENT:1m #7 ETH >={{ETH_SHORT_LIQ_PRICE}}-1% PERSISTENT:1m
8 ETH [{{ETH_SHORT_LIQ_PRICE}}-3%-->{{ETH_SHORT_LIQ_PRICE}}-1%) PERSISTENT:1h #8 ETH [{{ETH_SHORT_LIQ_PRICE}}-3%-->{{ETH_SHORT_LIQ_PRICE}}-1%) PERSISTENT:1h
9 ETH <={{ETH_SHORT_ENTRY_PRICE}}-5% PERSISTENT:1h #9 ETH <={{ETH_SHORT_ENTRY_PRICE}}-5% PERSISTENT:1h
# ETH LONG # ETH LONG
10 ETH <={{ETH_LONG_LIQ_PRICE}}+1% PERSISTENT:1m #10 ETH <={{ETH_LONG_LIQ_PRICE}}+1% PERSISTENT:1m
11 ETH ({{ETH_LONG_LIQ_PRICE}}+1%-->{{ETH_LONG_LIQ_PRICE}}+3%] PERSISTENT:1h #11 ETH ({{ETH_LONG_LIQ_PRICE}}+1%-->{{ETH_LONG_LIQ_PRICE}}+3%] PERSISTENT:1h
12 ETH >={{ETH_LONG_ENTRY_PRICE}}+5% PERSISTENT:1h #12 ETH >={{ETH_LONG_ENTRY_PRICE}}+5% PERSISTENT:1h
# BTC SHORT # BTC SHORT
13 BTC >={{BTC_SHORT_LIQ_PRICE}}-1% PERSISTENT:1m #13 BTC >={{BTC_SHORT_LIQ_PRICE}}-1% PERSISTENT:1m
14 BTC [{{BTC_SHORT_LIQ_PRICE}}-3%-->{{BTC_SHORT_LIQ_PRICE}}-1%) PERSISTENT:1h #14 BTC [{{BTC_SHORT_LIQ_PRICE}}-3%-->{{BTC_SHORT_LIQ_PRICE}}-1%) PERSISTENT:1h
15 BTC <={{BTC_SHORT_ENTRY_PRICE}}-5% PERSISTENT:1h #15 BTC <={{BTC_SHORT_ENTRY_PRICE}}-5% PERSISTENT:1h
# BTC LONG # BTC LONG
16 BTC <={{BTC_LONG_LIQ_PRICE}}+1% PERSISTENT:1m #16 BTC <={{BTC_LONG_LIQ_PRICE}}+1% PERSISTENT:1m
17 BTC ({{BTC_LONG_LIQ_PRICE}}+1%-->{{BTC_LONG_LIQ_PRICE}}+3%] PERSISTENT:1h #17 BTC ({{BTC_LONG_LIQ_PRICE}}+1%-->{{BTC_LONG_LIQ_PRICE}}+3%] PERSISTENT:1h
18 BTC >={{BTC_LONG_ENTRY_PRICE}}+5% PERSISTENT:1h #18 BTC >={{BTC_LONG_ENTRY_PRICE}}+5% PERSISTENT:1h
# CROSSING with hysteresis # CROSSING with hysteresis
# Triggers above the entry price and re-arms at or below entry price minus 0.25% # Triggers above the entry price and re-arms at or below entry price minus 0.25%
+2
View File
@@ -1,3 +1,5 @@
FORMAT_VERSION=1
# Iteration interval in seconds # Iteration interval in seconds
3600 3600
+2
View File
@@ -1,3 +1,5 @@
FORMAT_VERSION=1
# MintA = Solana # MintA = Solana
# MintB = SyrupUSDC # MintB = SyrupUSDC
# #
@@ -3,17 +3,19 @@ package com.fanitas.evelyn.core.impl.ref;
import com.fanitas.evelyn.core.State; import com.fanitas.evelyn.core.State;
import com.fanitas.evelyn.raydium.RaydiumLiquidityPoolPositionAccounting; import com.fanitas.evelyn.raydium.RaydiumLiquidityPoolPositionAccounting;
import com.fanitas.evelyn.raydium.RaydiumLiquidityPoolPositionConcentrated; import com.fanitas.evelyn.raydium.RaydiumLiquidityPoolPositionConcentrated;
import com.r35157.libs.configuration.ConfigurationFormatVersionValidator;
import com.r35157.libs.raydium.*; import com.r35157.libs.raydium.*;
import com.r35157.libs.valuetypes.basic.WellKnownCurrencyTypes; import com.r35157.libs.valuetypes.basic.WellKnownCurrencyTypes;
import com.r35157.libs.valuetypes.basic.*; import com.r35157.libs.valuetypes.basic.*;
import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -72,14 +74,26 @@ public class StateImpl implements State {
} }
private void updateStateFromPersistence() throws IOException { private void updateStateFromPersistence() throws IOException {
try (BufferedReader reader = Files.newBufferedReader(PATH_EVELYN_STATE, StandardCharsets.UTF_8)) { List<String> lines = Files.readAllLines(
iterationInterval = readIterationInterval(reader); PATH_EVELYN_STATE,
syrupOwnedByEvelyn = readSyrupOwnedByEvelyn(reader); StandardCharsets.UTF_8
solanaAddressForEvelynIOU = readSolanaAddress(reader); );
solanaAddressForEvelynIOUBurner = readSolanaAddress(reader); int formatVersionLineIndex = ConfigurationFormatVersionValidator.validate(
raydiumPoolId = readRaydiumPoolId(reader); PATH_EVELYN_STATE,
curveWidth = readCurveWidth(reader); 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 { private void updateStateFromRaydium() throws IOException, InterruptedException {
@@ -176,33 +190,37 @@ public class StateImpl implements State {
private Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionAccounting> readPositionAccounting() private Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionAccounting> readPositionAccounting()
throws IOException { throws IOException {
Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionAccounting> result = new HashMap<>(); 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)) { for (int lineIndex = formatVersionLineIndex + 1;
String line; lineIndex < lines.size();
lineIndex++) {
String line = lines.get(lineIndex).trim();
while ((line = reader.readLine()) != null) { if (line.isEmpty() || line.startsWith("#")) {
line = line.trim(); continue;
}
if (line.isEmpty() || line.startsWith("#")) { // Remove comments at end of line
continue; int index = line.indexOf('#');
} if(index > -1) {
line = line.substring(0, index).trim();
}
// Remove comments at end of line RaydiumLiquidityPoolPositionAccounting accounting = mapPositionAccounting(line);
int index = line.indexOf('#');
if(index > -1) {
line = line.substring(0, index).trim();
}
RaydiumLiquidityPoolPositionAccounting accounting = mapPositionAccounting(line); RaydiumLiquidityPoolPositionAccounting previous = result.put(
accounting.nftId(),
accounting
);
RaydiumLiquidityPoolPositionAccounting previous = result.put( if (previous != null) {
accounting.nftId(), throw new IOException("Duplicate position accounting for NFT id: " + accounting.nftId());
accounting
);
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 { private ΩSolanaAddressΩ readSolanaAddress(Iterator<String> lineIterator) throws IOException {
ΩSolanaAddressΩ sa = readLineFromFile(reader); ΩSolanaAddressΩ sa = readLineFromFile(lineIterator);
return sa; return sa;
} }
private ΩRaydiumLiquidityPoolIdΩ readRaydiumPoolId(BufferedReader reader) throws IOException { private ΩRaydiumLiquidityPoolIdΩ readRaydiumPoolId(Iterator<String> lineIterator) throws IOException {
ΩRaydiumLiquidityPoolIdΩ rlpid = readLineFromFile(reader); ΩRaydiumLiquidityPoolIdΩ rlpid = readLineFromFile(lineIterator);
return rlpid; return rlpid;
} }
private ΩCurveWidthΩ readCurveWidth(BufferedReader reader) throws IOException { private ΩCurveWidthΩ readCurveWidth(Iterator<String> lineIterator) throws IOException {
String cwStr = readLineFromFile(reader); String cwStr = readLineFromFile(lineIterator);
ΩCurveWidthΩ cw = new ΩCurveWidthΩ(cwStr); ΩCurveWidthΩ cw = new ΩCurveWidthΩ(cwStr);
return cw; return cw;
} }
private ΩmilliSecondsΩ readIterationInterval(BufferedReader reader) throws IOException { private ΩmilliSecondsΩ readIterationInterval(Iterator<String> lineIterator) throws IOException {
String iterationIntervalStr = readLineFromFile(reader); String iterationIntervalStr = readLineFromFile(lineIterator);
ΩmilliSecondsΩ millis = Long.parseLong(iterationIntervalStr) * 1000; ΩmilliSecondsΩ millis = Long.parseLong(iterationIntervalStr) * 1000;
return millis; return millis;
} }
private ΩSyrupAmountΩ readSyrupOwnedByEvelyn(BufferedReader reader) throws IOException { private ΩSyrupAmountΩ readSyrupOwnedByEvelyn(Iterator<String> lineIterator) throws IOException {
String syrupOwnedByEvelynStr = readLineFromFile(reader); String syrupOwnedByEvelynStr = readLineFromFile(lineIterator);
CurrencyType ct = WellKnownCurrencyTypes.SYRUPUSDC.getCurrencyType(); CurrencyType ct = WellKnownCurrencyTypes.SYRUPUSDC.getCurrencyType();
ΩSyrupAmountΩ ma = stringToMoneyAmount(syrupOwnedByEvelynStr, ct); ΩSyrupAmountΩ ma = stringToMoneyAmount(syrupOwnedByEvelynStr, ct);
return ma; return ma;
} }
private static String readLineFromFile(BufferedReader reader) throws IOException { private static String readLineFromFile(Iterator<String> lineIterator) {
String line; while (lineIterator.hasNext()) {
String line = lineIterator.next();
while ((line = reader.readLine()) != null) {
line = line.trim(); line = line.trim();
if (line.isEmpty() || line.startsWith("#")) { if (line.isEmpty() || line.startsWith("#")) {
continue; continue;
} }
break; return line;
} }
return line; return null;
} }
private MoneyAmount bigDecimalToMoneyAmount(BigDecimal bd, CurrencyType ct) { 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_EVELYN_STATE = Path.of("conf/evelyn.conf");
private static final Path PATH_POSITIONS = Path.of("conf/positions.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; private final Raydium raydium;
@@ -1,5 +1,7 @@
package com.r35157.jupiterperpsalarm.impl.ref; package com.r35157.jupiterperpsalarm.impl.ref;
import com.r35157.libs.configuration.ConfigurationFormatVersionValidator;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@@ -11,10 +13,19 @@ public final class AlarmConfigurationParser {
public static AlarmConfiguration parse(Path path) throws IOException { public static AlarmConfiguration parse(Path path) throws IOException {
List<String> lines = Files.readAllLines(path); List<String> lines = Files.readAllLines(path);
int formatVersionLineIndex = ConfigurationFormatVersionValidator.validate(
path,
lines,
SUPPORTED_FORMAT_VERSION
);
List<PriceAlarmDefinition> alarms = new ArrayList<>(); List<PriceAlarmDefinition> alarms = new ArrayList<>();
Map<String, String> constants = new LinkedHashMap<>(); Map<String, String> constants = new LinkedHashMap<>();
for (int lineNumber = 1; lineNumber <= lines.size(); lineNumber++) { for (int lineNumber = 1; lineNumber <= lines.size(); lineNumber++) {
if (lineNumber - 1 == formatVersionLineIndex) {
continue;
}
String line = lines.get(lineNumber - 1); String line = lines.get(lineNumber - 1);
String trimmed = line.trim(); String trimmed = line.trim();
@@ -313,6 +324,7 @@ public final class AlarmConfigurationParser {
private static final Pattern PERSISTENT_GRACE_PERIOD_PATTERN = private static final Pattern PERSISTENT_GRACE_PERIOD_PATTERN =
Pattern.compile("(\\d+)(ms|s|m|h|d|w|M|y)"); Pattern.compile("(\\d+)(ms|s|m|h|d|w|M|y)");
private static final int SUPPORTED_FORMAT_VERSION = 1;
private AlarmConfigurationParser() { private AlarmConfigurationParser() {
} }
@@ -1,5 +1,7 @@
package com.r35157.jupiterperpsalarm.impl.ref; package com.r35157.jupiterperpsalarm.impl.ref;
import com.r35157.libs.configuration.ConfigurationFormatVersionValidator;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@@ -49,9 +51,18 @@ public final class ConsoleAlarmAction implements ConfiguredAlarmAction {
private static Set<Integer> parseAlarmIds(Path path) throws IOException { private static Set<Integer> parseAlarmIds(Path path) throws IOException {
List<String> lines = Files.readAllLines(path); List<String> lines = Files.readAllLines(path);
int formatVersionLineIndex = ConfigurationFormatVersionValidator.validate(
path,
lines,
SUPPORTED_FORMAT_VERSION
);
Set<Integer> result = new LinkedHashSet<>(); Set<Integer> result = new LinkedHashSet<>();
for (int lineNumber = 1; lineNumber <= lines.size(); lineNumber++) { for (int lineNumber = 1; lineNumber <= lines.size(); lineNumber++) {
if (lineNumber - 1 == formatVersionLineIndex) {
continue;
}
String line = removeComment(lines.get(lineNumber - 1)).trim(); String line = removeComment(lines.get(lineNumber - 1)).trim();
if (line.isEmpty()) { if (line.isEmpty()) {
continue; continue;
@@ -86,5 +97,7 @@ public final class ConsoleAlarmAction implements ConfiguredAlarmAction {
return commentStart < 0 ? line : line.substring(0, commentStart); return commentStart < 0 ? line : line.substring(0, commentStart);
} }
private static final int SUPPORTED_FORMAT_VERSION = 1;
private final Set<Integer> alarmIds; private final Set<Integer> alarmIds;
} }
@@ -1,5 +1,7 @@
package com.r35157.jupiterperpsalarm.impl.ref; package com.r35157.jupiterperpsalarm.impl.ref;
import com.r35157.libs.configuration.ConfigurationFormatVersionValidator;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.nio.file.Files; import java.nio.file.Files;
@@ -17,6 +19,11 @@ public final class JupiterPerpsPositionDecreaseAlarmActionConfigurationParser {
) throws IOException { ) throws IOException {
Objects.requireNonNull(variableResolver, "variableResolver"); Objects.requireNonNull(variableResolver, "variableResolver");
List<String> lines = Files.readAllLines(path); List<String> lines = Files.readAllLines(path);
int formatVersionLineIndex = ConfigurationFormatVersionValidator.validate(
path,
lines,
SUPPORTED_FORMAT_VERSION
);
Map<Integer, JupiterPerpsPositionDecreaseAlarmActionConfiguration.PositionDecrease> Map<Integer, JupiterPerpsPositionDecreaseAlarmActionConfiguration.PositionDecrease>
positionDecreases = new LinkedHashMap<>(); positionDecreases = new LinkedHashMap<>();
ΩSolanaWalletIdΩ walletId = null; ΩSolanaWalletIdΩ walletId = null;
@@ -24,6 +31,10 @@ public final class JupiterPerpsPositionDecreaseAlarmActionConfigurationParser {
ΩUSDCAmountΩ reservePositionSizeUsdLimit = null; ΩUSDCAmountΩ reservePositionSizeUsdLimit = null;
for (int lineNumber = 1; lineNumber <= lines.size(); lineNumber++) { for (int lineNumber = 1; lineNumber <= lines.size(); lineNumber++) {
if (lineNumber - 1 == formatVersionLineIndex) {
continue;
}
String line = removeComment(lines.get(lineNumber - 1)).trim(); String line = removeComment(lines.get(lineNumber - 1)).trim();
if (line.isEmpty()) { if (line.isEmpty()) {
continue; continue;
@@ -162,6 +173,8 @@ public final class JupiterPerpsPositionDecreaseAlarmActionConfigurationParser {
: line.substring(0, commentStart); : line.substring(0, commentStart);
} }
private static final int SUPPORTED_FORMAT_VERSION = 1;
private JupiterPerpsPositionDecreaseAlarmActionConfigurationParser() { private JupiterPerpsPositionDecreaseAlarmActionConfigurationParser() {
} }
} }
@@ -1,5 +1,7 @@
package com.r35157.jupiterperpsalarm.impl.ref; package com.r35157.jupiterperpsalarm.impl.ref;
import com.r35157.libs.configuration.ConfigurationFormatVersionValidator;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.nio.file.Files; import java.nio.file.Files;
@@ -17,6 +19,11 @@ public final class JupiterPerpsPositionIncreaseAlarmActionConfigurationParser {
) throws IOException { ) throws IOException {
Objects.requireNonNull(variableResolver, "variableResolver"); Objects.requireNonNull(variableResolver, "variableResolver");
List<String> lines = Files.readAllLines(path); List<String> lines = Files.readAllLines(path);
int formatVersionLineIndex = ConfigurationFormatVersionValidator.validate(
path,
lines,
SUPPORTED_FORMAT_VERSION
);
Map<Integer, JupiterPerpsPositionIncreaseAlarmActionConfiguration.PositionIncrease> Map<Integer, JupiterPerpsPositionIncreaseAlarmActionConfiguration.PositionIncrease>
positionIncreases = new LinkedHashMap<>(); positionIncreases = new LinkedHashMap<>();
ΩSolanaWalletIdΩ walletId = null; ΩSolanaWalletIdΩ walletId = null;
@@ -24,6 +31,10 @@ public final class JupiterPerpsPositionIncreaseAlarmActionConfigurationParser {
ΩUSDCAmountΩ reserveUsdcLimit = null; ΩUSDCAmountΩ reserveUsdcLimit = null;
for (int lineNumber = 1; lineNumber <= lines.size(); lineNumber++) { for (int lineNumber = 1; lineNumber <= lines.size(); lineNumber++) {
if (lineNumber - 1 == formatVersionLineIndex) {
continue;
}
String line = removeComment(lines.get(lineNumber - 1)).trim(); String line = removeComment(lines.get(lineNumber - 1)).trim();
if (line.isEmpty()) { if (line.isEmpty()) {
continue; continue;
@@ -139,6 +150,8 @@ public final class JupiterPerpsPositionIncreaseAlarmActionConfigurationParser {
return commentStart < 0 ? line : line.substring(0, commentStart); return commentStart < 0 ? line : line.substring(0, commentStart);
} }
private static final int SUPPORTED_FORMAT_VERSION = 1;
private JupiterPerpsPositionIncreaseAlarmActionConfigurationParser() { private JupiterPerpsPositionIncreaseAlarmActionConfigurationParser() {
} }
} }
@@ -1,6 +1,7 @@
package com.r35157.jupiterperpsalarm.impl.ref; package com.r35157.jupiterperpsalarm.impl.ref;
import com.r35157.jupiterperpsalarm.AlarmSeverity; import com.r35157.jupiterperpsalarm.AlarmSeverity;
import com.r35157.libs.configuration.ConfigurationFormatVersionValidator;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@@ -18,12 +19,21 @@ public final class PushoverAlarmActionConfigurationParser {
) throws IOException { ) throws IOException {
Objects.requireNonNull(variableResolver, "variableResolver"); Objects.requireNonNull(variableResolver, "variableResolver");
List<String> lines = Files.readAllLines(path); List<String> lines = Files.readAllLines(path);
int formatVersionLineIndex = ConfigurationFormatVersionValidator.validate(
path,
lines,
SUPPORTED_FORMAT_VERSION
);
Map<Integer, PushoverAlarmActionConfiguration.Notification> notifications = Map<Integer, PushoverAlarmActionConfiguration.Notification> notifications =
new LinkedHashMap<>(); new LinkedHashMap<>();
String applicationToken = null; String applicationToken = null;
String userKey = null; String userKey = null;
for (int lineNumber = 1; lineNumber <= lines.size(); lineNumber++) { for (int lineNumber = 1; lineNumber <= lines.size(); lineNumber++) {
if (lineNumber - 1 == formatVersionLineIndex) {
continue;
}
String line = lines.get(lineNumber - 1); String line = lines.get(lineNumber - 1);
String trimmed = line.trim(); String trimmed = line.trim();
@@ -205,6 +215,8 @@ public final class PushoverAlarmActionConfigurationParser {
private int position; private int position;
} }
private static final int SUPPORTED_FORMAT_VERSION = 1;
private PushoverAlarmActionConfigurationParser() { 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() {
}
}