67: Add generic awaitTransaction() support to SolanaBlockChain
This commit is contained in:
@@ -5,6 +5,7 @@ import com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram;
|
||||
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -277,4 +278,38 @@ public interface SolanaBlockChain {
|
||||
ΩSolanaTransactionSignatureΩ sendTransaction(
|
||||
SolanaSignedTransaction transaction
|
||||
) throws IOException, InterruptedException;
|
||||
|
||||
/**
|
||||
* Waits for an already submitted transaction to reach a commitment level.
|
||||
*
|
||||
* <p>This operation does not submit or resubmit the transaction. Both
|
||||
* successful and failed transactions must reach or exceed the requested
|
||||
* commitment before a definitive result is returned.</p>
|
||||
*
|
||||
* <p>{@link SolanaTransactionOutcome.Status#TIMED_OUT TIMED_OUT} means the
|
||||
* outcome remains unknown: the transaction may still be confirmed,
|
||||
* finalized, dropped, or otherwise unresolved. Callers must not
|
||||
* automatically resubmit an equivalent transaction after a timeout.</p>
|
||||
*
|
||||
* @param signature Base58 transaction signature that decodes to exactly
|
||||
* 64 bytes
|
||||
* @param commitment minimum commitment required for a definitive outcome
|
||||
* @param timeout strictly positive overall timeout, including RPC-gate,
|
||||
* throttling, and HTTP time
|
||||
* @return definitive success or on-chain failure at the requested
|
||||
* commitment, or an unknown timed-out outcome
|
||||
* @throws IllegalArgumentException if the signature is {@code null},
|
||||
* blank, invalid Base58, not 64 bytes, or
|
||||
* the timeout is zero or negative
|
||||
* @throws NullPointerException if the commitment or timeout is null
|
||||
* @throws IOException if communication fails or Solana returns an invalid
|
||||
* HTTP, JSON-RPC, or protocol response
|
||||
* @throws InterruptedException if the calling thread is interrupted while
|
||||
* waiting for the RPC gate, throttling, or HTTP
|
||||
*/
|
||||
SolanaTransactionOutcome awaitTransaction(
|
||||
ΩSolanaTransactionSignatureΩ signature,
|
||||
SolanaCommitment commitment,
|
||||
Duration timeout
|
||||
) throws IOException, InterruptedException;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.r35157.libs.solana;
|
||||
|
||||
/**
|
||||
* Commitment level required when observing a Solana transaction.
|
||||
*
|
||||
* <p>The constants are ordered from the weakest to the strongest commitment:
|
||||
* {@link #PROCESSED}, {@link #CONFIRMED}, then {@link #FINALIZED}. An observed
|
||||
* level satisfies the same requested level and every weaker level.</p>
|
||||
*/
|
||||
public enum SolanaCommitment {
|
||||
/**
|
||||
* The transaction has been processed by the connected node.
|
||||
*/
|
||||
PROCESSED,
|
||||
|
||||
/**
|
||||
* The transaction has been voted on by a supermajority of the cluster.
|
||||
*/
|
||||
CONFIRMED,
|
||||
|
||||
/**
|
||||
* The transaction has been finalized by the cluster.
|
||||
*/
|
||||
FINALIZED
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.r35157.libs.solana;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Outcome observed for a Solana transaction at a requested commitment level.
|
||||
*
|
||||
* <p>A timed-out outcome remains unknown. It does not prove that the
|
||||
* transaction failed, was rejected, expired, or will never land, and callers
|
||||
* must not automatically resubmit an equivalent transaction.</p>
|
||||
*
|
||||
* @param status definitive success, definitive on-chain failure, or timeout
|
||||
* @param slot transaction slot for a definitive outcome, otherwise
|
||||
* {@code null}
|
||||
* @param failureDetails compact JSON containing Solana's complete on-chain
|
||||
* {@code err} value for a failed outcome, otherwise
|
||||
* {@code null}
|
||||
*/
|
||||
public record SolanaTransactionOutcome(
|
||||
@NotNull Status status,
|
||||
@Nullable ΩSolanaSlotΩ slot,
|
||||
@Nullable String failureDetails
|
||||
) {
|
||||
/**
|
||||
* Validates the field invariants associated with the outcome status.
|
||||
*/
|
||||
public SolanaTransactionOutcome {
|
||||
Objects.requireNonNull(status, "status");
|
||||
|
||||
switch (status) {
|
||||
case SUCCEEDED -> {
|
||||
if (slot == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"A successful transaction outcome requires a slot"
|
||||
);
|
||||
}
|
||||
if (failureDetails != null) {
|
||||
throw new IllegalArgumentException(
|
||||
"A successful transaction outcome must not contain "
|
||||
+ "failure details"
|
||||
);
|
||||
}
|
||||
}
|
||||
case FAILED -> {
|
||||
if (slot == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"A failed transaction outcome requires a slot"
|
||||
);
|
||||
}
|
||||
if (failureDetails == null || failureDetails.isBlank()) {
|
||||
throw new IllegalArgumentException(
|
||||
"A failed transaction outcome requires failure "
|
||||
+ "details"
|
||||
);
|
||||
}
|
||||
}
|
||||
case TIMED_OUT -> {
|
||||
if (slot != null || failureDetails != null) {
|
||||
throw new IllegalArgumentException(
|
||||
"A timed-out transaction outcome must not contain "
|
||||
+ "a slot or failure details"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Classification of a transaction observation.
|
||||
*/
|
||||
public enum Status {
|
||||
/**
|
||||
* The transaction reached the requested commitment without an
|
||||
* on-chain execution error.
|
||||
*/
|
||||
SUCCEEDED,
|
||||
|
||||
/**
|
||||
* The transaction reached the requested commitment with a non-null
|
||||
* Solana on-chain {@code err} value.
|
||||
*/
|
||||
FAILED,
|
||||
|
||||
/**
|
||||
* The requested definitive outcome remained unknown at the deadline.
|
||||
*/
|
||||
TIMED_OUT
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,12 @@ import com.r35157.libs.solana.SPLTokenHolding;
|
||||
import com.r35157.libs.solana.SPLTokenSupply;
|
||||
import com.r35157.libs.solana.SolanaAccountInfo;
|
||||
import com.r35157.libs.solana.SolanaBlockChain;
|
||||
import com.r35157.libs.solana.SolanaCommitment;
|
||||
import com.r35157.libs.solana.SolanaLatestBlockhash;
|
||||
import com.r35157.libs.solana.SolanaProgramAccountMemcmpFilter;
|
||||
import com.r35157.libs.solana.SolanaProgramAddressSeed;
|
||||
import com.r35157.libs.solana.SolanaSignedTransaction;
|
||||
import com.r35157.libs.solana.SolanaTransactionOutcome;
|
||||
import com.r35157.libs.solana.SolanaUnsignedTransaction;
|
||||
import com.r35157.libs.solana.valuetypes.SolanaProgramDerivedAddress;
|
||||
import com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram;
|
||||
@@ -17,6 +19,7 @@ import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -210,6 +213,15 @@ public final class CachedSolanaBlockChain implements SolanaBlockChain {
|
||||
return delegate.sendTransaction(transaction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SolanaTransactionOutcome awaitTransaction(
|
||||
ΩSolanaTransactionSignatureΩ signature,
|
||||
SolanaCommitment commitment,
|
||||
Duration timeout
|
||||
) throws IOException, InterruptedException {
|
||||
return delegate.awaitTransaction(signature, commitment, timeout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SolanaUnsignedTransaction buildSPLTokenTransferTransaction(
|
||||
ΩSolanaAddressΩ sender,
|
||||
|
||||
@@ -24,10 +24,13 @@ import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import static com.r35157.assetaz.services.cis.CurrencyTypeIds.SOLANA_ID;
|
||||
import static com.r35157.libs.solana.SolanaConstants.RPC_URL;
|
||||
@@ -803,6 +806,256 @@ public class SolanaBlockChainImpl implements SolanaBlockChain {
|
||||
return signature;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SolanaTransactionOutcome awaitTransaction(
|
||||
ΩSolanaTransactionSignatureΩ signature,
|
||||
SolanaCommitment commitment,
|
||||
Duration timeout
|
||||
) throws IOException, InterruptedException {
|
||||
long startedNanos = System.nanoTime();
|
||||
validateAwaitTransactionArguments(signature, commitment, timeout);
|
||||
Deadline deadline = Deadline.startingAt(startedNanos, timeout);
|
||||
String requestBody = createGetSignatureStatusesBody(signature);
|
||||
|
||||
while (!deadline.hasExpired()) {
|
||||
HttpResponse<String> response;
|
||||
try {
|
||||
response = sendThrottled(requestBody, deadline);
|
||||
} catch (IOException e) {
|
||||
if (deadline.hasExpired()) {
|
||||
return timedOutTransactionOutcome();
|
||||
}
|
||||
throw new IOException(
|
||||
"Solana getSignatureStatuses HTTP communication "
|
||||
+ "failed for "
|
||||
+ RPC_URL
|
||||
+ ": "
|
||||
+ e.getMessage(),
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
if (response == null || deadline.hasExpired()) {
|
||||
return timedOutTransactionOutcome();
|
||||
}
|
||||
|
||||
SolanaTransactionOutcome outcome = parseSignatureStatus(
|
||||
response,
|
||||
commitment
|
||||
);
|
||||
if (outcome != null) {
|
||||
return deadline.hasExpired()
|
||||
? timedOutTransactionOutcome()
|
||||
: outcome;
|
||||
}
|
||||
}
|
||||
|
||||
return timedOutTransactionOutcome();
|
||||
}
|
||||
|
||||
private void validateAwaitTransactionArguments(
|
||||
ΩSolanaTransactionSignatureΩ signature,
|
||||
SolanaCommitment commitment,
|
||||
Duration timeout
|
||||
) {
|
||||
if (signature == null || signature.isBlank()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Solana transaction signature must not be blank"
|
||||
);
|
||||
}
|
||||
|
||||
byte[] decodedSignature;
|
||||
try {
|
||||
decodedSignature = base58Decode(signature);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException(
|
||||
"Solana transaction signature must be valid Base58",
|
||||
e
|
||||
);
|
||||
}
|
||||
if (decodedSignature.length != SOLANA_SIGNATURE_LENGTH) {
|
||||
throw new IllegalArgumentException(
|
||||
"Solana transaction signature must decode to exactly "
|
||||
+ SOLANA_SIGNATURE_LENGTH
|
||||
+ " bytes, but decoded to "
|
||||
+ decodedSignature.length
|
||||
);
|
||||
}
|
||||
|
||||
Objects.requireNonNull(commitment, "commitment");
|
||||
Objects.requireNonNull(timeout, "timeout");
|
||||
if (timeout.isZero() || timeout.isNegative()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Solana transaction await timeout must be greater than "
|
||||
+ "zero"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private String createGetSignatureStatusesBody(
|
||||
ΩSolanaTransactionSignatureΩ signature
|
||||
) throws IOException {
|
||||
ObjectNode request = objectMapper.createObjectNode();
|
||||
request.put("jsonrpc", "2.0");
|
||||
request.put("id", 1);
|
||||
request.put("method", "getSignatureStatuses");
|
||||
|
||||
ArrayNode params = request.putArray("params");
|
||||
params.addArray().add(signature);
|
||||
params.addObject().put("searchTransactionHistory", true);
|
||||
|
||||
return objectMapper.writeValueAsString(request);
|
||||
}
|
||||
|
||||
private SolanaTransactionOutcome parseSignatureStatus(
|
||||
HttpResponse<String> response,
|
||||
SolanaCommitment requestedCommitment
|
||||
) throws IOException {
|
||||
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||
throw new IOException(
|
||||
"Solana getSignatureStatuses RPC call failed: HTTP "
|
||||
+ response.statusCode()
|
||||
+ ": "
|
||||
+ response.body()
|
||||
);
|
||||
}
|
||||
|
||||
JsonNode root;
|
||||
try {
|
||||
root = objectMapper.readTree(response.body());
|
||||
} catch (IOException e) {
|
||||
throw new IOException(
|
||||
"Solana getSignatureStatuses response contained invalid "
|
||||
+ "JSON: "
|
||||
+ response.body(),
|
||||
e
|
||||
);
|
||||
}
|
||||
if (root == null || !root.isObject()) {
|
||||
throw new IOException(
|
||||
"Solana getSignatureStatuses response was not a JSON "
|
||||
+ "object: "
|
||||
+ response.body()
|
||||
);
|
||||
}
|
||||
|
||||
JsonNode errorNode = root.get("error");
|
||||
if (errorNode != null && !errorNode.isNull()) {
|
||||
throw new IOException(
|
||||
"Solana getSignatureStatuses RPC error: "
|
||||
+ errorNode.toString()
|
||||
);
|
||||
}
|
||||
|
||||
JsonNode resultNode = root.get("result");
|
||||
JsonNode valueNode = resultNode == null
|
||||
? null
|
||||
: resultNode.get("value");
|
||||
if (valueNode == null || !valueNode.isArray()) {
|
||||
throw new IOException(
|
||||
"Solana getSignatureStatuses response did not contain a "
|
||||
+ "result.value array: "
|
||||
+ response.body()
|
||||
);
|
||||
}
|
||||
if (valueNode.size() != 1) {
|
||||
throw new IOException(
|
||||
"Solana getSignatureStatuses result.value must contain "
|
||||
+ "exactly one element, but contained "
|
||||
+ valueNode.size()
|
||||
+ ": "
|
||||
+ response.body()
|
||||
);
|
||||
}
|
||||
|
||||
JsonNode statusNode = valueNode.get(0);
|
||||
if (statusNode == null || statusNode.isNull()) {
|
||||
return null;
|
||||
}
|
||||
if (!statusNode.isObject()) {
|
||||
throw new IOException(
|
||||
"Solana getSignatureStatuses result contained a non-object "
|
||||
+ "status: "
|
||||
+ statusNode
|
||||
);
|
||||
}
|
||||
|
||||
JsonNode slotNode = statusNode.get("slot");
|
||||
if (slotNode == null
|
||||
|| !slotNode.isIntegralNumber()
|
||||
|| !slotNode.canConvertToLong()
|
||||
|| slotNode.longValue() < 0) {
|
||||
throw new IOException(
|
||||
"Solana getSignatureStatuses status contained an invalid "
|
||||
+ "slot: "
|
||||
+ slotNode
|
||||
);
|
||||
}
|
||||
|
||||
JsonNode confirmationStatusNode = statusNode.get(
|
||||
"confirmationStatus"
|
||||
);
|
||||
if (confirmationStatusNode == null
|
||||
|| !confirmationStatusNode.isTextual()) {
|
||||
throw new IOException(
|
||||
"Solana getSignatureStatuses status did not contain a "
|
||||
+ "textual confirmationStatus: "
|
||||
+ statusNode
|
||||
);
|
||||
}
|
||||
SolanaCommitment observedCommitment = parseCommitment(
|
||||
confirmationStatusNode.asText()
|
||||
);
|
||||
|
||||
if (!statusNode.has("err")) {
|
||||
throw new IOException(
|
||||
"Solana getSignatureStatuses status did not contain err: "
|
||||
+ statusNode
|
||||
);
|
||||
}
|
||||
if (observedCommitment.ordinal() < requestedCommitment.ordinal()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ΩSolanaSlotΩ slot = slotNode.longValue();
|
||||
JsonNode transactionError = statusNode.get("err");
|
||||
if (transactionError == null || transactionError.isNull()) {
|
||||
return new SolanaTransactionOutcome(
|
||||
SolanaTransactionOutcome.Status.SUCCEEDED,
|
||||
slot,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
return new SolanaTransactionOutcome(
|
||||
SolanaTransactionOutcome.Status.FAILED,
|
||||
slot,
|
||||
transactionError.toString()
|
||||
);
|
||||
}
|
||||
|
||||
private SolanaCommitment parseCommitment(String confirmationStatus)
|
||||
throws IOException {
|
||||
return switch (confirmationStatus) {
|
||||
case "processed" -> SolanaCommitment.PROCESSED;
|
||||
case "confirmed" -> SolanaCommitment.CONFIRMED;
|
||||
case "finalized" -> SolanaCommitment.FINALIZED;
|
||||
default -> throw new IOException(
|
||||
"Solana getSignatureStatuses status contained unknown "
|
||||
+ "confirmationStatus: "
|
||||
+ confirmationStatus
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private static SolanaTransactionOutcome timedOutTransactionOutcome() {
|
||||
return new SolanaTransactionOutcome(
|
||||
SolanaTransactionOutcome.Status.TIMED_OUT,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private String createSendTransactionBody(
|
||||
SolanaSignedTransaction transaction
|
||||
) throws IOException {
|
||||
@@ -1091,29 +1344,98 @@ public class SolanaBlockChainImpl implements SolanaBlockChain {
|
||||
""".formatted(programId, filtersJson);
|
||||
}
|
||||
|
||||
private synchronized HttpResponse<String> sendThrottled(HttpRequest request) throws IOException, InterruptedException {
|
||||
waitBeforeRemoteCall();
|
||||
|
||||
private HttpResponse<String> sendThrottled(HttpRequest request)
|
||||
throws IOException, InterruptedException {
|
||||
rpcGate.lockInterruptibly();
|
||||
try {
|
||||
waitBeforeRemoteCall();
|
||||
|
||||
try {
|
||||
return httpClient.send(
|
||||
request,
|
||||
HttpResponse.BodyHandlers.ofString()
|
||||
);
|
||||
} finally {
|
||||
previousRemoteCallTimeNanos = System.nanoTime();
|
||||
hasCompletedRemoteCall = true;
|
||||
}
|
||||
} finally {
|
||||
rpcGate.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void waitBeforeRemoteCall() throws InterruptedException {
|
||||
while (hasCompletedRemoteCall) {
|
||||
long elapsed = System.nanoTime() - previousRemoteCallTimeNanos;
|
||||
long remaining = MINIMUM_REMOTE_CALL_INTERVAL_NANOS - elapsed;
|
||||
if (remaining <= 0) {
|
||||
return;
|
||||
}
|
||||
TimeUnit.NANOSECONDS.sleep(remaining);
|
||||
}
|
||||
}
|
||||
|
||||
private HttpResponse<String> sendThrottled(
|
||||
String requestBody,
|
||||
Deadline deadline
|
||||
) throws IOException, InterruptedException {
|
||||
long remaining = deadline.remainingNanos();
|
||||
if (remaining <= 0
|
||||
|| !rpcGate.tryLock(remaining, TimeUnit.NANOSECONDS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean requestStarted = false;
|
||||
try {
|
||||
if (!waitBeforeRemoteCall(deadline)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
remaining = deadline.remainingNanos();
|
||||
if (remaining <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(RPC_URL))
|
||||
.timeout(Duration.ofNanos(remaining))
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
|
||||
.build();
|
||||
|
||||
requestStarted = true;
|
||||
return httpClient.send(
|
||||
request,
|
||||
HttpResponse.BodyHandlers.ofString()
|
||||
);
|
||||
} finally {
|
||||
previousRemoteCallTime = System.currentTimeMillis();
|
||||
if (requestStarted) {
|
||||
previousRemoteCallTimeNanos = System.nanoTime();
|
||||
hasCompletedRemoteCall = true;
|
||||
}
|
||||
rpcGate.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void waitBeforeRemoteCall() throws InterruptedException {
|
||||
long now = System.currentTimeMillis();
|
||||
long elapsed = now - previousRemoteCallTime;
|
||||
private boolean waitBeforeRemoteCall(Deadline deadline)
|
||||
throws InterruptedException {
|
||||
while (hasCompletedRemoteCall) {
|
||||
long elapsed = System.nanoTime() - previousRemoteCallTimeNanos;
|
||||
long throttleRemaining = MINIMUM_REMOTE_CALL_INTERVAL_NANOS
|
||||
- elapsed;
|
||||
if (throttleRemaining <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (elapsed < MINIMUM_REMOTE_CALL_INTERVAL) {
|
||||
ΩmilliSecondsΩ sleepTime = MINIMUM_REMOTE_CALL_INTERVAL - elapsed;
|
||||
//System.out.println("Throttling Solana request for " + sleepTime + "ms...");
|
||||
Thread.sleep(sleepTime);
|
||||
//System.out.println("Ready");
|
||||
long deadlineRemaining = deadline.remainingNanos();
|
||||
if (deadlineRemaining <= 0) {
|
||||
return false;
|
||||
}
|
||||
TimeUnit.NANOSECONDS.sleep(
|
||||
Math.min(throttleRemaining, deadlineRemaining)
|
||||
);
|
||||
}
|
||||
return !deadline.hasExpired();
|
||||
}
|
||||
|
||||
private boolean isSolanaNFTCandidate(SPLTokenHolding tokenHolding) {
|
||||
@@ -1389,6 +1711,33 @@ public class SolanaBlockChainImpl implements SolanaBlockChain {
|
||||
) {
|
||||
}
|
||||
|
||||
private record Deadline(long startedNanos, long timeoutNanos) {
|
||||
private static Deadline startingAt(
|
||||
long startedNanos,
|
||||
Duration timeout
|
||||
) {
|
||||
long timeoutNanos;
|
||||
try {
|
||||
timeoutNanos = timeout.toNanos();
|
||||
} catch (ArithmeticException e) {
|
||||
timeoutNanos = Long.MAX_VALUE;
|
||||
}
|
||||
return new Deadline(startedNanos, timeoutNanos);
|
||||
}
|
||||
|
||||
private long remainingNanos() {
|
||||
long elapsed = System.nanoTime() - startedNanos;
|
||||
if (elapsed < 0 || elapsed >= timeoutNanos) {
|
||||
return 0;
|
||||
}
|
||||
return timeoutNanos - elapsed;
|
||||
}
|
||||
|
||||
private boolean hasExpired() {
|
||||
return remainingNanos() <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static final String SYSTEM_PROGRAM_ADDRESS =
|
||||
"11111111111111111111111111111111";
|
||||
private static final String ASSOCIATED_TOKEN_PROGRAM_ADDRESS =
|
||||
@@ -1398,7 +1747,8 @@ public class SolanaBlockChainImpl implements SolanaBlockChain {
|
||||
private static final int SOLANA_ADDRESS_LENGTH = 32;
|
||||
private static final int SOLANA_SIGNATURE_LENGTH = 64;
|
||||
private static final ΩAmountΩ LAMPORTS_PER_SOL = new BigDecimal("1000000000");
|
||||
private static final ΩmilliSecondsΩ MINIMUM_REMOTE_CALL_INTERVAL = 5000L;
|
||||
private static final long MINIMUM_REMOTE_CALL_INTERVAL_NANOS =
|
||||
TimeUnit.SECONDS.toNanos(5);
|
||||
private static final byte[] PROGRAM_DERIVED_ADDRESS_MARKER = "ProgramDerivedAddress".getBytes(StandardCharsets.UTF_8);
|
||||
private static final String BASE58_ALPHABET_STRING = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||
private static final char[] BASE58_ALPHABET = BASE58_ALPHABET_STRING.toCharArray();
|
||||
@@ -1411,5 +1761,7 @@ public class SolanaBlockChainImpl implements SolanaBlockChain {
|
||||
private final CurrencyIdentityService currencyIdentityService;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final HttpClient httpClient;
|
||||
private ΩmilliSecondsΩ previousRemoteCallTime = 0L;
|
||||
private final ReentrantLock rpcGate = new ReentrantLock();
|
||||
private long previousRemoteCallTimeNanos;
|
||||
private boolean hasCompletedRemoteCall;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user