solana-ruby-kit 7.0.0 → 7.1.1

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.
@@ -115,6 +115,10 @@ module Solana::Ruby::Kit
115
115
  OFFCHAIN_MESSAGES__NON_PRINTABLE_ASCII_CHARACTER = :SOLANA_ERROR__OFFCHAIN_MESSAGES__NON_PRINTABLE_ASCII_CHARACTER
116
116
  OFFCHAIN_MESSAGES__MESSAGE_TOO_LONG = :SOLANA_ERROR__OFFCHAIN_MESSAGES__MESSAGE_TOO_LONG
117
117
  OFFCHAIN_MESSAGES__LEADING_ZERO_IN_SIGNING_DOMAIN = :SOLANA_ERROR__OFFCHAIN_MESSAGES__LEADING_ZERO_IN_SIGNING_DOMAIN
118
+ # context: { actual_bytes:, expected_bytes: }
119
+ OFFCHAIN_MESSAGES__CONTENT_DOES_NOT_MATCH_EXPECTED = :SOLANA_ERROR__OFFCHAIN_MESSAGE__CONTENT_DOES_NOT_MATCH_EXPECTED
120
+ # context: { actual_addresses:, expected_addresses: }
121
+ OFFCHAIN_MESSAGES__REQUIRED_SIGNATORIES_DO_NOT_MATCH_EXPECTED = :SOLANA_ERROR__OFFCHAIN_MESSAGE__REQUIRED_SIGNATORIES_DO_NOT_MATCH_EXPECTED
118
122
 
119
123
  # ── Instruction plans ─────────────────────────────────────────────────────
120
124
  # context: { num_bytes_required:, num_free_bytes: }
@@ -139,6 +143,7 @@ module Solana::Ruby::Kit
139
143
 
140
144
  # ── Subscribable ──────────────────────────────────────────────────────────
141
145
  SUBSCRIBABLE__RETRY_NOT_SUPPORTED = :SOLANA_ERROR__SUBSCRIBABLE__RETRY_NOT_SUPPORTED
146
+ SUBSCRIBABLE__STREAM_CLOSED_WITHOUT_ERROR = :SOLANA_ERROR__SUBSCRIBABLE__STREAM_CLOSED_WITHOUT_ERROR
142
147
 
143
148
  # ── Fixed-points ──────────────────────────────────────────────────────────
144
149
  FIXED_POINTS__STRICT_MODE_PRECISION_LOSS = :SOLANA_ERROR__FIXED_POINTS__STRICT_MODE_PRECISION_LOSS
@@ -255,6 +260,12 @@ module Solana::Ruby::Kit
255
260
  OFFCHAIN_MESSAGES__NON_PRINTABLE_ASCII_CHARACTER => 'Offchain message v0 contains non-printable ASCII character at index %{index}',
256
261
  OFFCHAIN_MESSAGES__MESSAGE_TOO_LONG => 'Offchain message is too long (%{length} bytes, max %{max})',
257
262
  OFFCHAIN_MESSAGES__LEADING_ZERO_IN_SIGNING_DOMAIN => 'Offchain message signing domain must not start with a null byte',
263
+ OFFCHAIN_MESSAGES__CONTENT_DOES_NOT_MATCH_EXPECTED => 'The content of the offchain message does not match the content that was expected. ' \
264
+ 'Expected content with a byte-length of %{expected_bytes}; got content with a byte-length of %{actual_bytes}. ' \
265
+ 'The signer may have signed different data than was requested; do not trust its signature.',
266
+ OFFCHAIN_MESSAGES__REQUIRED_SIGNATORIES_DO_NOT_MATCH_EXPECTED => 'The offchain message lists different required signatories than was expected. ' \
267
+ 'Expected [%{expected_addresses}]. Got [%{actual_addresses}]. ' \
268
+ 'The signer may have signed different data than was requested; do not trust its signature.',
258
269
 
