solana-ruby-kit 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12771cac3706269f104b012ee691d4cd42856f61560d54b9e959308184c90be6
4
- data.tar.gz: 52bc18d7dff9bce3c2f27115772275e8a2b0decd985faa4ef2f43723056030d9
3
+ metadata.gz: 4a6261831e387559dd450f4da24966c5fb21a3d6d079eca64bc1c19577f1517c
4
+ data.tar.gz: bb79df5d4217eb20f8fc07b9ae89e6ce070fbcb38f193b4e775d38b1cfe72998
5
5
  SHA512:
6
- metadata.gz: c1f7feeaf404a2911bf4498377e2e2426f10357cd415cb70e4a57d70b05a81a73b51b817983e33bd1a9aa169ee9dcd9d80018c050964b0a51ada8afebba772d2
7
- data.tar.gz: 6f3f9b4d8de10136120b08d4be8927ba13546edc902f81a3f46b64e89e5947b9623ef3645857c721c44bd5bc7d16c4ba13eee74d65bca03d09bee78fd0b3a932
6
+ metadata.gz: 6259e78a36520927779d1912d3cd2190690b207fdb538473ed2eabc47e153f9e16380570043087ff94aba46019ef66c5be25383f237f82ad21949cccdb6200c9
7
+ data.tar.gz: 9155affe61d202d5e9847b9df5da10958ee85089410cf0bd2f856b77087710d38b3318c3c178057fa9112cfee16d5bb85f51b3d9ae36bfcfce1c01b351f04a00
@@ -116,6 +116,10 @@ module Solana::Ruby::Kit
116
116
  INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN = :SOLANA_ERROR__INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN
117
117
  INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN = :SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN
118
118
 
119
+ # ── Wallet Standard ───────────────────────────────────────────────────────
120
+ WALLET_STANDARD__INVALID_WIRE_FORMAT = :SOLANA_ERROR__WALLET_STANDARD__INVALID_WIRE_FORMAT
121
+ WALLET_STANDARD__SIGNATURE_VERIFICATION_FAILED = :SOLANA_ERROR__WALLET_STANDARD__SIGNATURE_VERIFICATION_FAILED
122
+
119
123
  # ── Invariant violations (internal) ──────────────────────────────────────
120
124
  INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_STATE_MISSING = :SOLANA_ERROR__INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_STATE_MISSING
121
125
  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
@@ -227,6 +231,10 @@ module Solana::Ruby::Kit
227
231
  INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN => 'Instruction plan is empty and produced no transaction messages',
228
232
  INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN => 'Failed to execute transaction plan',
229
233
 
234
+ # Wallet Standard
235
+ WALLET_STANDARD__INVALID_WIRE_FORMAT => 'Invalid transaction wire format: %{reason}',
236
+ WALLET_STANDARD__SIGNATURE_VERIFICATION_FAILED => 'Signature verification failed for signer %{address}',
237
+
230
238
  # Invariant violations
231
239
  INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_STATE_MISSING => 'Subscription iterator state is missing (internal error)',
232
240
  INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE => 'Subscription iterator must not poll before resolving existing message (internal error)',
