47: Add explicit format versions to configuration files
This commit is contained in:
@@ -11,6 +11,39 @@ This module was initially created with the following command:
|
||||
## Usage
|
||||
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:
|
||||
| Date | Version | Description |
|
||||
|:-----------------|:--------------:|----------------------|
|
||||
|
||||
+32
-5
@@ -1,10 +1,10 @@
|
||||
# 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.
|
||||
|
||||
> **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
|
||||
|
||||
@@ -15,7 +15,7 @@ The application performs the following steps before it starts monitoring prices:
|
||||
3. Populate entry-price and liquidation-price variables.
|
||||
4. Resolve and validate every alarm condition.
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
The application uses four configuration files:
|
||||
The application uses five configuration files:
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
@@ -83,8 +83,9 @@ The application uses four configuration files:
|
||||
| `alarmaction_Console.conf` | Alarm IDs written to the console |
|
||||
| `alarmaction_Pushover.conf` | Pushover credentials and notifications |
|
||||
| `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:
|
||||
|
||||
@@ -92,6 +93,26 @@ Example files:
|
||||
- [alarmaction_Console.conf.example](conf/alarmaction_Console.conf.example)
|
||||
- [alarmaction_Pushover.conf.example](conf/alarmaction_Pushover.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
|
||||
|
||||
@@ -302,6 +323,8 @@ Actions are executed sequentially through `CompositeAlarmAction`. A runtime fail
|
||||
`alarmaction_Console.conf` contains one alarm ID per line:
|
||||
|
||||
```text
|
||||
FORMAT_VERSION=1
|
||||
|
||||
1
|
||||
4
|
||||
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:
|
||||
|
||||
```text
|
||||
FORMAT_VERSION=1
|
||||
|
||||
APPLICATION_TOKEN <pushover-application-token>
|
||||
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:
|
||||
|
||||
```text
|
||||
FORMAT_VERSION=1
|
||||
|
||||
WALLET_ID {{JUPITER_PERPS_WALLET}}
|
||||
SIGNER_KEY_NAME <jup-key-name>
|
||||
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
FORMAT_VERSION=1
|
||||
|
||||
# Alarm ids handled by ConsoleAlarmAction
|
||||
##########################################
|
||||
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10
|
||||
11
|
||||
12
|
||||
13
|
||||
14
|
||||
15
|
||||
16
|
||||
17
|
||||
18
|
||||
#1
|
||||
#2
|
||||
#3
|
||||
#4
|
||||
#5
|
||||
#6
|
||||
#7
|
||||
#8
|
||||
#9
|
||||
#10
|
||||
#11
|
||||
#12
|
||||
#13
|
||||
#14
|
||||
#15
|
||||
#16
|
||||
#17
|
||||
#18
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
FORMAT_VERSION=1
|
||||
|
||||
# Wallet and signer used for position-decrease transactions
|
||||
################################################################
|
||||
WALLET_ID {{JUPITER_PERPS_WALLET}}
|
||||
@@ -9,4 +11,4 @@ RESERVE_POSITION_SIZE_USD_LIMIT 10
|
||||
# Values may reference {{VARIABLES}} defined in alarms.conf.
|
||||
# ReceiveToken must be USDC or match Asset.
|
||||
#3 SOL SHORT USDC 5.00 200
|
||||
#6 SOL LONG USDC 5.00 200
|
||||
#6 SOL LONG USDC 5.00 200
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
FORMAT_VERSION=1
|
||||
|
||||
# Wallet and signer used for position-increase transactions
|
||||
################################################################
|
||||
WALLET_ID {{JUPITER_PERPS_WALLET}}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
FORMAT_VERSION=1
|
||||
|
||||
# Pushover credentials
|
||||
##########################################
|
||||
# Values and notes may reference {{VARIABLES}} defined in alarms.conf.
|
||||
@@ -9,31 +11,31 @@ USER_KEY <pushover-user-key>
|
||||
##########################################
|
||||
|
||||
# SOL SHORT
|
||||
1 CRITICAL "🚨 SOL Short add 1.1x"
|
||||
2 INFO "🌱 SOL Short add 3x"
|
||||
3 INFO "💵 SOL Short close"
|
||||
#1 CRITICAL "🚨 SOL Short add 1.1x"
|
||||
#2 INFO "🌱 SOL Short add 3x"
|
||||
#3 INFO "💵 SOL Short close"
|
||||
|
||||
# SOL LONG
|
||||
4 CRITICAL "🚨 SOL Long add 1.1x"
|
||||
5 INFO "🌱 SOL Long add 3x"
|
||||
6 INFO "💵 SOL Long close"
|
||||
#4 CRITICAL "🚨 SOL Long add 1.1x"
|
||||
#5 INFO "🌱 SOL Long add 3x"
|
||||
#6 INFO "💵 SOL Long close"
|
||||
|
||||
# ETH SHORT
|
||||
7 CRITICAL "🚨 ETH Short add 1.1x"
|
||||
8 INFO "🌱 ETH Short add 3x"
|
||||
9 INFO "💵 ETH Short close"
|
||||
#7 CRITICAL "🚨 ETH Short add 1.1x"
|
||||
#8 INFO "🌱 ETH Short add 3x"
|
||||
#9 INFO "💵 ETH Short close"
|
||||
|
||||
# ETH LONG
|
||||
10 CRITICAL "🚨 ETH Long add 1.1x"
|
||||
11 INFO "🌱 ETH Long add 3x"
|
||||
12 INFO "💵 ETH Long close"
|
||||
#10 CRITICAL "🚨 ETH Long add 1.1x"
|
||||
#11 INFO "🌱 ETH Long add 3x"
|
||||
#12 INFO "💵 ETH Long close"
|
||||
|
||||
# BTC SHORT
|
||||
13 CRITICAL "🚨 BTC Short add 1.1x"
|
||||
14 INFO "🌱 BTC Short add 3x"
|
||||
15 INFO "💵 BTC Short close"
|
||||
#13 CRITICAL "🚨 BTC Short add 1.1x"
|
||||
#14 INFO "🌱 BTC Short add 3x"
|
||||
#15 INFO "💵 BTC Short close"
|
||||
|
||||
# BTC LONG
|
||||
16 CRITICAL "🚨 BTC Long add 1.1x"
|
||||
17 INFO "🌱 BTC Long add 3x"
|
||||
18 INFO "💵 BTC Long close"
|
||||
#16 CRITICAL "🚨 BTC Long add 1.1x"
|
||||
#17 INFO "🌱 BTC Long add 3x"
|
||||
#18 INFO "💵 BTC Long close"
|
||||
|
||||
+20
-18
@@ -1,3 +1,5 @@
|
||||
FORMAT_VERSION=1
|
||||
|
||||
# General configurations
|
||||
#####################################################
|
||||
{{JUPITER_PERPS_WALLET}} vj98roDZ7744EBfxyuDFkKpEGCsKQLr7K8UFRumJNHf
|
||||
@@ -20,34 +22,34 @@
|
||||
################################################################################################
|
||||
|
||||
# SOL SHORT
|
||||
1 SOL >={{SOL_SHORT_LIQ_PRICE}}-1% PERSISTENT:1m
|
||||
2 SOL [{{SOL_SHORT_LIQ_PRICE}}-3%-->{{SOL_SHORT_LIQ_PRICE}}-1%) PERSISTENT:1h
|
||||
3 SOL <={{SOL_SHORT_ENTRY_PRICE}}-5% PERSISTENT:1h
|
||||
#1 SOL >={{SOL_SHORT_LIQ_PRICE}}-1% PERSISTENT:1m
|
||||
#2 SOL [{{SOL_SHORT_LIQ_PRICE}}-3%-->{{SOL_SHORT_LIQ_PRICE}}-1%) PERSISTENT:1h
|
||||
#3 SOL <={{SOL_SHORT_ENTRY_PRICE}}-5% PERSISTENT:1h
|
||||
|
||||
# SOL LONG
|
||||
4 SOL <={{SOL_LONG_LIQ_PRICE}}+1% PERSISTENT:1m
|
||||
5 SOL ({{SOL_LONG_LIQ_PRICE}}+1%-->{{SOL_LONG_LIQ_PRICE}}+3%] PERSISTENT:1h
|
||||
6 SOL >={{SOL_LONG_ENTRY_PRICE}}+5% PERSISTENT:1h
|
||||
#4 SOL <={{SOL_LONG_LIQ_PRICE}}+1% PERSISTENT:1m
|
||||
#5 SOL ({{SOL_LONG_LIQ_PRICE}}+1%-->{{SOL_LONG_LIQ_PRICE}}+3%] PERSISTENT:1h
|
||||
#6 SOL >={{SOL_LONG_ENTRY_PRICE}}+5% PERSISTENT:1h
|
||||
|
||||
# ETH SHORT
|
||||
7 ETH >={{ETH_SHORT_LIQ_PRICE}}-1% PERSISTENT:1m
|
||||
8 ETH [{{ETH_SHORT_LIQ_PRICE}}-3%-->{{ETH_SHORT_LIQ_PRICE}}-1%) PERSISTENT:1h
|
||||
9 ETH <={{ETH_SHORT_ENTRY_PRICE}}-5% PERSISTENT:1h
|
||||
#7 ETH >={{ETH_SHORT_LIQ_PRICE}}-1% PERSISTENT:1m
|
||||
#8 ETH [{{ETH_SHORT_LIQ_PRICE}}-3%-->{{ETH_SHORT_LIQ_PRICE}}-1%) PERSISTENT:1h
|
||||
#9 ETH <={{ETH_SHORT_ENTRY_PRICE}}-5% PERSISTENT:1h
|
||||
|
||||
# ETH LONG
|
||||
10 ETH <={{ETH_LONG_LIQ_PRICE}}+1% PERSISTENT:1m
|
||||
11 ETH ({{ETH_LONG_LIQ_PRICE}}+1%-->{{ETH_LONG_LIQ_PRICE}}+3%] PERSISTENT:1h
|
||||
12 ETH >={{ETH_LONG_ENTRY_PRICE}}+5% PERSISTENT:1h
|
||||
#10 ETH <={{ETH_LONG_LIQ_PRICE}}+1% PERSISTENT:1m
|
||||
#11 ETH ({{ETH_LONG_LIQ_PRICE}}+1%-->{{ETH_LONG_LIQ_PRICE}}+3%] PERSISTENT:1h
|
||||
#12 ETH >={{ETH_LONG_ENTRY_PRICE}}+5% PERSISTENT:1h
|
||||
|
||||
# BTC SHORT
|
||||
13 BTC >={{BTC_SHORT_LIQ_PRICE}}-1% PERSISTENT:1m
|
||||
14 BTC [{{BTC_SHORT_LIQ_PRICE}}-3%-->{{BTC_SHORT_LIQ_PRICE}}-1%) PERSISTENT:1h
|
||||
15 BTC <={{BTC_SHORT_ENTRY_PRICE}}-5% PERSISTENT:1h
|
||||
#13 BTC >={{BTC_SHORT_LIQ_PRICE}}-1% PERSISTENT:1m
|
||||
#14 BTC [{{BTC_SHORT_LIQ_PRICE}}-3%-->{{BTC_SHORT_LIQ_PRICE}}-1%) PERSISTENT:1h
|
||||
#15 BTC <={{BTC_SHORT_ENTRY_PRICE}}-5% PERSISTENT:1h
|
||||
|
||||
# BTC LONG
|
||||
16 BTC <={{BTC_LONG_LIQ_PRICE}}+1% PERSISTENT:1m
|
||||
17 BTC ({{BTC_LONG_LIQ_PRICE}}+1%-->{{BTC_LONG_LIQ_PRICE}}+3%] PERSISTENT:1h
|
||||
18 BTC >={{BTC_LONG_ENTRY_PRICE}}+5% PERSISTENT:1h
|
||||
#16 BTC <={{BTC_LONG_LIQ_PRICE}}+1% PERSISTENT:1m
|
||||
#17 BTC ({{BTC_LONG_LIQ_PRICE}}+1%-->{{BTC_LONG_LIQ_PRICE}}+3%] PERSISTENT:1h
|
||||
#18 BTC >={{BTC_LONG_ENTRY_PRICE}}+5% PERSISTENT:1h
|
||||
|
||||
# CROSSING with hysteresis
|
||||
# Triggers above the entry price and re-arms at or below entry price minus 0.25%
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
FORMAT_VERSION=1
|
||||
|
||||
# Iteration interval in seconds
|
||||
3600
|
||||
|
||||
@@ -15,4 +17,4 @@ vj98roDZ7744EBfxyuDFkKpEGCsKQLr7K8UFRumJNHf
|
||||
CndJzCXspuk39cwGAA43h3EtHJeHVZ81syt5bTh9dG3E
|
||||
|
||||
# Curve width in percent in both directions
|
||||
0.05
|
||||
0.05
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
FORMAT_VERSION=1
|
||||
|
||||
# MintA = Solana
|
||||
# MintB = SyrupUSDC
|
||||
#
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+14
-1
@@ -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() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -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() {
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -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() {
|
||||
}
|
||||
}
|
||||
|
||||
+152
@@ -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() {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user