259
270
  # Instruction plans
260
271
  INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN => 'Transaction message cannot accommodate the plan: requires %{num_bytes_required} bytes but only %{num_free_bytes} are available',
@@ -271,6 +282,7 @@ module Solana::Ruby::Kit
271
282
 
272
283
  # Subscribable
273
284
  SUBSCRIBABLE__RETRY_NOT_SUPPORTED => 'This reactive store does not support retry(); use create_reactive_store_from_data_publisher_factory for a retryable store',
285
+ SUBSCRIBABLE__STREAM_CLOSED_WITHOUT_ERROR => 'The stream store closed in an error state but did not report an error.',
274
286
 
275
287
  # Fixed-points
276
288
  FIXED_POINTS__STRICT_MODE_PRECISION_LOSS => 'Value has more than 9 fractional digits and cannot be represented exactly as a Sol amount',
@@ -300,8 +312,19 @@ module Solana::Ruby::Kit
300
312
  @context = T.let(context, T::Hash[Symbol, T.untyped])
301
313
 
302
314
  template = ERROR_MESSAGES[code] || code.to_s
303
- message = context.empty? ? template : (template % context rescue "#{template} #{context}")
315
+ message = context.empty? ? template : (template % display_context(context) rescue "#{template} #{context}")
304
316
  super(message)
305
317
  end