@@ -0,0 +1,162 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../addresses/address'
5
+ require_relative '../encoding/base58'
6
+ require_relative '../instructions/instruction'
7
+ require_relative '../instructions/accounts'
8
+ require_relative '../sysvars/addresses'
9
+ require_relative 'system_program'
10
+
11
+ module Solana::Ruby::Kit
12
+ module Programs
13
+ # Ruby interface for the Solana Stake Program.
14
+ #
15
+ # The Stake Program manages stake accounts used for delegating SOL to
16
+ # validators and earning staking rewards. Instruction data uses
17
+ # little-endian encoding: a u32 discriminator followed by any
18
+ # instruction-specific fields.
19
+ #
20
+ # Reference: https://github.com/solana-labs/solana-web3.js/blob/master/packages/library-legacy/src/programs/stake.ts
21
+ module StakeProgram
22
+ extend T::Sig
23
+
24
+ # The Stake Program address.
25
+ PROGRAM_ID = T.let(
26
+ Addresses::Address.new('Stake11111111111111111111111111111111111111'),
27
+ Addresses::Address
28
+ )
29
+
30
+ # The Stake Config sysvar / config account.
31
+ STAKE_CONFIG_ID = T.let(
32
+ Addresses::Address.new('StakeConfig11111111111111111111111111111111'),
33
+ Addresses::Address
34
+ )
35
+
36
+ # Number of bytes required to store a stake account on-chain.
37
+ STAKE_ACCOUNT_SPACE = T.let(200, Integer)
38
+
39
+ # Instruction discriminators (u32 little-endian).
40
+ DISCRIMINATOR_INITIALIZE = T.let(0, Integer)
41
+ DISCRIMINATOR_DELEGATE = T.let(2, Integer)
42
+
43
+ module_function
44
+
45
+ # Builds the two instructions needed to create and initialise a new stake
46
+ # account funded from +from+.
47
+ #
48
+ # Instruction 0 — System Program createAccount
49
+ # Data layout (52 bytes):
50
+ # [0..3] u32 LE = 0 (CREATE_ACCOUNT discriminant)
51
+ # [4..11] u64 LE = lamports
52
+ # [12..19] u64 LE = 200 (STAKE_ACCOUNT_SPACE)
53
+ # [20..51] 32 bytes (Stake Program ID — the owner to assign)
54
+ # Pack string: 'VQ<Q<a32'
55
+ #
56
+ # Instruction 1 — Stake Program Initialize
57
+ # Data layout (116 bytes):
58
+ # [0..3] u32 LE = 0 (Initialize discriminant)
59
+ # [4..35] 32 bytes (authorized staker pubkey)
60
+ # [36..67] 32 bytes (authorized withdrawer pubkey)
61
+ # [68..75] i64 LE = 0 (lockup unix_timestamp — no lockup)
62
+ # [76..83] u64 LE = 0 (lockup epoch — no lockup)
63
+ # [84..115] 32 bytes = \x00*32 (lockup custodian — zero pubkey)
64
+ # Pack string: 'Va32a32q<Q<a32'
65
+ #
66
+ # @param from [Addresses::Address] funding account (writable, signer)
67
+ # @param stake_account [Addresses::Address] new stake account (writable, signer)
68
+ # @param authorized [Addresses::Address] pubkey set as both staker and withdrawer
69
+ # @param lamports [Integer] total lamports to fund the stake account
70
+ # @return [T::Array[Instructions::Instruction]]
71
+ sig do
72
+ params(
73
+ from: Addresses::Address,
74
+ stake_account: Addresses::Address,
75
+ authorized: Addresses::Address,
76
+ lamports: Integer
77
+ ).returns(T::Array[Instructions::Instruction])
78
+ end
79
+ def create_account_instructions(from:, stake_account:, authorized:, lamports:)
80
+ stake_program_bytes = Encoding::Base58.decode(PROGRAM_ID.value)
81
+ authorized_bytes = Encoding::Base58.decode(authorized.value)
82
+ zero_pubkey = ("\x00" * 32).b
83
+
84
+ # Instruction 0: System Program createAccount (discriminant = 0)
85
+ create_data = [0, lamports, STAKE_ACCOUNT_SPACE, stake_program_bytes].pack('VQ<Q<a32').b
86
+
87
+ create_account_ix = Instructions::Instruction.new(
88
+ program_address: SystemProgram::PROGRAM_ID,
89
+ accounts: [
90
+ Instructions.writable_signer_account(from), # 0 from
91
+ Instructions.writable_signer_account(stake_account), # 1 stake_account
92
+ ],
93
+ data: create_data
94
+ )
95
+
96
+ # Instruction 1: Stake Program Initialize
97
+ initialize_data = [
98
+ DISCRIMINATOR_INITIALIZE,
99
+ authorized_bytes, # staker
100
+ authorized_bytes, # withdrawer
101
+ 0, # lockup.unix_timestamp (i64)
102
+ 0, # lockup.epoch (u64)
103
+ zero_pubkey # lockup.custodian
104
+ ].pack('Va32a32q<Q<a32').b
105
+
106
+ initialize_ix = Instructions::Instruction.new(
107
+ program_address: PROGRAM_ID,
108
+ accounts: [
109
+ Instructions.writable_account(stake_account), # 0 stake_account
110
+ Instructions.readonly_account(Addresses::Address.new(Sysvars::SYSVAR_RENT_ADDRESS)), # 1 sysvar rent
111
+ ],
112
+ data: initialize_data
113
+ )
114
+
115
+ [create_account_ix, initialize_ix]
116
+ end
117
+
118
+ # Builds a DelegateStake instruction that delegates an initialised stake
119
+ # account to a validator vote account.
120
+ #
121
+ # Data layout (4 bytes):
122
+ # [0..3] u32 LE = 2 (DelegateStake discriminant)
123
+ # Pack string: 'V'
124
+ #
125
+ # Accounts:
126
+ # 0. stake_account — writable, not signer
127
+ # 1. vote_account — readonly, not signer
128
+ # 2. SYSVAR_CLOCK — readonly, not signer
129
+ # 3. SYSVAR_STAKE_HISTORY — readonly, not signer
130
+ # 4. STAKE_CONFIG_ID — readonly, not signer
131
+ # 5. authorized — readonly, signer
132
+ #
133
+ # @param stake_account [Addresses::Address] stake account to delegate
134
+ # @param vote_account [Addresses::Address] validator's vote account
135
+ # @param authorized [Addresses::Address] authorised staker (must sign)
136
+ # @return [Instructions::Instruction]
137
+ sig do
138
+ params(
139
+ stake_account: Addresses::Address,
140
+ vote_account: Addresses::Address,
141
+ authorized: Addresses::Address
142
+ ).returns(Instructions::Instruction)
143
+ end
144
+ def delegate_instruction(stake_account:, vote_account:, authorized:)
145
+ data = [DISCRIMINATOR_DELEGATE].pack('V').b
146
+
147
+ Instructions::Instruction.new(
148
+ program_address: PROGRAM_ID,
149
+ accounts: [
150
+ Instructions.writable_account(stake_account), # 0 stake_account
151
+ Instructions.readonly_account(vote_account), # 1 vote_account
152
+ Instructions.readonly_account(Addresses::Address.new(Sysvars::SYSVAR_CLOCK_ADDRESS)), # 2 clock
153
+ Instructions.readonly_account(Addresses::Address.new(Sysvars::SYSVAR_STAKE_HISTORY_ADDRESS)), # 3 stake history
154
+ Instructions.readonly_account(STAKE_CONFIG_ID), # 4 stake config
155
+ Instructions.readonly_signer_account(authorized), # 5 authorized staker
156
+ ],
157
+ data: data
158
+ )
159
+ end
160
+ end
161
+ end
162
+ end
@@ -5,6 +5,7 @@
5
5
  # returned in Solana transaction failures.
