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,58 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Solana::Ruby::Kit
5
+ module RpcSubscriptions
6
+ # A live subscription to a Solana WebSocket notification channel.
7
+ # Wraps a DataPublisher channel and exposes an Enumerator interface.
8
+ #
9
+ # Typical usage:
10
+ # sub = client.slot_subscribe
11
+ # sub.take(10).each { |notification| puts notification }
12
+ # sub.unsubscribe # or break from the enumerator loop
13
+ class Subscription
14
+ extend T::Sig
15
+ extend T::Generic
16
+
17
+ Elem = type_member { { fixed: T.untyped } }
18
+
19
+ sig { returns(T::Enumerator[T.untyped]) }
20
+ attr_reader :enumerator
21
+
22
+ sig do
23
+ params(
24
+ enumerator: T::Enumerator[T.untyped],
25
+ unsubscribe: T.proc.void,
26
+ timeout: T.nilable(Float)
27
+ ).void
28
+ end
29
+ def initialize(enumerator:, unsubscribe:, timeout: nil)
30
+ @enumerator = enumerator
31
+ @unsubscribe = unsubscribe
32
+ @timeout = timeout
33
+ end
34
+
35
+ # Stop the subscription and clean up.
36
+ sig { void }
37
+ def unsubscribe
38
+ @unsubscribe.call
39
+ end
40
+
41
+ # Delegate Enumerable methods to the enumerator for convenience.
42
+ sig { override.params(block: T.proc.params(item: T.untyped).void).void }
43
+ def each(&block)
44
+ @enumerator.each(&block)
45
+ ensure
46
+ unsubscribe
47
+ end
48
+
49
+ sig { params(n: Integer).returns(T::Array[T.untyped]) }
50
+ def take(n) = @enumerator.take(n)
51
+
52
+ sig { returns(T.untyped) }
53
+ def next = @enumerator.next
54
+
55
+ include Enumerable
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,163 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'websocket-client-simple'
5
+ require 'json'
6
+ require 'thread'
7
+
8
+ module Solana::Ruby::Kit
9
+ module RpcSubscriptions
10
+ # WebSocket JSON-RPC transport for Solana subscription methods.
11
+ # Runs in a background thread; dispatches incoming messages to a
12
+ # DataPublisher keyed by subscription ID.
13
+ #
14
+ # Thread safety: all public methods are Mutex-protected.
15
+ class Transport
16
+ extend T::Sig
17
+
18
+ # Channel name used for raw incoming messages (before routing to sub-ID).
19
+ MESSAGE_CHANNEL = T.let(:__message__, Symbol)
20
+ ERROR_CHANNEL = T.let(:error, Symbol)
21
+ CLOSE_CHANNEL = T.let(:close, Symbol)
22
+
23
+ sig { returns(String) }
24
+ attr_reader :url
25
+
26
+ sig do
27
+ params(
28
+ url: String,
29
+ headers: T::Hash[String, String],
30
+ send_buffer_high_watermark: Integer
31
+ ).void
32
+ end
33
+ def initialize(url:, headers: {}, send_buffer_high_watermark: 40)
34
+ @url = url
35
+ @headers = headers
36
+ @hwm = send_buffer_high_watermark
37
+ @mutex = T.let(Mutex.new, Mutex)
38
+ @id_seq = T.let(0, Integer)
39
+ @pending = T.let({}, T::Hash[Integer, Queue]) # request id → response Queue
40
+ @subscribers = T.let(
41
+ Hash.new { |h, k| h[k] = [] },
42
+ T::Hash[T.untyped, T::Array[T.proc.params(msg: T::Hash[String, T.untyped]).void]]
43
+ )
44
+ @send_buffer = T.let([], T::Array[String])
45
+ @ws = T.let(nil, T.nilable(WebSocket::Client::Simple::Client))
46
+ @connected = T.let(false, T::Boolean)
47
+ @publisher = T.let(Subscribable::DataPublisher.new, Subscribable::DataPublisher)
48
+
49
+ _connect
50
+ end
51
+
52
+ # Send a JSON-RPC request and return the result synchronously (blocks).
53
+ sig do
54
+ params(method: String, params: T::Array[T.untyped])
55
+ .returns(T.untyped)
56
+ end
57
+ def request(method, params = [])
58
+ id = _next_id
59
+ q = Queue.new
60
+ @mutex.synchronize { @pending[id] = q }
61
+
62
+ payload = JSON.generate({ 'jsonrpc' => '2.0', 'id' => id, 'method' => method, 'params' => params })
63
+ _send(payload)
64
+
65
+ response = q.pop
66
+ raise Rpc::RpcError.new(response['error']['message'], response['error']['code']) if response['error']
67
+
68
+ response['result']
69
+ end
70
+
71
+ # Subscribe to a channel (subscription ID from the server).
72
+ # Returns an unsubscribe lambda.
73
+ sig do
74
+ params(
75
+ sub_id: T.untyped,
76
+ block: T.proc.params(msg: T::Hash[String, T.untyped]).void
77
+ ).returns(T.proc.void)
78
+ end
79
+ def subscribe(sub_id, &block)
80
+ @mutex.synchronize { T.must(@subscribers[sub_id]) << block }
81
+ lambda { @mutex.synchronize { T.must(@subscribers[sub_id]).delete(block) } }
82
+ end
83
+
84
+ # Access the underlying DataPublisher for AsyncIterable integration.
85
+ sig { returns(Subscribable::DataPublisher) }
86
+ attr_reader :publisher
87
+
88
+ sig { void }
89
+ def close
90
+ @ws&.close
91
+ @publisher.close
92
+ end
93
+
94
+ private
95
+
96
+ sig { returns(Integer) }
97
+ def _next_id
98
+ @mutex.synchronize { @id_seq += 1; @id_seq }
99
+ end
100
+
101
+ sig { params(payload: String).void }
102
+ def _send(payload)
103
+ if @connected
104
+ @ws&.send(payload)
105
+ else
106
+ @send_buffer << payload
107
+ end
108
+ end
109
+
110
+ sig { void }
111
+ def _connect # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
112
+ transport = self
113
+ @ws = WebSocket::Client::Simple.connect(@url, headers: @headers)
114
+
115
+ ws = @ws
116
+ ws.on(:open) do
117
+ transport.instance_variable_set(:@connected, true)
118
+ transport.instance_variable_get(:@send_buffer).each { |m| ws.send(m) }
119
+ transport.instance_variable_get(:@send_buffer).clear
120
+ end
121
+
122
+ ws.on(:message) do |msg|
123
+ next unless msg.type == :text
124
+
125
+ begin
126
+ data = JSON.parse(msg.data)
127
+ rescue JSON::ParserError
128
+ next
129
+ end
130
+
131
+ if data.key?('id')
132
+ # Response to a request
133
+ id = data['id']
134
+ q = transport.instance_variable_get(:@mutex).synchronize do
135
+ transport.instance_variable_get(:@pending).delete(id)
136
+ end
137
+ q&.push(data)
138
+ elsif data.key?('method')
139
+ # Push notification (subscription event)
140
+ sub_id = data.dig('params', 'subscription')
141
+ subs = transport.instance_variable_get(:@mutex).synchronize do
142
+ (transport.instance_variable_get(:@subscribers)[sub_id] || []).dup
143
+ end
144
+ subs.each { |blk| blk.call(data) }
145
+ transport.instance_variable_get(:@publisher).publish(sub_id, data)
146
+ end
147
+ end
148
+
149
+ ws.on(:error) do |e|
150
+ transport.instance_variable_get(:@publisher).publish(
151
+ Subscribable::DataPublisher::ERROR_CHANNEL,
152
+ e.is_a?(StandardError) ? e : RuntimeError.new(e.to_s)
153
+ )
154
+ end
155
+
156
+ ws.on(:close) do
157
+ transport.instance_variable_set(:@connected, false)
158
+ transport.instance_variable_get(:@publisher).close
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,12 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # Mirrors @solana/rpc-subscriptions + @solana/rpc-subscriptions-api.
5
+ # Provides a WebSocket-based subscription client for Solana push notifications.
6
+ require_relative 'subscribable'
7
+ require_relative 'rpc_subscriptions/client'
8
+
9
+ module Solana::Ruby::Kit
10
+ module RpcSubscriptions
11
+ end
12
+ end
@@ -0,0 +1,53 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../addresses/address'
5
+
6
+ module Solana::Ruby::Kit
7
+ module RpcTypes
8
+ # Base fields present in every account-info response.
9
+ # Mirrors TypeScript's `AccountInfoBase`.
10
+ class AccountInfoBase < T::Struct
11
+ const :executable, T::Boolean
12
+ # Balance in lamports.
13
+ const :lamports, Integer
14
+ # Address of the program that owns this account.
15
+ const :owner, String # base58 address string
16
+ # Allocated storage in bytes (excludes the 128-byte header).
17
+ # TypeScript uses bigint; Ruby uses Integer.
18
+ const :space, Integer
19
+ end
20
+
21
+ # Account info with raw base64-encoded data (the most common encoding).
22
+ # Mirrors `AccountInfoWithBase64EncodedData`.
23
+ class AccountInfoWithBase64Data < T::Struct
24
+ const :executable, T::Boolean
25
+ const :lamports, Integer
26
+ const :owner, String
27
+ const :space, Integer
28
+ const :rent_epoch, Integer, default: 0
29
+ # Tuple: [base64_string, "base64"]
30
+ const :data, T::Array[String]
31
+ end
32
+
33
+ # Account info with JSON-parsed data (program-specific parsing on the RPC node).
34
+ # Mirrors `AccountInfoWithJsonData`.
35
+ class AccountInfoWithJsonData < T::Struct
36
+ const :executable, T::Boolean
37
+ const :lamports, Integer
38
+ const :owner, String
39
+ const :space, Integer
40
+ const :rent_epoch, Integer, default: 0
41
+ # Either a hash (parsed JSON) or a base64 fallback tuple.
42
+ const :data, T.untyped
43
+ end
44
+
45
+ # Slot + context wrapper returned by context-aware RPC methods.
46
+ # Mirrors TypeScript's `SolanaRpcResponse<T>`:
47
+ # { context: { slot: bigint }, value: T }
48
+ class RpcContextualValue < T::Struct
49
+ const :slot, Integer # the slot at which the data was read
50
+ const :value, T.untyped # the actual result (typed by the caller)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,56 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Solana::Ruby::Kit
5
+ module RpcTypes
6
+ extend T::Sig
7
+ # Well-known cluster endpoint URLs.
8
+ MAINNET_URL = T.let('https://api.mainnet-beta.solana.com', String)
9
+ DEVNET_URL = T.let('https://api.devnet.solana.com', String)
10
+ TESTNET_URL = T.let('https://api.testnet.solana.com', String)
11
+
12
+ # A cluster-tagged URL string.
13
+ # Mirrors TypeScript's branded string types:
14
+ # MainnetUrl, DevnetUrl, TestnetUrl, ClusterUrl
15
+ #
16
+ # In Ruby we carry the cluster tag as a symbol on a wrapper struct,
17
+ # since Ruby cannot brand primitive String values.
18
+ class ClusterUrl < T::Struct
19
+ extend T::Sig
20
+ const :url, String
21
+ const :cluster, T.nilable(Symbol) # :mainnet | :devnet | :testnet | nil
22
+
23
+ sig { returns(String) }
24
+ def to_s = @url
25
+ end
26
+
27
+ module_function
28
+
29
+ # Wraps a URL string and tags it as mainnet.
30
+ # Mirrors `mainnet(url)`.
31
+ sig { params(url: String).returns(ClusterUrl) }
32
+ def mainnet(url = MAINNET_URL)
33
+ ClusterUrl.new(url: url, cluster: :mainnet)
34
+ end
35
+
36
+ # Wraps a URL string and tags it as devnet.
37
+ # Mirrors `devnet(url)`.
38
+ sig { params(url: String).returns(ClusterUrl) }
39
+ def devnet(url = DEVNET_URL)
40
+ ClusterUrl.new(url: url, cluster: :devnet)
41
+ end
42
+
43
+ # Wraps a URL string and tags it as testnet.
44
+ # Mirrors `testnet(url)`.
45
+ sig { params(url: String).returns(ClusterUrl) }
46
+ def testnet(url = TESTNET_URL)
47
+ ClusterUrl.new(url: url, cluster: :testnet)
48
+ end
49
+
50
+ # Wraps a custom URL with no cluster tag.
51
+ sig { params(url: String).returns(ClusterUrl) }
52
+ def cluster_url(url)
53
+ ClusterUrl.new(url: url, cluster: nil)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,52 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Solana::Ruby::Kit
5
+ module RpcTypes
6
+ extend T::Sig
7
+ # Network confirmation level for an RPC request.
8
+ # Mirrors TypeScript's `Commitment = 'finalized' | 'confirmed' | 'processed'`.
9
+ #
10
+ # Each level is a measure of how many validators have confirmed a block:
11
+ # - :finalized — the block has been voted on by a supermajority and is permanent.
12
+ # - :confirmed — the block has been voted on by a supermajority (not yet rooted).
13
+ # - :processed — the node has seen the block but it may yet be rolled back.
14
+ Commitment = T.type_alias { Symbol }
15
+
16
+ FINALIZED = T.let(:finalized, Symbol)
17
+ CONFIRMED = T.let(:confirmed, Symbol)
18
+ PROCESSED = T.let(:processed, Symbol)
19
+
20
+ VALID_COMMITMENTS = T.let(
21
+ [FINALIZED, CONFIRMED, PROCESSED].freeze,
22
+ T::Array[Symbol]
23
+ )
24
+
25
+ module_function
26
+
27
+ # Returns a numeric score for a commitment level (higher = more confirmed).
28
+ # Mirrors `getCommitmentScore()`.
29
+ sig { params(commitment: Symbol).returns(Integer) }
30
+ def commitment_score(commitment)
31
+ case commitment
32
+ when FINALIZED then 2
33
+ when CONFIRMED then 1
34
+ when PROCESSED then 0
35
+ else Kernel.raise ArgumentError, "Unknown commitment: #{commitment.inspect}"
36
+ end
37
+ end
38
+
39
+ # Compares two commitment levels. Returns -1, 0, or 1.
40
+ # Mirrors `commitmentComparator()`.
41
+ sig { params(a: Symbol, b: Symbol).returns(Integer) }
42
+ def commitment_comparator(a, b)
43
+ commitment_score(a) <=> commitment_score(b)
44
+ end
45
+
46
+ # Returns true if the symbol is a valid commitment level.
47
+ sig { params(value: T.untyped).returns(T::Boolean) }
48
+ def commitment?(value)
49
+ VALID_COMMITMENTS.include?(value)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,43 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../errors'
5
+
6
+ module Solana::Ruby::Kit
7
+ module RpcTypes
8
+ extend T::Sig
9
+ # The smallest denomination of SOL (1 SOL = 1_000_000_000 lamports).
10
+ # Mirrors TypeScript's `Lamports` branded bigint.
11
+ # In Ruby, arbitrary-precision Integer replaces JS bigint with no loss of range.
12
+ #
13
+ # Valid range: 0 .. 18_446_744_073_709_551_615 (u64)
14
+ Lamports = T.type_alias { Integer }
15
+
16
+ LAMPORTS_U64_MAX = T.let(T.unsafe(2**64 - 1), Integer)
17
+
18
+ module_function
19
+
20
+ # Returns true if the integer is a valid u64 lamport value.
21
+ # Mirrors `isLamports()`.
22
+ sig { params(value: T.untyped).returns(T::Boolean) }
23
+ def lamports?(value)
24
+ !!(value.is_a?(Integer) && value >= 0 && value <= LAMPORTS_U64_MAX)
25
+ end
26
+
27
+ # Raises SolanaError if the value is not a valid lamport amount.
28
+ # Mirrors `assertIsLamports()`.
29
+ sig { params(value: T.untyped).void }
30
+ def assert_lamports!(value)
31
+ Kernel.raise SolanaError.new(:SOLANA_ERROR__LAMPORTS__AMOUNT_MUST_BE_POSITIVE) if value.is_a?(Integer) && value < 0
32
+ Kernel.raise SolanaError.new(:SOLANA_ERROR__LAMPORTS__AMOUNT_OUT_OF_RANGE) unless lamports?(value)
33
+ end
34
+
35
+ # Validates and returns the lamport value.
36
+ # Mirrors `lamports()`.
37
+ sig { params(value: Integer).returns(Integer) }
38
+ def lamports(value)
39
+ assert_lamports!(value)
40
+ value
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,8 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # Core RPC type definitions — mirrors @solana/rpc-types.
5
+ require_relative 'rpc_types/commitment'
6
+ require_relative 'rpc_types/lamports'
7
+ require_relative 'rpc_types/cluster_url'
8
+ require_relative 'rpc_types/account_info'
@@ -0,0 +1,126 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'rbnacl'
5
+ require_relative '../errors'
6
+ require_relative '../addresses/address'
7
+ require_relative '../keys/key_pair'
8
+ require_relative '../keys/signatures'
9
+ require_relative '../transactions/transaction'
10
+
11
+ module Solana::Ruby::Kit
12
+ module Signers
13
+ extend T::Sig
14
+ # A signer backed by an Ed25519 key pair, capable of signing both
15
+ # transaction messages and off-chain messages.
16
+ #
17
+ # Mirrors TypeScript's `KeyPairSigner<TAddress>`, which combines
18
+ # `MessagePartialSigner` and `TransactionPartialSigner` with a
19
+ # `CryptoKeyPair` reference.
20
+ #
21
+ # In Ruby we keep it simple: the signer holds an RbNaCl::SigningKey
22
+ # (which contains the private key seed and can derive the public key)
23
+ # and exposes the signer's address.
24
+ class KeyPairSigner
25
+ extend T::Sig
26
+
27
+ sig { returns(Addresses::Address) }
28
+ attr_reader :address
29
+
30
+ sig { returns(Keys::KeyPair) }
31
+ attr_reader :key_pair
32
+
33
+ sig { params(key_pair: Keys::KeyPair).void }
34
+ def initialize(key_pair)
35
+ @key_pair = T.let(key_pair, Keys::KeyPair)
36
+ @address = T.let(
37
+ Addresses::Address.new(Addresses.encode_address(key_pair.verify_key.to_bytes)),
38
+ Addresses::Address
39
+ )
40
+ end
41
+
42
+ sig { returns(String) }
43
+ def to_s = @address.to_s
44
+
45
+ sig { returns(String) }
46
+ def inspect = "#<KeyPairSigner address=#{@address}>"
47
+
48
+ # Signs raw bytes with the private key, returning a SignatureBytes.
49
+ # Used internally by sign_transaction and sign_message.
50
+ sig { params(data: String).returns(Keys::SignatureBytes) }
51
+ def sign(data)
52
+ Keys.sign_bytes(@key_pair.signing_key, data)
53
+ end
54
+
55
+ # Verifies a signature against data using this signer's public key.
56
+ sig { params(sig_bytes: Keys::SignatureBytes, data: String).returns(T::Boolean) }
57
+ def verify(sig_bytes, data)
58
+ Keys.verify_signature(@key_pair.verify_key, sig_bytes, data)
59
+ end
60
+ end
61
+
62
+ module_function
63
+
64
+ # Creates a KeyPairSigner from an existing Keys::KeyPair.
65
+ # Mirrors `createSignerFromKeyPair(keyPair)`.
66
+ sig { params(key_pair: Keys::KeyPair).returns(KeyPairSigner) }
67
+ def create_signer_from_key_pair(key_pair)
68
+ KeyPairSigner.new(key_pair)
69
+ end
70
+
71
+ # Generates a fresh random key pair and wraps it in a KeyPairSigner.
72
+ # Mirrors `generateKeyPairSigner()`.
73
+ sig { returns(KeyPairSigner) }
74
+ def generate_key_pair_signer
75
+ KeyPairSigner.new(Keys.generate_key_pair)
76
+ end
77
+
78
+ # Creates a KeyPairSigner from 64 raw bytes (seed || public key).
79
+ # Mirrors `createKeyPairSignerFromBytes(bytes)`.
80
+ sig { params(bytes: String).returns(KeyPairSigner) }
81
+ def create_key_pair_signer_from_bytes(bytes)
82
+ KeyPairSigner.new(Keys.create_key_pair_from_bytes(bytes))
83
+ end
84
+
85
+ # Creates a KeyPairSigner from a 32-byte private key seed.
86
+ # Mirrors `createKeyPairSignerFromPrivateKeyBytes(bytes)`.
87
+ sig { params(bytes: String).returns(KeyPairSigner) }
88
+ def create_key_pair_signer_from_private_key_bytes(bytes)
89
+ KeyPairSigner.new(Keys.create_key_pair_from_private_key_bytes(bytes))
90
+ end
91
+
92
+ # Returns true if the value is a KeyPairSigner.
93
+ # Mirrors `isKeyPairSigner(value)`.
94
+ sig { params(value: T.untyped).returns(T::Boolean) }
95
+ def key_pair_signer?(value)
96
+ value.is_a?(KeyPairSigner)
97
+ end
98
+
99
+ # Raises SolanaError if the value is not a KeyPairSigner.
100
+ # Mirrors `assertIsKeyPairSigner(value)`.
101
+ sig { params(value: T.untyped).void }
102
+ def assert_key_pair_signer!(value)
103
+ Kernel.raise SolanaError.new(:SOLANA_ERROR__SIGNER__EXPECTED_KEY_PAIR_SIGNER) unless key_pair_signer?(value)
104
+ end
105
+
106
+ # Signs a transaction message's bytes using all provided signers.
107
+ # Returns a hash mapping each signer's address to its SignatureBytes.
108
+ #
109
+ # This is the Ruby analogue of the TypeScript signers' sign-transaction
110
+ # workflow: each signer produces its Ed25519 signature of the compiled
111
+ # message bytes, and the signatures are collected into a map.
112
+ #
113
+ # Mirrors the combined behaviour of `TransactionPartialSigner.signTransactions`.
114
+ sig do
115
+ params(
116
+ signers: T::Array[KeyPairSigner],
117
+ message_bytes: String
118
+ ).returns(T::Hash[String, Keys::SignatureBytes])
119
+ end
120
+ def sign_message_bytes_with_signers(signers, message_bytes)
121
+ signers.each_with_object({}) do |signer, map|
122
+ map[signer.address.value] = signer.sign(message_bytes)
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,5 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # Signer abstractions — mirrors @solana/signers.
5
+ require_relative 'signers/keypair_signer'
@@ -0,0 +1,80 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Solana::Ruby::Kit
5
+ module Subscribable
6
+ # Creates an Enumerator backed by a thread-safe Queue.
7
+ #
8
+ # Mirrors createAsyncIterableFromDataPublisher from @solana/subscribable.
9
+ #
10
+ # Usage:
11
+ # enum = AsyncIterable.from_publisher(publisher, data_channel: :accountNotification)
12
+ # enum.each { |notification| process(notification) }
13
+ #
14
+ # The enumerator blocks on Queue#pop until the publisher closes or the
15
+ # optional +timeout+ expires. When the publisher emits on the error
16
+ # channel the exception is re-raised inside the enumerator.
17
+ module AsyncIterable
18
+ extend T::Sig
19
+
20
+ # Sentinel value pushed into the queue when the stream is done.
21
+ DONE = T.let(Object.new.freeze, Object)
22
+
23
+ module_function
24
+
25
+ # @param publisher [DataPublisher]
26
+ # @param data_channel channel name for data messages
27
+ # @param error_channel channel name for errors (default: :error)
28
+ # @param timeout [Float, nil] per-item pop timeout in seconds
29
+ # @return [Enumerator]
30
+ sig do
31
+ params(
32
+ publisher: DataPublisher,
33
+ data_channel: T.untyped,
34
+ error_channel: T.untyped,
35
+ timeout: T.nilable(Float)
36
+ ).returns(T::Enumerator[T.untyped])
37
+ end
38
+ def from_publisher(publisher, data_channel:, error_channel: :error, timeout: nil)
39
+ queue = T.let(Queue.new, Queue)
40
+
41
+ # Subscribe to data
42
+ unsub_data = publisher.on(data_channel) { |data| queue.push([:data, data]) }
43
+
44
+ # Subscribe to errors — re-raise inside the enumerator
45
+ unsub_error = publisher.on(error_channel) { |err| queue.push([:error, err]) }
46
+
47
+ # Subscribe to close
48
+ unsub_close = publisher.on(DataPublisher::CLOSE_CHANNEL) { queue.push([:done, nil]) }
49
+
50
+ cleanup = Kernel.lambda do
51
+ unsub_data.call
52
+ unsub_error.call
53
+ unsub_close.call
54
+ end
55
+
56
+ Enumerator.new do |yielder|
57
+ Kernel.loop do
58
+ kind, payload = if timeout
59
+ begin
60
+ Timeout.timeout(timeout) { queue.pop }
61
+ rescue Timeout::Error
62
+ [:done, nil]
63
+ end
64
+ else
65
+ queue.pop
66
+ end
67
+
68
+ case kind
69
+ when :data then yielder.yield(payload)
70
+ when :error then Kernel.raise T.cast(payload, StandardError)
71
+ else break
72
+ end
73
+ end
74
+ ensure
75
+ cleanup.call
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end