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.
Files changed (109) hide show
  1. checksums.yaml +7 -0
  2. data/lib/core_extensions/tapioca/name_patch.rb +32 -0
  3. data/lib/core_extensions/tapioca/required_ancestors.rb +13 -0
  4. data/lib/generators/solana/ruby/kit/install/install_generator.rb +17 -0
  5. data/lib/generators/solana/ruby/kit/install/templates/solana_ruby_kit.rb.tt +8 -0
  6. data/lib/solana/ruby/kit/accounts/account.rb +47 -0
  7. data/lib/solana/ruby/kit/accounts/maybe_account.rb +86 -0
  8. data/lib/solana/ruby/kit/accounts.rb +6 -0
  9. data/lib/solana/ruby/kit/addresses/address.rb +133 -0
  10. data/lib/solana/ruby/kit/addresses/curve.rb +112 -0
  11. data/lib/solana/ruby/kit/addresses/program_derived_address.rb +155 -0
  12. data/lib/solana/ruby/kit/addresses/public_key.rb +39 -0
  13. data/lib/solana/ruby/kit/addresses.rb +11 -0
  14. data/lib/solana/ruby/kit/codecs/bytes.rb +58 -0
  15. data/lib/solana/ruby/kit/codecs/codec.rb +135 -0
  16. data/lib/solana/ruby/kit/codecs/data_structures.rb +177 -0
  17. data/lib/solana/ruby/kit/codecs/decoder.rb +43 -0
  18. data/lib/solana/ruby/kit/codecs/encoder.rb +52 -0
  19. data/lib/solana/ruby/kit/codecs/numbers.rb +217 -0
  20. data/lib/solana/ruby/kit/codecs/strings.rb +116 -0
  21. data/lib/solana/ruby/kit/codecs.rb +25 -0
  22. data/lib/solana/ruby/kit/configuration.rb +48 -0
  23. data/lib/solana/ruby/kit/encoding/base58.rb +62 -0
  24. data/lib/solana/ruby/kit/errors.rb +226 -0
  25. data/lib/solana/ruby/kit/fast_stable_stringify.rb +62 -0
  26. data/lib/solana/ruby/kit/functional.rb +29 -0
  27. data/lib/solana/ruby/kit/instruction_plans/plans.rb +27 -0
  28. data/lib/solana/ruby/kit/instruction_plans.rb +47 -0
  29. data/lib/solana/ruby/kit/instructions/accounts.rb +80 -0
  30. data/lib/solana/ruby/kit/instructions/instruction.rb +71 -0
  31. data/lib/solana/ruby/kit/instructions/roles.rb +84 -0
  32. data/lib/solana/ruby/kit/instructions.rb +7 -0
  33. data/lib/solana/ruby/kit/keys/key_pair.rb +84 -0
  34. data/lib/solana/ruby/kit/keys/private_key.rb +39 -0
  35. data/lib/solana/ruby/kit/keys/public_key.rb +31 -0
  36. data/lib/solana/ruby/kit/keys/signatures.rb +171 -0
  37. data/lib/solana/ruby/kit/keys.rb +11 -0
  38. data/lib/solana/ruby/kit/offchain_messages/codec.rb +107 -0
  39. data/lib/solana/ruby/kit/offchain_messages/message.rb +22 -0
  40. data/lib/solana/ruby/kit/offchain_messages.rb +16 -0
  41. data/lib/solana/ruby/kit/options/option.rb +132 -0
  42. data/lib/solana/ruby/kit/options.rb +5 -0
  43. data/lib/solana/ruby/kit/plugin_core.rb +58 -0
  44. data/lib/solana/ruby/kit/programs.rb +42 -0
  45. data/lib/solana/ruby/kit/promises.rb +85 -0
  46. data/lib/solana/ruby/kit/railtie.rb +18 -0
  47. data/lib/solana/ruby/kit/rpc/api/get_account_info.rb +76 -0
  48. data/lib/solana/ruby/kit/rpc/api/get_balance.rb +41 -0
  49. data/lib/solana/ruby/kit/rpc/api/get_block_height.rb +29 -0
  50. data/lib/solana/ruby/kit/rpc/api/get_epoch_info.rb +47 -0
  51. data/lib/solana/ruby/kit/rpc/api/get_latest_blockhash.rb +52 -0
  52. data/lib/solana/ruby/kit/rpc/api/get_minimum_balance_for_rent_exemption.rb +29 -0
  53. data/lib/solana/ruby/kit/rpc/api/get_multiple_accounts.rb +56 -0
  54. data/lib/solana/ruby/kit/rpc/api/get_program_accounts.rb +60 -0
  55. data/lib/solana/ruby/kit/rpc/api/get_signature_statuses.rb +56 -0
  56. data/lib/solana/ruby/kit/rpc/api/get_slot.rb +30 -0
  57. data/lib/solana/ruby/kit/rpc/api/get_token_account_balance.rb +38 -0
  58. data/lib/solana/ruby/kit/rpc/api/get_token_accounts_by_owner.rb +48 -0
  59. data/lib/solana/ruby/kit/rpc/api/get_transaction.rb +36 -0
  60. data/lib/solana/ruby/kit/rpc/api/get_vote_accounts.rb +62 -0
  61. data/lib/solana/ruby/kit/rpc/api/is_blockhash_valid.rb +41 -0
  62. data/lib/solana/ruby/kit/rpc/api/request_airdrop.rb +35 -0
  63. data/lib/solana/ruby/kit/rpc/api/send_transaction.rb +61 -0
  64. data/lib/solana/ruby/kit/rpc/api/simulate_transaction.rb +47 -0
  65. data/lib/solana/ruby/kit/rpc/client.rb +83 -0
  66. data/lib/solana/ruby/kit/rpc/transport.rb +137 -0
  67. data/lib/solana/ruby/kit/rpc.rb +13 -0
  68. data/lib/solana/ruby/kit/rpc_parsed_types/address_lookup_table.rb +33 -0
  69. data/lib/solana/ruby/kit/rpc_parsed_types/nonce_account.rb +33 -0
  70. data/lib/solana/ruby/kit/rpc_parsed_types/stake_account.rb +51 -0
  71. data/lib/solana/ruby/kit/rpc_parsed_types/token_account.rb +52 -0
  72. data/lib/solana/ruby/kit/rpc_parsed_types/vote_account.rb +38 -0
  73. data/lib/solana/ruby/kit/rpc_parsed_types.rb +16 -0
  74. data/lib/solana/ruby/kit/rpc_subscriptions/api/account_notifications.rb +29 -0
  75. data/lib/solana/ruby/kit/rpc_subscriptions/api/logs_notifications.rb +28 -0
  76. data/lib/solana/ruby/kit/rpc_subscriptions/api/program_notifications.rb +30 -0
  77. data/lib/solana/ruby/kit/rpc_subscriptions/api/root_notifications.rb +19 -0
  78. data/lib/solana/ruby/kit/rpc_subscriptions/api/signature_notifications.rb +28 -0
  79. data/lib/solana/ruby/kit/rpc_subscriptions/api/slot_notifications.rb +19 -0
  80. data/lib/solana/ruby/kit/rpc_subscriptions/autopinger.rb +42 -0
  81. data/lib/solana/ruby/kit/rpc_subscriptions/client.rb +80 -0
  82. data/lib/solana/ruby/kit/rpc_subscriptions/subscription.rb +58 -0
  83. data/lib/solana/ruby/kit/rpc_subscriptions/transport.rb +163 -0
  84. data/lib/solana/ruby/kit/rpc_subscriptions.rb +12 -0
  85. data/lib/solana/ruby/kit/rpc_types/account_info.rb +53 -0
  86. data/lib/solana/ruby/kit/rpc_types/cluster_url.rb +56 -0
  87. data/lib/solana/ruby/kit/rpc_types/commitment.rb +52 -0
  88. data/lib/solana/ruby/kit/rpc_types/lamports.rb +43 -0
  89. data/lib/solana/ruby/kit/rpc_types.rb +8 -0
  90. data/lib/solana/ruby/kit/signers/keypair_signer.rb +126 -0
  91. data/lib/solana/ruby/kit/signers.rb +5 -0
  92. data/lib/solana/ruby/kit/subscribable/async_iterable.rb +80 -0
  93. data/lib/solana/ruby/kit/subscribable/data_publisher.rb +90 -0
  94. data/lib/solana/ruby/kit/subscribable.rb +13 -0
  95. data/lib/solana/ruby/kit/sysvars/addresses.rb +19 -0
  96. data/lib/solana/ruby/kit/sysvars/clock.rb +37 -0
  97. data/lib/solana/ruby/kit/sysvars/epoch_schedule.rb +34 -0
  98. data/lib/solana/ruby/kit/sysvars/last_restart_slot.rb +22 -0
  99. data/lib/solana/ruby/kit/sysvars/rent.rb +29 -0
  100. data/lib/solana/ruby/kit/sysvars.rb +33 -0
  101. data/lib/solana/ruby/kit/transaction_confirmation.rb +159 -0
  102. data/lib/solana/ruby/kit/transaction_messages/transaction_message.rb +168 -0
  103. data/lib/solana/ruby/kit/transaction_messages.rb +5 -0
  104. data/lib/solana/ruby/kit/transactions/transaction.rb +135 -0
  105. data/lib/solana/ruby/kit/transactions.rb +5 -0
  106. data/lib/solana/ruby/kit/version.rb +10 -0
  107. data/lib/solana/ruby/kit.rb +100 -0
  108. data/solana-ruby-kit.gemspec +29 -0
  109. 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