Compare commits
71 Commits
51aeec2e3e
...
0.1-dev
| Author | SHA256 | Date | |
|---|---|---|---|
| f8387f6034 | |||
| dac76a2686 | |||
| fc1598ec4a | |||
| b989ab441d | |||
| 24a05ff382 | |||
| 0931dbe99a | |||
| 932b2589e4 | |||
| 3e61f35c98 | |||
| 3a739bde57 | |||
| 244a6f19fe | |||
| ff4f5a0356 | |||
| f86652df7d | |||
| 5e5f8ebd51 | |||
| d216c06ba1 | |||
| c1cbaf0c52 | |||
| 76446f8b73 | |||
| dac1b62628 | |||
| 0aff69429a | |||
| 384fad01bf | |||
| 520a0bcd92 | |||
| 896b5235d0 | |||
| 403d7af6e9 | |||
| 90fd1694fb | |||
| 1fb712b61d | |||
| 1355422597 | |||
| c6ba8dc009 | |||
| a5e5470c2b | |||
| 2953b07609 | |||
| 0d14bb3538 | |||
| 41bd1898c2 | |||
| 873086eeaf | |||
| d33289d8ce | |||
| b0eb4e6d93 | |||
| 2d359f59e4 | |||
| 3fc769a687 | |||
| 4edd5af9d7 | |||
| 7a06d87f4a | |||
| 05b0b6cb6a | |||
| 4a5450b4b0 | |||
| 1410c959e6 | |||
| 6e75cf3725 | |||
| c8c443fdc8 | |||
| 545b79de3b | |||
| 97ecb7572a | |||
| 940e1ece94 | |||
| a212791ba9 | |||
| d9f773c1f9 | |||
| 2d68b83cf4 | |||
| 7e6e9978a2 | |||
| 35052a045a | |||
| ce74550825 | |||
| a5c9c78fdd | |||
| c4f67fa0fc | |||
| c8ca946fcc | |||
| 760ba0e823 | |||
| 1cb78fa3db | |||
| 09bd70b348 | |||
| cd573b1ce0 | |||
| 96d1289730 | |||
| 539068978f | |||
| 48f087dc6e | |||
| be73fe9ee6 | |||
| 7dcfb9977c | |||
| 0a6a7b4a3b | |||
| b74e4ad286 | |||
| 74b0b1dc64 | |||
| ca03200847 | |||
| dee96cf704 | |||
| 781d008923 | |||
| f57c00b834 | |||
| 804b640af5 |
@@ -6,3 +6,5 @@ build
|
|||||||
|
|
||||||
logs/*.log
|
logs/*.log
|
||||||
logs/*.log.gz
|
logs/*.log.gz
|
||||||
|
conf/*.conf
|
||||||
|
conf/*.xml
|
||||||
|
|||||||
+2
-1
@@ -39,11 +39,12 @@ val detag by configurations.creating {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
detag("com.r35157.tools:detag-impl_ref:0.1-dev")
|
detag("com.r35157.tools:detag-impl_ref:0.1.0")
|
||||||
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")
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
# 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!"
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# 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"
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
#!/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,10 +5,5 @@ 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
|
|
||||||
|
|||||||
Executable
+41
@@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ "$#" -ne 1 ]; then
|
||||||
|
echo "Usage: $0 <github-snapshot-branch>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
BRANCH="$1"
|
||||||
|
|
||||||
|
SOURCE="$HOME/projects/com_r35157_nenjim-hubd-impl_ref"
|
||||||
|
TARGET="$HOME/projects/com_r35157_nenjim-hubd-impl_ref_github_snapshot"
|
||||||
|
|
||||||
|
cd "$TARGET"
|
||||||
|
|
||||||
|
git fetch origin
|
||||||
|
|
||||||
|
if git show-ref --verify --quiet "refs/heads/$BRANCH"; then
|
||||||
|
git switch "$BRANCH"
|
||||||
|
elif git show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then
|
||||||
|
git switch --track "origin/$BRANCH"
|
||||||
|
else
|
||||||
|
git switch --create "$BRANCH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rsync -a --delete \
|
||||||
|
--exclude '.git' \
|
||||||
|
--exclude 'conf/*.conf' \
|
||||||
|
--exclude 'conf/*.xml' \
|
||||||
|
"$SOURCE/" \
|
||||||
|
"$TARGET/"
|
||||||
|
|
||||||
|
git add -A
|
||||||
|
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "No snapshot changes to publish on branch '$BRANCH'."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
git commit -m "Mirror snapshot"
|
||||||
|
git push -u origin "$BRANCH"
|
||||||
@@ -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
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
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"]
|
|
||||||
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#!/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!"
|
|
||||||
@@ -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
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
docker stack deploy --compose-file /home/op/stack.yml stack --detach=true
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
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, PriceAlarmDefinition alarm);
|
void trigger(OraclePrice price, ResolvedPriceAlarm alarm);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+142
-13
@@ -1,28 +1,33 @@
|
|||||||
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.ArrayList;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
public final class AlarmConfigurationParser {
|
public final class AlarmConfigurationParser {
|
||||||
|
|
||||||
public static List<PriceAlarmDefinition> parse(Path path) throws IOException {
|
public static AlarmConfiguration parse(Path path) throws IOException {
|
||||||
List<String> lines = Files.readAllLines(path);
|
List<String> lines = Files.readAllLines(path);
|
||||||
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(),
|
||||||
@@ -35,24 +40,32 @@ public final class AlarmConfigurationParser {
|
|||||||
throw new IllegalArgumentException("No alarms found in " + path);
|
throw new IllegalArgumentException("No alarms found in " + path);
|
||||||
}
|
}
|
||||||
|
|
||||||
return List.copyOf(alarms);
|
return new AlarmConfiguration(alarms, constants);
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
);
|
);
|
||||||
|
|
||||||
BigDecimal target = new BigDecimal(cursor.nextToken("target"));
|
String targetExpression = cursor.nextToken("target");
|
||||||
AlarmTrigger trigger = AlarmTrigger.valueOf(
|
|
||||||
cursor.nextToken("trigger").toUpperCase(Locale.ROOT)
|
TriggerConfiguration triggerConfiguration = parseTrigger(
|
||||||
|
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();
|
||||||
@@ -63,15 +76,61 @@ public final class AlarmConfigurationParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new PriceAlarmDefinition(
|
return new PriceAlarmDefinition(
|
||||||
|
id,
|
||||||
asset,
|
asset,
|
||||||
direction,
|
direction,
|
||||||
target,
|
targetExpression,
|
||||||
trigger,
|
triggerConfiguration.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;
|
||||||
@@ -156,6 +215,76 @@ 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() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
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,6 +12,7 @@ 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;
|
||||||
@@ -22,7 +23,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, action);
|
return new PriceAlarm(definition, variableResolver, action);
|
||||||
})
|
})
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ public final class CompositeAlarmAction implements AlarmAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void trigger(OraclePrice price, PriceAlarmDefinition alarm) {
|
public void trigger(OraclePrice price, ResolvedPriceAlarm 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, PriceAlarmDefinition alarm) {
|
public void trigger(OraclePrice price, ResolvedPriceAlarm alarm) {
|
||||||
System.err.println();
|
System.err.println();
|
||||||
System.err.println("============================================================");
|
System.err.println("============================================================");
|
||||||
System.err.printf(
|
System.err.printf(
|
||||||
|
|||||||
@@ -48,25 +48,6 @@ 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,5 +1,10 @@
|
|||||||
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;
|
||||||
@@ -13,30 +18,44 @@ 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) {
|
||||||
System.err.println(exception.getMessage());
|
|
||||||
printUsage();
|
printUsage();
|
||||||
System.exit(2);
|
String errMsg = "Could not parse configuration for JupiterPerpsAlarm: " + exception.getMessage() + "!";
|
||||||
return;
|
throw new IllegalStateException(errMsg, exception);
|
||||||
}
|
|
||||||
|
|
||||||
if (config.selfTest()) {
|
|
||||||
SelfTest.run();
|
|
||||||
System.out.println("All self-tests passed.");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<PriceAlarmDefinition> definitions;
|
List<PriceAlarmDefinition> definitions;
|
||||||
|
AlarmConfiguration alarmConfiguration;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
definitions = AlarmConfigurationParser.parse(config.alarmConfiguration());
|
alarmConfiguration = AlarmConfigurationParser.parse(config.alarmConfiguration());
|
||||||
|
definitions = alarmConfiguration.definitions();
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
System.err.println("Could not load alarm configuration: " + exception.getMessage());
|
String errMsg = "Could not load alarm configuration: " + exception.getMessage() + "!";
|
||||||
System.exit(2);
|
throw new IllegalStateException(errMsg, exception);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
@@ -60,9 +79,10 @@ 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, action)
|
new AssetPriceAlarmMonitor(asset, assetDefinitions, variableResolver, action)
|
||||||
));
|
));
|
||||||
|
|
||||||
List<OracleWebSocketClient> clients = new ArrayList<>();
|
List<OracleWebSocketClient> clients = new ArrayList<>();
|
||||||
@@ -91,9 +111,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=%d%n",
|
" %s %s USD, %s, severity=%s%n",
|
||||||
definition.direction(),
|
definition.direction(),
|
||||||
definition.target().toPlainString(),
|
definition.targetExpression(),
|
||||||
definition.trigger(),
|
definition.trigger(),
|
||||||
definition.severity()
|
definition.severity()
|
||||||
));
|
));
|
||||||
@@ -128,7 +148,6 @@ 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
|
||||||
@@ -142,11 +161,9 @@ 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))
|
||||||
@@ -186,8 +203,7 @@ 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
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+92
@@ -0,0 +1,92 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
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,9 +1,17 @@
|
|||||||
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(PriceAlarmDefinition definition, AlarmAction action) {
|
public PriceAlarm(
|
||||||
|
PriceAlarmDefinition definition,
|
||||||
|
AlarmVariableResolver variableResolver,
|
||||||
|
AlarmAction action
|
||||||
|
) {
|
||||||
this.definition = definition;
|
this.definition = definition;
|
||||||
|
this.variableResolver = variableResolver;
|
||||||
this.action = action;
|
this.action = action;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,9 +22,23 @@ 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(),
|
||||||
definition.target()
|
target
|
||||||
);
|
);
|
||||||
|
|
||||||
boolean enteredTriggeredSide = previousReached == null
|
boolean enteredTriggeredSide = previousReached == null
|
||||||
@@ -25,16 +47,28 @@ public final class PriceAlarm {
|
|||||||
|
|
||||||
previousReached = reached;
|
previousReached = reached;
|
||||||
|
|
||||||
if (!enteredTriggeredSide) {
|
if (!reached) {
|
||||||
|
previousReached = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (definition.trigger() == AlarmTrigger.ONETIME && triggerCount > 0) {
|
if (definition.trigger() == AlarmTrigger.ONETIME) {
|
||||||
|
if (!enteredTriggeredSide || triggerCount > 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerCount++;
|
trigger(price, target);
|
||||||
action.trigger(price, definition);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
@@ -45,9 +79,58 @@ 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,28 +1,35 @@
|
|||||||
package com.r35157.jupiterperpsalarm.impl.ref;
|
package com.r35157.jupiterperpsalarm.impl.ref;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import com.r35157.jupiterperpsalarm.AlarmSeverity;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
public record PriceAlarmDefinition(
|
public record PriceAlarmDefinition(
|
||||||
|
int id,
|
||||||
JupiterPerpsAsset asset,
|
JupiterPerpsAsset asset,
|
||||||
PriceDirection direction,
|
PriceDirection direction,
|
||||||
BigDecimal target,
|
String targetExpression,
|
||||||
AlarmTrigger trigger,
|
AlarmTrigger trigger,
|
||||||
int severity,
|
ΩsecondsΩ triggerGracePeriod,
|
||||||
|
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(target, "target");
|
Objects.requireNonNull(targetExpression, "targetExpression");
|
||||||
Objects.requireNonNull(trigger, "trigger");
|
Objects.requireNonNull(trigger, "trigger");
|
||||||
|
Objects.requireNonNull(severity, "severity");
|
||||||
Objects.requireNonNull(note, "note");
|
Objects.requireNonNull(note, "note");
|
||||||
|
|
||||||
if (target.signum() <= 0) {
|
if (targetExpression.isBlank()) {
|
||||||
throw new IllegalArgumentException("Target price must be positive");
|
throw new IllegalArgumentException("Target expression cannot be blank");
|
||||||
}
|
}
|
||||||
if (severity < 0) {
|
|
||||||
throw new IllegalArgumentException("Severity must be zero or positive");
|
if (triggerGracePeriod < 0) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Trigger grace period cannot be negative: " + triggerGracePeriod
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
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;
|
||||||
@@ -16,25 +18,15 @@ public final class PushoverAlarmAction implements AlarmAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void trigger(OraclePrice price, PriceAlarmDefinition alarm) {
|
public void trigger(OraclePrice price, ResolvedPriceAlarm alarm) {
|
||||||
String title = "Jupiter Perps " + price.asset() + " alarm";
|
String title = "Jupiter Perps " + price.asset() + " alarm";
|
||||||
String message = String.format(
|
String message = createMessage(price, alarm);
|
||||||
"%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) + "&" +
|
||||||
"priority=2&retry=30&expire=10800&sound=persistent";
|
createPushoverSeverityParameters(alarm.severity());
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder(PUSHOVER_URI)
|
HttpRequest request = HttpRequest.newBuilder(PUSHOVER_URI)
|
||||||
.timeout(Duration.ofSeconds(15))
|
.timeout(Duration.ofSeconds(15))
|
||||||
@@ -56,7 +48,7 @@ public final class PushoverAlarmAction implements AlarmAction {
|
|||||||
response.body()
|
response.body()
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
System.out.println("Pushover emergency alarm sent.");
|
System.out.println("Pushover alarm sent: " + alarm.severity());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -66,6 +58,32 @@ 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;
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
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() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
package com.r35157.jupiterperpsalarm.impl.ref;
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.r35157.libs.codec;
|
||||||
|
|
||||||
|
public interface Base58Codec {
|
||||||
|
String encode(byte[] input);
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
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];
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.r35157.libs.jupiter.perps;
|
||||||
|
|
||||||
|
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a Jupiter Perps position.
|
||||||
|
*
|
||||||
|
* <p>A Jupiter Perps position is represented on-chain by a Solana account owned by
|
||||||
|
* the Jupiter Perps program. This record contains the public API view of such a
|
||||||
|
* position.</p>
|
||||||
|
*
|
||||||
|
* @param positionAccount the Solana account address of the Jupiter Perps position
|
||||||
|
* @param tradedTokenMint the mint address of the token being traded
|
||||||
|
* @param direction whether the position is long or short
|
||||||
|
* @param value the amount the position is worth if closed now
|
||||||
|
* @param size the leveraged amount used to open the contracts
|
||||||
|
* @param pnl the amount in usd in profit or loss on this position
|
||||||
|
* @param pnlPercent the profit and loss represented as a percentage
|
||||||
|
* @param leverage TODO
|
||||||
|
* @param entryPrice the entry price of the position, denominated in USDC
|
||||||
|
* @param marketPrice the current spot price of the token
|
||||||
|
* @param collateral the amount of USD representing the collateral for this position
|
||||||
|
* @param totalFees the total amount of fees (TODO: is that including pending/due fees)
|
||||||
|
* @param borrowFeesDue the amount of USD that is currently outstanding
|
||||||
|
* @param closeFeePending the fee in USD for closing the account (TODO: multiple accounts - when adding collateral?)
|
||||||
|
* @param accountRent refundable amount locked for Solana account renting
|
||||||
|
*/
|
||||||
|
public record JupiterPerpsPosition(
|
||||||
|
ΩJupiterPerpsPositionAccountΩ positionAccount,
|
||||||
|
ΩSPLMintAddressΩ tradedTokenMint,
|
||||||
|
JupiterPerpsPositionDirection direction,
|
||||||
|
ΩUSDCAmountΩ value,
|
||||||
|
ΩUSDCAmountΩ size,
|
||||||
|
ΩUSDCAmountΩ pnl,
|
||||||
|
BigDecimal pnlPercent,
|
||||||
|
BigDecimal leverage,
|
||||||
|
ΩUSDCPriceΩ entryPrice,
|
||||||
|
ΩUSDCPriceΩ marketPrice,
|
||||||
|
ΩUSDCAmountΩ collateral,
|
||||||
|
ΩUSDCAmountΩ totalFees,
|
||||||
|
ΩUSDCAmountΩ borrowFeesDue,
|
||||||
|
ΩUSDCAmountΩ closeFeePending,
|
||||||
|
ΩSolanaAmountΩ accountRent
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.r35157.libs.jupiter.perps;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Direction of a Jupiter Perps position.
|
||||||
|
*/
|
||||||
|
public enum JupiterPerpsPositionDirection {
|
||||||
|
LONG,
|
||||||
|
SHORT
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.r35157.libs.jupiter.perps;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for reading Jupiter Perps data.
|
||||||
|
*
|
||||||
|
* <p>This service is read-only. It does not open, close, modify, or sign transactions
|
||||||
|
* for Jupiter Perpetual Contracts.</p>
|
||||||
|
*
|
||||||
|
* <p>NOTICE: The first supported operation is reading a known Jupiter Perps position account
|
||||||
|
* and returning its decoded position data.</p>
|
||||||
|
*/
|
||||||
|
public interface JupiterPerpsService {
|
||||||
|
/**
|
||||||
|
* Reads a Jupiter Perps position from a known position account.
|
||||||
|
*
|
||||||
|
* <p>The supplied account must be the Solana account that stores the Jupiter Perps
|
||||||
|
* position state. It is not the wallet address, token account, custody account, pool
|
||||||
|
* account, or position request account.</p>
|
||||||
|
*
|
||||||
|
* @param positionAccount the Solana account address of the Jupiter Perps position
|
||||||
|
* @return the decoded Jupiter Perps position
|
||||||
|
* @throws IOException if the position account could not be fetched or decoded
|
||||||
|
* @throws InterruptedException if the calling thread is interrupted while fetching
|
||||||
|
* the position account
|
||||||
|
*/
|
||||||
|
@Nullable JupiterPerpsPosition getPosition(@NotNull ΩJupiterPerpsPositionAccountΩ positionAccount)
|
||||||
|
throws IOException, InterruptedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds open Jupiter Perps positions owned by a wallet.
|
||||||
|
*
|
||||||
|
* <p>This method returns decoded Jupiter Perps position objects. It does not return
|
||||||
|
* raw Solana accounts or account ids.</p>
|
||||||
|
*
|
||||||
|
* @param owner the wallet address that owns the Jupiter Perps positions
|
||||||
|
* @return the open Jupiter Perps positions owned by the wallet
|
||||||
|
* @throws IOException if the position accounts could not be fetched or decoded
|
||||||
|
* @throws InterruptedException if the calling thread is interrupted while fetching positions
|
||||||
|
*/
|
||||||
|
@NotNull Set<JupiterPerpsPosition> getOpenPositions(@NotNull ΩSolanaWalletIdΩ owner)
|
||||||
|
throws IOException, InterruptedException;
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
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.JupiterPerpsPositionDirection;
|
||||||
|
import com.r35157.libs.solana.SolanaAccountInfo;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
import static java.math.BigDecimal.ZERO;
|
||||||
|
|
||||||
|
class AnchorIdlJupiterPerpsPositionDecoder {
|
||||||
|
|
||||||
|
@Nullable JupiterPerpsPositionInfo decode(@NotNull SolanaAccountInfo accountInfo) {
|
||||||
|
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);
|
||||||
|
|
||||||
|
long rawCollateralUsd = ByteBuffer
|
||||||
|
.wrap(data, COLLATERAL_USD_OFFSET, U64_LENGTH)
|
||||||
|
.order(ByteOrder.LITTLE_ENDIAN)
|
||||||
|
.getLong();
|
||||||
|
|
||||||
|
ΩUSDCAmountΩ collateralUsd = BigDecimal
|
||||||
|
.valueOf(rawCollateralUsd)
|
||||||
|
.movePointLeft(6);
|
||||||
|
|
||||||
|
long rawSizeUsd = ByteBuffer
|
||||||
|
.wrap(data, SIZE_USD_OFFSET, U64_LENGTH)
|
||||||
|
.order(ByteOrder.LITTLE_ENDIAN)
|
||||||
|
.getLong();
|
||||||
|
|
||||||
|
ΩUSDCAmountΩ sizeUsd = BigDecimal
|
||||||
|
.valueOf(rawSizeUsd)
|
||||||
|
.movePointLeft(6);
|
||||||
|
|
||||||
|
JupiterPerpsPositionInfo posInfo = new JupiterPerpsPositionInfo(entryPrice, direction, sizeUsd, collateralUsd);
|
||||||
|
|
||||||
|
return posInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ω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;
|
||||||
|
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;
|
||||||
|
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 int SIZE_USD_OFFSET = PRICE_OFFSET + U64_LENGTH;
|
||||||
|
private static final int COLLATERAL_USD_OFFSET = SIZE_USD_OFFSET + U64_LENGTH;
|
||||||
|
|
||||||
|
private static final Base58Codec base58 = new Base58CodecImpl();
|
||||||
|
}
|
||||||
+172
@@ -0,0 +1,172 @@
|
|||||||
|
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 com.r35157.libs.solana.valuetypes.WellKnownCurrencyTypes;
|
||||||
|
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static java.math.BigDecimal.ZERO;
|
||||||
|
|
||||||
|
public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
|
||||||
|
|
||||||
|
public AnchorIdlJupiterPerpsServiceImpl(
|
||||||
|
SolanaBlockChain solanaBlockChain
|
||||||
|
) {
|
||||||
|
this.solanaBlockChain = solanaBlockChain;
|
||||||
|
this.positionDecoder = new AnchorIdlJupiterPerpsPositionDecoder();
|
||||||
|
this.custodyDecoder = new AnchorIdlJupiterPerpsCustodyDecoder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable JupiterPerpsPosition getPosition(@NotNull Ω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);
|
||||||
|
|
||||||
|
JupiterPerpsPositionInfo info = positionDecoder.decode(accountInfo);
|
||||||
|
|
||||||
|
JupiterPerpsPosition pos;
|
||||||
|
if(info == null) {
|
||||||
|
pos = null;
|
||||||
|
} else {
|
||||||
|
ΩUSDCAmountΩ value = ZERO; // TODO - Dummy
|
||||||
|
ΩUSDCAmountΩ pnl = ZERO; // TODO - Dummy
|
||||||
|
BigDecimal pnlPercent = ZERO; // TODO - Dummy
|
||||||
|
BigDecimal leverage = ZERO; // TODO - Dummy
|
||||||
|
ΩUSDCPriceΩ marketPrice = ZERO; // TODO - Dummy
|
||||||
|
ΩUSDCAmountΩ totalFees = ZERO; // TODO - Dummy
|
||||||
|
ΩUSDCAmountΩ borrowFeesDue = ZERO; // TODO - Dummy
|
||||||
|
ΩUSDCAmountΩ closeFeePending = ZERO; // TODO - Dummy
|
||||||
|
ΩSolanaAmountΩ accountRent = new MoneyAmount(ZERO, WellKnownCurrencyTypes.SOLANA.getCurrencyType()); // TODO - Dummy
|
||||||
|
|
||||||
|
pos = new JupiterPerpsPosition(
|
||||||
|
positionAccount,
|
||||||
|
tradedTokenMint,
|
||||||
|
info.direction(),
|
||||||
|
value,
|
||||||
|
info.sizeUsd(),
|
||||||
|
pnl,
|
||||||
|
pnlPercent,
|
||||||
|
leverage,
|
||||||
|
info.entryPrice(),
|
||||||
|
marketPrice,
|
||||||
|
info.collateralUsd(),
|
||||||
|
totalFees,
|
||||||
|
borrowFeesDue,
|
||||||
|
closeFeePending,
|
||||||
|
accountRent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Set<JupiterPerpsPosition> getOpenPositions(@NotNull Ω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) {
|
||||||
|
ΩSolanaProgramIdΩ programId = accountInfo.owner();
|
||||||
|
ΩJupiterPerpsPositionAccountΩ positionAccount = accountInfo.address();
|
||||||
|
|
||||||
|
if (!JUPITER_PERPS_PROGRAM_ID.equals(programId)) {
|
||||||
|
String errorMsg = "Account '" + positionAccount + "' is not owned by Jupiter Perps program '" +
|
||||||
|
programId + "'";
|
||||||
|
throw new IllegalArgumentException(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
JupiterPerpsPositionInfo info = positionDecoder.decode(accountInfo);
|
||||||
|
if(info == null || info.sizeUsd().compareTo(ZERO) == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ΩSPLMintAddressΩ tradedTokenMint = getTradedTokenMint(accountInfo);
|
||||||
|
|
||||||
|
ΩUSDCAmountΩ value = ZERO; // TODO - Dummy
|
||||||
|
ΩUSDCAmountΩ pnl = ZERO; // TODO - Dummy
|
||||||
|
BigDecimal pnlPercent = ZERO; // TODO - Dummy
|
||||||
|
BigDecimal leverage = ZERO; // TODO - Dummy
|
||||||
|
ΩUSDCPriceΩ marketPrice = ZERO; // TODO - Dummy
|
||||||
|
ΩUSDCAmountΩ totalFees = ZERO; // TODO - Dummy
|
||||||
|
ΩUSDCAmountΩ borrowFeesDue = ZERO; // TODO - Dummy
|
||||||
|
ΩUSDCAmountΩ closeFeePending = ZERO; // TODO - Dummy
|
||||||
|
ΩSolanaAmountΩ accountRent = new MoneyAmount(ZERO, WellKnownCurrencyTypes.SOLANA.getCurrencyType()); // TODO - Dummy
|
||||||
|
|
||||||
|
JupiterPerpsPosition pos = new JupiterPerpsPosition(
|
||||||
|
positionAccount,
|
||||||
|
tradedTokenMint,
|
||||||
|
info.direction(),
|
||||||
|
value,
|
||||||
|
info.sizeUsd(),
|
||||||
|
pnl,
|
||||||
|
pnlPercent,
|
||||||
|
leverage,
|
||||||
|
info.entryPrice(),
|
||||||
|
marketPrice,
|
||||||
|
info.collateralUsd(),
|
||||||
|
totalFees,
|
||||||
|
borrowFeesDue,
|
||||||
|
closeFeePending,
|
||||||
|
accountRent
|
||||||
|
);
|
||||||
|
|
||||||
|
positions.add(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
package com.r35157.libs.jupiter.perps.impl.anchoridl;
|
||||||
|
|
||||||
|
import com.r35157.libs.jupiter.perps.JupiterPerpsPositionDirection;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
public record JupiterPerpsPositionInfo(
|
||||||
|
ΩUSDCPriceΩ entryPrice,
|
||||||
|
JupiterPerpsPositionDirection direction,
|
||||||
|
ΩUSDCAmountΩ sizeUsd,
|
||||||
|
ΩUSDCAmountΩ collateralUsd
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -143,4 +143,25 @@ public interface SolanaBlockChain {
|
|||||||
ΩSPLMintAddressΩ mintAddress,
|
ΩSPLMintAddressΩ mintAddress,
|
||||||
SolanaSPLTokenProgram splProgram
|
SolanaSPLTokenProgram splProgram
|
||||||
) throws IOException, InterruptedException;
|
) throws IOException, InterruptedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches accounts owned by a Solana program using server-side account data filters.
|
||||||
|
*
|
||||||
|
* <p>This method uses Solana's {@code getProgramAccounts} RPC call. The supplied filters
|
||||||
|
* are sent to the RPC node, so matching is performed server-side instead of fetching all
|
||||||
|
* accounts owned by the program and filtering them locally.</p>
|
||||||
|
*
|
||||||
|
* <p>The initial supported filter type is {@link SolanaProgramAccountMemcmpFilter}, which
|
||||||
|
* matches bytes at a specific offset in the account data.</p>
|
||||||
|
*
|
||||||
|
* @param programId the Solana program id that owns the accounts to search
|
||||||
|
* @param filters the memcmp filters to apply when searching program accounts
|
||||||
|
* @return the matching program accounts
|
||||||
|
* @throws IOException if the program accounts could not be fetched or parsed
|
||||||
|
* @throws InterruptedException if the calling thread is interrupted while fetching program accounts
|
||||||
|
*/
|
||||||
|
Set<SolanaAccountInfo> getProgramAccounts(
|
||||||
|
ΩSolanaProgramIdΩ programId,
|
||||||
|
Set<SolanaProgramAccountMemcmpFilter> filters
|
||||||
|
) throws IOException, InterruptedException;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.r35157.libs.solana;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter used when fetching accounts owned by a Solana program.
|
||||||
|
*
|
||||||
|
* <p>The initial supported filter type is {@code memcmp}, which asks the
|
||||||
|
* Solana RPC node to only return accounts where the account data at a specific
|
||||||
|
* byte offset matches a base58 encoded value.</p>
|
||||||
|
*
|
||||||
|
* @param offset the byte offset in the account data where comparison starts
|
||||||
|
* @param bytes the base58 encoded bytes to match
|
||||||
|
*/
|
||||||
|
public record SolanaProgramAccountMemcmpFilter(
|
||||||
|
int offset,
|
||||||
|
String bytes
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -331,6 +331,113 @@ 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();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.r35157.libs.solana.valuetypes;
|
||||||
|
|
||||||
|
import com.r35157.libs.valuetypes.basic.CurrencyType;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines well-known currency types used by the Solana integration.
|
||||||
|
*
|
||||||
|
* <p>Each enum value wraps a {@link CurrencyType} with a stable identifier and a
|
||||||
|
* human-readable currency name. These predefined values are intended for common
|
||||||
|
* currencies that the Solana-related modules need to reference consistently.</p>
|
||||||
|
*/
|
||||||
|
public enum WellKnownCurrencyTypes {
|
||||||
|
/**
|
||||||
|
* Native Solana currency.
|
||||||
|
*/
|
||||||
|
SOLANA(new CurrencyType(
|
||||||
|
UUID.fromString("019e0116-fce5-792f-a647-fa6da4dffec5"),
|
||||||
|
"Solana",
|
||||||
|
"SOL")
|
||||||
|
),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syrup USDC token currency.
|
||||||
|
*/
|
||||||
|
SYRUPUSDC(new CurrencyType(
|
||||||
|
UUID.fromString("019e1d51-0600-7956-8231-f3b7058a91c2"),
|
||||||
|
"SyrupUSDC",
|
||||||
|
"SyrupUSDC")
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a well-known currency type entry.
|
||||||
|
*
|
||||||
|
* @param currencyType the currency type represented by this enum value
|
||||||
|
*/
|
||||||
|
WellKnownCurrencyTypes(CurrencyType currencyType) {
|
||||||
|
this.currencyType = currencyType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the currency type represented by this enum value.
|
||||||
|
*
|
||||||
|
* @return the represented currency type
|
||||||
|
*/
|
||||||
|
public CurrencyType getCurrencyType() {
|
||||||
|
return currencyType;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final CurrencyType currencyType;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.r35157.libs.valuetypes.basic;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
public record MoneyPrice(
|
||||||
|
ΩPriceΩ price,
|
||||||
|
CurrencyType currencyType
|
||||||
|
) { }
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
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;
|
||||||
@@ -12,12 +17,24 @@ 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 {
|
||||||
static void main(String[] args) throws Exception {
|
|
||||||
NenjimHub nenjimHub = new NenjimHubImpl();
|
|
||||||
|
|
||||||
/* try {
|
// 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 {
|
||||||
|
NenjimHubImpl nenjimHub = new NenjimHubImpl();
|
||||||
|
|
||||||
|
/*
|
||||||
|
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,5 +1,6 @@
|
|||||||
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;
|
||||||
@@ -8,6 +9,7 @@ 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 {
|
||||||
@@ -38,19 +40,24 @@ public class NenjimHubImpl implements NenjimHub {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void startAutoRunProcesses() throws Exception {
|
private void startAutoRunProcesses() throws Exception {
|
||||||
String[] processesToAutoStart = {
|
startJupiterPerpsAlarm(); // TODO: Hardcoded/hacky way to auto-start but good enough for now.
|
||||||
|
|
||||||
"crypto.r35157.nenjim.NenjimHubSocketAdminAdapter",
|
// TODO: Old but more correct way to auto start plugins - but it is currently broken.
|
||||||
"crypto.r35157.nenjim.NenjimHubRestAdminAdapter",
|
/*
|
||||||
"crypto.r35157.nenjim.NenjimHubRPCAdminAdapter",
|
String[] processesToAutoStart = {
|
||||||
"crypto.r35157.nenjim.SuwimoHub",
|
//"com.r35157.jupiterperpsalarm.impl.ref.JupiterPerpsAlarmImpl"
|
||||||
"crypto.r35157.nenjim.SodaTaskManager",
|
//"crypto.r35157.nenjim.NenjimHubSocketAdminAdapter",
|
||||||
"crypto.r35157.assetaz.hub.AssetAZHub"
|
//"crypto.r35157.nenjim.NenjimHubRestAdminAdapter",
|
||||||
|
//"crypto.r35157.nenjim.NenjimHubRPCAdminAdapter",
|
||||||
|
//"crypto.r35157.nenjim.SuwimoHub",
|
||||||
|
//"crypto.r35157.nenjim.SodaTaskManager",
|
||||||
|
//"crypto.r35157.assetaz.hub.AssetAZHub"
|
||||||
};
|
};
|
||||||
|
|
||||||
for (String processInterfaceName : processesToAutoStart) {
|
for (String processInterfaceName : processesToAutoStart) {
|
||||||
startProcess(processInterfaceName);
|
startProcess(processInterfaceName);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -90,7 +97,27 @@ 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;
|
||||||
|
|||||||
Reference in New Issue
Block a user