50 Commits

Author SHA256 Message Date
minimons 244a6f19fe 20: Add actual refreshing logic to loop 2026-06-28 18:40:05 +02:00
minimons ff4f5a0356 20: Adding refresher file filtering 2026-06-28 18:33:20 +02:00
minimons f86652df7d 20: More refresher logic 2026-06-28 18:30:58 +02:00
minimons 5e5f8ebd51 20: Prepare background refresher thread 2026-06-28 18:19:20 +02:00
minimons d216c06ba1 20: Add watcher wire-up 2026-06-28 18:14:13 +02:00
minimons c1cbaf0c52 20: Add new JupiterPerpsEntryPriceVariableRefreshWatcher class 2026-06-28 18:06:50 +02:00
minimons 76446f8b73 20: Clean-up before update 2026-06-28 11:48:08 +02:00
minimons dac1b62628 20: Update variable map with new data 2026-06-28 11:45:04 +02:00
minimons 0aff69429a 20: Map mint addresses to asset symbols 2026-06-28 11:40:13 +02:00
minimons 384fad01bf 20: Fetch open Perps positions JupiterPerpsEntryPriceVariableRefresher 2026-06-28 11:36:05 +02:00
minimons 520a0bcd92 20: Inject JupiterPerpsService into JupiterPerpsEntryPriceVariableRefresher 2026-06-28 11:30:03 +02:00
minimons 896b5235d0 20: Add new JupiterPerpsEntryPriceVariableRefresher class 2026-06-28 11:08:14 +02:00
minimons 403d7af6e9 20: CompositeAlarmAction compilation fix 2026-06-27 23:32:51 +02:00
minimons 90fd1694fb 20: Fix compilatoin of PushoverAlarmAction 2026-06-27 23:27:47 +02:00
minimons 1fb712b61d 20: Fix compilation of ConsoleAlarmAction 2026-06-27 23:25:04 +02:00
minimons 1355422597 20: Use resolved node in PriceAlarm trigger 2026-06-27 21:12:26 +02:00
minimons c6ba8dc009 20: Change AlarmAction to use resolved data 2026-06-27 21:07:17 +02:00
minimons a5e5470c2b 20: Create new ResolvedPriceAlarm and resolve note 2026-06-27 21:03:21 +02:00
minimons 2953b07609 20: Switch variables with brackets 2026-06-27 20:54:02 +02:00
minimons 0d14bb3538 20: Store variables keys without brackets 2026-06-27 20:48:28 +02:00
minimons 41bd1898c2 20: Change AlarmConfiguration to use ConcurrentHashMap 2026-06-27 20:43:09 +02:00
minimons 873086eeaf 20: Handle missing variables gracefully 2026-06-27 20:35:10 +02:00
minimons d33289d8ce 20: Wire-up resolver 2026-06-27 20:29:24 +02:00
minimons b0eb4e6d93 20: Make AlarmConfigurationParser return AlarmConfiguration 2026-06-27 20:20:03 +02:00
minimons 2d359f59e4 20: Add new AlarmConfiguration class 2026-06-27 20:15:41 +02:00
minimons 3fc769a687 20: Initialize AlarmVariableResolver 2026-06-27 20:12:10 +02:00
minimons 4edd5af9d7 20: Inject AlarmVariableResolver 2026-06-27 20:07:14 +02:00
minimons 7a06d87f4a 20: Use resolver in PriceAlarm 2026-06-27 20:01:47 +02:00
minimons 05b0b6cb6a 20: Add AlarmVariableResolver class 2026-06-27 19:57:02 +02:00
minimons 4a5450b4b0 20: Do not resolve while reading config 2026-06-27 19:54:12 +02:00
minimons 1410c959e6 20: Fix target price compilation issues 2026-06-27 19:49:36 +02:00
minimons 6e75cf3725 20: Update PriceAlarm to use parser 2026-06-27 19:40:46 +02:00
minimons c8c443fdc8 20: Move target price parsing to new AlarmTargetParser class 2026-06-27 19:27:25 +02:00
minimons 545b79de3b 20: Do not resolve target price 2026-06-27 19:14:01 +02:00
minimons 97ecb7572a 20: Update PriceAlarmDefinition to support expressions 2026-06-27 19:05:59 +02:00
minimons 940e1ece94 20: Change config file 2026-06-27 19:05:59 +02:00
minimons a212791ba9 X: Add publishGitHub 2026-06-27 19:05:59 +02:00
minimons d9f773c1f9 X: Updated for some manual testing in Main 2026-06-26 22:27:57 +02:00
minimons 2d68b83cf4 17: Add mint address field in JupiterPerpsPosition 2026-06-26 22:27:57 +02:00
minimons 7e6e9978a2 18: Add a codec class for Base58 2026-06-26 20:55:37 +02:00
minimons 35052a045a X: Rename AnchorIdlJupiterPerpsPositionServiceImpl 2026-06-26 19:26:24 +02:00
minimons ce74550825 X: Rename JupiterPerpsPositionService 2026-06-26 19:26:24 +02:00
minimons a5c9c78fdd X: Move JupiterPerpsPositionService to API instead of implementation 2026-06-26 19:26:24 +02:00
minimons c4f67fa0fc 16: Add 'side' to AnchorIdlJupiterPerpsPositionDecoder 2026-06-26 19:26:24 +02:00
minimons c8ca946fcc X: Move JupiterPerpsPosition to API from implementation 2026-06-26 18:08:26 +02:00
minimons 760ba0e823 X: Add some manuel testing code 2026-06-26 17:02:41 +02:00
minimons 1cb78fa3db 15: Add getOpenPositions() to Jupiter Perps API 2026-06-26 17:02:40 +02:00
minimons 09bd70b348 14: Add getProgramAccounts(...) API method 2026-06-26 14:14:06 +02:00
minimons cd573b1ce0 X: Some dependency cleanup 2026-06-25 19:08:52 +02:00
minimons 96d1289730 13: Create initial Jupiter Perps position API 2026-06-25 15:30:30 +02:00
25 changed files with 952 additions and 235 deletions
+1
View File
@@ -44,6 +44,7 @@ dependencies {
runtimeOnly("org.apache.logging.log4j:log4j-core: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.fazecast:jSerialComm:2.11.4")
implementation("com.google.code.gson:gson:2.14.0")
+55 -7
View File
@@ -1,8 +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
######################################################################################################################
1 SOL ABOVE 97.03-1.25% ONETIME CRITICAL "CRITICAL: Risiko for Perps Solana short LIKVIDERING!"
2 SOL BELOW 48.72+1.25% ONETIME CRITICAL "CRITICAL: Risiko for Perps Solana long LIKVIDERING!"
3 BTC ABOVE 85032.87-1% ONETIME CRITICAL "CRITICAL: Risiko for Perps Bitcoin short LIKVIDERING!"
4 BTC BELOW 42779.40+1% ONETIME CRITICAL "CRITICAL: Risiko for Perps Bitcoin long LIKVIDERING!"
5 ETH ABOVE 2296.13-1% ONETIME CRITICAL "CRITICAL: Risiko for Perps Ethereum short LIKVIDERING!"
6 ETH BELOW 1155.19+1% ONETIME CRITICAL "CRITICAL: Risiko for Perps Ethereum long LIKVIDERING!"
###############################################################################################################################
# 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"
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
SOURCE="$HOME/projects/com_r35157_nenjim-hubd-impl_ref"
TARGET="$HOME/projects/com_r35157_nenjim-hubd-impl_ref_github_snapshot"
rsync -a --delete \
--exclude '.git' \
--exclude 'conf/*.conf' \
--exclude 'conf/*.xml' \
"$SOURCE/" \
"$TARGET/"
cd "$TARGET"
git add -A
if git diff --cached --quiet; then
echo "No snapshot changes to publish."
exit 0
fi
git commit -m "Mirror snapshot"
git push
@@ -2,5 +2,5 @@ package com.r35157.jupiterperpsalarm.impl.ref;
@FunctionalInterface
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);
}
}
@@ -3,14 +3,13 @@ package com.r35157.jupiterperpsalarm.impl.ref;
import com.r35157.jupiterperpsalarm.AlarmSeverity;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
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<PriceAlarmDefinition> alarms = new ArrayList<>();
Map<String, String> constants = new LinkedHashMap<>();
@@ -27,8 +26,7 @@ public final class AlarmConfigurationParser {
if(isConstantDefinition(trimmed)) {
parseConstantDefinition(constants, line);
} else {
String resolvedLine = replaceConstants(line, constants);
alarms.add(parseLine(resolvedLine));
alarms.add(parseLine(line));
}
} catch (RuntimeException exception) {
throw new IllegalArgumentException(
@@ -42,7 +40,7 @@ public final class AlarmConfigurationParser {
throw new IllegalArgumentException("No alarms found in " + path);
}
return List.copyOf(alarms);
return new AlarmConfiguration(alarms, constants);
}
static PriceAlarmDefinition parseLine(String line) {
@@ -58,7 +56,7 @@ public final class AlarmConfigurationParser {
cursor.nextToken("direction").toUpperCase(Locale.ROOT)
);
BigDecimal target = parseTarget(cursor.nextToken("target"));
String targetExpression = cursor.nextToken("target");
TriggerConfiguration triggerConfiguration = parseTrigger(
cursor.nextToken("trigger")
@@ -81,7 +79,7 @@ public final class AlarmConfigurationParser {
id,
asset,
direction,
target,
targetExpression,
triggerConfiguration.trigger(),
triggerConfiguration.gracePeriod(),
severity,
@@ -89,28 +87,6 @@ public final class AlarmConfigurationParser {
);
}
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 TriggerConfiguration parseTrigger(String triggerText) {
String normalized = triggerText.toUpperCase(Locale.ROOT);
@@ -155,35 +131,6 @@ public final class AlarmConfigurationParser {
throw new IllegalArgumentException("Unknown trigger: " + triggerText);
}
private static void validateTarget(BigDecimal target, String originalTargetStr) {
if (target.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException(
"Target must be zero or positive: " + originalTargetStr
);
}
}
private static BigDecimal parseTarget(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 final class Cursor {
private Cursor(String line) {
this.line = line;
@@ -290,9 +237,21 @@ public final class AlarmConfigurationParser {
validateConstantName(name);
if (constants.putIfAbsent(name, value) != null) {
throw new IllegalArgumentException("Duplicate constant: " + 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(
@@ -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(
JupiterPerpsAsset asset,
List<PriceAlarmDefinition> definitions,
AlarmVariableResolver variableResolver,
AlarmAction action
) {
this.asset = asset;
@@ -22,7 +23,7 @@ public final class AssetPriceAlarmMonitor {
"Alarm asset " + definition.asset() + " does not match monitor " + asset
);
}
return new PriceAlarm(definition, action);
return new PriceAlarm(definition, variableResolver, action);
})
.toList();
@@ -9,7 +9,7 @@ public final class CompositeAlarmAction implements AlarmAction {
}
@Override
public void trigger(OraclePrice price, PriceAlarmDefinition alarm) {
public void trigger(OraclePrice price, ResolvedPriceAlarm alarm) {
for (AlarmAction action : actions) {
try {
action.trigger(price, alarm);
@@ -2,7 +2,7 @@ package com.r35157.jupiterperpsalarm.impl.ref;
public final class ConsoleAlarmAction implements AlarmAction {
@Override
public void trigger(OraclePrice price, PriceAlarmDefinition alarm) {
public void trigger(OraclePrice price, ResolvedPriceAlarm alarm) {
System.err.println();
System.err.println("============================================================");
System.err.printf(
@@ -1,5 +1,10 @@
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.nio.file.Path;
import java.util.ArrayList;
@@ -13,6 +18,7 @@ public final class JupiterPerpsAlarmImpl {
public static void main(String[] args) throws Exception {
Config config;
try {
config = Config.parse(args, System.getenv());
} catch (IllegalArgumentException exception) {
@@ -22,13 +28,34 @@ public final class JupiterPerpsAlarmImpl {
}
List<PriceAlarmDefinition> definitions;
AlarmConfiguration alarmConfiguration;
try {
definitions = AlarmConfigurationParser.parse(config.alarmConfiguration());
alarmConfiguration = AlarmConfigurationParser.parse(config.alarmConfiguration());
definitions = alarmConfiguration.definitions();
} catch (Exception exception) {
String errMsg = "Could not load alarm configuration: " + exception.getMessage() + "!";
throw new IllegalStateException(errMsg, exception);
}
AlarmVariableResolver variableResolver = new AlarmVariableResolver(
alarmConfiguration.variables()
);
SolanaBlockChain solanaBlockChain = new SolanaBlockChainImpl();
JupiterPerpsService jupiterPerpsService = new AnchorIdlJupiterPerpsServiceImpl(solanaBlockChain);
JupiterPerpsEntryPriceVariableRefresher entryPriceVariableRefresher =
new JupiterPerpsEntryPriceVariableRefresher(alarmConfiguration.variables(), jupiterPerpsService);
entryPriceVariableRefresher.refresh();
JupiterPerpsEntryPriceVariableRefreshWatcher entryPriceVariableRefreshWatcher =
new JupiterPerpsEntryPriceVariableRefreshWatcher(
config.alarmConfiguration().getParent(),
entryPriceVariableRefresher
);
entryPriceVariableRefreshWatcher.start();
List<AlarmAction> actions = new ArrayList<>();
actions.add(new ConsoleAlarmAction());
@@ -52,9 +79,10 @@ public final class JupiterPerpsAlarmImpl {
Map<JupiterPerpsAsset, AssetPriceAlarmMonitor> monitors = new EnumMap<>(
JupiterPerpsAsset.class
);
definitionsByAsset.forEach((asset, assetDefinitions) -> monitors.put(
asset,
new AssetPriceAlarmMonitor(asset, assetDefinitions, action)
new AssetPriceAlarmMonitor(asset, assetDefinitions, variableResolver, action)
));
List<OracleWebSocketClient> clients = new ArrayList<>();
@@ -85,7 +113,7 @@ public final class JupiterPerpsAlarmImpl {
assetDefinitions.forEach(definition -> System.out.printf(
" %s %s USD, %s, severity=%s%n",
definition.direction(),
definition.target().toPlainString(),
definition.targetExpression(),
definition.trigger(),
definition.severity()
));
@@ -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;
}
@@ -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,11 +1,17 @@
package com.r35157.jupiterperpsalarm.impl.ref;
import java.math.BigDecimal;
import java.time.Instant;
public final class PriceAlarm {
public PriceAlarm(PriceAlarmDefinition definition, AlarmAction action) {
public PriceAlarm(
PriceAlarmDefinition definition,
AlarmVariableResolver variableResolver,
AlarmAction action
) {
this.definition = definition;
this.variableResolver = variableResolver;
this.action = action;
}
@@ -16,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(
price.priceUsd(),
definition.target()
target
);
boolean enteredTriggeredSide = previousReached == null
@@ -37,13 +57,13 @@ public final class PriceAlarm {
return;
}
trigger(price);
trigger(price, target);
return;
}
if (definition.trigger() == AlarmTrigger.PERSISTENT) {
if (lastTriggeredAt == null || persistentGracePeriodHasPassed()) {
trigger(price);
trigger(price, target);
}
return;
}
@@ -75,13 +95,39 @@ public final class PriceAlarm {
);
}
private void trigger(OraclePrice price) {
private void trigger(OraclePrice price, BigDecimal target) {
triggerCount++;
lastTriggeredAt = Instant.now();
action.trigger(price, definition);
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 AlarmVariableResolver variableResolver;
private final AlarmAction action;
private Instant lastTriggeredAt;
@@ -2,14 +2,13 @@ package com.r35157.jupiterperpsalarm.impl.ref;
import com.r35157.jupiterperpsalarm.AlarmSeverity;
import java.math.BigDecimal;
import java.util.Objects;
public record PriceAlarmDefinition(
int id,
JupiterPerpsAsset asset,
PriceDirection direction,
BigDecimal target,
String targetExpression,
AlarmTrigger trigger,
ΩsecondsΩ triggerGracePeriod,
AlarmSeverity severity,
@@ -18,12 +17,13 @@ public record PriceAlarmDefinition(
public PriceAlarmDefinition {
Objects.requireNonNull(asset, "asset");
Objects.requireNonNull(direction, "direction");
Objects.requireNonNull(target, "target");
Objects.requireNonNull(targetExpression, "targetExpression");
Objects.requireNonNull(trigger, "trigger");
Objects.requireNonNull(severity, "severity");
Objects.requireNonNull(note, "note");
if (target.signum() < 0) {
throw new IllegalArgumentException("Target price cannot be negative (was: " + target.signum() + ")!");
if (targetExpression.isBlank()) {
throw new IllegalArgumentException("Target expression cannot be blank");
}
if (triggerGracePeriod < 0) {
@@ -18,7 +18,7 @@ public final class PushoverAlarmAction implements AlarmAction {
}
@Override
public void trigger(OraclePrice price, PriceAlarmDefinition alarm) {
public void trigger(OraclePrice price, ResolvedPriceAlarm alarm) {
String title = "Jupiter Perps " + price.asset() + " alarm";
String message = createMessage(price, alarm);
@@ -26,7 +26,7 @@ public final class PushoverAlarmAction implements AlarmAction {
form("user", userKey) + "&" +
form("title", title) + "&" +
form("message", message) + "&" +
createPushoverSeverityParameters(alarm.severity());;
createPushoverSeverityParameters(alarm.severity());
HttpRequest request = HttpRequest.newBuilder(PUSHOVER_URI)
.timeout(Duration.ofSeconds(15))
@@ -69,7 +69,7 @@ public final class PushoverAlarmAction implements AlarmAction {
};
}
private static String createMessage(OraclePrice price, PriceAlarmDefinition alarm) {
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(),
@@ -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
) {
}
@@ -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,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();
}
@@ -0,0 +1,114 @@
package com.r35157.libs.jupiter.perps.impl.anchoridl;
import com.r35157.libs.codec.Base58Codec;
import com.r35157.libs.codec.impl.ref.Base58CodecImpl;
import com.r35157.libs.jupiter.perps.JupiterPerpsPosition;
import com.r35157.libs.jupiter.perps.JupiterPerpsPositionDirection;
import com.r35157.libs.solana.SolanaAccountInfo;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Base64;
class AnchorIdlJupiterPerpsPositionDecoder {
JupiterPerpsPosition decode(
ΩJupiterPerpsPositionAccountΩ positionAccount,
SolanaAccountInfo accountInfo,
ΩSPLMintAddressΩ tradedTokenMint
) {
byte[] data = Base64.getDecoder().decode(accountInfo.dataBase64());
if (data.length < PRICE_OFFSET + U64_LENGTH) {
throw new IllegalArgumentException(
"Jupiter Perps position account data is too short: " + data.length
);
}
JupiterPerpsPositionDirection direction =
decodeDirection(data[SIDE_OFFSET]);
long rawEntryPrice = ByteBuffer
.wrap(data, PRICE_OFFSET, U64_LENGTH)
.order(ByteOrder.LITTLE_ENDIAN)
.getLong();
ΩUSDCPriceΩ entryPrice = BigDecimal
.valueOf(rawEntryPrice)
.movePointLeft(6);
JupiterPerpsPosition pos = new JupiterPerpsPosition(
positionAccount,
entryPrice,
direction,
tradedTokenMint
);
return pos;
}
ΩSolanaAddressΩ decodeCustodyAccount(SolanaAccountInfo accountInfo) {
byte[] data = Base64.getDecoder().decode(accountInfo.dataBase64());
if (data.length < CUSTODY_OFFSET + PUBLIC_KEY_LENGTH) {
throw new IllegalArgumentException(
"Jupiter Perps position account data is too short: " + data.length
);
}
return readPublicKey(
data,
CUSTODY_OFFSET
);
}
private JupiterPerpsPositionDirection decodeDirection(
byte rawSide
) {
// Jupiter Perps position side values are encoded as 1 = LONG, 2 = SHORT.
JupiterPerpsPositionDirection direction = switch (rawSide) {
case 1 -> JupiterPerpsPositionDirection.LONG;
case 2 -> JupiterPerpsPositionDirection.SHORT;
default -> throw new IllegalArgumentException(
"Unknown Jupiter Perps position side: " + rawSide
);
};
return direction;
}
private ΩSolanaAddressΩ readPublicKey(
byte[] data,
int offset
) {
byte[] publicKeyBytes = new byte[PUBLIC_KEY_LENGTH];
System.arraycopy(
data,
offset,
publicKeyBytes,
0,
PUBLIC_KEY_LENGTH
);
return base58.encode(publicKeyBytes);
}
private static final int ANCHOR_DISCRIMINATOR_LENGTH = 8;
private static final int PUBLIC_KEY_LENGTH = 32;
private static final int I64_LENGTH = 8;
private static final int SIDE_ENUM_LENGTH = 1;
private static final int U64_LENGTH = 8;
private static final int OWNER_OFFSET = ANCHOR_DISCRIMINATOR_LENGTH;
private static final int POOL_OFFSET = OWNER_OFFSET + PUBLIC_KEY_LENGTH;
private static final int CUSTODY_OFFSET = POOL_OFFSET + PUBLIC_KEY_LENGTH;
private static final int COLLATERAL_CUSTODY_OFFSET = CUSTODY_OFFSET + PUBLIC_KEY_LENGTH; // custody
private static final int OPEN_TIME_OFFSET = COLLATERAL_CUSTODY_OFFSET + PUBLIC_KEY_LENGTH;
private static final int UPDATE_TIME_OFFSET = OPEN_TIME_OFFSET + I64_LENGTH; // openTime
private static final int SIDE_OFFSET = UPDATE_TIME_OFFSET + I64_LENGTH;
private static final int PRICE_OFFSET = SIDE_OFFSET + SIDE_ENUM_LENGTH;
private static final Base58Codec base58 = new Base58CodecImpl();
}
@@ -0,0 +1,99 @@
package com.r35157.libs.jupiter.perps.impl.anchoridl;
import com.r35157.libs.jupiter.perps.JupiterPerpsPosition;
import com.r35157.libs.jupiter.perps.JupiterPerpsService;
import com.r35157.libs.solana.SolanaAccountInfo;
import com.r35157.libs.solana.SolanaBlockChain;
import com.r35157.libs.solana.SolanaProgramAccountMemcmpFilter;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
public AnchorIdlJupiterPerpsServiceImpl(
SolanaBlockChain solanaBlockChain
) {
this.solanaBlockChain = solanaBlockChain;
this.positionDecoder = new AnchorIdlJupiterPerpsPositionDecoder();
this.custodyDecoder = new AnchorIdlJupiterPerpsCustodyDecoder();
}
@Override
public JupiterPerpsPosition getPosition(ΩJupiterPerpsPositionAccountΩ positionAccount)
throws IOException, InterruptedException {
SolanaAccountInfo accountInfo = solanaBlockChain.getAccountInfo(positionAccount);
if (accountInfo == null) {
throw new IllegalArgumentException("Jupiter Perps position account does not exist: " + positionAccount);
}
if (!JUPITER_PERPS_PROGRAM_ID.equals(accountInfo.owner())) {
throw new IllegalArgumentException(
"Account is not owned by Jupiter Perps program: " + positionAccount
);
}
ΩSPLMintAddressΩ tradedTokenMint = getTradedTokenMint(accountInfo);
JupiterPerpsPosition pos = positionDecoder.decode(positionAccount, accountInfo, tradedTokenMint);
return pos;
}
@Override
public Set<JupiterPerpsPosition> getOpenPositions(ΩSolanaWalletIdΩ owner)
throws IOException, InterruptedException {
Set<SolanaAccountInfo> accountInfos = solanaBlockChain.getProgramAccounts(
JUPITER_PERPS_PROGRAM_ID,
Set.of(new SolanaProgramAccountMemcmpFilter(
POSITION_OWNER_OFFSET,
owner
))
);
Set<JupiterPerpsPosition> positions = new HashSet<>();
for (SolanaAccountInfo accountInfo : accountInfos) {
ΩSolanaAddressΩ address = accountInfo.address();
ΩSolanaProgramIdΩ programId = accountInfo.owner();
if (!JUPITER_PERPS_PROGRAM_ID.equals(programId)) {
String errorMsg = "Account '" + address + "' is not owned by Jupiter Perps program '" +
programId + "'";
throw new IllegalArgumentException(errorMsg);
}
ΩSPLMintAddressΩ tradedTokenMint = getTradedTokenMint(accountInfo);
JupiterPerpsPosition position = positionDecoder.decode(address, accountInfo, tradedTokenMint);
positions.add(position);
}
return Set.copyOf(positions);
}
private ΩSPLMintAddressΩ getTradedTokenMint(SolanaAccountInfo positionAccountInfo)
throws IOException, InterruptedException
{
ΩSolanaAddressΩ custodyAccount = positionDecoder.decodeCustodyAccount(positionAccountInfo);
SolanaAccountInfo custodyAccountInfo = solanaBlockChain.getAccountInfo(custodyAccount);
if (custodyAccountInfo == null) {
throw new IllegalArgumentException(
"Jupiter Perps custody account does not exist: " + custodyAccount
);
}
ΩSPLMintAddressΩ mintAddress = custodyDecoder.decodeMint(custodyAccountInfo);
return mintAddress;
}
private static final ΩJupiterPerpsProgramIdΩ JUPITER_PERPS_PROGRAM_ID =
"PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu";
private static final int POSITION_OWNER_OFFSET = 8;
private final SolanaBlockChain solanaBlockChain;
private final AnchorIdlJupiterPerpsPositionDecoder positionDecoder;
private final AnchorIdlJupiterPerpsCustodyDecoder custodyDecoder;
}
@@ -1,146 +0,0 @@
package com.r35157.libs.solana;
import com.r35157.libs.solana.valuetypes.SolanaProgramDerivedAddress;
import com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram;
import com.r35157.libs.valuetypes.basic.MoneyAmount;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Provides read-oriented access to the Solana blockchain.
*
* <p>This interface exposes the Solana operations needed by higher-level
* integrations. It can fetch native SOL balances, SPL token holdings, NFT-like
* token holding candidates, account information and program derived addresses.</p>
*
* <p>The interface is intentionally generic and does not contain Raydium-specific
* logic. Higher-level integrations are expected to interpret Solana accounts,
* token holdings and derived addresses according to their own domain rules.</p>
*/
public interface SolanaBlockChain {
/**
* Fetches the native SOL balance for a Solana address.
*
* @param address the Solana address to inspect
* @return the native SOL balance for the address
* @throws IOException if the balance could not be fetched or parsed
* @throws InterruptedException if the calling thread is interrupted while fetching the balance
*/
ΩSolanaAmountΩ getBalanceInSolana(ΩSolanaAddressΩ address) throws IOException, InterruptedException;
/**
* Fetches the native SOL balance for a Solana address in lamports.
*
* <p>Lamports are the smallest unit of native SOL.</p>
*
* @param address the Solana address to inspect
* @return the native SOL balance for the address in lamports
* @throws IOException if the balance could not be fetched or parsed
* @throws InterruptedException if the calling thread is interrupted while fetching the balance
*/
ΩlamportsΩ getBalanceInLamport(ΩSolanaAddressΩ address) throws IOException, InterruptedException;
/**
* Fetches SPL token holdings owned by a Solana address for a specific token program.
*
* <p>The supplied token program decides which token accounts are inspected. For example,
* callers may query the original SPL Token Program or the Token-2022 Program depending
* on which token accounts they need to discover.</p>
*
* @param ownerAddress the Solana owner address whose token holdings should be inspected
* @param splProgramId the SPL token program to query
* @return a map of SPL mint addresses to token holding information
* @throws IOException if the token holdings could not be fetched or parsed
* @throws InterruptedException if the calling thread is interrupted while fetching token holdings
*/
Map<ΩSPLMintAddressΩ, SPLTokenHolding> getSPLTokenHoldings(
ΩSolanaAddressΩ ownerAddress,
SolanaSPLTokenProgram splProgramId
) throws IOException, InterruptedException;
/**
* Fetches NFT-like token mint address candidates owned by a Solana address for a specific token program.
*
* <p>This method identifies token holdings that look like NFTs within the supplied token
* program. A returned address is only a candidate. Higher-level integrations are responsible
* for deciding whether the returned address has domain-specific meaning.</p>
*
* // TODO This method currently identifies candidates from the owner's token holdings only.
* // A token with zero decimals and an owner balance of one is not guaranteed to be a real NFT,
* // because the mint's total supply may still be greater than one. A future implementation
* // should verify the mint supply, for example by using Solana getTokenSupply, before treating
* // the result as a confirmed NFT.
*
* @param ownerAddress the Solana owner address whose NFT-like holdings should be inspected
* @param splProgram the SPL token program to query
* @return the NFT-like Solana mint address candidates owned by the address
* @throws IOException if the NFT candidate addresses could not be fetched or parsed
* @throws InterruptedException if the calling thread is interrupted while fetching NFT candidate addresses
*/
Set<ΩSolanaNFTAddressΩ> getSolanaNFTCandidateAddresses(
ΩSolanaAddressΩ ownerAddress,
SolanaSPLTokenProgram splProgram
) throws IOException, InterruptedException;
/**
* Finds a Solana program derived address for a program id and a set of seeds.
*
* <p>The seeds describe the logical inputs used to derive the address. The implementation
* is responsible for converting each seed into the byte representation required by Solana.</p>
*
* @param programId the Solana program id used to derive the address
* @param seeds the seeds used when deriving the program address
* @return the derived Solana address together with its bump value
*/
SolanaProgramDerivedAddress findProgramAddress(
ΩSolanaProgramIdΩ programId,
List<SolanaProgramAddressSeed> seeds
);
/**
* Fetches account information for a Solana account address.
*
* <p>If the account does not exist, this method returns {@code null}. If the account exists,
* the returned value contains the account address, the owning Solana program id and the
* account data encoded as Base64.</p>
*
* @param accountAddress the Solana account address to inspect
* @return account information, or {@code null} if the account does not exist
* @throws IOException if the account information could not be fetched or parsed
* @throws InterruptedException if the calling thread is interrupted while fetching account information
*/
SolanaAccountInfo getAccountInfo(ΩSolanaAddressΩ accountAddress)
throws IOException, InterruptedException;
/**
* Encodes a raw 32-byte Solana address into its textual Solana address representation.
*
* <p>This method is intended for callers that have obtained raw Solana address bytes from
* account data and need the normal string representation used elsewhere in the API.</p>
*
* @param addressBytes the raw 32-byte Solana address
* @return the encoded Solana address
* @throws IllegalArgumentException if the supplied byte array is not a valid Solana address length
*/
ΩSolanaAddressΩ encodeSolanaAddress(byte[] addressBytes);
/**
* Fetches the total supply of an SPL token mint for a specific token program.
*
* <p>The supplied token program identifies which SPL token program owns the mint,
* for example the original SPL Token Program or the Token-2022 Program.</p>
*
* @param mintAddress the SPL mint address whose supply should be fetched
* @param splProgram the SPL token program to query
* @return the SPL token supply for the mint
* @throws IOException if the token supply could not be fetched or parsed
* @throws InterruptedException if the calling thread is interrupted while fetching token supply
*/
SPLTokenSupply getSPLTokenSupply(
ΩSPLMintAddressΩ mintAddress,
SolanaSPLTokenProgram splProgram
) throws IOException, InterruptedException;
}
@@ -331,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 {
waitBeforeRemoteCall();
@@ -1,5 +1,10 @@
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.NenjimHub;
import org.slf4j.Logger;
@@ -12,12 +17,21 @@ import com.r35157.nenjim.hubd.journal.JournalManager;
import com.r35157.nenjim.hubd.impl.ref.JournalManagerImpl;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;
public class Main {
// TODO: Consider if we really need a Main class or we just need to move the main method to NenjimHubImpl?
static void main(String[] args) throws Exception {
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 {