Compare commits
13 Commits
539068978f
...
d9f773c1f9
| Author | SHA256 | Date | |
|---|---|---|---|
| d9f773c1f9 | |||
| 2d68b83cf4 | |||
| 7e6e9978a2 | |||
| 35052a045a | |||
| ce74550825 | |||
| a5c9c78fdd | |||
| c4f67fa0fc | |||
| c8ca946fcc | |||
| 760ba0e823 | |||
| 1cb78fa3db | |||
| 09bd70b348 | |||
| cd573b1ce0 | |||
| 96d1289730 |
@@ -44,6 +44,7 @@ dependencies {
|
|||||||
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")
|
||||||
|
|||||||
@@ -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];
|
||||||
|
}
|
||||||
+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();
|
||||||
|
}
|
||||||
+114
@@ -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();
|
||||||
|
}
|
||||||
+99
@@ -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 {
|
private synchronized HttpResponse<String> sendThrottled(HttpRequest request) throws IOException, InterruptedException {
|
||||||
waitBeforeRemoteCall();
|
waitBeforeRemoteCall();
|
||||||
|
|
||||||
|
|||||||
@@ -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,21 @@ import com.r35157.nenjim.hubd.journal.JournalManager;
|
|||||||
import com.r35157.nenjim.hubd.impl.ref.JournalManagerImpl;
|
import com.r35157.nenjim.hubd.impl.ref.JournalManagerImpl;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
public class Main {
|
public class Main {
|
||||||
|
|
||||||
// TODO: Consider if we really need a Main class or we just need to move the main method to NenjimHubImpl?
|
// TODO: Consider if we really need a Main class or we just need to move the main method to NenjimHubImpl?
|
||||||
static void main(String[] args) throws Exception {
|
static void main(String[] args) throws Exception {
|
||||||
NenjimHubImpl nenjimHub = new NenjimHubImpl();
|
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();
|
nenjimHub.awaitShutdown();
|
||||||
|
|
||||||
/* try {
|
/* try {
|
||||||
|
|||||||
Reference in New Issue
Block a user