solrengine-rpc 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +49 -0
- data/lib/solrengine/rpc/client.rb +122 -0
- data/lib/solrengine/rpc/configuration.rb +63 -0
- data/lib/solrengine/rpc/engine.rb +23 -0
- data/lib/solrengine/rpc/ssl_http.rb +26 -0
- data/lib/solrengine/rpc/version.rb +7 -0
- data/lib/solrengine/rpc.rb +25 -0
- metadata +68 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6153fbe152baa967ab457bd1cb90c16b978c9e270bdff09e087f2f85298fde6d
|
|
4
|
+
data.tar.gz: b7a4c693fae301f5c2faf511507a1864e3bdc7e280f1331c426739fd03428efa
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9e9192a9993b141904b5af5d539c0dd4a220d969ebdd075778c006cfab8826e9bbdba5e3b61a9b83ce89892d7bd5090c38356e4b60a0fcd8df47e54f3f40ccf0
|
|
7
|
+
data.tar.gz: ebac91864b9821fc2a1b3283b4a5ec60dea5cea930b9a4a676ec04312081ebd127cf48283a63097a89139ef1a588ec55bdb4199f59beb418cbc01e6715f69706
|
data/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# SolRengine RPC
|
|
2
|
+
|
|
3
|
+
Solana JSON-RPC client for Ruby. Handles balance queries, token accounts, signatures, transactions, and blockhash — with SSL CRL tolerance and multi-network support.
|
|
4
|
+
|
|
5
|
+
Part of the [SolRengine](https://github.com/solrengine) framework.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "solrengine-rpc"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
client = Solrengine::Rpc.client
|
|
17
|
+
|
|
18
|
+
client.get_balance("Abc...xyz") # => 2.5 (SOL)
|
|
19
|
+
client.get_token_accounts("Abc...xyz") # => [{ mint:, ui_amount:, ... }]
|
|
20
|
+
client.get_recent_signatures("Abc...xyz") # => [{ signature:, block_time:, ... }]
|
|
21
|
+
client.get_latest_blockhash # => "abc123..."
|
|
22
|
+
client.get_signature_status("sig...") # => { "confirmationStatus" => "finalized" }
|
|
23
|
+
client.get_transaction("sig...") # => { full tx details }
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Configuration
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
Solrengine::Rpc.configure do |config|
|
|
30
|
+
config.network = "mainnet" # or "devnet", "testnet"
|
|
31
|
+
config.rpc_url = "https://mainnet.helius-rpc.com/?api-key=YOUR_KEY"
|
|
32
|
+
config.ws_url = "wss://mainnet.helius-rpc.com/?api-key=YOUR_KEY"
|
|
33
|
+
end
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or via environment variables:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
SOLANA_NETWORK=devnet
|
|
40
|
+
SOLANA_RPC_URL=https://mainnet.helius-rpc.com/?api-key=xxx
|
|
41
|
+
SOLANA_WS_URL=wss://mainnet.helius-rpc.com/?api-key=xxx
|
|
42
|
+
SOLANA_RPC_DEVNET_URL=https://devnet.helius-rpc.com/?api-key=xxx
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
In a Rails app, you can also use `config/solana.yml` — the engine loads it automatically.
|
|
46
|
+
|
|
47
|
+
## License
|
|
48
|
+
|
|
49
|
+
MIT
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module Solrengine
|
|
4
|
+
module Rpc
|
|
5
|
+
class Client
|
|
6
|
+
include SslHttp
|
|
7
|
+
|
|
8
|
+
def initialize(rpc_url: nil)
|
|
9
|
+
@rpc_url = rpc_url || Solrengine::Rpc.configuration.rpc_url
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def get_balance(wallet_address, commitment: "confirmed")
|
|
13
|
+
result = rpc_request("getBalance", [ wallet_address, { "commitment" => commitment } ])
|
|
14
|
+
lamports = result.dig("result", "value")
|
|
15
|
+
return nil unless lamports
|
|
16
|
+
|
|
17
|
+
lamports.to_f / 1_000_000_000
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def get_token_accounts(wallet_address, program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
|
21
|
+
result = rpc_request("getTokenAccountsByOwner", [
|
|
22
|
+
wallet_address,
|
|
23
|
+
{ "programId" => program_id },
|
|
24
|
+
{ "encoding" => "jsonParsed" }
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
accounts = result.dig("result", "value") || []
|
|
28
|
+
|
|
29
|
+
accounts.filter_map do |account|
|
|
30
|
+
info = account.dig("account", "data", "parsed", "info")
|
|
31
|
+
next unless info
|
|
32
|
+
|
|
33
|
+
token_amount = info["tokenAmount"]
|
|
34
|
+
amount = token_amount["uiAmount"].to_f
|
|
35
|
+
next if amount.zero?
|
|
36
|
+
|
|
37
|
+
{
|
|
38
|
+
mint: info["mint"],
|
|
39
|
+
balance: token_amount["amount"],
|
|
40
|
+
decimals: token_amount["decimals"],
|
|
41
|
+
ui_amount: amount,
|
|
42
|
+
ui_amount_string: token_amount["uiAmountString"]
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def get_recent_signatures(wallet_address, limit: 10)
|
|
48
|
+
result = rpc_request("getSignaturesForAddress", [
|
|
49
|
+
wallet_address,
|
|
50
|
+
{ "limit" => limit }
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
signatures = result.dig("result") || []
|
|
54
|
+
|
|
55
|
+
signatures.map do |sig|
|
|
56
|
+
{
|
|
57
|
+
signature: sig["signature"],
|
|
58
|
+
slot: sig["slot"],
|
|
59
|
+
block_time: sig["blockTime"] ? Time.at(sig["blockTime"]) : nil,
|
|
60
|
+
error: sig["err"],
|
|
61
|
+
memo: sig["memo"],
|
|
62
|
+
confirmation_status: sig["confirmationStatus"]
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def get_latest_blockhash(commitment: "finalized")
|
|
68
|
+
result = rpc_request("getLatestBlockhash", [ { "commitment" => commitment } ])
|
|
69
|
+
value = result.dig("result", "value")
|
|
70
|
+
return nil unless value
|
|
71
|
+
|
|
72
|
+
{
|
|
73
|
+
blockhash: value["blockhash"],
|
|
74
|
+
last_valid_block_height: value["lastValidBlockHeight"]
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def get_signature_status(signature)
|
|
79
|
+
result = rpc_request("getSignatureStatuses", [ [ signature ] ])
|
|
80
|
+
result.dig("result", "value", 0)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def get_transaction(signature)
|
|
84
|
+
result = rpc_request("getTransaction", [
|
|
85
|
+
signature,
|
|
86
|
+
{ "encoding" => "jsonParsed", "maxSupportedTransactionVersion" => 0 }
|
|
87
|
+
])
|
|
88
|
+
|
|
89
|
+
result["result"]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Generic RPC call for methods not covered above
|
|
93
|
+
def request(method, params = [])
|
|
94
|
+
rpc_request(method, params)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def rpc_request(method, params = [])
|
|
100
|
+
uri = URI.parse(@rpc_url)
|
|
101
|
+
http = ssl_http(uri)
|
|
102
|
+
|
|
103
|
+
request = Net::HTTP::Post.new(uri.request_uri.empty? ? "/" : uri.request_uri)
|
|
104
|
+
request["Content-Type"] = "application/json"
|
|
105
|
+
request.body = {
|
|
106
|
+
jsonrpc: "2.0",
|
|
107
|
+
id: 1,
|
|
108
|
+
method: method,
|
|
109
|
+
params: params
|
|
110
|
+
}.to_json
|
|
111
|
+
|
|
112
|
+
response = http.request(request)
|
|
113
|
+
JSON.parse(response.body)
|
|
114
|
+
rescue => e
|
|
115
|
+
if defined?(Rails)
|
|
116
|
+
Rails.logger.error("Solrengine RPC error: #{e.class} - #{e.message}")
|
|
117
|
+
end
|
|
118
|
+
{}
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module Solrengine
|
|
2
|
+
module Rpc
|
|
3
|
+
class Configuration
|
|
4
|
+
NETWORKS = %w[mainnet devnet testnet].freeze
|
|
5
|
+
|
|
6
|
+
DEFAULT_RPC_URLS = {
|
|
7
|
+
"mainnet" => "https://api.mainnet-beta.solana.com",
|
|
8
|
+
"devnet" => "https://api.devnet.solana.com",
|
|
9
|
+
"testnet" => "https://api.testnet.solana.com"
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
DEFAULT_WS_URLS = {
|
|
13
|
+
"mainnet" => "wss://api.mainnet-beta.solana.com",
|
|
14
|
+
"devnet" => "wss://api.devnet.solana.com",
|
|
15
|
+
"testnet" => "wss://api.testnet.solana.com"
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
attr_writer :network, :rpc_url, :ws_url
|
|
19
|
+
|
|
20
|
+
def network
|
|
21
|
+
@network ||= ENV.fetch("SOLANA_NETWORK", "mainnet")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def rpc_url
|
|
25
|
+
@rpc_url ||= env_rpc_url || DEFAULT_RPC_URLS[network]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def ws_url
|
|
29
|
+
@ws_url ||= env_ws_url || DEFAULT_WS_URLS[network]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def mainnet?
|
|
33
|
+
network == "mainnet"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def explorer_base
|
|
37
|
+
mainnet? ? "https://solscan.io" : "https://explorer.solana.com"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def explorer_cluster
|
|
41
|
+
mainnet? ? "" : "?cluster=#{network}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def env_rpc_url
|
|
47
|
+
case network
|
|
48
|
+
when "mainnet" then ENV["SOLANA_RPC_URL"]
|
|
49
|
+
when "devnet" then ENV["SOLANA_RPC_DEVNET_URL"]
|
|
50
|
+
when "testnet" then ENV["SOLANA_RPC_TESTNET_URL"]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def env_ws_url
|
|
55
|
+
case network
|
|
56
|
+
when "mainnet" then ENV["SOLANA_WS_URL"]
|
|
57
|
+
when "devnet" then ENV["SOLANA_WS_DEVNET_URL"]
|
|
58
|
+
when "testnet" then ENV["SOLANA_WS_TESTNET_URL"]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Solrengine
|
|
2
|
+
module Rpc
|
|
3
|
+
class Engine < ::Rails::Engine
|
|
4
|
+
isolate_namespace Solrengine::Rpc
|
|
5
|
+
|
|
6
|
+
initializer "solrengine-rpc.configure" do
|
|
7
|
+
# Load config from solana.yml if it exists and no explicit config was set
|
|
8
|
+
config_path = Rails.root.join("config", "solana.yml")
|
|
9
|
+
if config_path.exist?
|
|
10
|
+
solana_config = Rails.application.config_for(:solana).deep_stringify_keys
|
|
11
|
+
cfg = Solrengine::Rpc.configuration
|
|
12
|
+
cfg.network = solana_config["network"] if solana_config["network"]
|
|
13
|
+
|
|
14
|
+
network_config = solana_config.dig("networks", cfg.network)
|
|
15
|
+
if network_config
|
|
16
|
+
cfg.rpc_url = network_config["rpc_url"] if network_config["rpc_url"]
|
|
17
|
+
cfg.ws_url = network_config["ws_url"] if network_config["ws_url"]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
|
|
3
|
+
module Solrengine
|
|
4
|
+
module Rpc
|
|
5
|
+
# HTTP helper that tolerates SSL certificates with unavailable CRLs.
|
|
6
|
+
# Some Solana and Jupiter endpoints have certs that Ruby's OpenSSL
|
|
7
|
+
# rejects due to missing Certificate Revocation Lists.
|
|
8
|
+
module SslHttp
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def ssl_http(uri)
|
|
12
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
13
|
+
http.use_ssl = (uri.scheme == "https")
|
|
14
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
15
|
+
http.verify_callback = ->(_preverify_ok, store_ctx) {
|
|
16
|
+
return true if store_ctx.error == 0
|
|
17
|
+
return true if store_ctx.error == OpenSSL::X509::V_ERR_UNABLE_TO_GET_CRL
|
|
18
|
+
false
|
|
19
|
+
}
|
|
20
|
+
http.open_timeout = 10
|
|
21
|
+
http.read_timeout = 10
|
|
22
|
+
http
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require_relative "rpc/version"
|
|
2
|
+
require_relative "rpc/configuration"
|
|
3
|
+
require_relative "rpc/ssl_http"
|
|
4
|
+
require_relative "rpc/client"
|
|
5
|
+
require_relative "rpc/engine" if defined?(Rails::Engine)
|
|
6
|
+
|
|
7
|
+
module Solrengine
|
|
8
|
+
module Rpc
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def configuration
|
|
13
|
+
@configuration ||= Configuration.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def configure
|
|
17
|
+
yield(configuration)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def client(rpc_url: nil)
|
|
21
|
+
Client.new(rpc_url: rpc_url || configuration.rpc_url)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: solrengine-rpc
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Jose Ferrer
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-21 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rails
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.1'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '7.1'
|
|
27
|
+
description: Ruby client for Solana's JSON-RPC API. Handles balance, token accounts,
|
|
28
|
+
signatures, transactions, and blockhash queries. Includes SSL CRL workaround and
|
|
29
|
+
multi-network configuration.
|
|
30
|
+
email:
|
|
31
|
+
- estoy@moviendo.me
|
|
32
|
+
executables: []
|
|
33
|
+
extensions: []
|
|
34
|
+
extra_rdoc_files: []
|
|
35
|
+
files:
|
|
36
|
+
- README.md
|
|
37
|
+
- lib/solrengine/rpc.rb
|
|
38
|
+
- lib/solrengine/rpc/client.rb
|
|
39
|
+
- lib/solrengine/rpc/configuration.rb
|
|
40
|
+
- lib/solrengine/rpc/engine.rb
|
|
41
|
+
- lib/solrengine/rpc/ssl_http.rb
|
|
42
|
+
- lib/solrengine/rpc/version.rb
|
|
43
|
+
homepage: https://github.com/solrengine/rpc
|
|
44
|
+
licenses:
|
|
45
|
+
- MIT
|
|
46
|
+
metadata:
|
|
47
|
+
homepage_uri: https://github.com/solrengine/rpc
|
|
48
|
+
source_code_uri: https://github.com/solrengine/rpc
|
|
49
|
+
post_install_message:
|
|
50
|
+
rdoc_options: []
|
|
51
|
+
require_paths:
|
|
52
|
+
- lib
|
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
54
|
+
requirements:
|
|
55
|
+
- - ">="
|
|
56
|
+
- !ruby/object:Gem::Version
|
|
57
|
+
version: 3.2.0
|
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '0'
|
|
63
|
+
requirements: []
|
|
64
|
+
rubygems_version: 3.5.22
|
|
65
|
+
signing_key:
|
|
66
|
+
specification_version: 4
|
|
67
|
+
summary: Solana JSON-RPC client for Ruby with SSL CRL fix and network config
|
|
68
|
+
test_files: []
|