package com.r35157.libs.solana.impl.ref;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.r35157.assetaz.services.cis.CurrencyIdentityService;
import com.r35157.assetaz.valuetypes.CurrencyType;
import 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 org.jetbrains.annotations.NotNull;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.ByteBuffer;
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;

public class SolanaBlockChainImpl implements SolanaBlockChain {

    public SolanaBlockChainImpl(
            @NotNull HttpClient httpClient,
            @NotNull ObjectMapper objectMapper,
            @NotNull CurrencyIdentityService currencyIdentityService
    ) {
        this.httpClient = Objects.requireNonNull(
                httpClient,
                "httpClient"
        );
        this.objectMapper = Objects.requireNonNull(
                objectMapper,
                "objectMapper"
        );
        this.currencyIdentityService = Objects.requireNonNull(
                currencyIdentityService,
                "currencyIdentityService"
        );
    }

    @Override
    public ΩSolanaAmountΩ getBalanceInSolana(ΩSolanaAddressΩ address) throws IOException, InterruptedException {
        ΩlamportsΩ lamport = getBalanceInLamport(address);
        ΩAmountΩ bd = ΩAmountΩ.valueOf(lamport).divide(LAMPORTS_PER_SOL);
        CurrencyType type = currencyIdentityService.resolve(SOLANA_ID);
        ΩSolanaAmountΩ sa = new ΩSolanaAmountΩ(bd, type);

        return sa;
    }

    @Override
    public ΩlamportsΩ getBalanceInLamport(ΩSolanaAddressΩ address) throws IOException, InterruptedException {
        String jsonBody = """
                {
                  "jsonrpc":"2.0",
                  "id":1,
                  "method":"getBalance",
                  "params":[
                    "%s",
                    {
                      "commitment":"finalized"
                    }
                  ]
                }
                """.formatted(address);

        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("Unexpected HTTP status: " + response.statusCode() + ", body: " + response.body());
        }

        JsonNode root = objectMapper.readTree(response.body());

        JsonNode errorNode = root.get("error");
        if (errorNode != null) {
            throw new IOException("RPC error: " + errorNode.toPrettyString());
        }

        JsonNode valueNode = root.path("result").path("value");
        if (valueNode.isMissingNode() || !valueNode.isNumber()) {
            throw new IOException("Could not read balance from response: " + response.body());
        }

