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,116 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'base64'
5
+
6
+ module Solana::Ruby::Kit
7
+ module Codecs
8
+ # String codecs — mirrors @solana/codecs-strings.
9
+ module Strings
10
+ extend T::Sig
11
+
12
+ module_function
13
+
14
+ # UTF-8 string codec.
15
+ # When +size+ is given the encoded bytes are fixed to that length
16
+ # (zero-padded or truncated); otherwise the codec is variable-length
17
+ # and must be used inside a size-prefixed container.
18
+ sig { params(size: T.nilable(Integer)).returns(Codec) }
19
+ def utf8_codec(size: nil)
20
+ enc = Encoder.new(fixed_size: size) do |v|
21
+ raw = v.to_s.encode('UTF-8').b
22
+ if size
23
+ raw.bytesize <= size ? raw.ljust(size, "\x00") : raw.byteslice(0, size) || ''.b
24
+ else
25
+ raw
26
+ end
27
+ end
28
+ dec = Decoder.new(fixed_size: size) do |bytes, offset|
29
+ len = size || (bytes.bytesize - offset)
30
+ slice = bytes.b.byteslice(offset, len) || ''.b
31
+ # Strip null padding for fixed-size strings
32
+ str = size ? slice.delete_suffix("\x00" * slice.bytesize.times.take_while { |i| slice.b[-1 - i] == "\x00" }.length) : slice
33
+ [str.force_encoding('UTF-8'), len]
34
+ end
35
+ Codec.new(enc, dec)
36
+ end
37
+
38
+ # Base58 codec — uses Solana::Ruby::Kit::Encoding::Base58.
39
+ sig { returns(Codec) }
40
+ def base58_codec
41
+ enc = Encoder.new do |v|
42
+ Solana::Ruby::Kit::Encoding::Base58.decode(v.to_s)
43
+ end
44
+ dec = Decoder.new do |bytes, offset|
45
+ remaining = bytes.b.byteslice(offset..) || ''.b
46
+ [Solana::Ruby::Kit::Encoding::Base58.encode(remaining), remaining.bytesize]
47
+ end
48
+ Codec.new(enc, dec)
49
+ end
50
+
51
+ # Base64 codec — strict (no newlines).
52
+ sig { returns(Codec) }
53
+ def base64_codec
54
+ enc = Encoder.new do |v|
55
+ Base64.strict_encode64(v.to_s)
56
+ end
57
+ dec = Decoder.new do |bytes, offset|
58
+ remaining = bytes.b.byteslice(offset..) || ''.b
59
+ [Base64.strict_decode64(remaining.force_encoding('ASCII')), remaining.bytesize]
60
+ end
61
+ Codec.new(enc, dec)
62
+ end
63
+
64
+ # Hex codec — lower-case hex string ↔ binary bytes.
65
+ sig { returns(Codec) }
66
+ def hex_codec
67
+ enc = Encoder.new do |v|
68
+ [v.to_s.tr(' ', '')].pack('H*')
69
+ end
70
+ dec = Decoder.new do |bytes, offset|
71
+ remaining = bytes.b.byteslice(offset..) || ''.b
72
+ [remaining.unpack1('H*'), remaining.bytesize]
73
+ end
74
+ Codec.new(enc, dec)
75
+ end
76
+
77
+ # Fixed-size raw bytes passthrough.
78
+ sig { params(size: Integer).returns(Codec) }
79
+ def bytes_codec(size)
80
+ enc = Encoder.new(fixed_size: size) do |v|
81
+ b = v.is_a?(String) ? v.b : v.to_s.b
82
+ Kernel.raise ArgumentError, "Expected #{size} bytes, got #{b.bytesize}" if b.bytesize != size
83
+
84
+ b
85
+ end
86
+ dec = Decoder.new(fixed_size: size) do |bytes, offset|
87
+ slice = bytes.b.byteslice(offset, size) || ''.b
88
+ [slice, size]
89
+ end
90
+ Codec.new(enc, dec)
91
+ end
92
+
93
+ # Bit-array codec.
94
+ # Encodes an Array of booleans into +size+ bytes (LSB-first within each byte).
95
+ sig { params(size: Integer).returns(Codec) }
96
+ def bit_array_codec(size)
97
+ total_bits = size * 8
98
+ enc = Encoder.new(fixed_size: size) do |bits|
99
+ arr = T.cast(bits, T::Array[T::Boolean])
100
+ bytes = Array.new(size, 0)
101
+ arr.first(total_bits).each_with_index do |bit, idx|
102
+ bytes[idx / 8] |= (1 << (idx % 8)) if bit
103
+ end
104
+ bytes.pack('C*')
105
+ end
106
+ dec = Decoder.new(fixed_size: size) do |bytes, offset|
107
+ slice = bytes.b.byteslice(offset, size) || ''.b
108
+ byte_arr = T.cast(T.unsafe(slice).unpack('C*'), T::Array[Integer])
109
+ bits = byte_arr.flat_map { |byte| 8.times.map { |i| byte[i] == 1 } }
110
+ [bits, size]
111
+ end
112
+ Codec.new(enc, dec)
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,25 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'set'
5
+
6
+ # Codec system — mirrors @solana/codecs.
7
+ # Provides Encoder, Decoder, and Codec classes plus helpers for numbers,
8
+ # strings, and composite data structures.
9
+ require_relative 'codecs/bytes'
10
+ require_relative 'codecs/encoder'
11
+ require_relative 'codecs/decoder'
12
+ require_relative 'codecs/codec'
13
+ require_relative 'codecs/numbers'
14
+ require_relative 'codecs/strings'
15
+ require_relative 'codecs/data_structures'
16
+
17
+ module Solana::Ruby::Kit
18
+ module Codecs
19
+ # Make number helpers directly available as Codecs.u8_codec etc.
20
+ extend Numbers
21
+ extend Strings
22
+ extend DataStructures
23
+ extend Bytes
24
+ end
25
+ end
@@ -0,0 +1,48 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Solana::Ruby::Kit
5
+ class Configuration
6
+ extend T::Sig
7
+
8
+ sig { returns(String) }
9
+ attr_reader :rpc_url
10
+
11
+ sig { returns(T.nilable(String)) }
12
+ attr_reader :ws_url
13
+
14
+ sig { returns(Symbol) }
15
+ attr_reader :commitment
16
+
17
+ sig { returns(Integer) }
18
+ attr_reader :timeout
19
+
20
+ sig { void }
21
+ def initialize
22
+ @rpc_url = T.let('https://api.mainnet-beta.solana.com', String)
23
+ @ws_url = T.let(nil, T.nilable(String))
24
+ @commitment = T.let(:confirmed, Symbol)
25
+ @timeout = T.let(30, Integer)
26
+ end
27
+
28
+ sig { params(value: String).void }
29
+ def rpc_url=(value)
30
+ @rpc_url = value
31
+ end
32
+
33
+ sig { params(value: T.nilable(String)).void }
34
+ def ws_url=(value)
35
+ @ws_url = value
36
+ end
37
+
38
+ sig { params(value: Symbol).void }
39
+ def commitment=(value)
40
+ @commitment = value
41
+ end
42
+
43
+ sig { params(value: Integer).void }
44
+ def timeout=(value)
45
+ @timeout = value
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,62 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Solana::Ruby::Kit
5
+ module Encoding
6
+ # Base58 codec using the Bitcoin/Solana alphabet (no check bytes).
7
+ # This module provides the shared base58 encoding used by both
8
+ # Solana::Ruby::Kit::Addresses and Solana::Ruby::Kit::Keys::Signatures.
9
+ module Base58
10
+ extend T::Sig
11
+ ALPHABET = T.let(
12
+ '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz',
13
+ String
14
+ )
15
+
16
+ module_function
17
+
18
+ # Encodes a binary String to a base58 string.
19
+ sig { params(bytes: String).returns(String) }
20
+ def encode(bytes)
21
+ # Each leading zero byte maps to a leading '1'.
22
+ leading_ones = 0
23
+ bytes.each_byte { |b| b == 0 ? leading_ones += 1 : break }
24
+
25
+ # Convert big-endian byte string to an integer.
26
+ n = bytes.unpack1('H*').to_i(16)
27
+
28
+ result = +''
29
+ while n > 0
30
+ result.prepend(T.must(ALPHABET[n % 58]))
31
+ n /= 58
32
+ end
33
+
34
+ ('1' * leading_ones) + result
35
+ end
36
+
37
+ # Decodes a base58 string to a binary String.
38
+ # Raises ArgumentError if the string contains characters not in the alphabet.
39
+ sig { params(str: String).returns(String) }
40
+ def decode(str)
41
+ # Each leading '1' maps to a leading zero byte.
42
+ leading_zeros = 0
43
+ str.each_char { |c| c == '1' ? leading_zeros += 1 : break }
44
+
45
+ n = 0
46
+ str.each_char do |c|
47
+ idx = ALPHABET.index(c)
48
+ Kernel.raise ArgumentError, "Invalid base58 character: #{c.inspect}" if idx.nil?
49
+
50
+ n = n * 58 + idx
51
+ end
52
+
53
+ # Convert integer back to a big-endian byte string.
54
+ hex = n.zero? ? '' : n.to_s(16)
55
+ hex = hex.rjust((hex.length + 1) & ~1, '0') # guarantee even hex length
56
+
57
+ raw = [hex].pack('H*')
58
+ ("\x00" * leading_zeros + raw).b
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,226 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Solana::Ruby::Kit
5
+ # Maps TypeScript's SolanaError pattern to Ruby exceptions.
6
+ # Each error code corresponds to a constant and carries structured context.
7
+ # Mirrors @solana/errors codes.ts + messages.ts.
8
+ class SolanaError < StandardError
9
+ extend T::Sig
10
+
11
+ # ── General ──────────────────────────────────────────────────────────────
12
+ INVALID_KEYPAIR_SEED_LENGTH = :SOLANA_ERROR__INVALID_KEYPAIR_SEED_LENGTH
13
+ INVALID_NONCE = :SOLANA_ERROR__INVALID_NONCE
14
+
15
+ # ── Addresses ─────────────────────────────────────────────────────────────
16
+ ADDRESSES__INVALID_BASE58_ENCODED_ADDRESS = :SOLANA_ERROR__ADDRESSES__INVALID_BASE58_ENCODED_ADDRESS
17
+ ADDRESSES__INVALID_BYTE_LENGTH_FOR_ADDRESS = :SOLANA_ERROR__ADDRESSES__INVALID_BYTE_LENGTH_FOR_ADDRESS
18
+ ADDRESSES__STRING_LENGTH_OUT_OF_RANGE = :SOLANA_ERROR__ADDRESSES__STRING_LENGTH_OUT_OF_RANGE_FOR_ADDRESS
19
+ ADDRESSES__INVALID_ED25519_PUBLIC_KEY = :SOLANA_ERROR__ADDRESSES__INVALID_ED25519_PUBLIC_KEY
20
+ ADDRESSES__SEEDS_POINT_ON_CURVE = :SOLANA_ERROR__ADDRESSES__SEEDS_POINT_ON_CURVE
21
+ ADDRESSES__MAX_SEED_LENGTH_EXCEEDED = :SOLANA_ERROR__ADDRESSES__MAX_SEED_LENGTH_EXCEEDED
22
+ ADDRESSES__TOO_MANY_SEEDS = :SOLANA_ERROR__ADDRESSES__TOO_MANY_SEEDS
23
+ ADDRESSES__FAILED_TO_FIND_VIABLE_PDA_BUMP_SEED = :SOLANA_ERROR__ADDRESSES__FAILED_TO_FIND_VIABLE_PDA_BUMP_SEED
24
+ ADDRESSES__INVALID_SEEDS_POINT_ON_CURVE = :SOLANA_ERROR__ADDRESSES__INVALID_SEEDS_POINT_ON_CURVE
25
+ ADDRESSES__PDA_BUMP_SEED_OUT_OF_RANGE = :SOLANA_ERROR__ADDRESSES__PDA_BUMP_SEED_OUT_OF_RANGE
26
+
27
+ # ── Accounts ──────────────────────────────────────────────────────────────
28
+ ACCOUNTS__ACCOUNT_NOT_FOUND = :SOLANA_ERROR__ACCOUNTS__ACCOUNT_NOT_FOUND
29
+ ACCOUNTS__ONE_OR_MORE_ACCOUNTS_NOT_FOUND = :SOLANA_ERROR__ACCOUNTS__ONE_OR_MORE_ACCOUNTS_NOT_FOUND
30
+ ACCOUNTS__EXPECTED_DECODED_ACCOUNT = :SOLANA_ERROR__ACCOUNTS__EXPECTED_DECODED_ACCOUNT
31
+ ACCOUNTS__EXPECTED_ALL_ACCOUNTS_TO_BE_DECODED = :SOLANA_ERROR__ACCOUNTS__EXPECTED_ALL_ACCOUNTS_TO_BE_DECODED
32
+ ACCOUNTS__FAILED_TO_DECODE_ACCOUNT = :SOLANA_ERROR__ACCOUNTS__FAILED_TO_DECODE_ACCOUNT
33
+
34
+ # ── Keys ──────────────────────────────────────────────────────────────────
35
+ KEYS__INVALID_KEY_PAIR_BYTE_LENGTH = :SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH
36
+ KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY = :SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY
37
+ KEYS__INVALID_SIGNATURE_BYTE_LENGTH = :SOLANA_ERROR__KEYS__INVALID_SIGNATURE_BYTE_LENGTH
38
+ KEYS__SIGNATURE_STRING_LENGTH_OUT_OF_RANGE = :SOLANA_ERROR__KEYS__SIGNATURE_STRING_LENGTH_OUT_OF_RANGE
39
+
40
+ # ── Instructions ──────────────────────────────────────────────────────────
41
+ INSTRUCTIONS__EXPECTED_TO_HAVE_ACCOUNTS = :SOLANA_ERROR__INSTRUCTIONS__EXPECTED_TO_HAVE_ACCOUNTS
42
+ INSTRUCTIONS__EXPECTED_TO_HAVE_DATA = :SOLANA_ERROR__INSTRUCTIONS__EXPECTED_TO_HAVE_DATA
43
+ INSTRUCTIONS__PROGRAM_ADDRESS_MISMATCH = :SOLANA_ERROR__INSTRUCTIONS__PROGRAM_ADDRESS_MISMATCH
44
+ INSTRUCTIONS__ACCOUNT_NOT_FOUND = :SOLANA_ERROR__INSTRUCTIONS__ACCOUNT_NOT_FOUND
45
+ INSTRUCTIONS__EXPECTED_ALL_ACCOUNTS_TO_BE_DECODED = :SOLANA_ERROR__INSTRUCTIONS__EXPECTED_ALL_ACCOUNTS_TO_BE_DECODED
46
+
47
+ # ── Transactions ──────────────────────────────────────────────────────────
48
+ TRANSACTIONS__TRANSACTION_NOT_SIGNABLE = :SOLANA_ERROR__TRANSACTIONS__TRANSACTION_NOT_SIGNABLE
49
+ TRANSACTIONS__MISSING_SIGNER = :SOLANA_ERROR__TRANSACTIONS__MISSING_SIGNER
50
+ TRANSACTIONS__VERSION_NUMBER_OUT_OF_RANGE = :SOLANA_ERROR__TRANSACTIONS__VERSION_NUMBER_OUT_OF_RANGE
51
+ TRANSACTIONS__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS = :SOLANA_ERROR__TRANSACTIONS__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS
52
+ TRANSACTIONS__FAILED_TO_DECOMPILE_FEE_PAYER_MISSING = :SOLANA_ERROR__TRANSACTIONS__FAILED_TO_DECOMPILE_FEE_PAYER_MISSING
53
+ TRANSACTIONS__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND = :SOLANA_ERROR__TRANSACTIONS__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND
54
+ TRANSACTIONS__SEND_TRANSACTION_PREFLIGHT_FAILURE = :SOLANA_ERROR__TRANSACTIONS__SEND_TRANSACTION_PREFLIGHT_FAILURE
55
+ TRANSACTIONS__BLOCKHASH_NOT_FOUND = :SOLANA_ERROR__TRANSACTIONS__BLOCKHASH_NOT_FOUND
56
+ TRANSACTIONS__FAILED_TRANSACTION_PLAN = :SOLANA_ERROR__TRANSACTIONS__FAILED_TRANSACTION_PLAN
57
+ TRANSACTIONS__TRANSACTION_EXPIRED_BLOCKHEIGHT_EXCEEDED = :SOLANA_ERROR__TRANSACTIONS__TRANSACTION_EXPIRED_BLOCKHEIGHT_EXCEEDED
58
+ TRANSACTIONS__TRANSACTION_EXPIRED_NONCE_INVALID = :SOLANA_ERROR__TRANSACTIONS__TRANSACTION_EXPIRED_NONCE_INVALID
59
+ TRANSACTIONS__TRANSACTION_CONFIRMATION_TIMEOUT = :SOLANA_ERROR__TRANSACTIONS__TRANSACTION_CONFIRMATION_TIMEOUT
60
+
61
+ # ── Signers ───────────────────────────────────────────────────────────────
62
+ SIGNERS__ADDRESS_CANNOT_HAVE_MULTIPLE_SIGNERS = :SOLANA_ERROR__SIGNERS__ADDRESS_CANNOT_HAVE_MULTIPLE_SIGNERS
63
+ SIGNERS__EXPECTED_MESSAGE_MODIFYING_SIGNER = :SOLANA_ERROR__SIGNERS__EXPECTED_MESSAGE_MODIFYING_SIGNER
64
+ SIGNERS__EXPECTED_TRANSACTION_MODIFYING_SIGNER = :SOLANA_ERROR__SIGNERS__EXPECTED_TRANSACTION_MODIFYING_SIGNER
65
+ SIGNERS__EXPECTED_TRANSACTION_SENDING_SIGNER = :SOLANA_ERROR__SIGNERS__EXPECTED_TRANSACTION_SENDING_SIGNER
66
+ SIGNERS__TRANSACTION_CANNOT_HAVE_MULTIPLE_SENDING_SIGNERS = :SOLANA_ERROR__SIGNERS__TRANSACTION_CANNOT_HAVE_MULTIPLE_SENDING_SIGNERS
67
+ SIGNERS__WALLET_MULTISIGN_UNIMPLEMENTED = :SOLANA_ERROR__SIGNERS__WALLET_MULTISIGN_UNIMPLEMENTED
68
+
69
+ # ── Codecs ────────────────────────────────────────────────────────────────
70
+ CODECS__CANNOT_DECODE_EMPTY_BYTE_ARRAY = :SOLANA_ERROR__CODECS__CANNOT_DECODE_EMPTY_BYTE_ARRAY
71
+ CODECS__EXPECTED_POSITIVE_BYTE_LENGTH = :SOLANA_ERROR__CODECS__EXPECTED_POSITIVE_BYTE_LENGTH
72
+ CODECS__ENCODER_DECODER_FIXED_SIZE_MISMATCH = :SOLANA_ERROR__CODECS__ENCODER_DECODER_FIXED_SIZE_MISMATCH
73
+ CODECS__ENCODER_DECODER_MAX_SIZE_MISMATCH = :SOLANA_ERROR__CODECS__ENCODER_DECODER_MAX_SIZE_MISMATCH
74
+ CODECS__INVALID_NUMBER_OF_ITEMS = :SOLANA_ERROR__CODECS__INVALID_NUMBER_OF_ITEMS
75
+ CODECS__ENUM_DISCRIMINATOR_OUT_OF_RANGE = :SOLANA_ERROR__CODECS__ENUM_DISCRIMINATOR_OUT_OF_RANGE
76
+ CODECS__UNION_VARIANT_OUT_OF_RANGE = :SOLANA_ERROR__CODECS__UNION_VARIANT_OUT_OF_RANGE
77
+ CODECS__OFFSET_OUT_OF_RANGE = :SOLANA_ERROR__CODECS__OFFSET_OUT_OF_RANGE
78
+ CODECS__SENTINEL_MISSING_IN_DECODED_BYTES = :SOLANA_ERROR__CODECS__SENTINEL_MISSING_IN_DECODED_BYTES
79
+ CODECS__INVALID_STRING_FOR_BASE58_CODEC = :SOLANA_ERROR__CODECS__INVALID_STRING_FOR_BASE58_CODEC
80
+ CODECS__INVALID_STRING_FOR_BASE64_CODEC = :SOLANA_ERROR__CODECS__INVALID_STRING_FOR_BASE64_CODEC
81
+ CODECS__INVALID_STRING_FOR_HEX_CODEC = :SOLANA_ERROR__CODECS__INVALID_STRING_FOR_HEX_CODEC
82
+ CODECS__INVALID_BYTE_LENGTH = :SOLANA_ERROR__CODECS__INVALID_BYTE_LENGTH
83
+ CODECS__EXPECTED_ZERO_VALUE_TO_MATCH_ITEM_FIXED_DISCRIMINATOR = :SOLANA_ERROR__CODECS__EXPECTED_ZERO_VALUE_TO_MATCH_ITEM_FIXED_DISCRIMINATOR
84
+ CODECS__FIXED_NULLABLE_CANNOT_WRAP_VARIABLE_SIZE_CODEC = :SOLANA_ERROR__CODECS__FIXED_NULLABLE_CANNOT_WRAP_VARIABLE_SIZE_CODEC
85
+
86
+ # ── RPC / JSON-RPC ────────────────────────────────────────────────────────
87
+ RPC__INTEGER_OVERFLOW_WHILE_SERIALIZING_LARGE_INTEGER = :SOLANA_ERROR__RPC__INTEGER_OVERFLOW_WHILE_SERIALIZING_LARGE_INTEGER
88
+ RPC__INTEGER_OVERFLOW_WHILE_DESERIALIZING_LARGE_INTEGER = :SOLANA_ERROR__RPC__INTEGER_OVERFLOW_WHILE_DESERIALIZING_LARGE_INTEGER
89
+ RPC__TRANSPORT_HTTP_ERROR = :SOLANA_ERROR__RPC__TRANSPORT_HTTP_ERROR
90
+ RPC_SUBSCRIPTIONS__CANNOT_CREATE_SUBSCRIPTION_REQUEST = :SOLANA_ERROR__RPC_SUBSCRIPTIONS__CANNOT_CREATE_SUBSCRIPTION_REQUEST
91
+ RPC_SUBSCRIPTIONS__EXPECTED_SERVER_SUBSCRIPTION_ID = :SOLANA_ERROR__RPC_SUBSCRIPTIONS__EXPECTED_SERVER_SUBSCRIPTION_ID
92
+ RPC_SUBSCRIPTIONS__CHANNEL_CLOSED_BEFORE_MESSAGE_BUFFERED = :SOLANA_ERROR__RPC_SUBSCRIPTIONS__CHANNEL_CLOSED_BEFORE_MESSAGE_BUFFERED
93
+ RPC_SUBSCRIPTIONS__CHANNEL_CONNECTION_CLOSED = :SOLANA_ERROR__RPC_SUBSCRIPTIONS__CHANNEL_CONNECTION_CLOSED
94
+ RPC_SUBSCRIPTIONS__WEBSOCKET_CONNECTION_FAILED = :SOLANA_ERROR__RPC_SUBSCRIPTIONS__WEBSOCKET_CONNECTION_FAILED
95
+
96
+ # ── Offchain messages ─────────────────────────────────────────────────────
97
+ OFFCHAIN_MESSAGES__FAILED_TO_DECODE_MESSAGE = :SOLANA_ERROR__OFFCHAIN_MESSAGES__FAILED_TO_DECODE_MESSAGE
98
+ OFFCHAIN_MESSAGES__INVALID_MESSAGE_FORMAT = :SOLANA_ERROR__OFFCHAIN_MESSAGES__INVALID_MESSAGE_FORMAT
99
+ OFFCHAIN_MESSAGES__INVALID_MESSAGE_VERSION = :SOLANA_ERROR__OFFCHAIN_MESSAGES__INVALID_MESSAGE_VERSION
100
+ OFFCHAIN_MESSAGES__NON_PRINTABLE_ASCII_CHARACTER = :SOLANA_ERROR__OFFCHAIN_MESSAGES__NON_PRINTABLE_ASCII_CHARACTER
101
+ OFFCHAIN_MESSAGES__MESSAGE_TOO_LONG = :SOLANA_ERROR__OFFCHAIN_MESSAGES__MESSAGE_TOO_LONG
102
+ OFFCHAIN_MESSAGES__LEADING_ZERO_IN_SIGNING_DOMAIN = :SOLANA_ERROR__OFFCHAIN_MESSAGES__LEADING_ZERO_IN_SIGNING_DOMAIN
103
+
104
+ # ── Invariant violations (internal) ──────────────────────────────────────
105
+ INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_STATE_MISSING = :SOLANA_ERROR__INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_STATE_MISSING
106
+ INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE = :SOLANA_ERROR__INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE
107
+
108
+ ERROR_MESSAGES = T.let(
109
+ {
110
+ # General
111
+ INVALID_KEYPAIR_SEED_LENGTH => 'Keypair seed must be 32 bytes, got %{actual_length}',
112
+ INVALID_NONCE => 'The supplied nonce is not a valid nonce',
113
+
114
+ # Addresses
115
+ ADDRESSES__INVALID_BASE58_ENCODED_ADDRESS => 'Not a valid base58-encoded address',
116
+ ADDRESSES__INVALID_BYTE_LENGTH_FOR_ADDRESS => 'Expected 32 bytes for an address, got %{byte_length}',
117
+ ADDRESSES__STRING_LENGTH_OUT_OF_RANGE => 'Address string length out of range (32–44 chars), got %{actual_length}',
118
+ ADDRESSES__INVALID_ED25519_PUBLIC_KEY => 'The public key is not a valid Ed25519 public key',
119
+ ADDRESSES__SEEDS_POINT_ON_CURVE => 'The seeds resolve to a point on the Ed25519 curve; it cannot be used as a PDA',
120
+ ADDRESSES__MAX_SEED_LENGTH_EXCEEDED => 'A seed exceeds the maximum allowed length of 32 bytes, got %{actual_length}',
121
+ ADDRESSES__TOO_MANY_SEEDS => 'Too many seeds provided (max 16)',
122
+ ADDRESSES__FAILED_TO_FIND_VIABLE_PDA_BUMP_SEED => 'Could not find a viable bump seed for the given program and seeds',
123
+ ADDRESSES__INVALID_SEEDS_POINT_ON_CURVE => 'The seeds result in a public key that lies on the Ed25519 curve',
124
+ ADDRESSES__PDA_BUMP_SEED_OUT_OF_RANGE => 'Bump seed must be in range [0, 255]',
125
+
126
+ # Accounts
127
+ ACCOUNTS__ACCOUNT_NOT_FOUND => 'Account not found at address %{address}',
128
+ ACCOUNTS__ONE_OR_MORE_ACCOUNTS_NOT_FOUND => 'One or more accounts were not found',
129
+ ACCOUNTS__EXPECTED_DECODED_ACCOUNT => 'Expected account at address %{address} to be decoded',
130
+ ACCOUNTS__EXPECTED_ALL_ACCOUNTS_TO_BE_DECODED => 'Expected all %{count} account(s) to be decoded',
131
+ ACCOUNTS__FAILED_TO_DECODE_ACCOUNT => 'Failed to decode account data at address %{address}',
132
+
133
+ # Keys
134
+ KEYS__INVALID_KEY_PAIR_BYTE_LENGTH => 'Key pair byte array must be 64 bytes, got %{byte_length}',
135
+ KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY => 'The public key does not match the private key',
136
+ KEYS__INVALID_SIGNATURE_BYTE_LENGTH => 'Signature must be 64 bytes, got %{actual_length}',
137
+ KEYS__SIGNATURE_STRING_LENGTH_OUT_OF_RANGE => 'Signature string length out of range (64–88 chars), got %{actual_length}',
138
+
139
+ # Instructions
140
+ INSTRUCTIONS__EXPECTED_TO_HAVE_ACCOUNTS => 'Expected instruction to have accounts',
141
+ INSTRUCTIONS__EXPECTED_TO_HAVE_DATA => 'Expected instruction to have data',
142
+ INSTRUCTIONS__PROGRAM_ADDRESS_MISMATCH => 'Instruction program address mismatch: expected %{expected}, got %{actual}',
143
+ INSTRUCTIONS__ACCOUNT_NOT_FOUND => 'Account at index %{index} not found in instruction',
144
+ INSTRUCTIONS__EXPECTED_ALL_ACCOUNTS_TO_BE_DECODED => 'Expected all instruction accounts to be decoded',
145
+
146
+ # Transactions
147
+ TRANSACTIONS__TRANSACTION_NOT_SIGNABLE => 'Transaction is not signable (missing fee payer or lifetime constraint)',
148
+ TRANSACTIONS__MISSING_SIGNER => 'Transaction is missing a required signer: %{address}',
149
+ TRANSACTIONS__VERSION_NUMBER_OUT_OF_RANGE => 'Transaction version %{version} is out of range',
150
+ TRANSACTIONS__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS => 'Failed to decompile address lookup table contents',
151
+ TRANSACTIONS__FAILED_TO_DECOMPILE_FEE_PAYER_MISSING => 'Failed to decompile transaction: fee payer is missing',
152
+ TRANSACTIONS__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND => 'Failed to decompile instruction: program address not found',
153
+ TRANSACTIONS__SEND_TRANSACTION_PREFLIGHT_FAILURE => 'Transaction simulation failed: %{message}',
154
+ TRANSACTIONS__BLOCKHASH_NOT_FOUND => 'Blockhash not found',
155
+ TRANSACTIONS__FAILED_TRANSACTION_PLAN => 'Failed to execute transaction plan',
156
+ TRANSACTIONS__TRANSACTION_EXPIRED_BLOCKHEIGHT_EXCEEDED => 'Transaction expired: block height exceeded',
157
+ TRANSACTIONS__TRANSACTION_EXPIRED_NONCE_INVALID => 'Transaction expired: nonce is no longer valid',
158
+ TRANSACTIONS__TRANSACTION_CONFIRMATION_TIMEOUT => 'Timed out waiting for transaction %{signature} to confirm',
159
+
160
+ # Signers
161
+ SIGNERS__ADDRESS_CANNOT_HAVE_MULTIPLE_SIGNERS => 'Address %{address} cannot be assigned multiple signers',
162
+ SIGNERS__EXPECTED_MESSAGE_MODIFYING_SIGNER => 'Expected signer to be a message modifying signer',
163
+ SIGNERS__EXPECTED_TRANSACTION_MODIFYING_SIGNER => 'Expected signer to be a transaction modifying signer',
164
+ SIGNERS__EXPECTED_TRANSACTION_SENDING_SIGNER => 'Expected signer to be a transaction sending signer',
165
+ SIGNERS__TRANSACTION_CANNOT_HAVE_MULTIPLE_SENDING_SIGNERS => 'Transaction cannot have multiple sending signers',
166
+ SIGNERS__WALLET_MULTISIGN_UNIMPLEMENTED => 'Wallet multisign is not yet implemented',
167
+
168
+ # Codecs
169
+ CODECS__CANNOT_DECODE_EMPTY_BYTE_ARRAY => 'Cannot decode an empty byte array',
170
+ CODECS__EXPECTED_POSITIVE_BYTE_LENGTH => 'Expected a positive byte length, got %{byte_length}',
171
+ CODECS__ENCODER_DECODER_FIXED_SIZE_MISMATCH => 'Encoder fixed size (%{encoder_size}) does not match decoder fixed size (%{decoder_size})',
172
+ CODECS__ENCODER_DECODER_MAX_SIZE_MISMATCH => 'Encoder max size (%{encoder_max}) does not match decoder max size (%{decoder_max})',
173
+ CODECS__INVALID_NUMBER_OF_ITEMS => 'Expected %{expected} items but got %{actual}',
174
+ CODECS__ENUM_DISCRIMINATOR_OUT_OF_RANGE => 'Enum discriminator %{discriminator} is out of range [0, %{max}]',
175
+ CODECS__UNION_VARIANT_OUT_OF_RANGE => 'Union variant index %{index} is out of range',
176
+ CODECS__OFFSET_OUT_OF_RANGE => 'Codec offset %{offset} is out of range for byte array of length %{byte_length}',
177
+ CODECS__SENTINEL_MISSING_IN_DECODED_BYTES => 'Sentinel bytes not found in decoded data',
178
+ CODECS__INVALID_STRING_FOR_BASE58_CODEC => 'Invalid base58 string: %{value}',
179
+ CODECS__INVALID_STRING_FOR_BASE64_CODEC => 'Invalid base64 string: %{value}',
180
+ CODECS__INVALID_STRING_FOR_HEX_CODEC => 'Invalid hex string: %{value}',
181
+ CODECS__INVALID_BYTE_LENGTH => 'Expected %{expected} bytes but got %{actual}',
182
+ CODECS__EXPECTED_ZERO_VALUE_TO_MATCH_ITEM_FIXED_DISCRIMINATOR => 'Expected zero value to match fixed discriminator',
183
+ CODECS__FIXED_NULLABLE_CANNOT_WRAP_VARIABLE_SIZE_CODEC => 'A fixed-size nullable codec cannot wrap a variable-size codec',
184
+
185
+ # RPC
186
+ RPC__INTEGER_OVERFLOW_WHILE_SERIALIZING_LARGE_INTEGER => 'Integer overflow while serializing large integer %{value}',
187
+ RPC__INTEGER_OVERFLOW_WHILE_DESERIALIZING_LARGE_INTEGER => 'Integer overflow while deserializing large integer %{value}',
188
+ RPC__TRANSPORT_HTTP_ERROR => 'HTTP transport error: %{status} %{message}',
189
+ RPC_SUBSCRIPTIONS__CANNOT_CREATE_SUBSCRIPTION_REQUEST => 'Cannot create subscription request',
190
+ RPC_SUBSCRIPTIONS__EXPECTED_SERVER_SUBSCRIPTION_ID => 'Expected server to return a subscription ID',
191
+ RPC_SUBSCRIPTIONS__CHANNEL_CLOSED_BEFORE_MESSAGE_BUFFERED => 'WebSocket channel closed before message could be buffered',
192
+ RPC_SUBSCRIPTIONS__CHANNEL_CONNECTION_CLOSED => 'WebSocket connection closed: %{reason}',
193
+ RPC_SUBSCRIPTIONS__WEBSOCKET_CONNECTION_FAILED => 'Failed to establish WebSocket connection to %{url}',
194
+
195
+ # Offchain messages
196
+ OFFCHAIN_MESSAGES__FAILED_TO_DECODE_MESSAGE => 'Failed to decode offchain message',
197
+ OFFCHAIN_MESSAGES__INVALID_MESSAGE_FORMAT => 'Invalid offchain message format',
198
+ OFFCHAIN_MESSAGES__INVALID_MESSAGE_VERSION => 'Invalid offchain message version %{version}',
199
+ OFFCHAIN_MESSAGES__NON_PRINTABLE_ASCII_CHARACTER => 'Offchain message v0 contains non-printable ASCII character at index %{index}',
200
+ OFFCHAIN_MESSAGES__MESSAGE_TOO_LONG => 'Offchain message is too long (%{length} bytes, max %{max})',
201
+ OFFCHAIN_MESSAGES__LEADING_ZERO_IN_SIGNING_DOMAIN => 'Offchain message signing domain must not start with a null byte',
202
+
203
+ # Invariant violations
204
+ INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_STATE_MISSING => 'Subscription iterator state is missing (internal error)',
205
+ INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE => 'Subscription iterator must not poll before resolving existing message (internal error)',
206
+ }.freeze,
207
+ T::Hash[Symbol, String]
208
+ )
209
+
210
+ sig { returns(Symbol) }
211
+ attr_reader :code
212
+
213
+ sig { returns(T::Hash[Symbol, T.untyped]) }
214
+ attr_reader :context
215
+
216
+ sig { params(code: Symbol, context: T::Hash[Symbol, T.untyped]).void }
217
+ def initialize(code, context = {})
218
+ @code = T.let(code, Symbol)
219
+ @context = T.let(context, T::Hash[Symbol, T.untyped])
220
+
221
+ template = ERROR_MESSAGES[code] || code.to_s
222
+ message = context.empty? ? template : (template % context rescue "#{template} #{context}")
223
+ super(message)
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,62 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # Mirrors @solana/fast-stable-stringify.
5
+ # Produces deterministic JSON by sorting Hash keys recursively.
6
+ # Used internally by RPC transport so request bodies are stable.
7
+ module Solana::Ruby::Kit
8
+ module FastStableStringify
9
+ extend T::Sig
10
+
11
+ module_function
12
+
13
+ # Serialize +value+ to a deterministic JSON string.
14
+ # Hash keys are sorted lexicographically at every level.
15
+ # Unsupported types (custom objects, Symbols as values, etc.) are treated
16
+ # as +nil+ (same behaviour as JSON.generate's default replacer).
17
+ sig { params(value: T.untyped).returns(String) }
18
+ def stringify(value)
19
+ serialize(value)
20
+ end
21
+
22
+ # Internal recursive serializer (exposed as module_function for testability).
23
+ sig { params(value: T.untyped).returns(String) }
24
+ def serialize(value) # rubocop:disable Metrics/MethodLength
25
+ case value
26
+ when NilClass then 'null'
27
+ when TrueClass then 'true'
28
+ when FalseClass then 'false'
29
+ when Integer
30
+ # Sorbet: plain integer — output as-is
31
+ value.to_s
32
+ when Float
33
+ # Match JSON behaviour: NaN / Infinity → null
34
+ if value.nan? || value.infinite?
35
+ 'null'
36
+ else
37
+ # Avoid trailing zeros while staying valid JSON
38
+ int = value.to_i
39
+ int.to_f == value ? int.to_s : value.to_s
40
+ end
41
+ when String
42
+ value.to_json
43
+ when Array
44
+ inner = value.map { |el| serialize(el) }.join(',')
45
+ "[#{inner}]"
46
+ when Hash
47
+ pairs = value.keys.map(&:to_s).sort.filter_map do |k|
48
+ raw_val = value[k] || value[k.to_sym]
49
+ serialized = serialize(raw_val)
50
+ next if serialized == 'undefined' # skip undefined-equivalent keys
51
+
52
+ "#{k.to_s.to_json}:#{serialized}"
53
+ end
54
+ "{#{pairs.join(',')}}"
55
+ else
56
+ # Symbol, custom class, etc. → null
57
+ 'null'
58
+ end
59
+ end
60
+ private_class_method :serialize
61
+ end
62
+ end
@@ -0,0 +1,29 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Solana::Ruby::Kit
5
+ # Function composition utilities.
6
+ # Mirrors the TypeScript package @solana/functional.
7
+ module Functional
8
+ extend T::Sig
9
+ module_function
10
+
11
+ # Passes an initial value through a series of single-argument callables,
12
+ # returning the final result.
13
+ #
14
+ # Mirrors TypeScript's `pipe(init, fn1, fn2, ...)`. TypeScript requires
15
+ # 10 overloads for static typing; Ruby achieves the same with a single
16
+ # variadic implementation.
17
+ #
18
+ # @example Building a transaction message
19
+ # msg = Solana::Ruby::Kit::Functional.pipe(
20
+ # Solana::Ruby::Kit::TransactionMessages.create_transaction_message(version: 0),
21
+ # ->(tx) { Solana::Ruby::Kit::TransactionMessages.set_fee_payer(fee_payer_address, tx) },
22
+ # ->(tx) { Solana::Ruby::Kit::TransactionMessages.set_blockhash_lifetime(blockhash, tx) },
23
+ # )
24
+ sig { params(value: T.untyped, fns: T::Array[T.proc.params(arg0: T.untyped).returns(T.untyped)]).returns(T.untyped) }
25
+ def pipe(value, *fns)
26
+ fns.reduce(value) { |acc, fn| T.unsafe(fn).call(acc) }
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,27 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Solana::Ruby::Kit
5
+ module InstructionPlans
6
+ # A plan that wraps a single instruction.
7
+ class SingleInstructionPlan < T::Struct
8
+ const :instruction, Instructions::Instruction
9
+ end
10
+
11
+ # A plan that executes a sequence of sub-plans in order.
12
+ # When +divisible+ is true the planner may split steps across transactions.
13
+ class SequentialInstructionPlan < T::Struct
14
+ const :steps, T::Array[T.untyped] # Array[InstructionPlan]
15
+ const :divisible, T::Boolean, default: false
16
+ end
17
+
18
+ # A plan whose sub-plans may execute concurrently or be packed into the
19
+ # same transaction in any order.
20
+ class ParallelInstructionPlan < T::Struct
21
+ const :plans, T::Array[T.untyped] # Array[InstructionPlan]
22
+ end
23
+
24
+ # Union type alias (used only in doc strings; Ruby is duck-typed).
25
+ # InstructionPlan = SingleInstructionPlan | SequentialInstructionPlan | ParallelInstructionPlan
26
+ end
27
+ end
@@ -0,0 +1,47 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # Mirrors @solana/instruction-plans.
5
+ # An InstructionPlan describes operations that may span multiple transactions.
6
+ require_relative 'instruction_plans/plans'
7
+
8
+ module Solana::Ruby::Kit
9
+ module InstructionPlans
10
+ extend T::Sig
11
+
12
+ module_function
13
+
14
+ # Build a plan that wraps a single instruction.
15
+ sig { params(instruction: Instructions::Instruction).returns(SingleInstructionPlan) }
16
+ def single_instruction_plan(instruction)
17
+ SingleInstructionPlan.new(instruction: instruction)
18
+ end
19
+
20
+ # Build a sequential plan from an array of sub-plans.
21
+ sig { params(steps: T::Array[T.untyped], divisible: T::Boolean).returns(SequentialInstructionPlan) }
22
+ def sequential_instruction_plan(steps, divisible: false)
23
+ SequentialInstructionPlan.new(steps: steps, divisible: divisible)
24
+ end
25
+
26
+ # Build a parallel plan from an array of sub-plans.
27
+ sig { params(plans: T::Array[T.untyped]).returns(ParallelInstructionPlan) }
28
+ def parallel_instruction_plan(plans)
29
+ ParallelInstructionPlan.new(plans: plans)
30
+ end
31
+
32
+ # Flatten a plan tree into a single ordered Array of Instructions.
33
+ sig { params(plan: T.untyped).returns(T::Array[Instructions::Instruction]) }
34
+ def flatten_instruction_plan(plan)
35
+ case plan
36
+ when SingleInstructionPlan
37
+ [plan.instruction]
38
+ when SequentialInstructionPlan
39
+ plan.steps.flat_map { |s| flatten_instruction_plan(s) }
40
+ when ParallelInstructionPlan
41
+ plan.plans.flat_map { |p| flatten_instruction_plan(p) }
42
+ else
43
+ Kernel.raise ArgumentError, "Unknown InstructionPlan type: #{plan.class}"
44
+ end
45
+ end
46
+ end
47
+ end