6
6
  require_relative 'programs/associated_token_account'
7
7
  require_relative 'programs/system_program'
8
+ require_relative 'programs/stake_program'
8
9
  #
9
10
  # A program error in a transaction result looks like:
10
11
  # { "InstructionError" => [0, { "Custom" => 1234 }] }
@@ -4,7 +4,7 @@
4
4
  module Solana
5
5
  module Ruby
6
6
  module Kit
7
- VERSION = '0.1.5'
7
+ VERSION = '0.1.7'
8
8
  end
9
9
  end
10
10
  end
@@ -0,0 +1,254 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'base64'
5
+ require 'rbnacl'
6
+ require_relative 'addresses/address'
7
+ require_relative 'keys/signatures'
8
+ require_relative 'transactions/transaction'
9
+ require_relative 'errors'
10
+
11
+ module Solana::Ruby::Kit
12
+ # Server-side Ruby port of the @solana/wallet-standard signTransaction interface.
13
+ #
14
+ # When a browser wallet (Phantom, Backpack, Solflare, …) calls
15
+ # wallet.features['solana:signTransaction'].signTransaction(transaction)
16
+ # the signed transaction is returned as serialised wire bytes. Rails receives
17
+ # those bytes (typically base64-encoded in a JSON request body) and uses this
18
+ # module to:
19
+ #
20
+ # 1. Decode the wire bytes back into a +Transactions::Transaction+ struct.
21
+ # 2. Verify every present Ed25519 signature against the message bytes.
22
+ # 3. Confirm that a specific wallet address (or all required addresses) signed.
23
+ # 4. Optionally broadcast the verified transaction via +Rpc+.
24
+ #
25
+ # Solana addresses ARE Ed25519 public keys (32-byte base58), so signature
26
+ # verification requires no external key lookup — the verify key is derived
27
+ # directly from the signer's address string.
28
+ #
29
+ # Typical Rails controller usage:
30
+ #
31
+ # wire_bytes = Base64.strict_decode64(params[:signed_transaction])
32
+ # tx = Solana::Ruby::Kit::WalletStandard.verify_signed_transaction!(wire_bytes)
33
+ # Solana::Ruby::Kit::Transactions.assert_fully_signed_transaction!(tx)
34
+ # # tx is now safe to inspect or forward to the RPC node
35
+ #
36
+ # Mirrors @solana/wallet-standard (anza-xyz/kit) and @wallet-standard/features.
37
+ module WalletStandard
38
+ extend T::Sig
39
+
40
+ # ── Wallet Standard feature identifiers ──────────────────────────────────
41
+ # String constants that identify which signing capabilities a browser wallet
42
+ # supports. Client-side JavaScript reads wallet.features to detect these;
43
+ # the constants are documented here so Rails code can reference them by name
44
+ # when building feature-negotiation metadata for the frontend.
45
+
46
+ # The wallet can sign transactions without broadcasting them.
47
+ SIGN_TRANSACTION = T.let('solana:signTransaction', String)
48
+ # The wallet can sign and broadcast in a single round-trip.
49
+ SIGN_AND_SEND_TRANSACTION = T.let('solana:signAndSendTransaction', String)
50
+ # The wallet can sign arbitrary off-chain messages.
51
+ SIGN_MESSAGE = T.let('solana:signMessage', String)
52
+ # Base Wallet Standard lifecycle features.
53
+ CONNECT = T.let('standard:connect', String)
54
+ DISCONNECT = T.let('standard:disconnect', String)
55
+ EVENTS = T.let('standard:events', String)
56
+
57
+ module_function
58
+
59
+ # ── Wire-format decoder ───────────────────────────────────────────────────
60
+
61
+ # Decodes a Solana wire-encoded transaction into a +Transactions::Transaction+.
62
+ #
63
+ # Accepts the binary output of a wallet's +signTransaction+ call. Binary
64
+ # (ASCII_8BIT) strings are used as-is; any other encoding is treated as
65
+ # standard base64 and decoded automatically.
66
+ #
67
+ # Solana wire format (legacy; v0 versioned transactions add a one-byte
68
+ # version prefix to the message section):
69
+ #
70
+ # [compact-u16] num_signatures
71
+ # [64 bytes × n] signature slots (all-zero slot means unfilled)
72
+ # [message bytes …]
73
+ #
74
+ # The returned Transaction carries:
75
+ # +message_bytes+ — raw bytes that were (or will be) signed
76
+ # +signatures+ — ordered Hash of signer_address → raw_sig_bytes or nil
77
+ #
78
+ # Raises +SolanaError::WALLET_STANDARD__INVALID_WIRE_FORMAT+ for malformed input.
79
+ #
80
+ # Mirrors +getTransactionDecoder()+ from @solana/transactions.
81
+ sig { params(wire_bytes: String).returns(Transactions::Transaction) }
82
+ def decode_wire_transaction(wire_bytes)
83
+ bytes = coerce_to_binary(wire_bytes)
84
+ offset = 0
85
+
86
+ # 1. Signature count (compact-u16)
87
+ sig_count, offset = decode_compact_u16(bytes, offset)
88
+ if sig_count > 19
89
+ Kernel.raise SolanaError.new(
90
+ SolanaError::WALLET_STANDARD__INVALID_WIRE_FORMAT,
91
+ reason: "signature count #{sig_count} exceeds the maximum of 19"
92
+ )
93
+ end
94
+
95
+ # 2. Signature slots — 64 bytes each; all-zero means the slot is unfilled.
96
+ zero64 = ("\x00" * 64).b
97
+ raw_sigs = T.let([], T::Array[T.nilable(String)])
98
+ sig_count.times do
99
+ slot = bytes[offset, 64]
100
+ if slot.nil? || slot.bytesize != 64
101
+ Kernel.raise SolanaError.new(
102
+ SolanaError::WALLET_STANDARD__INVALID_WIRE_FORMAT,
103
+ reason: 'truncated signature slot'
104
+ )
105
+ end
106
+ raw_sigs << (slot.b == zero64 ? nil : slot.b)
107
+ offset += 64
108
+ end
109
+
110
+ # 3. Everything after the signatures section is the message.
111
+ msg_slice = bytes[offset..]
112
+ if msg_slice.nil? || msg_slice.empty?
113
+ Kernel.raise SolanaError.new(
114
+ SolanaError::WALLET_STANDARD__INVALID_WIRE_FORMAT,
115
+ reason: 'message section is missing'
116
+ )
117
+ end
118
+ message_bytes = T.must(msg_slice).b
119
+
120
+ # 4. Determine version and locate the message header.
121
+ # Legacy transactions: first byte = num_required_signatures (0..127)
122
+ # Versioned (v0) trans.: first byte = 0x80 | version (≥ 128); skip it.
123
+ msg_pos = 0
124
+ first_byte = message_bytes.getbyte(msg_pos).to_i
125
+ msg_pos += 1 if (first_byte & 0x80) != 0
126
+
127
+ num_required_sigs = message_bytes.getbyte(msg_pos).to_i
128
+ msg_pos += 1
129
+ msg_pos += 2 # skip num_readonly_signed + num_readonly_unsigned
130
+
131
+ # 5. Parse the account list to recover signer addresses.
132
+ num_accounts, msg_pos = decode_compact_u16(message_bytes, msg_pos)
133
+ if msg_pos + (num_accounts * 32) > message_bytes.bytesize
134
+ Kernel.raise SolanaError.new(
135
+ SolanaError::WALLET_STANDARD__INVALID_WIRE_FORMAT,
136
+ reason: 'account table truncated'
137
+ )
138
+ end
139
+
140
+ account_addrs = T.let([], T::Array[String])
141
+ num_accounts.times do
142
+ account_addrs << Addresses.encode_address(message_bytes[msg_pos, 32].b)
143
+ msg_pos += 32
144
+ end
145
+
146
+ # 6. Build the signatures map.
147
+ # The first +num_required_sigs+ accounts in the account table are the
148
+ # signers; their raw_sigs slots are positionally aligned.
149
+ signer_addrs = account_addrs[0, num_required_sigs]
150
+ signatures = T.let({}, T::Hash[String, T.nilable(String)])
151
+ signer_addrs.each_with_index { |addr, i| signatures[addr] = raw_sigs[i] }
152
+
153
+ Transactions::Transaction.new(message_bytes: message_bytes, signatures: signatures)
154
+ end
155
+
156
+ # ── Signature verification ────────────────────────────────────────────────
157
+
158
+ # Verifies every filled (non-nil) signature in +transaction.signatures+.
159
+ #
160
+ # For each signed slot the signer's Ed25519 public key is reconstructed
161
+ # directly from their address (Solana addresses are base58-encoded 32-byte
162
+ # Ed25519 public keys), so no external key lookup is required.
163
+ #
164
+ # Raises +SolanaError::WALLET_STANDARD__SIGNATURE_VERIFICATION_FAILED+ if
165
+ # any signature does not verify. Nil (unfilled) slots are skipped silently;
166
+ # use +Transactions.assert_fully_signed_transaction!+ to enforce that all
167
+ # required signers have signed.
168
+ sig { params(transaction: Transactions::Transaction).void }
169
+ def verify_transaction_signatures!(transaction)
170
+ transaction.signatures.each do |addr_str, sig_raw|
171
+ next if sig_raw.nil?
172
+
173
+ verify_key = RbNaCl::VerifyKey.new(Addresses.decode_address(Addresses::Address.new(addr_str)))
174
+ sig_bytes = Keys::SignatureBytes.new(sig_raw)
175
+
176
+ unless Keys.verify_signature(verify_key, sig_bytes, transaction.message_bytes)
177
+ Kernel.raise SolanaError.new(
178
+ SolanaError::WALLET_STANDARD__SIGNATURE_VERIFICATION_FAILED,
179
+ address: addr_str
180
+ )
181
+ end
182
+ end
183
+ end
184
+
185
+ # Convenience method for Rails controllers: decode wire bytes received from a
186
+ # browser wallet and verify all present signatures in one call.
187
+ #
188
+ # Returns the decoded +Transaction+ on success.
189
+ # Raises +SolanaError+ for malformed wire bytes or any invalid signature.
190
+ # Does NOT require the transaction to be fully signed; follow up with
191
+ # +Transactions.assert_fully_signed_transaction!+ when broadcasting.
192
+ #
193
+ # Accepts binary or base64-encoded input (see +decode_wire_transaction+).
194
+ sig { params(wire_bytes: String).returns(Transactions::Transaction) }
195
+ def verify_signed_transaction!(wire_bytes)
196
+ tx = decode_wire_transaction(wire_bytes)
197
+ verify_transaction_signatures!(tx)
198
+ tx
199
+ end
200
+
201
+ # Returns +true+ if +address+ has provided a cryptographically valid
202
+ # signature for +transaction+; +false+ if the slot is absent, nil, or the
203
+ # signature does not verify against +transaction.message_bytes+.
204
+ sig { params(transaction: Transactions::Transaction, address: Addresses::Address).returns(T::Boolean) }
205
+ def signed_by?(transaction, address)
206
+ sig_raw = transaction.signatures[address.value]
207
+ return false if sig_raw.nil?
208
+
209
+ verify_key = RbNaCl::VerifyKey.new(Addresses.decode_address(address))
210
+ sig_bytes = Keys::SignatureBytes.new(sig_raw)
211
+ Keys.verify_signature(verify_key, sig_bytes, transaction.message_bytes)
212
+ end
213
+
214
+ # ── Private helpers ───────────────────────────────────────────────────────
215
+
216
+ # Forces +input+ to a binary (ASCII_8BIT) String.
217
+ # ASCII_8BIT input is returned as-is; anything else is treated as base64.
218
+ sig { params(input: String).returns(String) }
219
+ def coerce_to_binary(input)
220
+ return input.b if input.encoding == ::Encoding::ASCII_8BIT
221
+
222
+ stripped = input.strip
223
+ begin
224
+ Base64.strict_decode64(stripped).b
225
+ rescue ArgumentError
226
+ Base64.decode64(stripped).b
227
+ end
228
+ end
229
+ private_class_method :coerce_to_binary
230
+
231
+ # Reads a Solana compact-u16 from +bytes+ starting at +offset+.
232
+ # Returns +[decoded_integer, next_offset]+.
233
+ sig { params(bytes: String, offset: Integer).returns([Integer, Integer]) }
234
+ def decode_compact_u16(bytes, offset)
235
+ value = 0
236
+ shift = 0
237
+ loop do
238
+ byte = bytes.getbyte(offset)
239
+ if byte.nil?
240
+ Kernel.raise SolanaError.new(
241
+ SolanaError::WALLET_STANDARD__INVALID_WIRE_FORMAT,
242
+ reason: 'compact-u16 read past end of buffer'
243
+ )
244
+ end
245
+ offset += 1
246
+ value |= (byte & 0x7f) << shift
247
+ shift += 7
248
+ break unless (byte & 0x80) != 0
249
+ end
250
+ [value, offset]
251
+ end
252
+ private_class_method :decode_compact_u16
253
+ end
254
+ end
@@ -41,6 +41,9 @@ require_relative 'kit/rpc_subscriptions'
41
41
  # ── Plugin system ─────────────────────────────────────────────────────────────