        return valueNode.longValue();
    }

    @Override
    public SolanaLatestBlockhash getLatestBlockhash()
            throws IOException, InterruptedException {
        String jsonBody = """
                {
                  "jsonrpc": "2.0",
                  "id": 1,
                  "method": "getLatestBlockhash",
                  "params": [
                    {
                      "commitment": "confirmed"
                    }
                  ]
                }
                """;

        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(
                    "Solana getLatestBlockhash RPC call failed: HTTP "
                            + response.statusCode()
                            + "\n"
                            + response.body()
            );
        }

        JsonNode root = objectMapper.readTree(response.body());
        JsonNode errorNode = root.get("error");

        if (errorNode != null && !errorNode.isNull()) {
            throw new IOException(
                    "Solana getLatestBlockhash RPC error: "
                            + errorNode.toPrettyString()
            );
        }

        JsonNode valueNode = root.path("result").path("value");
        JsonNode blockhashNode = valueNode.path("blockhash");
        JsonNode lastValidBlockHeightNode =
                valueNode.path("lastValidBlockHeight");

        if (!blockhashNode.isTextual()
                || blockhashNode.asText().isBlank()
                || !lastValidBlockHeightNode.isIntegralNumber()) {
            throw new IOException(
                    "Solana getLatestBlockhash response did not contain "
                            + "a blockhash and last valid block height: "
                            + response.body()
            );
        }

        ΩSolanaBlockhashΩ blockhash = blockhashNode.asText();

        return new SolanaLatestBlockhash(
                blockhash,
                lastValidBlockHeightNode.longValue()
        );
    }

    @Override
    public ΩlamportsΩ getFeeForMessage(
            ΩBase64StringΩ serializedMessage
    ) throws IOException, InterruptedException {
        if (serializedMessage == null || serializedMessage.isBlank()) {
            throw new IllegalArgumentException(
                    "Serialized Solana message must not be blank"
            );
        }

        ObjectNode rpcRequest = objectMapper.createObjectNode();
        rpcRequest.put("jsonrpc", "2.0");
        rpcRequest.put("id", 1);
        rpcRequest.put("method", "getFeeForMessage");

        ArrayNode params = rpcRequest.putArray("params");
        params.add(serializedMessage);
        params.addObject().put("commitment", "confirmed");

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(RPC_URL))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(
                        objectMapper.writeValueAsString(rpcRequest)
                ))
                .build();

        HttpResponse<String> response = sendThrottled(request);

        if (response.statusCode() != 200) {
            throw new IOException(
                    "Solana getFeeForMessage RPC call failed: HTTP "
                            + response.statusCode()
                            + "\n"
                            + response.body()
            );
        }

        JsonNode root = objectMapper.readTree(response.body());
        JsonNode errorNode = root.get("error");

        if (errorNode != null && !errorNode.isNull()) {
            throw new IOException(
                    "Solana getFeeForMessage RPC error: "
                            + errorNode.toPrettyString()
            );
        }

        JsonNode valueNode = root.path("result").path("value");

        if (!valueNode.isIntegralNumber()) {
            throw new IOException(
                    "Solana getFeeForMessage response did not contain a fee; "
                            + "the transaction blockhash may have expired: "
                            + response.body()
            );
        }

        return valueNode.longValue();
    }

    @Override
    public SolanaUnsignedTransaction buildSolanaTransferTransaction(
            ΩSolanaAddressΩ sender,
            ΩSolanaAddressΩ recipient,
            ΩSolanaAmountΩ amount
    ) throws IOException, InterruptedException {
        validateSolanaTransferAddresses(sender, recipient);

        return buildSolanaTransferTransaction(
                sender,
                recipient,
                getLatestBlockhash(),
                toLamports(amount)
        );
    }

    @Override
    public SolanaUnsignedTransaction buildSolanaTransferAllTransaction(
            ΩSolanaAddressΩ sender,
            ΩSolanaAddressΩ recipient
    ) throws IOException, InterruptedException {
        validateSolanaTransferAddresses(sender, recipient);

        ΩlamportsΩ balance = getBalanceInLamport(sender);

        if (balance <= 0) {
            throw new IllegalStateException(
                    "Wallet does not contain any SOL to transfer"
            );
        }

        SolanaLatestBlockhash latestBlockhash = getLatestBlockhash();

        // The transfer amount is always serialized as one fixed-width u64.
        // Replacing it with balance - fee therefore cannot change the fee.
        SerializedSolanaTransfer feeCandidate =
                serializeSolanaTransferTransaction(
                        sender,
                        recipient,
                        latestBlockhash,
                        balance
                );

        ΩlamportsΩ fee = getFeeForMessage(
                feeCandidate.serializedMessage()
        );

        if (fee < 0) {
            throw new IllegalStateException(
                    "Solana transaction fee must not be negative"
            );
        }

        ΩlamportsΩ transferableLamports = balance - fee;

        if (transferableLamports <= 0) {
            throw new IllegalStateException(
                    "Wallet balance of "
                            + balance
                            + " lamports cannot cover the transaction fee of "
                            + fee
                            + " lamports and a positive transfer amount"
            );
        }

        return buildSolanaTransferTransaction(
                sender,
                recipient,
                latestBlockhash,
                transferableLamports
        );
    }

    @Override
    public ΩSPLTokenAccountΩ findAssociatedTokenAccount(
            ΩSolanaAddressΩ owner,
            ΩSPLMintAddressΩ mintAddress,
            SolanaSPLTokenProgram splProgram
    ) {
        Objects.requireNonNull(splProgram, "SPL token program must not be null");

        ΩSPLTokenAccountΩ tokenAccount = findProgramAddress(
                ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
                List.of(
                        SolanaProgramAddressSeed.solanaAddress(owner),
                        SolanaProgramAddressSeed.solanaAddress(
                                splProgram.getAddress()
                        ),
                        SolanaProgramAddressSeed.solanaAddress(mintAddress)
                )
        ).address();

        return tokenAccount;
    }

    @Override
    public SolanaUnsignedTransaction buildSPLTokenTransferTransaction(
            ΩSolanaAddressΩ sender,
            ΩSPLTokenAccountΩ senderTokenAccount,
            ΩSPLMintAddressΩ mintAddress,
            ΩSolanaAddressΩ recipient,
            ΩSPLTokenAccountΩ recipientTokenAccount,
            ΩRawAmountΩ rawAmount,
            ΩamountDecimalsΩ decimals,
            SolanaSPLTokenProgram splProgram,
            boolean createRecipientTokenAccount
    ) throws IOException, InterruptedException {
        Objects.requireNonNull(splProgram, "SPL token program must not be null");

        BigInteger transferAmount;
        try {
            transferAmount = new BigInteger(rawAmount);
        } catch (RuntimeException e) {
            throw new IllegalArgumentException(
                    "Raw SPL token amount must be an integer",
                    e
            );
        }
        if (transferAmount.signum() <= 0
                || transferAmount.bitLength() > 64) {
            throw new IllegalArgumentException(
                    "Raw SPL token amount must fit in an unsigned u64 and "
                            + "be greater than zero"
            );
        }
        if (decimals < 0 || decimals > 255) {
            throw new IllegalArgumentException(
                    "SPL token decimals must fit in an unsigned byte"
            );
        }

        List<ΩSolanaAddressΩ> accountKeys = new ArrayList<>();
        accountKeys.add(sender);
        accountKeys.add(senderTokenAccount);
        accountKeys.add(recipientTokenAccount);
        accountKeys.add(mintAddress);

        int tokenProgramIndex;
        int readOnlyUnsignedAccounts;
        if (createRecipientTokenAccount) {
            accountKeys.add(recipient);
            accountKeys.add(SYSTEM_PROGRAM_ADDRESS);
            accountKeys.add(splProgram.getAddress());
            accountKeys.add(ASSOCIATED_TOKEN_PROGRAM_ADDRESS);
            tokenProgramIndex = 6;
            readOnlyUnsignedAccounts = 5;
        } else {
            accountKeys.add(splProgram.getAddress());
            tokenProgramIndex = 4;
            readOnlyUnsignedAccounts = 2;
        }

        List<CompiledInstruction> instructions = new ArrayList<>();
        if (createRecipientTokenAccount) {
            instructions.add(new CompiledInstruction(
                    7,
                    new byte[] { 0, 2, 4, 3, 5, 6 },
                    new byte[] { 1 }
            ));
        }

        byte[] transferCheckedData = ByteBuffer
                .allocate(10)
                .order(ByteOrder.LITTLE_ENDIAN)
                .put((byte) TOKEN_TRANSFER_CHECKED_INSTRUCTION)
                .putLong(transferAmount.longValue())
                .put((byte) decimals)
                .array();
        instructions.add(new CompiledInstruction(
                tokenProgramIndex,
                new byte[] { 1, 3, 2, 0 },
                transferCheckedData
        ));

        SolanaLatestBlockhash latestBlockhash = getLatestBlockhash();
        ByteArrayOutputStream message = new ByteArrayOutputStream();
        message.write(1);
        message.write(0);
        message.write(readOnlyUnsignedAccounts);
        writeCompactU16(message, accountKeys.size());

        for (String accountKey : accountKeys) {
            message.writeBytes(decodeSolanaTransferAddress(
                    "transaction account",
                    accountKey
            ));
        }
        message.writeBytes(decodeSolanaTransferAddress(
                "blockhash",
                latestBlockhash.blockhash()
        ));
        writeCompactU16(message, instructions.size());

        for (CompiledInstruction instruction : instructions) {
            message.write(instruction.programIndex());
            writeCompactU16(message, instruction.accountIndexes().length);
            message.writeBytes(instruction.accountIndexes());
            writeCompactU16(message, instruction.data().length);
            message.writeBytes(instruction.data());
        }

        ByteArrayOutputStream transaction = new ByteArrayOutputStream();
        writeCompactU16(transaction, 1);
        transaction.writeBytes(new byte[SOLANA_SIGNATURE_LENGTH]);
        transaction.writeBytes(message.toByteArray());

        return new SolanaUnsignedTransaction(
                Base64.getEncoder().encodeToString(transaction.toByteArray()),
                latestBlockhash.blockhash(),
                latestBlockhash.lastValidBlockHeight()
        );
    }

    @Override
    public SolanaUnsignedTransaction buildSPLTokenBurnTransaction(
            ΩSolanaAddressΩ owner,
            ΩSPLTokenAccountΩ tokenAccount,
            ΩSPLMintAddressΩ mintAddress,
            ΩRawAmountΩ rawAmount,
            ΩamountDecimalsΩ decimals,
            SolanaSPLTokenProgram splProgram
    ) throws IOException, InterruptedException {
        Objects.requireNonNull(
                splProgram,
                "SPL token program must not be null"
        );

        BigInteger burnAmount;
        try {
            burnAmount = new BigInteger(rawAmount);
        } catch (RuntimeException e) {
            throw new IllegalArgumentException(
                    "Raw SPL token burn amount must be an integer",
                    e
            );
        }
        if (burnAmount.signum() <= 0 || burnAmount.bitLength() > 64) {
            throw new IllegalArgumentException(
                    "Raw SPL token burn amount must fit in an unsigned u64 "
                            + "and be greater than zero"
            );
        }
        if (decimals < 0 || decimals > 255) {
            throw new IllegalArgumentException(
                    "SPL token decimals must fit in an unsigned byte"
            );
        }

        byte[] ownerBytes = decodeSolanaTransferAddress("owner", owner);
        byte[] tokenAccountBytes = decodeSolanaTransferAddress(
                "token account",
                tokenAccount
        );
        byte[] mintBytes = decodeSolanaTransferAddress("mint", mintAddress);
        byte[] tokenProgramBytes = decodeSolanaTransferAddress(
                "token program",
                splProgram.getAddress()
        );

        SolanaLatestBlockhash latestBlockhash = getLatestBlockhash();
        byte[] blockhashBytes = decodeSolanaTransferAddress(
                "blockhash",
                latestBlockhash.blockhash()
        );

        byte[] burnCheckedData = ByteBuffer
                .allocate(10)
                .order(ByteOrder.LITTLE_ENDIAN)
                .put((byte) TOKEN_BURN_CHECKED_INSTRUCTION)
                .putLong(burnAmount.longValue())
                .put((byte) decimals)
                .array();

        ByteArrayOutputStream message = new ByteArrayOutputStream();
        message.write(1);
        message.write(0);
        message.write(1);
        writeCompactU16(message, 4);
        message.writeBytes(ownerBytes);
        message.writeBytes(tokenAccountBytes);
        message.writeBytes(mintBytes);
        message.writeBytes(tokenProgramBytes);
        message.writeBytes(blockhashBytes);
        writeCompactU16(message, 1);
        message.write(3);
        writeCompactU16(message, 3);
        message.writeBytes(new byte[] { 1, 2, 0 });
        writeCompactU16(message, burnCheckedData.length);
        message.writeBytes(burnCheckedData);

        ByteArrayOutputStream transaction = new ByteArrayOutputStream();
        writeCompactU16(transaction, 1);
        transaction.writeBytes(new byte[SOLANA_SIGNATURE_LENGTH]);
        transaction.writeBytes(message.toByteArray());

        return new SolanaUnsignedTransaction(
                Base64.getEncoder().encodeToString(transaction.toByteArray()),
                latestBlockhash.blockhash(),
                latestBlockhash.lastValidBlockHeight()
        );
    }

    @Override
    public Map<ΩSPLMintAddressΩ, SPLTokenHolding> getSPLTokenHoldings(
            ΩSolanaAddressΩ ownerAddress,
            SolanaSPLTokenProgram splProgram
    ) throws IOException, InterruptedException {

        String body = """
                {
                  "jsonrpc": "2.0",
                  "id": 1,
                  "method": "getTokenAccountsByOwner",
                  "params": [
                    "%s",
                    {
                      "programId": "%s"
                    },
                    {
                      "encoding": "jsonParsed"
                    }
                  ]
                }
                """.formatted(ownerAddress, splProgram.getAddress());

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(RPC_URL))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(body))
                .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());
        }

        Map<String, SPLTokenHolding> result = new HashMap<>();

        JsonNode accounts = root.path("result").path("value");

        for (JsonNode account : accounts) {
            String tokenAccount = account.path("pubkey").asText();

            JsonNode parsedInfo = account
                    .path("account")
                    .path("data")
                    .path("parsed")
                    .path("info");

            String mint = parsedInfo.path("mint").asText();
            JsonNode tokenAmount = parsedInfo.path("tokenAmount");
            String rawAmount = tokenAmount.path("amount").asText();
            int decimals = tokenAmount.path("decimals").asInt();
            String uiAmountString = tokenAmount.path("uiAmountString").asText();
            BigDecimal uiAmount = new BigDecimal(uiAmountString);

            SPLTokenHolding holding = new SPLTokenHolding(
                    tokenAccount,
                    mint,
                    uiAmount,
                    rawAmount,
                    decimals,
                    splProgram.getAddress()
            );

            SPLTokenHolding previous = result.putIfAbsent(mint, holding);

            if (previous != null) {
                throw new IllegalStateException(
                        "Multiple SPL token accounts found for mint "
                                + mint
                                + " and owner "
                                + ownerAddress
                );
            }
        }

        return result;
    }

    @Override
    public Set<ΩSolanaNFTAddressΩ> getSolanaNFTCandidateAddresses(
            ΩSolanaAddressΩ ownerAddress,
            SolanaSPLTokenProgram splProgram
    ) throws IOException, InterruptedException {
        Map<ΩSPLMintAddressΩ, SPLTokenHolding> tokenHoldings =
                getSPLTokenHoldings(ownerAddress, splProgram);

        Set<ΩSolanaNFTAddressΩ> nftCandidateAddresses = new HashSet<>();

        for (SPLTokenHolding tokenHolding : tokenHoldings.values()) {
            if (isSolanaNFTCandidate(tokenHolding)) {
                nftCandidateAddresses.add(tokenHolding.mintAddress());
            }
        }

        return Set.copyOf(nftCandidateAddresses);
    }

    @Override
    public SolanaProgramDerivedAddress findProgramAddress(
            ΩSolanaProgramIdΩ programId,
            List<SolanaProgramAddressSeed> seeds
    ) {
        List<byte[]> seedBytes = toSeedBytes(seeds);

        return findProgramAddressFromSeedBytes(programId, seedBytes);
    }

    @Override
    public SolanaAccountInfo getAccountInfo(
            ΩSolanaAddressΩ accountAddress
    ) throws IOException, InterruptedException {
        String jsonBody = """
            {
              "jsonrpc": "2.0",
              "id": 1,
              "method": "getAccountInfo",
              "params": [
                "%s",
                {
                  "commitment": "finalized",
                  "encoding": "base64"
                }
              ]
            }
            """.formatted(accountAddress);

        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 value = root.path("result").path("value");

        if (value.isMissingNode() || value.isNull()) {
            return null;
        }

        JsonNode ownerNode = value.path("owner");
        if (ownerNode.isMissingNode() || !ownerNode.isTextual()) {
            throw new IOException("getAccountInfo response did not contain textual owner field!");
        }

        JsonNode dataNode = value.path("data");
        if (!dataNode.isArray() || dataNode.isEmpty() || !dataNode.get(0).isTextual()) {
            throw new IOException("getAccountInfo response did not contain base64 data!");
        }

        return new SolanaAccountInfo(
                accountAddress,
                ownerNode.asText(),
                dataNode.get(0).asText()
        );
    }

    @Override
    public ΩSolanaAddressΩ encodeSolanaAddress(byte[] addressBytes) {
        if (addressBytes.length != 32) {
            throw new IllegalArgumentException(
                    "Solana address must be 32 bytes, but was " + addressBytes.length
            );
        }

        return base58Encode(addressBytes);
    }

    @Override
    public SPLTokenSupply getSPLTokenSupply(
            ΩSPLMintAddressΩ mintAddress,
            SolanaSPLTokenProgram splProgram
    ) throws IOException, InterruptedException {
        String jsonBody = """
                {
                  "jsonrpc": "2.0",
                  "id": 1,
                  "method": "getTokenSupply",
                  "params": [
                    "%s",
                    {
                      "commitment": "finalized"
                    }
                  ]
                }
                """.formatted(mintAddress);

        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() + " " + response.body());
        }

        JsonNode root = objectMapper.readTree(response.body());

        if (root.has("error")) {
            throw new IOException("Solana RPC error: " + root.get("error").toPrettyString());
        }

        JsonNode value = root.path("result").path("value");

        if (value.isMissingNode() || value.isNull()) {
            throw new IOException("getTokenSupply response did not contain a value for mint: " + mintAddress);
        }

        JsonNode rawAmountNode = value.path("amount");
        if (rawAmountNode.isMissingNode() || !rawAmountNode.isTextual()) {
            throw new IOException("getTokenSupply response did not contain textual amount field!");
        }

        JsonNode decimalsNode = value.path("decimals");
        if (decimalsNode.isMissingNode() || !decimalsNode.isNumber()) {
            throw new IOException("getTokenSupply response did not contain numeric decimals field!");
        }

        JsonNode uiAmountStringNode = value.path("uiAmountString");
        if (uiAmountStringNode.isMissingNode() || !uiAmountStringNode.isTextual()) {
            throw new IOException("getTokenSupply response did not contain textual uiAmountString field!");
        }

        ΩRawAmountΩ rawAmount = rawAmountNode.asText();
        ΩamountDecimalsΩ decimals = decimalsNode.asInt();
        ΩAmountΩ uiAmount = new BigDecimal(uiAmountStringNode.asText());

        return new SPLTokenSupply(
                mintAddress,
                uiAmount,
                rawAmount,
                decimals,
                splProgram.getAddress()
        );
    }

    @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);
    }

    @Override
    public ΩSolanaTransactionSignatureΩ sendTransaction(
            SolanaSignedTransaction transaction
    ) throws IOException, InterruptedException {
        String jsonBody = createSendTransactionBody(transaction);

        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(
                    "Solana sendTransaction RPC call failed: HTTP "
                            + response.statusCode()
                            + "\n"
                            + response.body()
            );
        }

        JsonNode root = objectMapper.readTree(response.body());

        JsonNode errorNode = root.get("error");

        if (errorNode != null && !errorNode.isNull()) {
            throw new IOException(
                    "Solana sendTransaction RPC error: "
                            + errorNode.toPrettyString()
            );
        }

        JsonNode resultNode = root.get("result");

        if (resultNode == null
                || !resultNode.isTextual()
                || resultNode.asText().isBlank()) {
            throw new IOException(
                    "Solana sendTransaction response did not contain "
                            + "a transaction signature: "
                            + response.body()
            );
        }

        ΩSolanaTransactionSignatureΩ signature = resultNode.asText();
        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 {
        if (transaction == null) {
            throw new IllegalArgumentException(
                    "Signed transaction must not be null"
            );
        }

        ΩBase64StringΩ serializedTransaction =
                transaction.serializedTransaction();

        if (serializedTransaction == null
                || serializedTransaction.isBlank()) {
            throw new IllegalArgumentException(
                    "Serialized signed transaction must not be blank"
            );
        }

        ObjectNode request = objectMapper.createObjectNode();
        request.put("jsonrpc", "2.0");
        request.put("id", 1);
        request.put("method", "sendTransaction");

        ArrayNode params = request.putArray("params");
        params.add(serializedTransaction);

        ObjectNode configuration = params.addObject();
        configuration.put("encoding", "base64");
        configuration.put("skipPreflight", false);
        configuration.put("preflightCommitment", "confirmed");

        return objectMapper.writeValueAsString(request);
    }

    private SolanaUnsignedTransaction buildSolanaTransferTransaction(
            ΩSolanaAddressΩ sender,
            ΩSolanaAddressΩ recipient,
            SolanaLatestBlockhash latestBlockhash,
            ΩlamportsΩ lamports
    ) {
        SerializedSolanaTransfer transfer =
                serializeSolanaTransferTransaction(
                        sender,
                        recipient,
                        latestBlockhash,
                        lamports
                );

        return new SolanaUnsignedTransaction(
                transfer.serializedTransaction(),
                latestBlockhash.blockhash(),
                latestBlockhash.lastValidBlockHeight()
        );
    }

    private SerializedSolanaTransfer serializeSolanaTransferTransaction(
            ΩSolanaAddressΩ sender,
            ΩSolanaAddressΩ recipient,
            SolanaLatestBlockhash latestBlockhash,
            ΩlamportsΩ lamports
    ) {
        if (lamports <= 0) {
            throw new IllegalArgumentException(
                    "SOL transfer amount must be greater than zero lamports"
            );
        }

        byte[] senderBytes = decodeSolanaTransferAddress(
                "sender",
                sender
        );
        byte[] recipientBytes = decodeSolanaTransferAddress(
                "recipient",
                recipient
        );
        byte[] systemProgramBytes = decodeSolanaTransferAddress(
                "System Program",
                SYSTEM_PROGRAM_ADDRESS
        );
        byte[] blockhashBytes = decodeSolanaTransferAddress(
                "blockhash",
                latestBlockhash.blockhash()
        );

        ByteArrayOutputStream message = new ByteArrayOutputStream();

        message.write(1);
        message.write(0);
        message.write(1);

        writeCompactU16(message, 3);
        message.writeBytes(senderBytes);
        message.writeBytes(recipientBytes);
        message.writeBytes(systemProgramBytes);
        message.writeBytes(blockhashBytes);

        writeCompactU16(message, 1);
        message.write(2);
        writeCompactU16(message, 2);
        message.write(0);
        message.write(1);

        byte[] instructionData = ByteBuffer
                .allocate(12)
                .order(ByteOrder.LITTLE_ENDIAN)
                .putInt(SYSTEM_TRANSFER_INSTRUCTION)
                .putLong(lamports)
                .array();

        writeCompactU16(message, instructionData.length);
        message.writeBytes(instructionData);

        byte[] messageBytes = message.toByteArray();
        ByteArrayOutputStream transaction = new ByteArrayOutputStream();

        writeCompactU16(transaction, 1);
        transaction.writeBytes(new byte[SOLANA_SIGNATURE_LENGTH]);
        transaction.writeBytes(messageBytes);

        ΩBase64StringΩ serializedMessage =
                Base64.getEncoder().encodeToString(messageBytes);
        ΩBase64StringΩ serializedTransaction =
                Base64.getEncoder().encodeToString(
                        transaction.toByteArray()
                );

        return new SerializedSolanaTransfer(
                serializedMessage,
                serializedTransaction
        );
    }

    private ΩlamportsΩ toLamports(ΩSolanaAmountΩ amount) {
        if (amount == null || amount.amount() == null) {
            throw new IllegalArgumentException(
                    "SOL transfer amount must not be null"
            );
        }

        if (!currencyIdentityService.resolve(SOLANA_ID).equals(amount.currencyType())) {
            throw new IllegalArgumentException(
                    "SOL transfer amount must use the SOL currency type"
            );
        }

        BigDecimal lamports =
                amount.amount().multiply(LAMPORTS_PER_SOL);

        try {
            ΩlamportsΩ result = lamports.longValueExact();

            if (result <= 0) {
                throw new IllegalArgumentException(
                        "SOL transfer amount must be greater than zero"
                );
            }

            return result;
        } catch (ArithmeticException e) {
            throw new IllegalArgumentException(
                    "SOL transfer amount must be expressible as a whole "
                            + "number of lamports",
                    e
            );
        }
    }

    private void validateSolanaTransferAddresses(
            ΩSolanaAddressΩ sender,
            ΩSolanaAddressΩ recipient
    ) {
        if (sender == null || sender.isBlank()) {
            throw new IllegalArgumentException(
                    "Solana sender must not be blank"
            );
        }

        if (recipient == null || recipient.isBlank()) {
            throw new IllegalArgumentException(
                    "SOL transfer recipient must not be blank"
            );
        }

        if (recipient.equals(sender)) {
            throw new IllegalArgumentException(
                    "SOL transfer recipient must be different from the sender"
            );
        }
    }

    private byte[] decodeSolanaTransferAddress(
            String description,
            String address
    ) {
        if (address == null || address.isBlank()) {
            throw new IllegalArgumentException(
                    "Solana " + description + " must not be blank"
            );
        }

        byte[] decoded = base58Decode(address);

        if (decoded.length != SOLANA_ADDRESS_LENGTH) {
            throw new IllegalArgumentException(
                    "Solana "
                            + description
                            + " must decode to "
                            + SOLANA_ADDRESS_LENGTH
                            + " bytes, but was "
                            + decoded.length
            );
        }

        return decoded;
    }

    private void writeCompactU16(
            ByteArrayOutputStream output,
            int value
    ) {
        if (value < 0 || value > 0xffff) {
            throw new IllegalArgumentException(
                    "Compact-u16 value is outside the supported range: "
                            + value
            );
        }

        int remaining = value;

        do {
            int nextByte = remaining & 0x7f;
            remaining >>>= 7;

            if (remaining != 0) {
                nextByte |= 0x80;
            }

            output.write(nextByte);
        } while (remaining != 0);
    }

    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 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 {
            if (requestStarted) {
                previousRemoteCallTimeNanos = System.nanoTime();
                hasCompletedRemoteCall = true;
            }
            rpcGate.unlock();
        }
    }

    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;
            }

            long deadlineRemaining = deadline.remainingNanos();
            if (deadlineRemaining <= 0) {
                return false;
            }
            TimeUnit.NANOSECONDS.sleep(
                    Math.min(throttleRemaining, deadlineRemaining)
            );
        }
        return !deadline.hasExpired();
    }

    private boolean isSolanaNFTCandidate(SPLTokenHolding tokenHolding) {
        // TODO This only checks the owner's token holding.
        // 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.
        return tokenHolding.decimals() == 0
                && "1".equals(tokenHolding.rawAmount());
    }

    private byte[] createProgramAddressBytes(
            ΩSolanaProgramIdΩ programId,
            List<byte[]> seeds
    ) {
        if (seeds.size() > 16) {
            throw new IllegalArgumentException("A Solana program address can have at most 16 seeds.");
        }

        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");

            for (byte[] seed : seeds) {
                if (seed.length > 32) {
                    throw new IllegalArgumentException("A Solana program address seed can be at most 32 bytes.");
                }

                digest.update(seed);
            }

            digest.update(base58Decode(programId));
            digest.update(PROGRAM_DERIVED_ADDRESS_MARKER);

            return digest.digest();
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("SHA-256 is not available.", e);
        }
    }

    private boolean isOnEd25519Curve(byte[] encodedPoint) {
        if (encodedPoint.length != 32) {
            throw new IllegalArgumentException("Ed25519 encoded point must be 32 bytes.");
        }

        byte[] yBytes = encodedPoint.clone();

        int signBit = (yBytes[31] & 0x80) >>> 7;
        yBytes[31] &= 0x7F;

        BigInteger y = littleEndianToBigInteger(yBytes);

        if (y.compareTo(ED25519_P) >= 0) {
            return false;
        }

        BigInteger ySquared = y.multiply(y).mod(ED25519_P);

        BigInteger numerator = ySquared
                .subtract(BigInteger.ONE)
                .mod(ED25519_P);

        BigInteger denominator = ED25519_D
                .multiply(ySquared)
                .add(BigInteger.ONE)
                .mod(ED25519_P);

        if (denominator.equals(BigInteger.ZERO)) {
            return false;
        }

        BigInteger xSquared = numerator
                .multiply(denominator.modInverse(ED25519_P))
                .mod(ED25519_P);

        if (xSquared.equals(BigInteger.ZERO)) {
            return signBit == 0;
        }

        return isQuadraticResidue(xSquared);
    }

    private boolean isQuadraticResidue(BigInteger value) {
        return value.modPow(
                ED25519_P.subtract(BigInteger.ONE).shiftRight(1),
                ED25519_P
        ).equals(BigInteger.ONE);
    }

    private BigInteger littleEndianToBigInteger(byte[] littleEndianBytes) {
        byte[] bigEndianBytes = new byte[littleEndianBytes.length + 1];

        for (int i = 0; i < littleEndianBytes.length; i++) {
            bigEndianBytes[bigEndianBytes.length - 1 - i] = littleEndianBytes[i];
        }

        return new BigInteger(bigEndianBytes);
    }

    private static String base58Encode(byte[] inputBytes) {
        if (inputBytes.length == 0) {
            return "";
        }

        byte[] input = Arrays.copyOf(inputBytes, inputBytes.length);

        int zeros = 0;
        while (zeros < input.length && input[zeros] == 0) {
            zeros++;
        }

        char[] encoded = new char[input.length * 2];
        int outputStart = encoded.length;

        int inputStart = zeros;
        while (inputStart < input.length) {
            int mod = divmod(input, inputStart, 256, 58);

            if (input[inputStart] == 0) {
                inputStart++;
            }

            encoded[--outputStart] = BASE58_ALPHABET[mod];
        }

        while (outputStart < encoded.length && encoded[outputStart] == BASE58_ALPHABET[0]) {
            outputStart++;
        }

        while (zeros-- > 0) {
            encoded[--outputStart] = BASE58_ALPHABET[0];
        }

        return new String(encoded, outputStart, encoded.length - outputStart);
    }

    private static byte[] base58Decode(String input) {
        if (input.isEmpty()) {
            return new byte[0];
        }

        byte[] input58 = new byte[input.length()];

        for (int i = 0; i < input.length(); i++) {
            char c = input.charAt(i);

            if (c >= 128 || BASE58_INDEXES[c] < 0) {
                throw new IllegalArgumentException("Invalid Base58 character: " + c);
            }

            input58[i] = (byte) BASE58_INDEXES[c];
        }

        int zeros = 0;
        while (zeros < input58.length && input58[zeros] == 0) {
            zeros++;
        }

        byte[] decoded = new byte[input.length()];
        int outputStart = decoded.length;

        int inputStart = zeros;
        while (inputStart < input58.length) {
            int mod = divmod(input58, inputStart, 58, 256);

            if (input58[inputStart] == 0) {
                inputStart++;
            }

            decoded[--outputStart] = (byte) mod;
        }

        while (outputStart < decoded.length && decoded[outputStart] == 0) {
            outputStart++;
        }

        return Arrays.copyOfRange(decoded, outputStart - zeros, decoded.length);
    }

    private static int divmod(byte[] number, int firstDigit, int base, int divisor) {
        int remainder = 0;

        for (int i = firstDigit; i < number.length; i++) {
            int digit = number[i] & 0xFF;
            int temporary = remainder * base + digit;

            number[i] = (byte) (temporary / divisor);
            remainder = temporary % divisor;
        }

        return remainder;
    }

    private List<byte[]> toSeedBytes(List<SolanaProgramAddressSeed> seeds) {
        List<byte[]> seedBytes = new ArrayList<>();

        for (SolanaProgramAddressSeed seed : seeds) {
            seedBytes.add(toSeedBytes(seed));
        }

        return seedBytes;
    }

    private byte[] toSeedBytes(SolanaProgramAddressSeed seed) {
        return switch (seed.kind()) {
            case UTF8 -> seed.value().getBytes(StandardCharsets.UTF_8);

            case SOLANA_ADDRESS -> {
                byte[] addressBytes = base58Decode(seed.value());

                if (addressBytes.length != 32) {
                    throw new IllegalArgumentException(
                            "Solana address seed must decode to 32 bytes, but was " + addressBytes.length
                    );
                }

                yield addressBytes;
            }
        };
    }

    private SolanaProgramDerivedAddress findProgramAddressFromSeedBytes(
            ΩSolanaProgramIdΩ programId,
            List<byte[]> seedBytes
    ) {
        if (seedBytes.size() > 15) {
            throw new IllegalArgumentException(
                    "findProgramAddress(...) can have at most 15 caller-provided seeds because the bump is added as the final seed."
            );
        }

        for (int bump = 255; bump >= 0; bump--) {
            List<byte[]> seedBytesWithBump = new ArrayList<>(seedBytes);
            seedBytesWithBump.add(new byte[] { (byte) bump });

            byte[] candidateAddress = createProgramAddressBytes(
                    programId,
                    seedBytesWithBump
            );

            if (!isOnEd25519Curve(candidateAddress)) {
                return new SolanaProgramDerivedAddress(
                        base58Encode(candidateAddress),
                        bump
                );
            }
        }

        throw new IllegalArgumentException("Could not find valid program derived address.");
    }

    private static int[] createBase58Indexes() {
        int[] indexes = new int[128];
        Arrays.fill(indexes, -1);

        for (int i = 0; i < BASE58_ALPHABET.length; i++) {
            indexes[BASE58_ALPHABET[i]] = i;
        }

        return indexes;
    }

    private record SerializedSolanaTransfer(
            ΩBase64StringΩ serializedMessage,
            ΩBase64StringΩ serializedTransaction
    ) {
    }

    private record CompiledInstruction(
            int programIndex,
            byte[] accountIndexes,
            byte[] data
    ) {
    }

    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 =
            "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
    private static final int SYSTEM_TRANSFER_INSTRUCTION = 2;
    private static final int TOKEN_TRANSFER_CHECKED_INSTRUCTION = 12;
    private static final int TOKEN_BURN_CHECKED_INSTRUCTION = 15;
    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 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();
    private static final int[] BASE58_INDEXES = createBase58Indexes();
    private static final BigInteger ED25519_P = BigInteger.ONE.shiftLeft(255).subtract(BigInteger.valueOf(19));
    private static final BigInteger ED25519_D =
            BigInteger.valueOf(-121665)
                    .multiply(BigInteger.valueOf(121666).modInverse(ED25519_P))
                    .mod(ED25519_P);
    private final CurrencyIdentityService currencyIdentityService;
    private final ObjectMapper objectMapper;
    private final HttpClient httpClient;
    private final ReentrantLock rpcGate = new ReentrantLock();
    private long previousRemoteCallTimeNanos;
    private boolean hasCompletedRemoteCall;
}
