solana-ruby-kit 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/lib/core_extensions/tapioca/name_patch.rb +32 -0
- data/lib/core_extensions/tapioca/required_ancestors.rb +13 -0
- data/lib/generators/solana/ruby/kit/install/install_generator.rb +17 -0
- data/lib/generators/solana/ruby/kit/install/templates/solana_ruby_kit.rb.tt +8 -0
- data/lib/solana/ruby/kit/accounts/account.rb +47 -0
- data/lib/solana/ruby/kit/accounts/maybe_account.rb +86 -0
- data/lib/solana/ruby/kit/accounts.rb +6 -0
- data/lib/solana/ruby/kit/addresses/address.rb +133 -0
- data/lib/solana/ruby/kit/addresses/curve.rb +112 -0
- data/lib/solana/ruby/kit/addresses/program_derived_address.rb +155 -0
- data/lib/solana/ruby/kit/addresses/public_key.rb +39 -0
- data/lib/solana/ruby/kit/addresses.rb +11 -0
- data/lib/solana/ruby/kit/codecs/bytes.rb +58 -0
- data/lib/solana/ruby/kit/codecs/codec.rb +135 -0
- data/lib/solana/ruby/kit/codecs/data_structures.rb +177 -0
- data/lib/solana/ruby/kit/codecs/decoder.rb +43 -0
- data/lib/solana/ruby/kit/codecs/encoder.rb +52 -0
- data/lib/solana/ruby/kit/codecs/numbers.rb +217 -0
- data/lib/solana/ruby/kit/codecs/strings.rb +116 -0
- data/lib/solana/ruby/kit/codecs.rb +25 -0
- data/lib/solana/ruby/kit/configuration.rb +48 -0
- data/lib/solana/ruby/kit/encoding/base58.rb +62 -0
- data/lib/solana/ruby/kit/errors.rb +226 -0
- data/lib/solana/ruby/kit/fast_stable_stringify.rb +62 -0
- data/lib/solana/ruby/kit/functional.rb +29 -0
- data/lib/solana/ruby/kit/instruction_plans/plans.rb +27 -0
- data/lib/solana/ruby/kit/instruction_plans.rb +47 -0
- data/lib/solana/ruby/kit/instructions/accounts.rb +80 -0
- data/lib/solana/ruby/kit/instructions/instruction.rb +71 -0
- data/lib/solana/ruby/kit/instructions/roles.rb +84 -0
- data/lib/solana/ruby/kit/instructions.rb +7 -0
- data/lib/solana/ruby/kit/keys/key_pair.rb +84 -0
- data/lib/solana/ruby/kit/keys/private_key.rb +39 -0
- data/lib/solana/ruby/kit/keys/public_key.rb +31 -0
- data/lib/solana/ruby/kit/keys/signatures.rb +171 -0
- data/lib/solana/ruby/kit/keys.rb +11 -0
- data/lib/solana/ruby/kit/offchain_messages/codec.rb +107 -0
- data/lib/solana/ruby/kit/offchain_messages/message.rb +22 -0
- data/lib/solana/ruby/kit/offchain_messages.rb +16 -0
- data/lib/solana/ruby/kit/options/option.rb +132 -0
- data/lib/solana/ruby/kit/options.rb +5 -0
- data/lib/solana/ruby/kit/plugin_core.rb +58 -0
- data/lib/solana/ruby/kit/programs.rb +42 -0
- data/lib/solana/ruby/kit/promises.rb +85 -0
- data/lib/solana/ruby/kit/railtie.rb +18 -0
- data/lib/solana/ruby/kit/rpc/api/get_account_info.rb +76 -0
- data/lib/solana/ruby/kit/rpc/api/get_balance.rb +41 -0
- data/lib/solana/ruby/kit/rpc/api/get_block_height.rb +29 -0
- data/lib/solana/ruby/kit/rpc/api/get_epoch_info.rb +47 -0
- data/lib/solana/ruby/kit/rpc/api/get_latest_blockhash.rb +52 -0
- data/lib/solana/ruby/kit/rpc/api/get_minimum_balance_for_rent_exemption.rb +29 -0
- data/lib/solana/ruby/kit/rpc/api/get_multiple_accounts.rb +56 -0
- data/lib/solana/ruby/kit/rpc/api/get_program_accounts.rb +60 -0
- data/lib/solana/ruby/kit/rpc/api/get_signature_statuses.rb +56 -0
- data/lib/solana/ruby/kit/rpc/api/get_slot.rb +30 -0
- data/lib/solana/ruby/kit/rpc/api/get_token_account_balance.rb +38 -0
- data/lib/solana/ruby/kit/rpc/api/get_token_accounts_by_owner.rb +48 -0
- data/lib/solana/ruby/kit/rpc/api/get_transaction.rb +36 -0
- data/lib/solana/ruby/kit/rpc/api/get_vote_accounts.rb +62 -0
- data/lib/solana/ruby/kit/rpc/api/is_blockhash_valid.rb +41 -0
- data/lib/solana/ruby/kit/rpc/api/request_airdrop.rb +35 -0
- data/lib/solana/ruby/kit/rpc/api/send_transaction.rb +61 -0
- data/lib/solana/ruby/kit/rpc/api/simulate_transaction.rb +47 -0
- data/lib/solana/ruby/kit/rpc/client.rb +83 -0
- data/lib/solana/ruby/kit/rpc/transport.rb +137 -0
- data/lib/solana/ruby/kit/rpc.rb +13 -0
- data/lib/solana/ruby/kit/rpc_parsed_types/address_lookup_table.rb +33 -0
- data/lib/solana/ruby/kit/rpc_parsed_types/nonce_account.rb +33 -0
- data/lib/solana/ruby/kit/rpc_parsed_types/stake_account.rb +51 -0
- data/lib/solana/ruby/kit/rpc_parsed_types/token_account.rb +52 -0
- data/lib/solana/ruby/kit/rpc_parsed_types/vote_account.rb +38 -0
- data/lib/solana/ruby/kit/rpc_parsed_types.rb +16 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/api/account_notifications.rb +29 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/api/logs_notifications.rb +28 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/api/program_notifications.rb +30 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/api/root_notifications.rb +19 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/api/signature_notifications.rb +28 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/api/slot_notifications.rb +19 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/autopinger.rb +42 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/client.rb +80 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/subscription.rb +58 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/transport.rb +163 -0
- data/lib/solana/ruby/kit/rpc_subscriptions.rb +12 -0
- data/lib/solana/ruby/kit/rpc_types/account_info.rb +53 -0
- data/lib/solana/ruby/kit/rpc_types/cluster_url.rb +56 -0
- data/lib/solana/ruby/kit/rpc_types/commitment.rb +52 -0
- data/lib/solana/ruby/kit/rpc_types/lamports.rb +43 -0
- data/lib/solana/ruby/kit/rpc_types.rb +8 -0
- data/lib/solana/ruby/kit/signers/keypair_signer.rb +126 -0
- data/lib/solana/ruby/kit/signers.rb +5 -0
- data/lib/solana/ruby/kit/subscribable/async_iterable.rb +80 -0
- data/lib/solana/ruby/kit/subscribable/data_publisher.rb +90 -0
- data/lib/solana/ruby/kit/subscribable.rb +13 -0
- data/lib/solana/ruby/kit/sysvars/addresses.rb +19 -0
- data/lib/solana/ruby/kit/sysvars/clock.rb +37 -0
- data/lib/solana/ruby/kit/sysvars/epoch_schedule.rb +34 -0
- data/lib/solana/ruby/kit/sysvars/last_restart_slot.rb +22 -0
- data/lib/solana/ruby/kit/sysvars/rent.rb +29 -0
- data/lib/solana/ruby/kit/sysvars.rb +33 -0
- data/lib/solana/ruby/kit/transaction_confirmation.rb +159 -0
- data/lib/solana/ruby/kit/transaction_messages/transaction_message.rb +168 -0
- data/lib/solana/ruby/kit/transaction_messages.rb +5 -0
- data/lib/solana/ruby/kit/transactions/transaction.rb +135 -0
- data/lib/solana/ruby/kit/transactions.rb +5 -0
- data/lib/solana/ruby/kit/version.rb +10 -0
- data/lib/solana/ruby/kit.rb +100 -0
- data/solana-ruby-kit.gemspec +29 -0
- metadata +311 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'uri'
|
|
7
|
+
require_relative '../errors'
|
|
8
|
+
|
|
9
|
+
module Solana::Ruby::Kit
|
|
10
|
+
module Rpc
|
|
11
|
+
# JSON-RPC error returned by the Solana node.
|
|
12
|
+
class RpcError < StandardError
|
|
13
|
+
extend T::Sig
|
|
14
|
+
|
|
15
|
+
sig { returns(Integer) }
|
|
16
|
+
attr_reader :code
|
|
17
|
+
|
|
18
|
+
sig { returns(T.untyped) }
|
|
19
|
+
attr_reader :data
|
|
20
|
+
|
|
21
|
+
sig { params(code: Integer, message: String, data: T.untyped).void }
|
|
22
|
+
def initialize(code, message, data = nil)
|
|
23
|
+
@code = T.let(code, Integer)
|
|
24
|
+
@data = T.let(data, T.untyped)
|
|
25
|
+
super("JSON-RPC error #{code}: #{message}")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# HTTP error from the transport layer (non-2xx response).
|
|
30
|
+
class HttpTransportError < StandardError
|
|
31
|
+
extend T::Sig
|
|
32
|
+
|
|
33
|
+
sig { returns(Integer) }
|
|
34
|
+
attr_reader :status_code
|
|
35
|
+
|
|
36
|
+
sig { params(status_code: Integer, message: String).void }
|
|
37
|
+
def initialize(status_code, message)
|
|
38
|
+
@status_code = T.let(status_code, Integer)
|
|
39
|
+
super("HTTP #{status_code}: #{message}")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# HTTP transport for Solana's JSON-RPC API.
|
|
44
|
+
# Mirrors TypeScript's `createHttpTransport(config)` from @solana/rpc-transport-http.
|
|
45
|
+
#
|
|
46
|
+
# Makes synchronous POST requests — TypeScript's async `fetch` maps to
|
|
47
|
+
# Ruby's blocking `Net::HTTP`. Use threads or Fibers for concurrency.
|
|
48
|
+
class Transport
|
|
49
|
+
extend T::Sig
|
|
50
|
+
|
|
51
|
+
DEFAULT_TIMEOUT = T.let(30, Integer) # seconds
|
|
52
|
+
DEFAULT_OPEN_TIMEOUT = T.let(10, Integer) # seconds
|
|
53
|
+
|
|
54
|
+
sig { returns(String) }
|
|
55
|
+
attr_reader :url
|
|
56
|
+
|
|
57
|
+
sig do
|
|
58
|
+
params(
|
|
59
|
+
url: String,
|
|
60
|
+
headers: T::Hash[String, String],
|
|
61
|
+
timeout: Integer,
|
|
62
|
+
open_timeout: Integer,
|
|
63
|
+
to_json: T.nilable(T.proc.params(arg0: T.untyped).returns(String)),
|
|
64
|
+
from_json: T.nilable(T.proc.params(arg0: String, arg1: T.untyped).returns(T.untyped))
|
|
65
|
+
).void
|
|
66
|
+
end
|
|
67
|
+
def initialize(
|
|
68
|
+
url:,
|
|
69
|
+
headers: {},
|
|
70
|
+
timeout: DEFAULT_TIMEOUT,
|
|
71
|
+
open_timeout: DEFAULT_OPEN_TIMEOUT,
|
|
72
|
+
to_json: nil,
|
|
73
|
+
from_json: nil
|
|
74
|
+
)
|
|
75
|
+
@url = T.let(url, String)
|
|
76
|
+
@headers = T.let(headers, T::Hash[String, String])
|
|
77
|
+
@timeout = T.let(timeout, Integer)
|
|
78
|
+
@open_timeout = T.let(open_timeout, Integer)
|
|
79
|
+
@to_json = T.let(to_json, T.nilable(T.proc.params(arg0: T.untyped).returns(String)))
|
|
80
|
+
@from_json = T.let(from_json, T.nilable(T.proc.params(arg0: String, arg1: T.untyped).returns(T.untyped)))
|
|
81
|
+
@request_id = T.let(0, Integer)
|
|
82
|
+
@uri = T.let(URI.parse(url), URI::Generic)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Sends a single JSON-RPC request and returns the parsed `result`.
|
|
86
|
+
# Raises `RpcError` on a JSON-RPC error, `HttpTransportError` on HTTP failure.
|
|
87
|
+
sig { params(method: String, params: T::Array[T.untyped]).returns(T.untyped) }
|
|
88
|
+
def request(method, params = [])
|
|
89
|
+
payload = build_payload(method, params)
|
|
90
|
+
body = @to_json ? @to_json.call(payload) : JSON.generate(payload)
|
|
91
|
+
|
|
92
|
+
response = post(body)
|
|
93
|
+
|
|
94
|
+
parsed = @from_json ? @from_json.call(T.must(response.body), payload) : JSON.parse(T.must(response.body))
|
|
95
|
+
|
|
96
|
+
if parsed.key?('error')
|
|
97
|
+
err = parsed['error']
|
|
98
|
+
raise RpcError.new(err['code'].to_i, err['message'].to_s, err['data'])
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
parsed['result']
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
sig { params(method: String, params: T::Array[T.untyped]).returns(T::Hash[String, T.untyped]) }
|
|
107
|
+
def build_payload(method, params)
|
|
108
|
+
@request_id += 1
|
|
109
|
+
{ 'jsonrpc' => '2.0', 'id' => @request_id, 'method' => method, 'params' => params }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
sig { params(body: String).returns(Net::HTTPResponse) }
|
|
113
|
+
def post(body)
|
|
114
|
+
http = Net::HTTP.new(@uri.host, @uri.port)
|
|
115
|
+
http.use_ssl = (@uri.scheme == 'https')
|
|
116
|
+
http.read_timeout = @timeout
|
|
117
|
+
http.open_timeout = @open_timeout
|
|
118
|
+
|
|
119
|
+
req = Net::HTTP::Post.new(T.cast(@uri, URI::HTTP).request_uri)
|
|
120
|
+
req['Content-Type'] = 'application/json; charset=utf-8'
|
|
121
|
+
req['Accept'] = 'application/json'
|
|
122
|
+
req['Content-Length'] = body.bytesize.to_s
|
|
123
|
+
req['solana-client'] = 'ruby-kit'
|
|
124
|
+
@headers.each { |k, v| req[k] = v }
|
|
125
|
+
req.body = body
|
|
126
|
+
|
|
127
|
+
response = http.request(req)
|
|
128
|
+
|
|
129
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
130
|
+
raise HttpTransportError.new(response.code.to_i, response.message)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
response
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# RPC package — mirrors @solana/rpc + @solana/rpc-transport-http.
|
|
5
|
+
# Provides a synchronous JSON-RPC client for the Solana network.
|
|
6
|
+
require_relative 'rpc_types'
|
|
7
|
+
require_relative 'rpc/transport'
|
|
8
|
+
require_relative 'rpc/client'
|
|
9
|
+
|
|
10
|
+
module Solana::Ruby::Kit
|
|
11
|
+
module Rpc
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module RpcParsedTypes
|
|
6
|
+
extend T::Sig
|
|
7
|
+
AddressLookupTableData = T.let(
|
|
8
|
+
Struct.new(:deactivation_slot, :last_extended_slot, :addresses, keyword_init: true),
|
|
9
|
+
T.untyped
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
ParsedAddressLookupTable = T.let(
|
|
13
|
+
Struct.new(:program, :parsed, :space, keyword_init: true),
|
|
14
|
+
T.untyped
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
sig { params(raw: T::Hash[String, T.untyped]).returns(T.untyped) }
|
|
20
|
+
def parse_address_lookup_table(raw)
|
|
21
|
+
info = raw.dig('parsed', 'info') || {}
|
|
22
|
+
ParsedAddressLookupTable.new(
|
|
23
|
+
program: raw['program'],
|
|
24
|
+
space: raw['space'],
|
|
25
|
+
parsed: AddressLookupTableData.new(
|
|
26
|
+
deactivation_slot: info['deactivationSlot'] ? Kernel.Integer(info['deactivationSlot']) : nil,
|
|
27
|
+
last_extended_slot: info['lastExtendedSlot'] ? Kernel.Integer(info['lastExtendedSlot']) : nil,
|
|
28
|
+
addresses: Kernel.Array(info['addresses'])
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module RpcParsedTypes
|
|
6
|
+
extend T::Sig
|
|
7
|
+
NonceAccountData = T.let(
|
|
8
|
+
Struct.new(:authority, :blockhash, :fee_calculator, keyword_init: true),
|
|
9
|
+
T.untyped
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
ParsedNonceAccount = T.let(
|
|
13
|
+
Struct.new(:program, :parsed, :space, keyword_init: true),
|
|
14
|
+
T.untyped
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
sig { params(raw: T::Hash[String, T.untyped]).returns(T.untyped) }
|
|
20
|
+
def parse_nonce_account(raw)
|
|
21
|
+
info = raw.dig('parsed', 'info') || {}
|
|
22
|
+
ParsedNonceAccount.new(
|
|
23
|
+
program: raw['program'],
|
|
24
|
+
space: raw['space'],
|
|
25
|
+
parsed: NonceAccountData.new(
|
|
26
|
+
authority: info['authority'],
|
|
27
|
+
blockhash: info['blockhash'],
|
|
28
|
+
fee_calculator: info['feeCalculator']
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module RpcParsedTypes
|
|
6
|
+
extend T::Sig
|
|
7
|
+
StakeDelegation = T.let(
|
|
8
|
+
Struct.new(:stake, :voter, :activation_epoch, :deactivation_epoch,
|
|
9
|
+
:warmup_cooldown_rate, keyword_init: true),
|
|
10
|
+
T.untyped
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
StakeAccountData = T.let(
|
|
14
|
+
Struct.new(:type, :stake, :meta, keyword_init: true),
|
|
15
|
+
T.untyped
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
ParsedStakeAccount = T.let(
|
|
19
|
+
Struct.new(:program, :parsed, :space, keyword_init: true),
|
|
20
|
+
T.untyped
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
sig { params(raw: T::Hash[String, T.untyped]).returns(T.untyped) }
|
|
26
|
+
def parse_stake_account(raw)
|
|
27
|
+
parsed = raw['parsed'] || {}
|
|
28
|
+
info = parsed['info'] || {}
|
|
29
|
+
stake = info['stake'] || {}
|
|
30
|
+
deleg = stake.dig('delegation') || {}
|
|
31
|
+
|
|
32
|
+
delegation = StakeDelegation.new(
|
|
33
|
+
stake: deleg['stake'] ? Kernel.Integer(deleg['stake']) : nil,
|
|
34
|
+
voter: deleg['voter'],
|
|
35
|
+
activation_epoch: deleg['activationEpoch'] ? Kernel.Integer(deleg['activationEpoch']) : nil,
|
|
36
|
+
deactivation_epoch: deleg['deactivationEpoch'] ? Kernel.Integer(deleg['deactivationEpoch']) : nil,
|
|
37
|
+
warmup_cooldown_rate: deleg['warmupCooldownRate']&.to_f
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
ParsedStakeAccount.new(
|
|
41
|
+
program: raw['program'],
|
|
42
|
+
space: raw['space'],
|
|
43
|
+
parsed: StakeAccountData.new(
|
|
44
|
+
type: parsed['type'],
|
|
45
|
+
stake: delegation,
|
|
46
|
+
meta: info['meta']
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module RpcParsedTypes
|
|
6
|
+
extend T::Sig
|
|
7
|
+
# Parsed SPL Token amount.
|
|
8
|
+
TokenAmount = T.let(
|
|
9
|
+
Struct.new(:amount, :decimals, :ui_amount, :ui_amount_string, keyword_init: true),
|
|
10
|
+
T.untyped
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
# Parsed SPL Token account data (inside jsonParsed account info).
|
|
14
|
+
TokenAccountInfo = T.let(
|
|
15
|
+
Struct.new(:is_native, :mint, :owner, :state, :token_amount, keyword_init: true),
|
|
16
|
+
T.untyped
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Full parsed token account wrapper.
|
|
20
|
+
ParsedTokenAccount = T.let(
|
|
21
|
+
Struct.new(:program, :parsed, :space, keyword_init: true),
|
|
22
|
+
T.untyped
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
sig { params(raw: T::Hash[String, T.untyped]).returns(T.untyped) }
|
|
28
|
+
def parse_token_account(raw)
|
|
29
|
+
info_raw = raw.dig('parsed', 'info') || {}
|
|
30
|
+
ta_raw = info_raw['tokenAmount'] || {}
|
|
31
|
+
|
|
32
|
+
info = TokenAccountInfo.new(
|
|
33
|
+
is_native: info_raw['isNative'],
|
|
34
|
+
mint: info_raw['mint'],
|
|
35
|
+
owner: info_raw['owner'],
|
|
36
|
+
state: info_raw['state'],
|
|
37
|
+
token_amount: TokenAmount.new(
|
|
38
|
+
amount: ta_raw['amount'],
|
|
39
|
+
decimals: Kernel.Integer(ta_raw.fetch('decimals', 0)),
|
|
40
|
+
ui_amount: ta_raw['uiAmount'],
|
|
41
|
+
ui_amount_string: ta_raw['uiAmountString']
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
ParsedTokenAccount.new(
|
|
46
|
+
program: raw['program'],
|
|
47
|
+
parsed: info,
|
|
48
|
+
space: raw['space']
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module RpcParsedTypes
|
|
6
|
+
extend T::Sig
|
|
7
|
+
VoteState = T.let(
|
|
8
|
+
Struct.new(:node, :authorized_voter, :authorized_withdrawer, :commission,
|
|
9
|
+
:votes, :root_slot, :epoch_credits, keyword_init: true),
|
|
10
|
+
T.untyped
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
ParsedVoteAccount = T.let(
|
|
14
|
+
Struct.new(:program, :parsed, :space, keyword_init: true),
|
|
15
|
+
T.untyped
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
sig { params(raw: T::Hash[String, T.untyped]).returns(T.untyped) }
|
|
21
|
+
def parse_vote_account(raw)
|
|
22
|
+
info = raw.dig('parsed', 'info') || {}
|
|
23
|
+
ParsedVoteAccount.new(
|
|
24
|
+
program: raw['program'],
|
|
25
|
+
space: raw['space'],
|
|
26
|
+
parsed: VoteState.new(
|
|
27
|
+
node: info['nodePubkey'],
|
|
28
|
+
authorized_voter: info['authorizedVoters'],
|
|
29
|
+
authorized_withdrawer: info['authorizedWithdrawer'],
|
|
30
|
+
commission: info['commission'] ? Kernel.Integer(info['commission']) : nil,
|
|
31
|
+
votes: info['votes'],
|
|
32
|
+
root_slot: info['rootSlot'] ? Kernel.Integer(info['rootSlot']) : nil,
|
|
33
|
+
epoch_credits: info['epochCredits']
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Parsed RPC response types — mirrors @solana/rpc-parsed-types.
|
|
5
|
+
# These structs are populated when using encoding: 'jsonParsed' with
|
|
6
|
+
# RPC methods like getAccountInfo / getMultipleAccounts.
|
|
7
|
+
require_relative 'rpc_parsed_types/token_account'
|
|
8
|
+
require_relative 'rpc_parsed_types/nonce_account'
|
|
9
|
+
require_relative 'rpc_parsed_types/stake_account'
|
|
10
|
+
require_relative 'rpc_parsed_types/vote_account'
|
|
11
|
+
require_relative 'rpc_parsed_types/address_lookup_table'
|
|
12
|
+
|
|
13
|
+
module Solana::Ruby::Kit
|
|
14
|
+
module RpcParsedTypes
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module RpcSubscriptions
|
|
6
|
+
module Api
|
|
7
|
+
# Subscribe to account change notifications.
|
|
8
|
+
# Mirrors TypeScript's AccountNotificationsApi.accountSubscribe.
|
|
9
|
+
module AccountNotifications
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig do
|
|
13
|
+
params(
|
|
14
|
+
pubkey: String,
|
|
15
|
+
commitment: T.nilable(Symbol),
|
|
16
|
+
encoding: String
|
|
17
|
+
).returns(Subscription)
|
|
18
|
+
end
|
|
19
|
+
def account_subscribe(pubkey, commitment: nil, encoding: 'base64')
|
|
20
|
+
config = { 'encoding' => encoding }
|
|
21
|
+
config['commitment'] = commitment.to_s if commitment
|
|
22
|
+
|
|
23
|
+
sub_id = transport.request('accountSubscribe', [pubkey, config])
|
|
24
|
+
_build_subscription(sub_id, 'accountUnsubscribe')
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module RpcSubscriptions
|
|
6
|
+
module Api
|
|
7
|
+
# Subscribe to transaction log notifications.
|
|
8
|
+
# +filter+ may be 'all', 'allWithVotes', or { 'mentions' => [pubkey] }.
|
|
9
|
+
module LogsNotifications
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig do
|
|
13
|
+
params(
|
|
14
|
+
filter: T.untyped,
|
|
15
|
+
commitment: T.nilable(Symbol)
|
|
16
|
+
).returns(Subscription)
|
|
17
|
+
end
|
|
18
|
+
def logs_subscribe(filter = 'all', commitment: nil)
|
|
19
|
+
config = {}
|
|
20
|
+
config['commitment'] = commitment.to_s if commitment
|
|
21
|
+
|
|
22
|
+
sub_id = transport.request('logsSubscribe', config.empty? ? [filter] : [filter, config])
|
|
23
|
+
_build_subscription(sub_id, 'logsUnsubscribe')
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module RpcSubscriptions
|
|
6
|
+
module Api
|
|
7
|
+
# Subscribe to program account change notifications.
|
|
8
|
+
module ProgramNotifications
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
sig do
|
|
12
|
+
params(
|
|
13
|
+
program_id: String,
|
|
14
|
+
commitment: T.nilable(Symbol),
|
|
15
|
+
encoding: String,
|
|
16
|
+
filters: T::Array[T::Hash[String, T.untyped]]
|
|
17
|
+
).returns(Subscription)
|
|
18
|
+
end
|
|
19
|
+
def program_subscribe(program_id, commitment: nil, encoding: 'base64', filters: [])
|
|
20
|
+
config = { 'encoding' => encoding }
|
|
21
|
+
config['commitment'] = commitment.to_s if commitment
|
|
22
|
+
config['filters'] = filters unless filters.empty?
|
|
23
|
+
|
|
24
|
+
sub_id = transport.request('programSubscribe', [program_id, config])
|
|
25
|
+
_build_subscription(sub_id, 'programUnsubscribe')
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module RpcSubscriptions
|
|
6
|
+
module Api
|
|
7
|
+
# Subscribe to root change notifications.
|
|
8
|
+
module RootNotifications
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
sig { returns(Subscription) }
|
|
12
|
+
def root_subscribe
|
|
13
|
+
sub_id = transport.request('rootSubscribe', [])
|
|
14
|
+
_build_subscription(sub_id, 'rootUnsubscribe')
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module RpcSubscriptions
|
|
6
|
+
module Api
|
|
7
|
+
# Subscribe to signature status notifications.
|
|
8
|
+
# The subscription auto-unsubscribes once the signature is confirmed.
|
|
9
|
+
module SignatureNotifications
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig do
|
|
13
|
+
params(
|
|
14
|
+
signature: String,
|
|
15
|
+
commitment: T.nilable(Symbol)
|
|
16
|
+
).returns(Subscription)
|
|
17
|
+
end
|
|
18
|
+
def signature_subscribe(signature, commitment: nil)
|
|
19
|
+
config = {}
|
|
20
|
+
config['commitment'] = commitment.to_s if commitment
|
|
21
|
+
|
|
22
|
+
sub_id = transport.request('signatureSubscribe', config.empty? ? [signature] : [signature, config])
|
|
23
|
+
_build_subscription(sub_id, 'signatureUnsubscribe')
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module RpcSubscriptions
|
|
6
|
+
module Api
|
|
7
|
+
# Subscribe to slot change notifications.
|
|
8
|
+
module SlotNotifications
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
sig { returns(Subscription) }
|
|
12
|
+
def slot_subscribe
|
|
13
|
+
sub_id = transport.request('slotSubscribe', [])
|
|
14
|
+
_build_subscription(sub_id, 'slotUnsubscribe')
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module RpcSubscriptions
|
|
6
|
+
# Sends a WebSocket ping on a fixed interval to keep the connection alive.
|
|
7
|
+
# Mirrors @solana/rpc-subscriptions autopinger.
|
|
8
|
+
class Autopinger
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
sig { params(transport: Transport, interval: Float).void }
|
|
12
|
+
def initialize(transport, interval: 5.0)
|
|
13
|
+
@transport = transport
|
|
14
|
+
@interval = interval
|
|
15
|
+
@thread = T.let(nil, T.nilable(Thread))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
sig { void }
|
|
19
|
+
def start
|
|
20
|
+
@thread = Thread.new do
|
|
21
|
+
loop do
|
|
22
|
+
sleep(@interval)
|
|
23
|
+
break if @transport.publisher.closed?
|
|
24
|
+
|
|
25
|
+
begin
|
|
26
|
+
@transport.instance_variable_get(:@ws)&.send(nil, type: :ping)
|
|
27
|
+
rescue StandardError
|
|
28
|
+
break
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
@thread.abort_on_exception = false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
sig { void }
|
|
36
|
+
def stop
|
|
37
|
+
@thread&.kill
|
|
38
|
+
@thread = nil
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative 'transport'
|
|
5
|
+
require_relative 'subscription'
|
|
6
|
+
require_relative 'autopinger'
|
|
7
|
+
require_relative 'api/account_notifications'
|
|
8
|
+
require_relative 'api/logs_notifications'
|
|
9
|
+
require_relative 'api/program_notifications'
|
|
10
|
+
require_relative 'api/root_notifications'
|
|
11
|
+
require_relative 'api/signature_notifications'
|
|
12
|
+
require_relative 'api/slot_notifications'
|
|
13
|
+
|
|
14
|
+
module Solana::Ruby::Kit
|
|
15
|
+
module RpcSubscriptions
|
|
16
|
+
# WebSocket subscription client for Solana.
|
|
17
|
+
# Mirrors TypeScript's createSolanaRpcSubscriptions(url) factory.
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
# subs = Solana::Ruby::Kit::RpcSubscriptions::Client.new('wss://api.devnet.solana.com')
|
|
21
|
+
# sub = subs.slot_subscribe
|
|
22
|
+
# sub.take(3).each { |n| puts n.inspect }
|
|
23
|
+
class Client
|
|
24
|
+
extend T::Sig
|
|
25
|
+
|
|
26
|
+
include Api::AccountNotifications
|
|
27
|
+
include Api::LogsNotifications
|
|
28
|
+
include Api::ProgramNotifications
|
|
29
|
+
include Api::RootNotifications
|
|
30
|
+
include Api::SignatureNotifications
|
|
31
|
+
include Api::SlotNotifications
|
|
32
|
+
|
|
33
|
+
sig { returns(Transport) }
|
|
34
|
+
attr_reader :transport
|
|
35
|
+
|
|
36
|
+
sig do
|
|
37
|
+
params(
|
|
38
|
+
url: String,
|
|
39
|
+
headers: T::Hash[String, String],
|
|
40
|
+
ping_interval: Float
|
|
41
|
+
).void
|
|
42
|
+
end
|
|
43
|
+
def initialize(url, headers: {}, ping_interval: 5.0)
|
|
44
|
+
@transport = T.let(Transport.new(url: url, headers: headers), Transport)
|
|
45
|
+
@pinger = T.let(Autopinger.new(@transport, interval: ping_interval), Autopinger)
|
|
46
|
+
@pinger.start
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
sig { void }
|
|
50
|
+
def close
|
|
51
|
+
@pinger.stop
|
|
52
|
+
@transport.close
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# Build a Subscription wrapping an AsyncIterable enumerator and
|
|
58
|
+
# an auto-unsubscribe on close.
|
|
59
|
+
sig { params(sub_id: T.untyped, unsub_method: String).returns(Subscription) }
|
|
60
|
+
def _build_subscription(sub_id, unsub_method)
|
|
61
|
+
publisher = @transport.publisher
|
|
62
|
+
enumerator = Subscribable::AsyncIterable.from_publisher(
|
|
63
|
+
publisher,
|
|
64
|
+
data_channel: sub_id,
|
|
65
|
+
error_channel: Subscribable::DataPublisher::ERROR_CHANNEL
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
unsubscribe = lambda do
|
|
69
|
+
begin
|
|
70
|
+
@transport.request(unsub_method, [sub_id])
|
|
71
|
+
rescue StandardError
|
|
72
|
+
nil # Best-effort: transport may already be closed
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
Subscription.new(enumerator: enumerator, unsubscribe: unsubscribe)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|