42
42
  require_relative 'kit/plugin_core'
43
43
 
44
+ # ── Wallet Standard (server-side signature verification) ──────────────────────
45
+ require_relative 'kit/wallet_standard'
46
+
44
47
  # ── Higher-level helpers ──────────────────────────────────────────────────────
45
48
  require_relative 'kit/offchain_messages'
46
49
  require_relative 'kit/programs'
@@ -75,6 +78,7 @@ require_relative 'kit/instruction_plans'
75
78
  # Solana::Ruby::Kit::Sysvars — sysvar fetch/decode (@solana/sysvars)
76
79
  # Solana::Ruby::Kit::TransactionConfirmation — confirmation polling (@solana/transaction-confirmation)
77
80
  # Solana::Ruby::Kit::InstructionPlans — multi-tx planning (@solana/instruction-plans)
81
+ # Solana::Ruby::Kit::WalletStandard — server-side sig verify (@solana/wallet-standard)
78
82
 
79
83
  module Solana::Ruby::Kit
80
84
  extend T::Sig
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solana-ruby-kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paul Zupan, Idhra Inc.
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-04-09 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: openssl
@@ -231,6 +231,7 @@ files:
231
231
  - lib/solana/ruby/kit/plugin_core.rb
232
232
  - lib/solana/ruby/kit/programs.rb
233
233
  - lib/solana/ruby/kit/programs/associated_token_account.rb
234
+ - lib/solana/ruby/kit/programs/stake_program.rb
234
235
  - lib/solana/ruby/kit/programs/system_program.rb
235
236
  - lib/solana/ruby/kit/promises.rb
236
237
  - lib/solana/ruby/kit/railtie.rb
@@ -295,6 +296,7 @@ files:
295
296
  - lib/solana/ruby/kit/transactions/compiler.rb
296
297
  - lib/solana/ruby/kit/transactions/transaction.rb
297
298
  - lib/solana/ruby/kit/version.rb
299
+ - lib/solana/ruby/kit/wallet_standard.rb
298
300
  - solana-ruby-kit.gemspec
299
301
  licenses:
300
302
  - MIT
@@ -313,7 +315,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
313
315
  - !ruby/object:Gem::Version
314
316
  version: '0'
315
317
  requirements: []
316
- rubygems_version: 3.6.2
318
+ rubygems_version: 4.0.10
317
319
  specification_version: 4
318
320
  summary: Ruby port of the Anza TypeScript SDK (@anza-xyz/kit)
319
321
  test_files: []