9 Commits

Author SHA256 Message Date
minimons 641cf3d2d4 2: Swap System.exit from JupiterPerpsAlarmImpl with thrown exceptions 2026-06-19 09:51:05 +02:00
minimons dbdde7b7e3 2: Disable old autostart plugins 2026-06-19 09:42:56 +02:00
minimons 8b4c37b8ad Enable logging 2026-06-19 09:41:05 +02:00
minimons beffce2762 Lots of small updates 2026-06-18 15:58:36 +02:00
minimons 9c9c1e0e4e Lots of small updates 2026-06-18 15:58:20 +02:00
minimons 4821e1ff4b Add 2 applications as auto starting 2026-06-18 13:27:00 +02:00
minimons baf9bc91aa Add TickerService and API (dummy empty) 2026-06-18 13:27:00 +02:00
minimons fb83a8999a NenjimHub now autostarts Jupiter Alarm 2026-06-18 12:44:43 +02:00
minimons 0dd187dfb5 Enable logging 2026-06-18 10:14:36 +02:00
43 changed files with 510 additions and 1221 deletions
-2
View File
@@ -6,5 +6,3 @@ build
logs/*.log logs/*.log
logs/*.log.gz logs/*.log.gz
conf/*.conf
conf/*.xml
+1 -2
View File
@@ -39,12 +39,11 @@ val detag by configurations.creating {
} }
dependencies { dependencies {
detag("com.r35157.tools:detag-impl_ref:0.1.0") detag("com.r35157.tools:detag-impl_ref:0.1-dev")
compileOnly("org.jetbrains:annotations:26.1.0") compileOnly("org.jetbrains:annotations:26.1.0")
runtimeOnly("org.apache.logging.log4j:log4j-core:2.26.0") runtimeOnly("org.apache.logging.log4j:log4j-core:2.26.0")
runtimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl:2.26.0") runtimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl:2.26.0")
implementation("com.r35157.nenjim:hubd-api:0.1-dev")
implementation("com.fasterxml.jackson.core:jackson-databind:2.18.6") implementation("com.fasterxml.jackson.core:jackson-databind:2.18.6")
implementation("com.fazecast:jSerialComm:2.11.4") implementation("com.fazecast:jSerialComm:2.11.4")
implementation("com.google.code.gson:gson:2.14.0") implementation("com.google.code.gson:gson:2.14.0")
+4
View File
@@ -0,0 +1,4 @@
# Asset Direction Target TRIGGER SEVERITY NOTE
#############################################################
SOL BELOW 70.0 ONETIME 2 "ALARM: Risiko for Perps Solana long LIKVIDERING!"
SOL BELOW 71.4 ONETIME 2 "ALARM: Risiko for Solana Raydium LÅN LIKVIDERING!"
-56
View File
@@ -1,56 +0,0 @@
# General configurations
#####################################################
{{JUPITER_PERPS_WALLET}} 8abcYourWalletAddressHere
# Constant Name Value
####################################
{{SOL_LONG_ENTRY_PRICE}} 73.67
{{SOL_LONG_LIQ_PRICE}} 70.28
{{SOL_SHORT_ENTRY_PRICE}} 70.47
{{SOL_SHORT_LIQ_PRICE}} 75.01
{{ETH_LONG_ENTRY_PRICE}} 1589.63
{{ETH_LONG_LIQ_PRICE}} 1461.11
{{ETH_SHORT_ENTRY_PRICE}} 1545.29
{{ETH_SHORT_LIQ_PRICE}} 1698.42
{{BTC_LONG_ENTRY_PRICE}} 61236.03
{{BTC_LONG_LIQ_PRICE}} 40984.70
{{BTC_SHORT_ENTRY_PRICE}} 59451.94
{{BTC_SHORT_LIQ_PRICE}} 65084.53
# Id Asset Direction Target Trigger Severity Note
###############################################################################################################################
# SOL SHORT
1 SOL ABOVE {{SOL_SHORT_LIQ_PRICE}}-1.25% ONETIME CRITICAL "🚨 LIKVIDERINGS-risiko SOL Short!"
2 SOL ABOVE {{SOL_SHORT_ENTRY_PRICE}}+3% PERSISTENT:900 INFO "🌱 Short SOL now"
3 SOL BELOW {{SOL_SHORT_ENTRY_PRICE}}-3% PERSISTENT:900 INFO "💵 Close SOL Short"
# SOL LONG
4 SOL BELOW {{SOL_LONG_LIQ_PRICE}}+1.25% ONETIME CRITICAL "🚨 LIKVIDERINGS-risiko SOL Long!"
5 SOL BELOW {{SOL_LONG_ENTRY_PRICE}}-3% PERSISTENT:900 INFO "🌱 Long SOL now"
6 SOL ABOVE {{SOL_LONG_ENTRY_PRICE}}+3% PERSISTENT:900 INFO "💵 Close SOL Long"
# ETH SHORT
7 ETH ABOVE {{ETH_SHORT_LIQ_PRICE}}-1% ONETIME CRITICAL "🚨 LIKVIDERINGS-risiko ETH Short!"
8 ETH ABOVE {{ETH_SHORT_ENTRY_PRICE}}+3% PERSISTENT:900 INFO "🌱 Short ETH now"
9 ETH BELOW {{ETH_SHORT_ENTRY_PRICE}}-3% PERSISTENT:900 INFO "💵 Close ETH Short"
# ETH LONG
10 ETH BELOW {{ETH_LONG_LIQ_PRICE}}+1% ONETIME CRITICAL "🚨 LIKVIDERINGS-risiko ETH Long!"
11 ETH BELOW {{ETH_LONG_ENTRY_PRICE}}-3% PERSISTENT:900 INFO "🌱 Long ETH now"
12 ETH ABOVE {{ETH_LONG_ENTRY_PRICE}}+3% PERSISTENT:900 INFO "💵 Close ETH Long"
# BTC SHORT
13 BTC ABOVE {{BTC_SHORT_LIQ_PRICE}}-1% ONETIME CRITICAL "🚨 LIKVIDERINGS-risiko BTC Short!"
14 BTC ABOVE {{BTC_SHORT_ENTRY_PRICE}}+3% PERSISTENT:900 INFO "🌱 Short BTC now"
15 BTC BELOW {{BTC_SHORT_ENTRY_PRICE}}-3% PERSISTENT:900 INFO "💵 Close BTC Short"
# BTC LONG
16 BTC BELOW {{BTC_LONG_LIQ_PRICE}}+1% ONETIME CRITICAL "🚨 LIKVIDERINGS-risiko BTC Long!"
17 BTC BELOW {{BTC_LONG_ENTRY_PRICE}}-3% PERSISTENT:900 INFO "🌱 Long BTC now"
18 BTC ABOVE {{BTC_LONG_ENTRY_PRICE}}+3% PERSISTENT:900 INFO "💵 Close BTC Long"
Executable
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
set -euo pipefail
JREVERSION=25.0.2-0
export VERSION=$(git describe --tags --exact-match 2>/dev/null \
|| git symbolic-ref --short -q HEAD \
|| git rev-parse --short HEAD)
GITHASH=$(git rev-parse --short=8 HEAD)
echo "Preparing Docker image for '${VERSION}'..."
mkdir -p build/docker/{conf,libs}
cp src/main/docker/run.sh build/docker/run.sh
cp libs/*.jar build/docker/libs/
cp conf/* build/docker/conf/
sed \
-e "s|_VERSION_|${VERSION}|g" \
-e "s|_JREVERSION_|${JREVERSION}|g" \
src/main/docker/Dockerfile_template \
> build/docker/Dockerfile
GROUP=r35157
NAME=nenjimhub
BASETAG=dockerreg.r35157.com/${GROUP}/${NAME}
HASHTAG=${BASETAG}:${GITHASH}_amd64
VERSIONTAG=${BASETAG}:${VERSION}_amd64
LATESTTAG=${BASETAG}:latest_amd64
echo "Building ${HASHTAG}..."
docker buildx build \
--load \
-t ${HASHTAG} \
build/docker
docker tag ${HASHTAG} ${VERSIONTAG}
docker tag ${HASHTAG} ${LATESTTAG}
sed \
-e "s|_HASHTAG_|${HASHTAG}|g" \
-e "s|_VERSIONTAG_|${VERSIONTAG}|g" \
-e "s|_LATESTTAG_|${LATESTTAG}|g" \
src/main/docker/publish_template.sh \
> build/docker/publish.sh
chmod 755 build/docker/publish.sh
echo ""
echo "Docker image ready:"
echo " ${HASHTAG}"
echo " ${VERSIONTAG}"
echo " ${LATESTTAG}"
+5
View File
@@ -5,5 +5,10 @@ export VERSION=$(git describe --tags --exact-match 2>/dev/null \
|| git symbolic-ref --short -q HEAD \ || git symbolic-ref --short -q HEAD \
|| git rev-parse --short HEAD) || git rev-parse --short HEAD)
./docker.sh
echo "Publishing artifact to local Maven repo ($VERSION)..." echo "Publishing artifact to local Maven repo ($VERSION)..."
./gradlew -Pversion=$VERSION publishToMavenLocal ./gradlew -Pversion=$VERSION publishToMavenLocal
echo "Publishing docker container to public 'dockerreg.r35157.com' ($VERSION)..."
./build/docker/publish.sh
-23
View File
@@ -1,23 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SOURCE="$HOME/projects/com_r35157_nenjim-hubd-impl_ref"
TARGET="$HOME/projects/com_r35157_nenjim-hubd-impl_ref_github_snapshot"
rsync -a --delete \
--exclude '.git' \
--exclude 'conf/*.conf' \
--exclude 'conf/*.xml' \
"$SOURCE/" \
"$TARGET/"
cd "$TARGET"
git add -A
if git diff --cached --quiet; then
echo "No snapshot changes to publish."
exit 0
fi
git commit -m "Mirror snapshot"
git push
-18
View File
@@ -1,18 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
shopt -s nullglob
jars=(libs/*.jar)
if (( ${#jars[@]} == 0 )); then
echo "ERROR: No JARs found in libs/" >&2
exit 1
fi
CLASSPATH=$(IFS=:; echo "${jars[*]}")
exec java \
--enable-preview \
-Dlog4j.configurationFile=conf/log4j2.xml \
-cp "$CLASSPATH" \
com.r35157.nenjim.hubd.impl.ref.Main
Symlink
+1
View File
@@ -0,0 +1 @@
src/main/docker/run.sh
+43
View File
@@ -0,0 +1,43 @@
FROM dockerreg.r35157.com/r35157/jre:_JREVERSION__amd64
LABEL maintainer="Minimons <minimons@r35157.com>"
# Setup environment
ENV APP_HOME=/usr/local/software/nenjimhub
WORKDIR /usr/local/software
USER root
RUN mkdir nenjimhub-_VERSION_ \
&& ln -s nenjimhub-_VERSION_ nenjimhub
WORKDIR nenjimhub
RUN mkdir libs
# These dirs are expected to be overshadowed by host mounts
RUN mkdir conf logs && chown user:user logs
# Set timezone
ENV TZ=Europe/Copenhagen
RUN apt-get update
RUN apt-get install -y tzdata \
&& ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \
&& echo $TZ > /etc/timezone \
&& dpkg-reconfigure -f noninteractive tzdata
# Clean-up
RUN apt-get autoremove -y \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Install application and configuration (Do this as late as possible to be able to reuse layers between builds)
COPY run.sh .
COPY conf/* conf/
COPY libs/*.jar libs/
RUN chown root:root -R conf
RUN chmod 755 ./run.sh \
&& chmod -R a+rX /usr/local/software/nenjimhub-_VERSION_
USER user:user
CMD ["./run.sh"]
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
echo "Publishing '_HASHTAG_', '_VERSIONTAG_' and '_LATESTTAG_'..."
docker push _HASHTAG_
docker push _VERSIONTAG_
docker push _LATESTTAG_
echo "Publishing completed!"
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail
shopt -s nullglob
jars=(libs/*.jar)
if (( ${#jars[@]} == 0 )); then
echo "ERROR: No JARs found in libs/" >&2
exit 1
fi
CLASSPATH=$(IFS=:; echo "${jars[*]}")
exec java \
--enable-preview \
-Dlog4j.configurationFile=conf/log4j2.xml \
-cp "$CLASSPATH" \
com.r35157.nenjim.hubd.impl.ref.Main
+19
View File
@@ -0,0 +1,19 @@
version: '3.9'
services:
nenjimhub:
hostname: nenjimhub
image: dockerreg.r35157.com/r35157/nenjimhub:latest
deploy:
replicas: 1
resources:
limits:
memory: 128m
restart_policy:
condition: any
delay: 5s
max_attempts: 3
window: 120s
volumes:
- /home/op/nenjimhub/conf:/usr/local/software/nenjimhub/conf
- /home/op/nenjimhub/logs:/usr/local/software/nenjimhub/logs
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
docker stack deploy --compose-file /home/op/stack.yml stack --detach=true
@@ -1,10 +0,0 @@
package com.r35157.jupiterperpsalarm;
public enum AlarmSeverity {
EMERGENCY, // Repeated wake-up alarm - sound always
CRITICAL, // One-shot wake-up alarm - sound always
WARN, // Audible warning - respecting quiet hours though
INFO, // Normal notification with visual and audible feedback
SILENT, // Low-priority notification - visual feedback without sound/vibration
GHOST // No visual/audible notification - only visible inside the Pushover app
}
@@ -2,5 +2,5 @@ package com.r35157.jupiterperpsalarm.impl.ref;
@FunctionalInterface @FunctionalInterface
public interface AlarmAction { public interface AlarmAction {
void trigger(OraclePrice price, ResolvedPriceAlarm alarm); void trigger(OraclePrice price, PriceAlarmDefinition alarm);
} }
@@ -1,19 +0,0 @@
package com.r35157.jupiterperpsalarm.impl.ref;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
public record AlarmConfiguration(
List<PriceAlarmDefinition> definitions,
Map<String, String> variables
) {
public AlarmConfiguration {
Objects.requireNonNull(definitions, "definitions");
Objects.requireNonNull(variables, "variables");
definitions = List.copyOf(definitions);
variables = new ConcurrentHashMap<>(variables);
}
}
@@ -1,33 +1,28 @@
package com.r35157.jupiterperpsalarm.impl.ref; package com.r35157.jupiterperpsalarm.impl.ref;
import com.r35157.jupiterperpsalarm.AlarmSeverity;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public final class AlarmConfigurationParser { public final class AlarmConfigurationParser {
public static AlarmConfiguration parse(Path path) throws IOException { public static List<PriceAlarmDefinition> parse(Path path) throws IOException {
List<String> lines = Files.readAllLines(path); List<String> lines = Files.readAllLines(path);
List<PriceAlarmDefinition> alarms = new ArrayList<>(); List<PriceAlarmDefinition> alarms = new ArrayList<>();
Map<String, String> constants = new LinkedHashMap<>();
for (int lineNumber = 1; lineNumber <= lines.size(); lineNumber++) { for (int lineNumber = 1; lineNumber <= lines.size(); lineNumber++) {
String line = lines.get(lineNumber - 1); String line = lines.get(lineNumber - 1);
String trimmed = line.trim(); String trimmed = line.trim();
if (trimmed.isEmpty() || trimmed.startsWith("#")) {
if(trimmed.isEmpty() || trimmed.startsWith("#")) {
continue; continue;
} }
try { try {
if(isConstantDefinition(trimmed)) {
parseConstantDefinition(constants, line);
} else {
alarms.add(parseLine(line)); alarms.add(parseLine(line));
}
} catch (RuntimeException exception) { } catch (RuntimeException exception) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
path + ":" + lineNumber + ": " + exception.getMessage(), path + ":" + lineNumber + ": " + exception.getMessage(),
@@ -40,32 +35,24 @@ public final class AlarmConfigurationParser {
throw new IllegalArgumentException("No alarms found in " + path); throw new IllegalArgumentException("No alarms found in " + path);
} }
return new AlarmConfiguration(alarms, constants); return List.copyOf(alarms);
} }
static PriceAlarmDefinition parseLine(String line) { static PriceAlarmDefinition parseLine(String line) {
Cursor cursor = new Cursor(line); Cursor cursor = new Cursor(line);
int id = Integer.parseInt(cursor.nextToken("id"));
JupiterPerpsAsset asset = JupiterPerpsAsset.valueOf( JupiterPerpsAsset asset = JupiterPerpsAsset.valueOf(
cursor.nextToken("asset").toUpperCase(Locale.ROOT) cursor.nextToken("asset").toUpperCase(Locale.ROOT)
); );
PriceDirection direction = PriceDirection.valueOf( PriceDirection direction = PriceDirection.valueOf(
cursor.nextToken("direction").toUpperCase(Locale.ROOT) cursor.nextToken("direction").toUpperCase(Locale.ROOT)
); );
String targetExpression = cursor.nextToken("target"); BigDecimal target = new BigDecimal(cursor.nextToken("target"));
AlarmTrigger trigger = AlarmTrigger.valueOf(
TriggerConfiguration triggerConfiguration = parseTrigger( cursor.nextToken("trigger").toUpperCase(Locale.ROOT)
cursor.nextToken("trigger")
); );
int severity = Integer.parseInt(cursor.nextToken("severity"));
AlarmSeverity severity = AlarmSeverity.valueOf(
cursor.nextToken("severity").toUpperCase()
);
String note = cursor.nextQuotedString("note"); String note = cursor.nextQuotedString("note");
cursor.skipWhitespace(); cursor.skipWhitespace();
@@ -76,61 +63,15 @@ public final class AlarmConfigurationParser {
} }
return new PriceAlarmDefinition( return new PriceAlarmDefinition(
id,
asset, asset,
direction, direction,
targetExpression, target,
triggerConfiguration.trigger(), trigger,
triggerConfiguration.gracePeriod(),
severity, severity,
note note
); );
} }
private static TriggerConfiguration parseTrigger(String triggerText) {
String normalized = triggerText.toUpperCase(Locale.ROOT);
if (normalized.equals("ONETIME")) {
return new TriggerConfiguration(AlarmTrigger.ONETIME, 0);
}
if (normalized.equals("PERSISTENT")) {
return new TriggerConfiguration(AlarmTrigger.PERSISTENT, 0);
}
if (normalized.startsWith("PERSISTENT:")) {
String graceText = normalized.substring("PERSISTENT:".length());
if (graceText.isEmpty()) {
throw new IllegalArgumentException("Missing persistent grace period: " + triggerText);
}
ΩsecondsΩ gracePeriodSeconds;
try {
gracePeriodSeconds = Integer.parseInt(graceText);
} catch (NumberFormatException exception) {
throw new IllegalArgumentException(
"Invalid persistent grace period: " + triggerText,
exception
);
}
if (gracePeriodSeconds < 0) {
throw new IllegalArgumentException(
"Persistent grace period cannot be negative: " + triggerText
);
}
return new TriggerConfiguration(AlarmTrigger.PERSISTENT, gracePeriodSeconds);
}
if (normalized.startsWith("ONETIME:")) {
throw new IllegalArgumentException("ONETIME cannot have a grace period: " + triggerText);
}
throw new IllegalArgumentException("Unknown trigger: " + triggerText);
}
private static final class Cursor { private static final class Cursor {
private Cursor(String line) { private Cursor(String line) {
this.line = line; this.line = line;
@@ -215,76 +156,6 @@ public final class AlarmConfigurationParser {
private int position; private int position;
} }
private static boolean isConstantDefinition(String trimmedLine) {
return trimmedLine.startsWith("{{");
}
private static void parseConstantDefinition(
Map<String, String> constants,
String line
) {
Cursor cursor = new Cursor(line);
String name = cursor.nextToken("constant name");
String value = cursor.nextToken("constant value");
cursor.skipWhitespace();
if (!cursor.atEnd() && cursor.current() != '#') {
throw new IllegalArgumentException(
"Unexpected text after constant value: " + cursor.remaining()
);
}
validateConstantName(name);
constants.put(parseVariableName(name), value);
}
private static String parseVariableName(String variableName) {
if (!variableName.startsWith("{{") || !variableName.endsWith("}}")) {
throw new IllegalArgumentException("Invalid variable name: " + variableName);
}
String parsedVariableName = variableName.substring(2, variableName.length() - 2);
if (parsedVariableName.isBlank()) {
throw new IllegalArgumentException("Variable name cannot be blank: " + variableName);
}
return parsedVariableName;
}
private static String replaceConstants(
String line,
Map<String, String> constants
) {
String result = line;
for (Map.Entry<String, String> entry : constants.entrySet()) {
result = result.replace(entry.getKey(), entry.getValue());
}
if (result.contains("{{") || result.contains("}}")) {
throw new IllegalArgumentException(
"Unknown or malformed constant in line: " + line
);
}
return result;
}
private static void validateConstantName(String name) {
if (!name.matches("\\{\\{[A-Z0-9_]+}}")) {
throw new IllegalArgumentException("Invalid constant name: " + name);
}
}
private record TriggerConfiguration(
AlarmTrigger trigger,
ΩsecondsΩ gracePeriod
) {
}
private AlarmConfigurationParser() { private AlarmConfigurationParser() {
} }
} }
@@ -1,60 +0,0 @@
package com.r35157.jupiterperpsalarm.impl.ref;
import java.math.BigDecimal;
public final class AlarmTargetParser {
private AlarmTargetParser() {
}
public static BigDecimal parse(String targetStr) {
String trimmedTargetStr = targetStr.trim();
if (trimmedTargetStr.endsWith("%")) {
int plusIndex = trimmedTargetStr.indexOf('+');
int minusIndex = trimmedTargetStr.indexOf('-', 1);
if (plusIndex >= 0) {
return parseTargetPercentage(trimmedTargetStr, plusIndex, true);
}
if (minusIndex >= 0) {
return parseTargetPercentage(trimmedTargetStr, minusIndex, false);
}
}
BigDecimal target = new BigDecimal(trimmedTargetStr);
validateTarget(target, targetStr);
return target;
}
private static BigDecimal parseTargetPercentage(
String targetStr,
int operatorIndex,
boolean add
) {
BigDecimal base = new BigDecimal(targetStr.substring(0, operatorIndex));
String percentStr = targetStr.substring(operatorIndex + 1, targetStr.length() - 1);
BigDecimal percent = new BigDecimal(percentStr);
BigDecimal delta = base
.multiply(percent)
.divide(BigDecimal.valueOf(100));
BigDecimal target = add ? base.add(delta) : base.subtract(delta);
validateTarget(base, targetStr);
validateTarget(percent, targetStr);
validateTarget(target, targetStr);
return target;
}
private static void validateTarget(BigDecimal target, String originalTargetStr) {
if (target.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException(
"Target must be zero or positive: " + originalTargetStr
);
}
}
}
@@ -1,30 +0,0 @@
package com.r35157.jupiterperpsalarm.impl.ref;
import java.util.Map;
import java.util.Objects;
public final class AlarmVariableResolver {
public AlarmVariableResolver(Map<String, String> variables) {
this.variables = Objects.requireNonNull(variables, "variables");
}
public String resolve(String text) {
Objects.requireNonNull(text, "text");
String resolvedText = text;
for (Map.Entry<String, String> entry : variables.entrySet()) {
String placeholder = "{{" + entry.getKey() + "}}";
resolvedText = resolvedText.replace(placeholder, entry.getValue());
}
if (resolvedText.contains("{{") || resolvedText.contains("}}")) {
throw new IllegalArgumentException("Unresolved variable in text: " + text);
}
return resolvedText;
}
private final Map<String, String> variables;
}
@@ -12,7 +12,6 @@ public final class AssetPriceAlarmMonitor {
public AssetPriceAlarmMonitor( public AssetPriceAlarmMonitor(
JupiterPerpsAsset asset, JupiterPerpsAsset asset,
List<PriceAlarmDefinition> definitions, List<PriceAlarmDefinition> definitions,
AlarmVariableResolver variableResolver,
AlarmAction action AlarmAction action
) { ) {
this.asset = asset; this.asset = asset;
@@ -23,7 +22,7 @@ public final class AssetPriceAlarmMonitor {
"Alarm asset " + definition.asset() + " does not match monitor " + asset "Alarm asset " + definition.asset() + " does not match monitor " + asset
); );
} }
return new PriceAlarm(definition, variableResolver, action); return new PriceAlarm(definition, action);
}) })
.toList(); .toList();
@@ -9,7 +9,7 @@ public final class CompositeAlarmAction implements AlarmAction {
} }
@Override @Override
public void trigger(OraclePrice price, ResolvedPriceAlarm alarm) { public void trigger(OraclePrice price, PriceAlarmDefinition alarm) {
for (AlarmAction action : actions) { for (AlarmAction action : actions) {
try { try {
action.trigger(price, alarm); action.trigger(price, alarm);
@@ -2,7 +2,7 @@ package com.r35157.jupiterperpsalarm.impl.ref;
public final class ConsoleAlarmAction implements AlarmAction { public final class ConsoleAlarmAction implements AlarmAction {
@Override @Override
public void trigger(OraclePrice price, ResolvedPriceAlarm alarm) { public void trigger(OraclePrice price, PriceAlarmDefinition alarm) {
System.err.println(); System.err.println();
System.err.println("============================================================"); System.err.println("============================================================");
System.err.printf( System.err.printf(
@@ -48,6 +48,25 @@ public final class DovesAgPriceFeedDecoder {
); );
} }
public static void selfTest() {
byte[] data = new byte[ACCOUNT_SIZE];
System.arraycopy(AG_PRICE_FEED_DISCRIMINATOR, 0, data, 0, 8);
putUnsignedLongLittleEndian(data, PRICE_OFFSET, new BigInteger("123456789"));
data[EXPONENT_OFFSET] = (byte) -6;
ByteBuffer.wrap(data, TIMESTAMP_OFFSET, Long.BYTES)
.order(ByteOrder.LITTLE_ENDIAN)
.putLong(1_700_000_000L);
OraclePrice decoded = decode(JupiterPerpsAsset.SOL, data, 42L, "self-test");
if (decoded.priceUsd().compareTo(new BigDecimal("123.456789")) != 0) {
throw new IllegalStateException("Decoder self-test failed: " + decoded.priceUsd());
}
if (!decoded.oracleTime().equals(Instant.ofEpochSecond(1_700_000_000L))) {
throw new IllegalStateException("Timestamp self-test failed");
}
}
private static BigInteger readUnsignedLongLittleEndian(byte[] data, int offset) { private static BigInteger readUnsignedLongLittleEndian(byte[] data, int offset) {
byte[] positiveBigEndian = new byte[Long.BYTES + 1]; byte[] positiveBigEndian = new byte[Long.BYTES + 1];
for (int index = 0; index < Long.BYTES; index++) { for (int index = 0; index < Long.BYTES; index++) {
@@ -1,10 +1,5 @@
package com.r35157.jupiterperpsalarm.impl.ref; package com.r35157.jupiterperpsalarm.impl.ref;
import com.r35157.libs.jupiter.perps.JupiterPerpsService;
import com.r35157.libs.jupiter.perps.impl.anchoridl.AnchorIdlJupiterPerpsServiceImpl;
import com.r35157.libs.solana.SolanaBlockChain;
import com.r35157.libs.solana.impl.ref.SolanaBlockChainImpl;
import java.net.URI; import java.net.URI;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
@@ -18,7 +13,6 @@ public final class JupiterPerpsAlarmImpl {
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
Config config; Config config;
try { try {
config = Config.parse(args, System.getenv()); config = Config.parse(args, System.getenv());
} catch (IllegalArgumentException exception) { } catch (IllegalArgumentException exception) {
@@ -27,35 +21,20 @@ public final class JupiterPerpsAlarmImpl {
throw new IllegalStateException(errMsg, exception); throw new IllegalStateException(errMsg, exception);
} }
List<PriceAlarmDefinition> definitions; if (config.selfTest()) {
AlarmConfiguration alarmConfiguration; SelfTest.run();
System.out.println("All self-tests passed.");
return;
}
List<PriceAlarmDefinition> definitions;
try { try {
alarmConfiguration = AlarmConfigurationParser.parse(config.alarmConfiguration()); definitions = AlarmConfigurationParser.parse(config.alarmConfiguration());
definitions = alarmConfiguration.definitions();
} catch (Exception exception) { } catch (Exception exception) {
String errMsg = "Could not load alarm configuration: " + exception.getMessage() + "!"; String errMsg = "Could not load alarm configuration: " + exception.getMessage() + "!";
throw new IllegalStateException(errMsg, exception); throw new IllegalStateException(errMsg, exception);
} }
AlarmVariableResolver variableResolver = new AlarmVariableResolver(
alarmConfiguration.variables()
);
SolanaBlockChain solanaBlockChain = new SolanaBlockChainImpl();
JupiterPerpsService jupiterPerpsService = new AnchorIdlJupiterPerpsServiceImpl(solanaBlockChain);
JupiterPerpsEntryPriceVariableRefresher entryPriceVariableRefresher =
new JupiterPerpsEntryPriceVariableRefresher(alarmConfiguration.variables(), jupiterPerpsService);
entryPriceVariableRefresher.refresh();
JupiterPerpsEntryPriceVariableRefreshWatcher entryPriceVariableRefreshWatcher =
new JupiterPerpsEntryPriceVariableRefreshWatcher(
config.alarmConfiguration().getParent(),
entryPriceVariableRefresher
);
entryPriceVariableRefreshWatcher.start();
List<AlarmAction> actions = new ArrayList<>(); List<AlarmAction> actions = new ArrayList<>();
actions.add(new ConsoleAlarmAction()); actions.add(new ConsoleAlarmAction());
@@ -79,10 +58,9 @@ public final class JupiterPerpsAlarmImpl {
Map<JupiterPerpsAsset, AssetPriceAlarmMonitor> monitors = new EnumMap<>( Map<JupiterPerpsAsset, AssetPriceAlarmMonitor> monitors = new EnumMap<>(
JupiterPerpsAsset.class JupiterPerpsAsset.class
); );
definitionsByAsset.forEach((asset, assetDefinitions) -> monitors.put( definitionsByAsset.forEach((asset, assetDefinitions) -> monitors.put(
asset, asset,
new AssetPriceAlarmMonitor(asset, assetDefinitions, variableResolver, action) new AssetPriceAlarmMonitor(asset, assetDefinitions, action)
)); ));
List<OracleWebSocketClient> clients = new ArrayList<>(); List<OracleWebSocketClient> clients = new ArrayList<>();
@@ -111,9 +89,9 @@ public final class JupiterPerpsAlarmImpl {
asset.oracleAccount() asset.oracleAccount()
); );
assetDefinitions.forEach(definition -> System.out.printf( assetDefinitions.forEach(definition -> System.out.printf(
" %s %s USD, %s, severity=%s%n", " %s %s USD, %s, severity=%d%n",
definition.direction(), definition.direction(),
definition.targetExpression(), definition.target().toPlainString(),
definition.trigger(), definition.trigger(),
definition.severity() definition.severity()
)); ));
@@ -148,6 +126,7 @@ public final class JupiterPerpsAlarmImpl {
Options: Options:
--config=<path> Default: price-alarms.conf --config=<path> Default: price-alarms.conf
--ws=<url1,url2,...> Default: wss://api.mainnet-beta.solana.com --ws=<url1,url2,...> Default: wss://api.mainnet-beta.solana.com
--self-test Test parser, alarm semantics and oracle decoder
Environment: Environment:
PRICE_ALARMS_CONFIG Alternative default configuration path PRICE_ALARMS_CONFIG Alternative default configuration path
@@ -161,9 +140,11 @@ public final class JupiterPerpsAlarmImpl {
Path alarmConfiguration, Path alarmConfiguration,
List<URI> webSocketEndpoints, List<URI> webSocketEndpoints,
String pushoverToken, String pushoverToken,
String pushoverUserKey String pushoverUserKey,
boolean selfTest
) { ) {
private static Config parse(String[] args, Map<String, String> environment) { private static Config parse(String[] args, Map<String, String> environment) {
boolean selfTest = Arrays.asList(args).contains("--self-test");
Map<String, String> options = Arrays.stream(args) Map<String, String> options = Arrays.stream(args)
.filter(argument -> argument.startsWith("--") && argument.contains("=")) .filter(argument -> argument.startsWith("--") && argument.contains("="))
.map(argument -> argument.substring(2).split("=", 2)) .map(argument -> argument.substring(2).split("=", 2))
@@ -203,7 +184,8 @@ public final class JupiterPerpsAlarmImpl {
Path.of(configurationText), Path.of(configurationText),
endpoints, endpoints,
blankToNull(environment.get("PUSHOVER_APP_TOKEN")), blankToNull(environment.get("PUSHOVER_APP_TOKEN")),
blankToNull(environment.get("PUSHOVER_USER_KEY")) blankToNull(environment.get("PUSHOVER_USER_KEY")),
selfTest
); );
} }
@@ -1,92 +0,0 @@
package com.r35157.jupiterperpsalarm.impl.ref;
import java.nio.file.*;
import java.util.Objects;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.FileSystems;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
public final class JupiterPerpsEntryPriceVariableRefreshWatcher {
public JupiterPerpsEntryPriceVariableRefreshWatcher(
Path confDirectory,
JupiterPerpsEntryPriceVariableRefresher refresher
) {
this.confDirectory = Objects.requireNonNull(confDirectory, "confDirectory");
this.refresher = Objects.requireNonNull(refresher, "refresher");
}
public void start() {
Thread thread = new Thread(
this::run,
"jupiter-perps-entry-price-variable-refresh-watcher"
);
thread.setDaemon(true);
thread.start();
}
private void run() {
try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
confDirectory.register(
watchService,
StandardWatchEventKinds.ENTRY_CREATE
);
System.out.println(
"Jupiter Perps entry price variable refresh watcher started for directory: "
+ confDirectory
);
while (true) {
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
Path path = (Path) event.context();
if (!REFRESH_TRIGGER_FILE_NAME.equals(path.getFileName().toString())) {
continue;
}
Path triggerFile = confDirectory.resolve(path);
System.out.println("Refresh trigger file detected: " + triggerFile);
try {
refresher.refresh();
} finally {
Files.deleteIfExists(triggerFile);
}
}
if (!key.reset()) {
System.err.println(
"Jupiter Perps entry price variable refresh watcher stopped: watch key is no longer valid"
);
return;
}
}
} catch (IOException exception) {
System.err.println(
"Could not start Jupiter Perps entry price variable refresh watcher: "
+ exception.getMessage()
);
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
System.err.println(
"Jupiter Perps entry price variable refresh watcher interrupted"
);
}
}
private static final String REFRESH_TRIGGER_FILE_NAME =
"jupiter-perps-alarm-var.refresh";
private final Path confDirectory;
private final JupiterPerpsEntryPriceVariableRefresher refresher;
}
@@ -1,86 +0,0 @@
package com.r35157.jupiterperpsalarm.impl.ref;
import com.r35157.libs.jupiter.perps.JupiterPerpsPosition;
import com.r35157.libs.jupiter.perps.JupiterPerpsService;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
public final class JupiterPerpsEntryPriceVariableRefresher {
public JupiterPerpsEntryPriceVariableRefresher(
Map<String, String> variables,
JupiterPerpsService jupiterPerpsService
) {
this.variables = Objects.requireNonNull(variables, "variables");
this.jupiterPerpsService = Objects.requireNonNull(jupiterPerpsService, "jupiterPerpsService");
}
public void refresh() {
ΩSolanaWalletIdΩ wallet = variables.get("JUPITER_PERPS_WALLET");
if (wallet == null || wallet.isBlank()) {
System.err.println("Cannot refresh Jupiter Perps entry price variables: JUPITER_PERPS_WALLET is not configured");
return;
}
try {
Set<JupiterPerpsPosition> positions = jupiterPerpsService.getOpenPositions(wallet);
System.out.println(
"Fetched " + positions.size()
+ " open Jupiter Perps positions for wallet: " + wallet
);
removeEntryPriceVariables();
for (JupiterPerpsPosition position : positions) {
String variableName = createEntryPriceVariableName(position);
System.out.println(
"Jupiter Perps position maps to variable "
+ variableName
+ " = "
+ position.entryPrice()
);
variables.put(variableName, position.entryPrice().toPlainString());
}
} catch (IOException | InterruptedException exception) {
if (exception instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
System.err.println(
"Could not refresh Jupiter Perps entry price variables: "
+ exception.getMessage()
);
}
}
private void removeEntryPriceVariables() {
variables.remove("SOL_LONG_ENTRY_PRICE");
variables.remove("SOL_SHORT_ENTRY_PRICE");
variables.remove("BTC_LONG_ENTRY_PRICE");
variables.remove("BTC_SHORT_ENTRY_PRICE");
variables.remove("ETH_LONG_ENTRY_PRICE");
variables.remove("ETH_SHORT_ENTRY_PRICE");
}
private static String createEntryPriceVariableName(JupiterPerpsPosition position) {
String asset = switch (position.tradedTokenMint()) {
case "So11111111111111111111111111111111111111112" -> "SOL";
case "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh" -> "BTC";
case "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs" -> "ETH";
default -> throw new IllegalArgumentException(
"Unsupported Jupiter Perps traded token mint: " + position.tradedTokenMint()
);
};
return asset + "_" + position.direction() + "_ENTRY_PRICE";
}
private final Map<String, String> variables;
private final JupiterPerpsService jupiterPerpsService;
}
@@ -1,17 +1,9 @@
package com.r35157.jupiterperpsalarm.impl.ref; package com.r35157.jupiterperpsalarm.impl.ref;
import java.math.BigDecimal;
import java.time.Instant;
public final class PriceAlarm { public final class PriceAlarm {
public PriceAlarm( public PriceAlarm(PriceAlarmDefinition definition, AlarmAction action) {
PriceAlarmDefinition definition,
AlarmVariableResolver variableResolver,
AlarmAction action
) {
this.definition = definition; this.definition = definition;
this.variableResolver = variableResolver;
this.action = action; this.action = action;
} }
@@ -22,23 +14,9 @@ public final class PriceAlarm {
); );
} }
BigDecimal target;
try {
target = AlarmTargetParser.parse(
variableResolver.resolve(definition.targetExpression())
);
} catch (RuntimeException exception) {
System.err.printf(
"Could not resolve target for alarm %d: %s%n",
definition.id(),
exception.getMessage()
);
return;
}
boolean reached = definition.direction().reached( boolean reached = definition.direction().reached(
price.priceUsd(), price.priceUsd(),
target definition.target()
); );
boolean enteredTriggeredSide = previousReached == null boolean enteredTriggeredSide = previousReached == null
@@ -47,28 +25,16 @@ public final class PriceAlarm {
previousReached = reached; previousReached = reached;
if (!reached) { if (!enteredTriggeredSide) {
previousReached = false;
return; return;
} }
if (definition.trigger() == AlarmTrigger.ONETIME) { if (definition.trigger() == AlarmTrigger.ONETIME && triggerCount > 0) {
if (!enteredTriggeredSide || triggerCount > 0) {
return; return;
} }
trigger(price, target); triggerCount++;
return; action.trigger(price, definition);
}
if (definition.trigger() == AlarmTrigger.PERSISTENT) {
if (lastTriggeredAt == null || persistentGracePeriodHasPassed()) {
trigger(price, target);
}
return;
}
throw new IllegalStateException("Unsupported alarm trigger: " + definition.trigger());
} }
public PriceAlarmDefinition definition() { public PriceAlarmDefinition definition() {
@@ -79,58 +45,9 @@ public final class PriceAlarm {
return triggerCount; return triggerCount;
} }
private boolean persistentGracePeriodHasPassed() {
if (lastTriggeredAt == null) {
return true;
}
ΩsecondsΩ gracePeriod = definition.triggerGracePeriod();
if (gracePeriod == 0) {
return true;
}
return !Instant.now().isBefore(
lastTriggeredAt.plusSeconds(gracePeriod)
);
}
private void trigger(OraclePrice price, BigDecimal target) {
triggerCount++;
lastTriggeredAt = Instant.now();
String note;
try {
note = variableResolver.resolve(definition.note());
} catch (RuntimeException exception) {
System.err.printf(
"Could not resolve note for alarm %d: %s%n",
definition.id(),
exception.getMessage()
);
return;
}
ResolvedPriceAlarm resolvedAlarm = new ResolvedPriceAlarm(
definition.id(),
definition.asset(),
definition.direction(),
target,
definition.trigger(),
definition.triggerGracePeriod(),
definition.severity(),
note
);
action.trigger(price, resolvedAlarm);
}
private final PriceAlarmDefinition definition; private final PriceAlarmDefinition definition;
private final AlarmVariableResolver variableResolver;
private final AlarmAction action; private final AlarmAction action;
private Instant lastTriggeredAt;
private Boolean previousReached; private Boolean previousReached;
private long triggerCount; private long triggerCount;
} }
@@ -1,35 +1,28 @@
package com.r35157.jupiterperpsalarm.impl.ref; package com.r35157.jupiterperpsalarm.impl.ref;
import com.r35157.jupiterperpsalarm.AlarmSeverity; import java.math.BigDecimal;
import java.util.Objects; import java.util.Objects;
public record PriceAlarmDefinition( public record PriceAlarmDefinition(
int id,
JupiterPerpsAsset asset, JupiterPerpsAsset asset,
PriceDirection direction, PriceDirection direction,
String targetExpression, BigDecimal target,
AlarmTrigger trigger, AlarmTrigger trigger,
ΩsecondsΩ triggerGracePeriod, int severity,
AlarmSeverity severity,
String note String note
) { ) {
public PriceAlarmDefinition { public PriceAlarmDefinition {
Objects.requireNonNull(asset, "asset"); Objects.requireNonNull(asset, "asset");
Objects.requireNonNull(direction, "direction"); Objects.requireNonNull(direction, "direction");
Objects.requireNonNull(targetExpression, "targetExpression"); Objects.requireNonNull(target, "target");
Objects.requireNonNull(trigger, "trigger"); Objects.requireNonNull(trigger, "trigger");
Objects.requireNonNull(severity, "severity");
Objects.requireNonNull(note, "note"); Objects.requireNonNull(note, "note");
if (targetExpression.isBlank()) { if (target.signum() <= 0) {
throw new IllegalArgumentException("Target expression cannot be blank"); throw new IllegalArgumentException("Target price must be positive");
} }
if (severity < 0) {
if (triggerGracePeriod < 0) { throw new IllegalArgumentException("Severity must be zero or positive");
throw new IllegalArgumentException(
"Trigger grace period cannot be negative: " + triggerGracePeriod
);
} }
} }
} }
@@ -1,7 +1,5 @@
package com.r35157.jupiterperpsalarm.impl.ref; package com.r35157.jupiterperpsalarm.impl.ref;
import com.r35157.jupiterperpsalarm.AlarmSeverity;
import java.net.URI; import java.net.URI;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.net.http.HttpClient; import java.net.http.HttpClient;
@@ -18,15 +16,25 @@ public final class PushoverAlarmAction implements AlarmAction {
} }
@Override @Override
public void trigger(OraclePrice price, ResolvedPriceAlarm alarm) { public void trigger(OraclePrice price, PriceAlarmDefinition alarm) {
String title = "Jupiter Perps " + price.asset() + " alarm"; String title = "Jupiter Perps " + price.asset() + " alarm";
String message = createMessage(price, alarm); String message = String.format(
"%s is %s USD. Target: %s %s USD. Oracle time: %s. Slot: %d.",
price.asset(),
price.priceUsd().toPlainString(),
alarm.direction(),
alarm.target().toPlainString(),
price.oracleTime(),
price.slot()
);
// Severity and note are intentionally parsed and retained in the model,
// but are not used by Pushover yet.
String body = form("token", applicationToken) + "&" + String body = form("token", applicationToken) + "&" +
form("user", userKey) + "&" + form("user", userKey) + "&" +
form("title", title) + "&" + form("title", title) + "&" +
form("message", message) + "&" + form("message", message) + "&" +
createPushoverSeverityParameters(alarm.severity()); "priority=2&retry=30&expire=10800&sound=persistent";
HttpRequest request = HttpRequest.newBuilder(PUSHOVER_URI) HttpRequest request = HttpRequest.newBuilder(PUSHOVER_URI)
.timeout(Duration.ofSeconds(15)) .timeout(Duration.ofSeconds(15))
@@ -48,7 +56,7 @@ public final class PushoverAlarmAction implements AlarmAction {
response.body() response.body()
); );
} else { } else {
System.out.println("Pushover alarm sent: " + alarm.severity()); System.out.println("Pushover emergency alarm sent.");
} }
}); });
} }
@@ -58,32 +66,6 @@ public final class PushoverAlarmAction implements AlarmAction {
URLEncoder.encode(value, StandardCharsets.UTF_8); URLEncoder.encode(value, StandardCharsets.UTF_8);
} }
private static String createPushoverSeverityParameters(AlarmSeverity severity) {
return switch (severity) {
case EMERGENCY -> "priority=2&retry=30&expire=10800&sound=persistent";
case CRITICAL -> "priority=1&sound=spacealarm";
case WARN -> "priority=0&sound=siren";
case INFO -> "priority=0";
case SILENT -> "priority=-1";
case GHOST -> "priority=-2";
};
}
private static String createMessage(OraclePrice price, ResolvedPriceAlarm alarm) {
return String.format(
"%d - %s: %s%n%n%s is %s USD.%nTarget: %s %s USD.%nOracle time: %s.%nSlot: %d.",
alarm.id(),
alarm.severity(),
alarm.note(),
price.asset(),
price.priceUsd().toPlainString(),
alarm.direction(),
alarm.target().toPlainString(),
price.oracleTime(),
price.slot()
);
}
private final HttpClient httpClient = HttpClient.newHttpClient(); private final HttpClient httpClient = HttpClient.newHttpClient();
private final String applicationToken; private final String applicationToken;
private final String userKey; private final String userKey;
@@ -1,16 +0,0 @@
package com.r35157.jupiterperpsalarm.impl.ref;
import com.r35157.jupiterperpsalarm.AlarmSeverity;
import java.math.BigDecimal;
public record ResolvedPriceAlarm(
int id,
JupiterPerpsAsset asset,
PriceDirection direction,
BigDecimal target,
AlarmTrigger trigger,
ΩsecondsΩ triggerGracePeriod,
AlarmSeverity severity,
String note
) {
}
@@ -0,0 +1,118 @@
package com.r35157.jupiterperpsalarm.impl.ref;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
public final class SelfTest {
public static void run() {
DovesAgPriceFeedDecoder.selfTest();
configurationParserTest();
oneTimeAlarmTest();
persistentAlarmTest();
initialTriggerTest();
}
private static void configurationParserTest() {
PriceAlarmDefinition alarm = AlarmConfigurationParser.parseLine(
"SOL ABOVE 75.7 ONETIME 2 \"ALARM: Risiko for likvidering!\""
);
require(alarm.asset() == JupiterPerpsAsset.SOL, "Asset parsing failed");
require(alarm.direction() == PriceDirection.ABOVE, "Direction parsing failed");
require(alarm.target().compareTo(new BigDecimal("75.7")) == 0, "Target parsing failed");
require(alarm.trigger() == AlarmTrigger.ONETIME, "Trigger parsing failed");
require(alarm.severity() == 2, "Severity parsing failed");
require(alarm.note().equals("ALARM: Risiko for likvidering!"), "Note parsing failed");
}
private static void oneTimeAlarmTest() {
List<BigDecimal> triggeredPrices = new ArrayList<>();
PriceAlarm alarm = new PriceAlarm(
definition(PriceDirection.ABOVE, "100", AlarmTrigger.ONETIME),
(price, definition) -> triggeredPrices.add(price.priceUsd())
);
accept(alarm, "90");
accept(alarm, "100");
accept(alarm, "110");
accept(alarm, "90");
accept(alarm, "101");
require(triggeredPrices.equals(List.of(new BigDecimal("100"))), "ONETIME semantics failed");
}
private static void persistentAlarmTest() {
List<BigDecimal> triggeredPrices = new ArrayList<>();
PriceAlarm alarm = new PriceAlarm(
definition(PriceDirection.BELOW, "100", AlarmTrigger.PERSISTENT),
(price, definition) -> triggeredPrices.add(price.priceUsd())
);
accept(alarm, "110");
accept(alarm, "99");
accept(alarm, "98");
accept(alarm, "101");
accept(alarm, "100");
require(
triggeredPrices.equals(List.of(new BigDecimal("99"), new BigDecimal("100"))),
"PERSISTENT crossing semantics failed"
);
}
private static void initialTriggerTest() {
List<BigDecimal> triggeredPrices = new ArrayList<>();
PriceAlarm alarm = new PriceAlarm(
definition(PriceDirection.ABOVE, "100", AlarmTrigger.PERSISTENT),
(price, definition) -> triggeredPrices.add(price.priceUsd())
);
accept(alarm, "105");
accept(alarm, "106");
require(
triggeredPrices.equals(List.of(new BigDecimal("105"))),
"Initial satisfied alarm did not trigger exactly once"
);
}
private static PriceAlarmDefinition definition(
PriceDirection direction,
String target,
AlarmTrigger trigger
) {
return new PriceAlarmDefinition(
JupiterPerpsAsset.SOL,
direction,
new BigDecimal(target),
trigger,
2,
"ignored for now"
);
}
private static void accept(PriceAlarm alarm, String price) {
BigDecimal decimal = new BigDecimal(price);
alarm.accept(new OraclePrice(
JupiterPerpsAsset.SOL,
decimal.unscaledValue().abs(),
-decimal.scale(),
decimal,
Instant.ofEpochSecond(1_700_000_000L),
1L,
"self-test"
));
}
private static void require(boolean condition, String message) {
if (!condition) {
throw new IllegalStateException(message);
}
}
private SelfTest() {
}
}
@@ -1,2 +0,0 @@
package com.r35157.jupiterperpsalarm.impl.ref;
@@ -1,70 +0,0 @@
package com.r35157.libs.codec.impl.ref;
import com.r35157.libs.codec.Base58Codec;
public class Base58CodecImpl implements Base58Codec {
public String encode(byte[] input) {
if (input.length == 0) {
return "";
}
byte[] copy = input.clone();
int zeros = 0;
while (zeros < copy.length && copy[zeros] == 0) {
zeros++;
}
char[] encoded = new char[copy.length * 2];
int outputStart = encoded.length;
int inputStart = zeros;
while (inputStart < copy.length) {
int remainder = divmod58(
copy,
inputStart
);
if (copy[inputStart] == 0) {
inputStart++;
}
encoded[--outputStart] = ALPHABET[remainder];
}
while (outputStart < encoded.length && encoded[outputStart] == ENCODED_ZERO) {
outputStart++;
}
while (zeros-- > 0) {
encoded[--outputStart] = ENCODED_ZERO;
}
return new String(
encoded,
outputStart,
encoded.length - outputStart
);
}
private static int divmod58(byte[] number, int startAt) {
int remainder = 0;
for (int i = startAt; i < number.length; i++) {
int digit = number[i] & 0xff;
int temp = remainder * 256 + digit;
number[i] = (byte) (temp / 58);
remainder = temp % 58;
}
return remainder;
}
private static final char[] ALPHABET =
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray();
private static final char ENCODED_ZERO =
ALPHABET[0];
}
@@ -1,50 +0,0 @@
package com.r35157.libs.jupiter.perps.impl.anchoridl;
import com.r35157.libs.codec.Base58Codec;
import com.r35157.libs.codec.impl.ref.Base58CodecImpl;
import com.r35157.libs.solana.SolanaAccountInfo;
import java.util.Base64;
class AnchorIdlJupiterPerpsCustodyDecoder {
ΩSPLMintAddressΩ decodeMint(
SolanaAccountInfo custodyAccountInfo
) {
byte[] data = Base64.getDecoder().decode(custodyAccountInfo.dataBase64());
if (data.length < MINT_OFFSET + PUBLIC_KEY_LENGTH) {
throw new IllegalArgumentException(
"Jupiter Perps custody account data is too short: " + data.length
);
}
return readPublicKey(data, MINT_OFFSET);
}
private ΩSPLMintAddressΩ readPublicKey(
byte[] data,
int offset
) {
byte[] publicKeyBytes = new byte[PUBLIC_KEY_LENGTH];
System.arraycopy(
data,
offset,
publicKeyBytes,
0,
PUBLIC_KEY_LENGTH
);
return base58.encode(publicKeyBytes);
}
private static final int ANCHOR_DISCRIMINATOR_LENGTH = 8;
private static final int PUBLIC_KEY_LENGTH = 32;
private static final int MINT_OFFSET =
ANCHOR_DISCRIMINATOR_LENGTH
+ PUBLIC_KEY_LENGTH; // pool
private static final Base58Codec base58 = new Base58CodecImpl();
}
@@ -1,114 +0,0 @@
package com.r35157.libs.jupiter.perps.impl.anchoridl;
import com.r35157.libs.codec.Base58Codec;
import com.r35157.libs.codec.impl.ref.Base58CodecImpl;
import com.r35157.libs.jupiter.perps.JupiterPerpsPosition;
import com.r35157.libs.jupiter.perps.JupiterPerpsPositionDirection;
import com.r35157.libs.solana.SolanaAccountInfo;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Base64;
class AnchorIdlJupiterPerpsPositionDecoder {
JupiterPerpsPosition decode(
ΩJupiterPerpsPositionAccountΩ positionAccount,
SolanaAccountInfo accountInfo,
ΩSPLMintAddressΩ tradedTokenMint
) {
byte[] data = Base64.getDecoder().decode(accountInfo.dataBase64());
if (data.length < PRICE_OFFSET + U64_LENGTH) {
throw new IllegalArgumentException(
"Jupiter Perps position account data is too short: " + data.length
);
}
JupiterPerpsPositionDirection direction =
decodeDirection(data[SIDE_OFFSET]);
long rawEntryPrice = ByteBuffer
.wrap(data, PRICE_OFFSET, U64_LENGTH)
.order(ByteOrder.LITTLE_ENDIAN)
.getLong();
ΩUSDCPriceΩ entryPrice = BigDecimal
.valueOf(rawEntryPrice)
.movePointLeft(6);
JupiterPerpsPosition pos = new JupiterPerpsPosition(
positionAccount,
entryPrice,
direction,
tradedTokenMint
);
return pos;
}
ΩSolanaAddressΩ decodeCustodyAccount(SolanaAccountInfo accountInfo) {
byte[] data = Base64.getDecoder().decode(accountInfo.dataBase64());
if (data.length < CUSTODY_OFFSET + PUBLIC_KEY_LENGTH) {
throw new IllegalArgumentException(
"Jupiter Perps position account data is too short: " + data.length
);
}
return readPublicKey(
data,
CUSTODY_OFFSET
);
}
private JupiterPerpsPositionDirection decodeDirection(
byte rawSide
) {
// Jupiter Perps position side values are encoded as 1 = LONG, 2 = SHORT.
JupiterPerpsPositionDirection direction = switch (rawSide) {
case 1 -> JupiterPerpsPositionDirection.LONG;
case 2 -> JupiterPerpsPositionDirection.SHORT;
default -> throw new IllegalArgumentException(
"Unknown Jupiter Perps position side: " + rawSide
);
};
return direction;
}
private ΩSolanaAddressΩ readPublicKey(
byte[] data,
int offset
) {
byte[] publicKeyBytes = new byte[PUBLIC_KEY_LENGTH];
System.arraycopy(
data,
offset,
publicKeyBytes,
0,
PUBLIC_KEY_LENGTH
);
return base58.encode(publicKeyBytes);
}
private static final int ANCHOR_DISCRIMINATOR_LENGTH = 8;
private static final int PUBLIC_KEY_LENGTH = 32;
private static final int I64_LENGTH = 8;
private static final int SIDE_ENUM_LENGTH = 1;
private static final int U64_LENGTH = 8;
private static final int OWNER_OFFSET = ANCHOR_DISCRIMINATOR_LENGTH;
private static final int POOL_OFFSET = OWNER_OFFSET + PUBLIC_KEY_LENGTH;
private static final int CUSTODY_OFFSET = POOL_OFFSET + PUBLIC_KEY_LENGTH;
private static final int COLLATERAL_CUSTODY_OFFSET = CUSTODY_OFFSET + PUBLIC_KEY_LENGTH; // custody
private static final int OPEN_TIME_OFFSET = COLLATERAL_CUSTODY_OFFSET + PUBLIC_KEY_LENGTH;
private static final int UPDATE_TIME_OFFSET = OPEN_TIME_OFFSET + I64_LENGTH; // openTime
private static final int SIDE_OFFSET = UPDATE_TIME_OFFSET + I64_LENGTH;
private static final int PRICE_OFFSET = SIDE_OFFSET + SIDE_ENUM_LENGTH;
private static final Base58Codec base58 = new Base58CodecImpl();
}
@@ -1,99 +0,0 @@
package com.r35157.libs.jupiter.perps.impl.anchoridl;
import com.r35157.libs.jupiter.perps.JupiterPerpsPosition;
import com.r35157.libs.jupiter.perps.JupiterPerpsService;
import com.r35157.libs.solana.SolanaAccountInfo;
import com.r35157.libs.solana.SolanaBlockChain;
import com.r35157.libs.solana.SolanaProgramAccountMemcmpFilter;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
public AnchorIdlJupiterPerpsServiceImpl(
SolanaBlockChain solanaBlockChain
) {
this.solanaBlockChain = solanaBlockChain;
this.positionDecoder = new AnchorIdlJupiterPerpsPositionDecoder();
this.custodyDecoder = new AnchorIdlJupiterPerpsCustodyDecoder();
}
@Override
public JupiterPerpsPosition getPosition(ΩJupiterPerpsPositionAccountΩ positionAccount)
throws IOException, InterruptedException {
SolanaAccountInfo accountInfo = solanaBlockChain.getAccountInfo(positionAccount);
if (accountInfo == null) {
throw new IllegalArgumentException("Jupiter Perps position account does not exist: " + positionAccount);
}
if (!JUPITER_PERPS_PROGRAM_ID.equals(accountInfo.owner())) {
throw new IllegalArgumentException(
"Account is not owned by Jupiter Perps program: " + positionAccount
);
}
ΩSPLMintAddressΩ tradedTokenMint = getTradedTokenMint(accountInfo);
JupiterPerpsPosition pos = positionDecoder.decode(positionAccount, accountInfo, tradedTokenMint);
return pos;
}
@Override
public Set<JupiterPerpsPosition> getOpenPositions(ΩSolanaWalletIdΩ owner)
throws IOException, InterruptedException {
Set<SolanaAccountInfo> accountInfos = solanaBlockChain.getProgramAccounts(
JUPITER_PERPS_PROGRAM_ID,
Set.of(new SolanaProgramAccountMemcmpFilter(
POSITION_OWNER_OFFSET,
owner
))
);
Set<JupiterPerpsPosition> positions = new HashSet<>();
for (SolanaAccountInfo accountInfo : accountInfos) {
ΩSolanaAddressΩ address = accountInfo.address();
ΩSolanaProgramIdΩ programId = accountInfo.owner();
if (!JUPITER_PERPS_PROGRAM_ID.equals(programId)) {
String errorMsg = "Account '" + address + "' is not owned by Jupiter Perps program '" +
programId + "'";
throw new IllegalArgumentException(errorMsg);
}
ΩSPLMintAddressΩ tradedTokenMint = getTradedTokenMint(accountInfo);
JupiterPerpsPosition position = positionDecoder.decode(address, accountInfo, tradedTokenMint);
positions.add(position);
}
return Set.copyOf(positions);
}
private ΩSPLMintAddressΩ getTradedTokenMint(SolanaAccountInfo positionAccountInfo)
throws IOException, InterruptedException
{
ΩSolanaAddressΩ custodyAccount = positionDecoder.decodeCustodyAccount(positionAccountInfo);
SolanaAccountInfo custodyAccountInfo = solanaBlockChain.getAccountInfo(custodyAccount);
if (custodyAccountInfo == null) {
throw new IllegalArgumentException(
"Jupiter Perps custody account does not exist: " + custodyAccount
);
}
ΩSPLMintAddressΩ mintAddress = custodyDecoder.decodeMint(custodyAccountInfo);
return mintAddress;
}
private static final ΩJupiterPerpsProgramIdΩ JUPITER_PERPS_PROGRAM_ID =
"PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu";
private static final int POSITION_OWNER_OFFSET = 8;
private final SolanaBlockChain solanaBlockChain;
private final AnchorIdlJupiterPerpsPositionDecoder positionDecoder;
private final AnchorIdlJupiterPerpsCustodyDecoder custodyDecoder;
}
@@ -0,0 +1,146 @@
package com.r35157.libs.solana;
import com.r35157.libs.solana.valuetypes.SolanaProgramDerivedAddress;
import com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram;
import com.r35157.libs.valuetypes.basic.MoneyAmount;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Provides read-oriented access to the Solana blockchain.
*
* <p>This interface exposes the Solana operations needed by higher-level
* integrations. It can fetch native SOL balances, SPL token holdings, NFT-like
* token holding candidates, account information and program derived addresses.</p>
*
* <p>The interface is intentionally generic and does not contain Raydium-specific
* logic. Higher-level integrations are expected to interpret Solana accounts,
* token holdings and derived addresses according to their own domain rules.</p>
*/
public interface SolanaBlockChain {
/**
* Fetches the native SOL balance for a Solana address.
*
* @param address the Solana address to inspect
* @return the native SOL balance for the address
* @throws IOException if the balance could not be fetched or parsed
* @throws InterruptedException if the calling thread is interrupted while fetching the balance
*/
ΩSolanaAmountΩ getBalanceInSolana(ΩSolanaAddressΩ address) throws IOException, InterruptedException;
/**
* Fetches the native SOL balance for a Solana address in lamports.
*
* <p>Lamports are the smallest unit of native SOL.</p>
*
* @param address the Solana address to inspect
* @return the native SOL balance for the address in lamports
* @throws IOException if the balance could not be fetched or parsed
* @throws InterruptedException if the calling thread is interrupted while fetching the balance
*/
ΩlamportsΩ getBalanceInLamport(ΩSolanaAddressΩ address) throws IOException, InterruptedException;
/**
* Fetches SPL token holdings owned by a Solana address for a specific token program.
*
* <p>The supplied token program decides which token accounts are inspected. For example,
* callers may query the original SPL Token Program or the Token-2022 Program depending
* on which token accounts they need to discover.</p>
*
* @param ownerAddress the Solana owner address whose token holdings should be inspected
* @param splProgramId the SPL token program to query
* @return a map of SPL mint addresses to token holding information
* @throws IOException if the token holdings could not be fetched or parsed
* @throws InterruptedException if the calling thread is interrupted while fetching token holdings
*/
Map<ΩSPLMintAddressΩ, SPLTokenHolding> getSPLTokenHoldings(
ΩSolanaAddressΩ ownerAddress,
SolanaSPLTokenProgram splProgramId
) throws IOException, InterruptedException;
/**
* Fetches NFT-like token mint address candidates owned by a Solana address for a specific token program.
*
* <p>This method identifies token holdings that look like NFTs within the supplied token
* program. A returned address is only a candidate. Higher-level integrations are responsible
* for deciding whether the returned address has domain-specific meaning.</p>
*
* // TODO This method currently identifies candidates from the owner's token holdings only.
* // A token with zero decimals and an owner balance of one is not guaranteed to be a real NFT,
* // because the mint's total supply may still be greater than one. A future implementation
* // should verify the mint supply, for example by using Solana getTokenSupply, before treating
* // the result as a confirmed NFT.
*
* @param ownerAddress the Solana owner address whose NFT-like holdings should be inspected
* @param splProgram the SPL token program to query
* @return the NFT-like Solana mint address candidates owned by the address
* @throws IOException if the NFT candidate addresses could not be fetched or parsed
* @throws InterruptedException if the calling thread is interrupted while fetching NFT candidate addresses
*/
Set<ΩSolanaNFTAddressΩ> getSolanaNFTCandidateAddresses(
ΩSolanaAddressΩ ownerAddress,
SolanaSPLTokenProgram splProgram
) throws IOException, InterruptedException;
/**
* Finds a Solana program derived address for a program id and a set of seeds.
*
* <p>The seeds describe the logical inputs used to derive the address. The implementation
* is responsible for converting each seed into the byte representation required by Solana.</p>
*
* @param programId the Solana program id used to derive the address
* @param seeds the seeds used when deriving the program address
* @return the derived Solana address together with its bump value
*/
SolanaProgramDerivedAddress findProgramAddress(
ΩSolanaProgramIdΩ programId,
List<SolanaProgramAddressSeed> seeds
);
/**
* Fetches account information for a Solana account address.
*
* <p>If the account does not exist, this method returns {@code null}. If the account exists,
* the returned value contains the account address, the owning Solana program id and the
* account data encoded as Base64.</p>
*
* @param accountAddress the Solana account address to inspect
* @return account information, or {@code null} if the account does not exist
* @throws IOException if the account information could not be fetched or parsed
* @throws InterruptedException if the calling thread is interrupted while fetching account information
*/
SolanaAccountInfo getAccountInfo(ΩSolanaAddressΩ accountAddress)
throws IOException, InterruptedException;
/**
* Encodes a raw 32-byte Solana address into its textual Solana address representation.
*
* <p>This method is intended for callers that have obtained raw Solana address bytes from
* account data and need the normal string representation used elsewhere in the API.</p>
*
* @param addressBytes the raw 32-byte Solana address
* @return the encoded Solana address
* @throws IllegalArgumentException if the supplied byte array is not a valid Solana address length
*/
ΩSolanaAddressΩ encodeSolanaAddress(byte[] addressBytes);
/**
* Fetches the total supply of an SPL token mint for a specific token program.
*
* <p>The supplied token program identifies which SPL token program owns the mint,
* for example the original SPL Token Program or the Token-2022 Program.</p>
*
* @param mintAddress the SPL mint address whose supply should be fetched
* @param splProgram the SPL token program to query
* @return the SPL token supply for the mint
* @throws IOException if the token supply could not be fetched or parsed
* @throws InterruptedException if the calling thread is interrupted while fetching token supply
*/
SPLTokenSupply getSPLTokenSupply(
ΩSPLMintAddressΩ mintAddress,
SolanaSPLTokenProgram splProgram
) throws IOException, InterruptedException;
}
@@ -331,113 +331,6 @@ public class SolanaBlockChainImpl implements SolanaBlockChain {
); );
} }
@Override
public Set<SolanaAccountInfo> getProgramAccounts(
ΩSolanaProgramIdΩ programId,
Set<SolanaProgramAccountMemcmpFilter> filters
) throws IOException, InterruptedException {
String jsonBody = createGetProgramAccountsBody(programId, filters);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(RPC_URL))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse<String> response = sendThrottled(request);
if (response.statusCode() != 200) {
throw new IOException("RPC call failed: HTTP " + response.statusCode() + "\n" + response.body());
}
JsonNode root = objectMapper.readTree(response.body());
if (root.has("error")) {
throw new IOException("Solana RPC error: " + root.get("error").toPrettyString());
}
JsonNode result = root.path("result");
if (!result.isArray()) {
throw new IOException("getProgramAccounts response did not contain result array!");
}
Set<SolanaAccountInfo> accountInfos = new HashSet<>();
for (JsonNode accountNode : result) {
JsonNode pubkeyNode = accountNode.path("pubkey");
JsonNode account = accountNode.path("account");
if (!pubkeyNode.isTextual()) {
throw new IOException("getProgramAccounts response contained account without textual pubkey!");
}
JsonNode ownerNode = account.path("owner");
if (!ownerNode.isTextual()) {
throw new IOException("getProgramAccounts response contained account without textual owner!");
}
JsonNode dataNode = account.path("data");
if (!dataNode.isArray() || dataNode.isEmpty() || !dataNode.get(0).isTextual()) {
throw new IOException("getProgramAccounts response contained account without base64 data!");
}
accountInfos.add(new SolanaAccountInfo(
pubkeyNode.asText(),
ownerNode.asText(),
dataNode.get(0).asText()
));
}
return Set.copyOf(accountInfos);
}
private String createGetProgramAccountsBody(
ΩSolanaProgramIdΩ programId,
Set<SolanaProgramAccountMemcmpFilter> filters
) throws IOException {
StringBuilder filtersJson = new StringBuilder();
boolean first = true;
for (SolanaProgramAccountMemcmpFilter filter : filters) {
if (!first) {
filtersJson.append(",");
}
filtersJson.append("""
{
"memcmp": {
"offset": %d,
"bytes": "%s"
}
}
""".formatted(
filter.offset(),
filter.bytes()
));
first = false;
}
return """
{
"jsonrpc": "2.0",
"id": 1,
"method": "getProgramAccounts",
"params": [
"%s",
{
"commitment": "finalized",
"encoding": "base64",
"filters": [
%s
]
}
]
}
""".formatted(programId, filtersJson);
}
private synchronized HttpResponse<String> sendThrottled(HttpRequest request) throws IOException, InterruptedException { private synchronized HttpResponse<String> sendThrottled(HttpRequest request) throws IOException, InterruptedException {
waitBeforeRemoteCall(); waitBeforeRemoteCall();
@@ -1,10 +1,5 @@
package com.r35157.nenjim.hubd.impl.ref; package com.r35157.nenjim.hubd.impl.ref;
import com.r35157.libs.jupiter.perps.JupiterPerpsPosition;
import com.r35157.libs.jupiter.perps.JupiterPerpsService;
import com.r35157.libs.jupiter.perps.impl.anchoridl.AnchorIdlJupiterPerpsServiceImpl;
import com.r35157.libs.solana.SolanaBlockChain;
import com.r35157.libs.solana.impl.ref.SolanaBlockChainImpl;
import com.r35157.nenjim.hubd.ctx.Context; import com.r35157.nenjim.hubd.ctx.Context;
import com.r35157.nenjim.hubd.NenjimHub; import com.r35157.nenjim.hubd.NenjimHub;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -17,24 +12,12 @@ import com.r35157.nenjim.hubd.journal.JournalManager;
import com.r35157.nenjim.hubd.impl.ref.JournalManagerImpl; import com.r35157.nenjim.hubd.impl.ref.JournalManagerImpl;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Set;
public class Main { public class Main {
// TODO: Consider if we really need a Main class or we just need to move the main method to NenjimHubImpl?
static void main(String[] args) throws Exception { static void main(String[] args) throws Exception {
NenjimHubImpl nenjimHub = new NenjimHubImpl(); NenjimHub nenjimHub = new NenjimHubImpl();
/* /* try {
SolanaBlockChain sbc = new SolanaBlockChainImpl();
JupiterPerpsService jupiter = new AnchorIdlJupiterPerpsServiceImpl(sbc);
ΩSolanaWalletIdΩ walletId = "vj98roDZ7744EBfxyuDFkKpEGCsKQLr7K8UFRumJNHf";
Set<JupiterPerpsPosition> positions = jupiter.getOpenPositions(walletId);
int a=0;
*/
nenjimHub.awaitShutdown();
/* try {
log.info("Auto-starting 2 Nenjim application(s)..."); log.info("Auto-starting 2 Nenjim application(s)...");
log.info(" Starting AssetAZCore..."); log.info(" Starting AssetAZCore...");
//com.r35157.; //com.r35157.;
@@ -1,6 +1,5 @@
package com.r35157.nenjim.hubd.impl.ref; package com.r35157.nenjim.hubd.impl.ref;
import com.r35157.jupiterperpsalarm.impl.ref.JupiterPerpsAlarmImpl;
import com.r35157.nenjim.hubd.NenjimHub; import com.r35157.nenjim.hubd.NenjimHub;
import com.r35157.nenjim.hubd.journal.Journal; import com.r35157.nenjim.hubd.journal.Journal;
import crypto.r35157.nenjim.NenjimProcess; import crypto.r35157.nenjim.NenjimProcess;
@@ -9,7 +8,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.HashMap; import java.util.HashMap;
import java.util.concurrent.CountDownLatch;
public class NenjimHubImpl implements NenjimHub { public class NenjimHubImpl implements NenjimHub {
public NenjimHubImpl() throws Exception { public NenjimHubImpl() throws Exception {
@@ -40,10 +38,6 @@ public class NenjimHubImpl implements NenjimHub {
} }
private void startAutoRunProcesses() throws Exception { private void startAutoRunProcesses() throws Exception {
startJupiterPerpsAlarm(); // TODO: Hardcoded/hacky way to auto-start but good enough for now.
// TODO: Old but more correct way to auto start plugins - but it is currently broken.
/*
String[] processesToAutoStart = { String[] processesToAutoStart = {
//"com.r35157.jupiterperpsalarm.impl.ref.JupiterPerpsAlarmImpl" //"com.r35157.jupiterperpsalarm.impl.ref.JupiterPerpsAlarmImpl"
//"crypto.r35157.nenjim.NenjimHubSocketAdminAdapter", //"crypto.r35157.nenjim.NenjimHubSocketAdminAdapter",
@@ -57,7 +51,6 @@ public class NenjimHubImpl implements NenjimHub {
for (String processInterfaceName : processesToAutoStart) { for (String processInterfaceName : processesToAutoStart) {
startProcess(processInterfaceName); startProcess(processInterfaceName);
} }
*/
} }
@Override @Override
@@ -97,27 +90,7 @@ public class NenjimHubImpl implements NenjimHub {
System.out.println("NenjimHub command: 'noop'"); System.out.println("NenjimHub command: 'noop'");
} }
public void awaitShutdown() throws InterruptedException {
shutdownLatch.await();
}
private void startJupiterPerpsAlarm() {
Thread thread = new Thread(() -> {
try {
JupiterPerpsAlarmImpl.main(new String[] {
"--config=conf/alarms.conf"
});
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}, "Nenjim Plugin - JupiterPerpsAlarm");
thread.setDaemon(false);
thread.start();
}
private static final Logger log = LoggerFactory.getLogger(NenjimHubImpl.class); private static final Logger log = LoggerFactory.getLogger(NenjimHubImpl.class);
private final CountDownLatch shutdownLatch = new CountDownLatch(1);
private HashMap<Integer, NenjimProcess> processes; private HashMap<Integer, NenjimProcess> processes;
//private StructuredTaskScope.ShutdownOnFailure processesScope; //private StructuredTaskScope.ShutdownOnFailure processesScope;