Files
proxmox/scripts/lib/solana_jsonrpc.py
defiQUG 4ebf2d7902
Some checks failed
Deploy to Phoenix / validate (push) Failing after 1s
Deploy to Phoenix / deploy (push) Has been skipped
Deploy to Phoenix / deploy-atomic-swap-dapp (push) Has been skipped
Deploy to Phoenix / cloudflare (push) Has been skipped
chore(repo): sync operator workspace (config, scripts, docs, multi-chain)
Add optional Cosmos/Engine-X/act-runner templates, CWUSDC/EI-matrix tooling,
non-EVM route planner in multi-chain-execution (tests passing), token list and
extraction updates, and documentation (MetaMask matrix, GRU/CWUSDC packets).

Ignore institutional evidence tarballs/sha256 under reports/status.

Validated with: bash scripts/verify/run-all-validation.sh --skip-genesis

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 16:25:08 -07:00

189 lines
5.8 KiB
Python

"""
Minimal Solana JSON-RPC over HTTP (stdlib only).
Some public RPCs return a bare string for ``sendTransaction`` ``result`` without
extra fields that ``solana-py``'s ``SendTransactionResp`` expects, which makes
``Client.send_raw_transaction`` panic while deserializing (missing JSON field
``data``). Use :func:`send_transaction_wire` for submission; keep ``solders``
(or ``solana-py``) only for signing and local serialization.
"""
from __future__ import annotations
import base64
import json
import time
import urllib.error
import urllib.request
from typing import Any
DEFAULT_USER_AGENT = "proxmox-scripts/solana-jsonrpc/1.0"
class SolanaJsonRpcError(RuntimeError):
"""JSON-RPC error object or unexpected HTTP / parse failure."""
def __init__(self, message: str, *, payload: dict[str, Any] | None = None) -> None:
super().__init__(message)
self.payload = payload
def post_json_rpc(
rpc_url: str,
method: str,
params: list[Any],
*,
request_id: int = 1,
timeout_s: float = 90.0,
user_agent: str = DEFAULT_USER_AGENT,
) -> dict[str, Any]:
body = json.dumps(
{"jsonrpc": "2.0", "id": request_id, "method": method, "params": params}
).encode("utf-8")
req = urllib.request.Request(
rpc_url,
data=body,
headers={"Content-Type": "application/json", "User-Agent": user_agent},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=timeout_s) as resp:
raw = resp.read().decode("utf-8")
except urllib.error.HTTPError as e:
try:
detail = e.read().decode("utf-8", errors="replace")
except Exception:
detail = str(e)
raise SolanaJsonRpcError(f"HTTP {e.code}: {detail}") from e
try:
out: dict[str, Any] = json.loads(raw)
except json.JSONDecodeError as e:
raise SolanaJsonRpcError(f"invalid JSON from RPC: {raw[:500]!r}") from e
err = out.get("error")
if err:
raise SolanaJsonRpcError(f"RPC error: {err}", payload=out)
return out
def get_latest_blockhash(
rpc_url: str, *, commitment: str = "confirmed", timeout_s: float = 30.0
) -> str:
out = post_json_rpc(
rpc_url,
"getLatestBlockhash",
[{"commitment": commitment}],
timeout_s=timeout_s,
)
try:
return str(out["result"]["value"]["blockhash"])
except (KeyError, TypeError) as e:
raise SolanaJsonRpcError(f"unexpected getLatestBlockhash shape: {out!r}") from e
def get_balance_lamports(
rpc_url: str, pubkey_b58: str, *, commitment: str = "confirmed"
) -> int:
out = post_json_rpc(
rpc_url,
"getBalance",
[pubkey_b58, {"commitment": commitment}],
)
try:
return int(out["result"]["value"])
except (KeyError, TypeError, ValueError) as e:
raise SolanaJsonRpcError(f"unexpected getBalance shape: {out!r}") from e
def send_transaction_wire(
rpc_url: str,
signed_wire: bytes,
*,
skip_preflight: bool = False,
preflight_commitment: str = "confirmed",
max_retries: int | None = None,
timeout_s: float = 90.0,
) -> str:
"""
Submit a fully signed legacy or versioned transaction (wire bytes).
Returns base58 transaction signature string from ``result``.
"""
opts: dict[str, Any] = {
"encoding": "base64",
"skipPreflight": skip_preflight,
"preflightCommitment": preflight_commitment,
}
if max_retries is not None:
opts["maxRetries"] = max_retries
params: list[Any] = [base64.b64encode(signed_wire).decode("ascii"), opts]
out = post_json_rpc(rpc_url, "sendTransaction", params, timeout_s=timeout_s)
result = out.get("result")
if not isinstance(result, str):
raise SolanaJsonRpcError(f"unexpected sendTransaction result: {out!r}")
return result
def get_signature_statuses(
rpc_url: str,
signatures: list[str],
*,
search_transaction_history: bool = False,
) -> list[dict[str, Any] | None]:
"""Return one status object (or null) per signature, same order as input."""
if search_transaction_history:
params: list[Any] = [signatures, {"searchTransactionHistory": True}]
else:
params = [signatures]
out = post_json_rpc(rpc_url, "getSignatureStatuses", params)
try:
val = out["result"]["value"]
except (KeyError, TypeError) as e:
raise SolanaJsonRpcError(f"unexpected getSignatureStatuses shape: {out!r}") from e
if not isinstance(val, list):
raise SolanaJsonRpcError(f"unexpected getSignatureStatuses value: {val!r}")
out_list: list[dict[str, Any] | None] = []
for item in val:
if item is None:
out_list.append(None)
elif isinstance(item, dict):
out_list.append(item)
else:
raise SolanaJsonRpcError(f"unexpected status entry: {item!r}")
return out_list
def wait_until_signature_confirmed(
rpc_url: str,
signature: str,
*,
timeout_s: float = 90.0,
poll_interval_s: float = 1.0,
) -> dict[str, Any]:
"""
Poll ``getSignatureStatuses`` until the signature has a terminal ``err`` or
reaches ``confirmationStatus`` of ``confirmed`` / ``finalized``.
"""
deadline = time.monotonic() + timeout_s
last: dict[str, Any] | None = None
while time.monotonic() < deadline:
statuses = get_signature_statuses(rpc_url, [signature])
st = statuses[0] if statuses else None
last = st
if st is None:
time.sleep(poll_interval_s)
continue
err = st.get("err")
if err:
raise SolanaJsonRpcError(f"transaction failed: err={err!r}", payload=st)
conf = st.get("confirmationStatus")
if conf in ("confirmed", "finalized"):
return st
time.sleep(poll_interval_s)
raise SolanaJsonRpcError(
f"timeout waiting for confirmation of {signature!r}; last={last!r}"
)