318
+
319
+ private
320
+
321
+ # Values used to fill a message template. Array values — lists of addresses, say —
322
+ # interpolate as a comma-separated list rather than as Ruby's `inspect` output, so
323
+ # that `[%{expected_addresses}]` reads like the TypeScript original. `#context` is
324
+ # untouched and still hands back the array itself.
325
+ sig { params(context: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
326
+ def display_context(context)
327
+ context.transform_values { |value| value.is_a?(Array) ? value.join(', ') : value }
328
+ end
306
329
  end
307
330
  end
@@ -13,11 +13,22 @@ module Solana::Ruby::Kit
13
13
  # executing each single transaction message and collecting results.
14
14
  #
15
15
  # Configuration:
16
- # execute_transaction_message: ->(message) { { transaction:, context: } }
17
- # Called for each SingleTransactionPlan. Must return a hash with:
18
- # :transaction the signed Transaction
19
- # :context optional Hash of extra data (defaults to {})
20
- # Raise a SolanaError to signal failure; remaining plans will be canceled.
16
+ # execute_transaction_message: ->(context, message) { context_to_report }
17
+ # Called once per SingleTransactionPlan with a fresh, mutable +context+ Hash and
18
+ # the transaction message to execute. Store data on +context+ as execution
19
+ # progresses, and return the context a successful result should carry. The two
20
+ # serve different outcomes: what you *store* reaches a failed or canceled result,
21
+ # what you *return* reaches a successful one. On success the returned Hash is
22
+ # merged over the stored one, the returned value winning, so anything recorded
23
+ # but left out of the return value is still reported.
24
+ #
25
+ # Raise a SolanaError to signal failure; remaining plans will be canceled. The
26
+ # context accumulated up to the point of failure is preserved on the resulting
27
+ # failed result, which is useful for debugging or building recovery plans.
28
+ #
29
+ # A one-argument callable is still accepted for backwards compatibility with the
30
+ # shape this method used to require — `->(message) { { transaction:, context: } }`
31
+ # — and is adapted onto the context flow above.
21
32
  #
22
33
  # The returned executor is a lambda: executor.call(transaction_plan) -> TransactionPlanResult
23
34
  #
@@ -77,26 +88,55 @@ module Solana::Ruby::Kit
77
88
  end
78
89
 
79
90
  def executor_traverse_single(plan, execute_fn, state)
80
- return canceled_single_transaction_plan_result(plan.message) if state[:canceled]
91
+ # A fresh context per single transaction plan. Nothing is populated yet — filling
92
+ # it in is the callback's job, which is what lets a partial context survive an error.
93
+ context = {}
94
+ return canceled_single_transaction_plan_result(plan.message, context) if state[:canceled]
81
95
 
82
96
  begin
83
- result = execute_fn.call(plan.message)
84
- transaction = result.fetch(:transaction)
85
- context = result.fetch(:context, {})
86
- successful_single_transaction_plan_result(plan.message, transaction, context)
97
+ returned = executor_invoke(execute_fn, context, plan.message)
98
+ # The callback told us what the successful result should carry, so take it as-is
99
+ # and derive nothing from it. Anything it stored but left out of the return value
100
+ # is kept, since dropping it would lose data it deliberately recorded.
101
+ successful_single_transaction_plan_result(plan.message, nil, context.merge(returned))
87
102
  rescue SolanaError => e
88
103
  state[:canceled] = true
89
- failed_single_transaction_plan_result(plan.message, e)
104
+ failed_single_transaction_plan_result(plan.message, e, context)
90
105
  rescue => e
91
106
  state[:canceled] = true
92
107
  wrapped = SolanaError.new(
93
108
  SolanaError::INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN,
94
109
  { cause: e }
95
110
  )
96
- failed_single_transaction_plan_result(plan.message, wrapped)
111
+ failed_single_transaction_plan_result(plan.message, wrapped, context)
97
112
  end
98
113
  end
99
114
 
115
+ # Calls the configured callback, adapting the legacy one-argument shape onto the
116
+ # context flow. Returns the Hash the successful result should carry.
117
+ sig do
118
+ params(
119
+ execute_fn: T.untyped,
120
+ context: T::Hash[T.untyped, T.untyped],
121
+ message: TransactionMessages::TransactionMessage
122
+ ).returns(T::Hash[T.untyped, T.untyped])
123
+ end
124
+ def executor_invoke(execute_fn, context, message)
125
+ unless execute_fn.arity == 1
126
+ returned = execute_fn.call(context, message)
127
+ return returned.is_a?(Hash) ? returned : {}
128
+ end
129
+
130
+ # Legacy shape: `->(message) { { transaction:, context: } }`. There is no mutable
131
+ # context to record into, so everything it reports arrives at once on the way out.
132
+ legacy = execute_fn.call(message)
133
+ legacy = {} unless legacy.is_a?(Hash)
134
+ returned = legacy.fetch(:context, {}).dup
135
+ transaction = legacy[:transaction]
136
+ returned[:transaction] = transaction if transaction
137
+ returned
138
+ end
139
+
100
140
  def executor_find_error(result)
101
141
  return result.status.error if result.kind == :single && result.status.kind == :failed
102
142
  return nil if result.kind == :single
@@ -112,6 +152,7 @@ module Solana::Ruby::Kit
112
152
  private_class_method :executor_traverse_sequential
113
153
  private_class_method :executor_traverse_parallel
114
154
  private_class_method :executor_traverse_single
155
+ private_class_method :executor_invoke
115
156
  private_class_method :executor_find_error
116
157
  end
117
158
  end
@@ -13,23 +13,27 @@ module Solana::Ruby::Kit
13
13
  #
14
14
  # Each single transaction produces one of three statuses:
15
15
  # successful: { kind: :successful, transaction:, context: }
16
- # failed: { kind: :failed, error: }
17
- # canceled: { kind: :canceled }
16
+ # failed: { kind: :failed, error:, context: }
17
+ # canceled: { kind: :canceled, context: }
18
18
  #
19
- # Mirrors TypeScript's TransactionPlanResultStatus union.
19
+ # Every status carries a context, so whatever the executor callback recorded
20
+ # before it failed — or before an earlier failure canceled it — survives into
21
+ # the result. Mirrors TypeScript's TransactionPlanResultStatus union.
20
22
 
21
23
  class SuccessfulStatus < T::Struct
22
- const :transaction, Transactions::Transaction
24
+ const :transaction, T.nilable(Transactions::Transaction)
23
25
  const :context, T::Hash[T.untyped, T.untyped]
24
26
  def kind = :successful
25
27
  end
26
28
 
27
29
  class FailedStatus < T::Struct
28
- const :error, SolanaError
30
+ const :error, SolanaError
31
+ const :context, T::Hash[T.untyped, T.untyped], default: {}.freeze
29
32
  def kind = :failed
30
33
  end
31
34
 
32
35
  class CanceledStatus < T::Struct
36
+ const :context, T::Hash[T.untyped, T.untyped], default: {}.freeze
33
37
  def kind = :canceled
34
38
  end
35
39
 
@@ -74,43 +78,55 @@ module Solana::Ruby::Kit
74
78
  ParallelTransactionPlanResult.new(plans: plans)
75
79
  end
76
80
 
77
- # Mirrors `successfulSingleTransactionPlanResult(message, transaction, context)`.
81
+ # Mirrors `successfulSingleTransactionPlanResult(message, context)`.
82
+ #
83
+ # Upstream carries the transaction inside the context; Ruby keeps the separate
84
+ # +transaction+ reader it has always exposed. Pass it either way — as the positional
85
+ # argument, or under +:transaction+ in the context, where the reader picks it up.
86
+ # Nothing is written back into the context, which stays exactly what was passed.
78
87
  sig do
79
88
  params(
80
89
  message: TransactionMessages::TransactionMessage,
81
- transaction: Transactions::Transaction,
90
+ transaction: T.nilable(Transactions::Transaction),
82
91
  context: T::Hash[T.untyped, T.untyped]
83
92
  ).returns(SingleTransactionPlanResult)
84
93
  end
85
- def successful_single_transaction_plan_result(message, transaction, context = {})
94
+ def successful_single_transaction_plan_result(message, transaction = nil, context = {})
95
+ from_context = context[:transaction]
96
+ transaction ||= from_context if from_context.is_a?(Transactions::Transaction)
97
+
86
98
  SingleTransactionPlanResult.new(
87
99
  message: message,
88
- status: SuccessfulStatus.new(transaction: transaction, context: context)
100
+ status: SuccessfulStatus.new(transaction: transaction, context: context.dup.freeze)
89
101
  )
90
102
  end
91
103
 
92
- # Mirrors `failedSingleTransactionPlanResult(message, error)`.
104
+ # Mirrors `failedSingleTransactionPlanResult(message, error, context)`.
93
105
  sig do
94
106
  params(
95
107
  message: TransactionMessages::TransactionMessage,
96
- error: SolanaError
108
+ error: SolanaError,
109
+ context: T::Hash[T.untyped, T.untyped]
97
110
  ).returns(SingleTransactionPlanResult)
98
111
  end
99
- def failed_single_transaction_plan_result(message, error)
112
+ def failed_single_transaction_plan_result(message, error, context = {})
100
113
  SingleTransactionPlanResult.new(
101
114
  message: message,
102
- status: FailedStatus.new(error: error)
115
+ status: FailedStatus.new(error: error, context: context.dup.freeze)
103
116
  )
104
117
  end
105
118
 
106
- # Mirrors `canceledSingleTransactionPlanResult(message)`.
119
+ # Mirrors `canceledSingleTransactionPlanResult(message, context)`.
107
120
  sig do
108
- params(message: TransactionMessages::TransactionMessage).returns(SingleTransactionPlanResult)
121
+ params(
122
+ message: TransactionMessages::TransactionMessage,
123
+ context: T::Hash[T.untyped, T.untyped]
124
+ ).returns(SingleTransactionPlanResult)
109
125
  end
110
- def canceled_single_transaction_plan_result(message)
126
+ def canceled_single_transaction_plan_result(message, context = {})
111
127
  SingleTransactionPlanResult.new(
112
128
  message: message,
113
- status: CanceledStatus.new
129
+ status: CanceledStatus.new(context: context.dup.freeze)
114
130
  )
115
131
  end
116
132
  end
@@ -31,8 +31,12 @@ module Solana::Ruby::Kit
31
31
  buf << domain_b
32
32
  buf << [msg.version].pack('C')
33
33
 
34
- if msg.version >= 1 && msg.application_domain
35
- app_b = T.must(msg.application_domain).encode('ASCII').b
34
+ # The v1 application-domain block is always emitted, using a zero length when
35
+ # there is no application domain. The decoder reads this length unconditionally
36
+ # for v1, so omitting the block would desynchronise it and make it read the
37
+ # message length as an application-domain length.
38
+ if msg.version >= 1
39
+ app_b = msg.application_domain ? T.must(msg.application_domain).encode('ASCII').b : ''.b
36
40
  Kernel.raise ArgumentError, 'Application domain exceeds 65535 bytes' if app_b.bytesize > 0xFFFF
37
41
 
38
42
  buf << [app_b.bytesize].pack('v')
@@ -62,7 +66,9 @@ module Solana::Ruby::Kit
62
66
  if version >= 1
63
67
  app_len = b.byteslice(offset, 2)&.unpack1('v') || 0
64
68
  offset += 2
65
- application_domain = b.byteslice(offset, app_len)&.force_encoding('ASCII')
69
+ # A zero length means there was no application domain, not an empty one, so
70
+ # that encode/decode round-trips a message that never had one.
71
+ application_domain = app_len.positive? ? b.byteslice(offset, app_len)&.force_encoding('ASCII') : nil
66
72
  offset += app_len
67
73
  end
68
74
 
@@ -94,12 +100,14 @@ module Solana::Ruby::Kit
94
100
  ).returns(T::Boolean)
95
101
  end
96
102
  def verify_offchain_message_signature(verify_key, signature, msg)
97
- payload = encode_offchain_message(msg)
98
- vk = RbNaCl::VerifyKey.new(verify_key)
99
- sig_bytes = signature.respond_to?(:to_bytes) ? signature.to_s : [signature.value].pack('H*')
103
+ payload = encode_offchain_message(msg)
104
+ vk = RbNaCl::VerifyKey.new(verify_key)
105
+ # `Signature` holds a base58 string (that is what `sign_offchain_message` returns),
106
+ # so it has to be decoded back to the 64 raw bytes RbNaCl expects.
107
+ sig_bytes = Solana::Ruby::Kit::Encoding::Base58.decode(signature.to_s)
100
108
  vk.verify(sig_bytes, payload)
101
109
  true
102
- rescue RbNaCl::BadSignatureError
110
+ rescue RbNaCl::BadSignatureError, RbNaCl::LengthError
103
111
  false
104
112
  end
105
113
  end
@@ -0,0 +1,115 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../errors'
5
+ require_relative '../addresses/address'
6
+
7
+ module Solana::Ruby::Kit
8
+ module OffchainMessages
9
+ extend T::Sig
10
+
11
+ # An address that is required to sign an offchain message for it to be valid.
12
+ # Mirrors TypeScript's `OffchainMessageSignatory`.
13
+ class Signatory < T::Struct
14
+ const :address, Addresses::Address
15
+ end
16
+
17
+ # A version 1 offchain message.
18
+ #
19
+ # Mirrors TypeScript's `OffchainMessageV1` — the shape from the newer
20
+ # `@solana/offchain-messages` package, whose content is UTF-8 and whose preamble
21
+ # lists the addresses required to sign it. This is a different type from the
22
+ # legacy Message in message.rb, which models the older `@solana/signers`
23
+ # domain/application-domain form; the two are not interchangeable.
24
+ class MessageV1 < T::Struct
25
+ # UTF-8 message content.
26
+ const :content, String
27
+
28
+ # Addresses required to sign this message. The offchain message specification
29
+ # mandates that these be serialized in lexicographic order, so a decoded message
30
+ # always lists them that way; one you build yourself may list them in any order.
31
+ const :required_signatories, T::Array[Signatory], default: [].freeze
32
+
33
+ extend T::Sig
34
+
35
+ sig { returns(Integer) }
36
+ def version = 1
37
+ end
38
+
39
+ module_function
40
+
41
+ # Asserts that a version 1 offchain message received from an untrusted source is the
42
+ # message it was expected to be.
43
+ #
44
+ # A signer (a wallet, say) returns the message bytes it signed alongside its signature.
45
+ # Verifying that signature proves only that the signer produced it over *those* bytes;
46
+ # it says nothing about whether those bytes represent the message that was asked for.
47
+ # Use this to establish that they do, then verify the signature separately.
48
+ #
49
+ # Perform this assertion *before* verifying signatures. A signer that signed the wrong
50
+ # message would otherwise surface as a signature verification failure, misattributing
51
+ # the problem to the cryptography rather than to the content.
52
+ #
53
+ # Required signatories are compared without regard to order — both lists are sorted
54
+ # first, and reported sorted so they can be compared by eye. Order is the only thing
55
+ # ignored: the lists are otherwise compared element by element, so listing an address
56
+ # twice on one side and once on the other is a mismatch rather than a no-op.
57
+ #
58
+ # Message content is never placed in the error context, since it can carry data better
59
+ # kept out of logs and error reporting. Its length in UTF-8 bytes — the encoding in
60
+ # which version 1 content is serialized — is reported instead.
61
+ #
62
+ # Raises SolanaError OFFCHAIN_MESSAGES__CONTENT_DOES_NOT_MATCH_EXPECTED if the two
63
+ # messages' contents differ, or
64
+ # OFFCHAIN_MESSAGES__REQUIRED_SIGNATORIES_DO_NOT_MATCH_EXPECTED if they require
65
+ # signatures from different addresses. Content is checked first, so a message that
66
+ # differs in both reports the content mismatch.
67
+ #
68
+ # Mirrors `assertOffchainMessageV1Equal(receivedMessage, expectedMessage)`.
69
+ sig { params(received_message: MessageV1, expected_message: MessageV1).void }
70
+ def assert_offchain_message_v1_equal(received_message, expected_message)
71
+ if received_message.content != expected_message.content
72
+ Kernel.raise SolanaError.new(
73
+ SolanaError::OFFCHAIN_MESSAGES__CONTENT_DOES_NOT_MATCH_EXPECTED,
74
+ {
75
+ actual_bytes: utf8_byte_length(received_message.content),
76
+ expected_bytes: utf8_byte_length(expected_message.content)
77
+ }
78
+ )
79
+ end
80
+
81
+ actual_addresses = sorted_signatory_addresses(received_message)
82
+ expected_addresses = sorted_signatory_addresses(expected_message)
83
+ return if actual_addresses == expected_addresses
84
+
85
+ Kernel.raise SolanaError.new(
86
+ SolanaError::OFFCHAIN_MESSAGES__REQUIRED_SIGNATORIES_DO_NOT_MATCH_EXPECTED,
87
+ {
88
+ actual_addresses: actual_addresses.map(&:to_s),
89
+ expected_addresses: expected_addresses.map(&:to_s)
90
+ }
91
+ )
92
+ end
93
+
94
+ # ── Private helpers ────────────────────────────────────────────────────────
95
+
96
+ sig { params(message: MessageV1).returns(T::Array[Addresses::Address]) }
97
+ def sorted_signatory_addresses(message)
98
+ message.required_signatories.map(&:address).sort
99
+ end
100
+
101
+ # Length of a string in UTF-8 bytes, which is how version 1 content is serialized.
102
+ # Ruby strings carry their own encoding, so one that arrived as anything else is
103
+ # converted first rather than having its raw bytes counted.
104
+ #
105
+ # NOTE: `::Encoding` must be spelled with the leading `::`. Bare `Encoding` resolves
106
+ # to Solana::Ruby::Kit::Encoding — this gem's own base58 namespace — not Ruby's.
107
+ sig { params(content: String).returns(Integer) }
108
+ def utf8_byte_length(content)
109
+ content.encoding == ::Encoding::UTF_8 ? content.bytesize : content.encode(::Encoding::UTF_8).bytesize
110
+ end
111
+
112
+ private_class_method :sorted_signatory_addresses
113
+ private_class_method :utf8_byte_length
114
+ end
115
+ end
@@ -3,14 +3,41 @@
3
3
 
4
4
  require 'rbnacl'
5
5
 
6
- # Mirrors @solana/signers off-chain message signing.
6
+ # Mirrors @solana/signers off-chain message signing, plus the version 1 message
7
+ # comparison helper from @solana/offchain-messages.
7
8
  require_relative 'offchain_messages/message'
9
+ require_relative 'offchain_messages/message_v1'
8
10
  require_relative 'offchain_messages/codec'
9
11
 
10
12
  module Solana::Ruby::Kit
11
13
  module OffchainMessages
12
- # Re-export codec helpers at module level for convenience.
13
14
  extend T::Sig
14
- extend Codec
15
+
16
+ # Re-export codec helpers at module level for convenience.
17
+ #
18
+ # NOTE: `extend Codec` does not work here. Codec declares its helpers with
19
+ # `module_function`, which makes the instance-method copies private, and `extend`
20
+ # only imports public instance methods — so it would silently import nothing.
21
+ # Delegate to Codec's module methods explicitly instead.
22
+
23
+ sig { params(msg: Message).returns(String) }
24
+ def self.encode_offchain_message(msg) = Codec.encode_offchain_message(msg)
25
+
26
+ sig { params(bytes: String).returns(Message) }
27
+ def self.decode_offchain_message(bytes) = Codec.decode_offchain_message(bytes)
28
+
29
+ sig { params(signer: Signers::KeyPairSigner, msg: Message).returns(Keys::Signature) }
30
+ def self.sign_offchain_message(signer, msg) = Codec.sign_offchain_message(signer, msg)
31
+
32
+ sig do
33
+ params(
34
+ verify_key: String,
35
+ signature: Keys::Signature,
36
+ msg: Message
37
+ ).returns(T::Boolean)
38
+ end
39
+ def self.verify_offchain_message_signature(verify_key, signature, msg)
40
+ Codec.verify_offchain_message_signature(verify_key, signature, msg)
41
+ end
15
42
  end
16
43
  end
@@ -9,7 +9,9 @@ module Solana::Ruby::Kit
9
9
  InflationReward = T.let(
10
10
  Struct.new(
11
11
  :amount, # Integer (Lamports) — reward credited
12
- :commission, # Integer — vote account commission at reward time
12
+ :commission, # Integer, nilable — vote account commission at reward time;
13
+ # null once the vote account reports commission through
14
+ # getVoteAccounts' inflationRewardsCommissionBps instead
13
15
  :effective_slot, # Integer — slot in which rewards were delivered
14
16
  :epoch, # Integer — epoch for which reward occurred
15
17
  :post_balance, # Integer (Lamports) — post-reward account balance
@@ -46,7 +48,7 @@ module Solana::Ruby::Kit
46
48
 
47
49
  InflationReward.new(
48
50
  amount: Kernel.Integer(entry['amount']),
49
- commission: Kernel.Integer(entry['commission']),
51
+ commission: entry['commission'] ? Kernel.Integer(entry['commission']) : nil,
50
52
  effective_slot: Kernel.Integer(entry['effectiveSlot']),
51
53
  epoch: Kernel.Integer(entry['epoch']),
52
54
  post_balance: Kernel.Integer(entry['postBalance'])