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.
- checksums.yaml +7 -0
- data/lib/core_extensions/tapioca/name_patch.rb +32 -0
- data/lib/core_extensions/tapioca/required_ancestors.rb +13 -0
- data/lib/generators/solana/ruby/kit/install/install_generator.rb +17 -0
- data/lib/generators/solana/ruby/kit/install/templates/solana_ruby_kit.rb.tt +8 -0
- data/lib/solana/ruby/kit/accounts/account.rb +47 -0
- data/lib/solana/ruby/kit/accounts/maybe_account.rb +86 -0
- data/lib/solana/ruby/kit/accounts.rb +6 -0
- data/lib/solana/ruby/kit/addresses/address.rb +133 -0
- data/lib/solana/ruby/kit/addresses/curve.rb +112 -0
- data/lib/solana/ruby/kit/addresses/program_derived_address.rb +155 -0
- data/lib/solana/ruby/kit/addresses/public_key.rb +39 -0
- data/lib/solana/ruby/kit/addresses.rb +11 -0
- data/lib/solana/ruby/kit/codecs/bytes.rb +58 -0
- data/lib/solana/ruby/kit/codecs/codec.rb +135 -0
- data/lib/solana/ruby/kit/codecs/data_structures.rb +177 -0
- data/lib/solana/ruby/kit/codecs/decoder.rb +43 -0
- data/lib/solana/ruby/kit/codecs/encoder.rb +52 -0
- data/lib/solana/ruby/kit/codecs/numbers.rb +217 -0
- data/lib/solana/ruby/kit/codecs/strings.rb +116 -0
- data/lib/solana/ruby/kit/codecs.rb +25 -0
- data/lib/solana/ruby/kit/configuration.rb +48 -0
- data/lib/solana/ruby/kit/encoding/base58.rb +62 -0
- data/lib/solana/ruby/kit/errors.rb +226 -0
- data/lib/solana/ruby/kit/fast_stable_stringify.rb +62 -0
- data/lib/solana/ruby/kit/functional.rb +29 -0
- data/lib/solana/ruby/kit/instruction_plans/plans.rb +27 -0
- data/lib/solana/ruby/kit/instruction_plans.rb +47 -0
- data/lib/solana/ruby/kit/instructions/accounts.rb +80 -0
- data/lib/solana/ruby/kit/instructions/instruction.rb +71 -0
- data/lib/solana/ruby/kit/instructions/roles.rb +84 -0
- data/lib/solana/ruby/kit/instructions.rb +7 -0
- data/lib/solana/ruby/kit/keys/key_pair.rb +84 -0
- data/lib/solana/ruby/kit/keys/private_key.rb +39 -0
- data/lib/solana/ruby/kit/keys/public_key.rb +31 -0
- data/lib/solana/ruby/kit/keys/signatures.rb +171 -0
- data/lib/solana/ruby/kit/keys.rb +11 -0
- data/lib/solana/ruby/kit/offchain_messages/codec.rb +107 -0
- data/lib/solana/ruby/kit/offchain_messages/message.rb +22 -0
- data/lib/solana/ruby/kit/offchain_messages.rb +16 -0
- data/lib/solana/ruby/kit/options/option.rb +132 -0
- data/lib/solana/ruby/kit/options.rb +5 -0
- data/lib/solana/ruby/kit/plugin_core.rb +58 -0
- data/lib/solana/ruby/kit/programs.rb +42 -0
- data/lib/solana/ruby/kit/promises.rb +85 -0
- data/lib/solana/ruby/kit/railtie.rb +18 -0
- data/lib/solana/ruby/kit/rpc/api/get_account_info.rb +76 -0
- data/lib/solana/ruby/kit/rpc/api/get_balance.rb +41 -0
- data/lib/solana/ruby/kit/rpc/api/get_block_height.rb +29 -0
- data/lib/solana/ruby/kit/rpc/api/get_epoch_info.rb +47 -0
- data/lib/solana/ruby/kit/rpc/api/get_latest_blockhash.rb +52 -0
- data/lib/solana/ruby/kit/rpc/api/get_minimum_balance_for_rent_exemption.rb +29 -0
- data/lib/solana/ruby/kit/rpc/api/get_multiple_accounts.rb +56 -0
- data/lib/solana/ruby/kit/rpc/api/get_program_accounts.rb +60 -0
- data/lib/solana/ruby/kit/rpc/api/get_signature_statuses.rb +56 -0
- data/lib/solana/ruby/kit/rpc/api/get_slot.rb +30 -0
- data/lib/solana/ruby/kit/rpc/api/get_token_account_balance.rb +38 -0
- data/lib/solana/ruby/kit/rpc/api/get_token_accounts_by_owner.rb +48 -0
- data/lib/solana/ruby/kit/rpc/api/get_transaction.rb +36 -0
- data/lib/solana/ruby/kit/rpc/api/get_vote_accounts.rb +62 -0
- data/lib/solana/ruby/kit/rpc/api/is_blockhash_valid.rb +41 -0
- data/lib/solana/ruby/kit/rpc/api/request_airdrop.rb +35 -0
- data/lib/solana/ruby/kit/rpc/api/send_transaction.rb +61 -0
- data/lib/solana/ruby/kit/rpc/api/simulate_transaction.rb +47 -0
- data/lib/solana/ruby/kit/rpc/client.rb +83 -0
- data/lib/solana/ruby/kit/rpc/transport.rb +137 -0
- data/lib/solana/ruby/kit/rpc.rb +13 -0
- data/lib/solana/ruby/kit/rpc_parsed_types/address_lookup_table.rb +33 -0
- data/lib/solana/ruby/kit/rpc_parsed_types/nonce_account.rb +33 -0
- data/lib/solana/ruby/kit/rpc_parsed_types/stake_account.rb +51 -0
- data/lib/solana/ruby/kit/rpc_parsed_types/token_account.rb +52 -0
- data/lib/solana/ruby/kit/rpc_parsed_types/vote_account.rb +38 -0
- data/lib/solana/ruby/kit/rpc_parsed_types.rb +16 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/api/account_notifications.rb +29 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/api/logs_notifications.rb +28 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/api/program_notifications.rb +30 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/api/root_notifications.rb +19 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/api/signature_notifications.rb +28 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/api/slot_notifications.rb +19 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/autopinger.rb +42 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/client.rb +80 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/subscription.rb +58 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/transport.rb +163 -0
- data/lib/solana/ruby/kit/rpc_subscriptions.rb +12 -0
- data/lib/solana/ruby/kit/rpc_types/account_info.rb +53 -0
- data/lib/solana/ruby/kit/rpc_types/cluster_url.rb +56 -0
- data/lib/solana/ruby/kit/rpc_types/commitment.rb +52 -0
- data/lib/solana/ruby/kit/rpc_types/lamports.rb +43 -0
- data/lib/solana/ruby/kit/rpc_types.rb +8 -0
- data/lib/solana/ruby/kit/signers/keypair_signer.rb +126 -0
- data/lib/solana/ruby/kit/signers.rb +5 -0
- data/lib/solana/ruby/kit/subscribable/async_iterable.rb +80 -0
- data/lib/solana/ruby/kit/subscribable/data_publisher.rb +90 -0
- data/lib/solana/ruby/kit/subscribable.rb +13 -0
- data/lib/solana/ruby/kit/sysvars/addresses.rb +19 -0
- data/lib/solana/ruby/kit/sysvars/clock.rb +37 -0
- data/lib/solana/ruby/kit/sysvars/epoch_schedule.rb +34 -0
- data/lib/solana/ruby/kit/sysvars/last_restart_slot.rb +22 -0
- data/lib/solana/ruby/kit/sysvars/rent.rb +29 -0
- data/lib/solana/ruby/kit/sysvars.rb +33 -0
- data/lib/solana/ruby/kit/transaction_confirmation.rb +159 -0
- data/lib/solana/ruby/kit/transaction_messages/transaction_message.rb +168 -0
- data/lib/solana/ruby/kit/transaction_messages.rb +5 -0
- data/lib/solana/ruby/kit/transactions/transaction.rb +135 -0
- data/lib/solana/ruby/kit/transactions.rb +5 -0
- data/lib/solana/ruby/kit/version.rb +10 -0
- data/lib/solana/ruby/kit.rb +100 -0
- data/solana-ruby-kit.gemspec +29 -0
- metadata +311 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module OffchainMessages
|
|
6
|
+
# An off-chain message ready for signing.
|
|
7
|
+
# Mirrors the OffchainMessage type from @solana/signers.
|
|
8
|
+
class Message < T::Struct
|
|
9
|
+
# Header version: 0 = legacy ASCII, 1 = extended UTF-8.
|
|
10
|
+
const :version, Integer
|
|
11
|
+
|
|
12
|
+
# Application domain (up to 255 bytes).
|
|
13
|
+
const :domain, String
|
|
14
|
+
|
|
15
|
+
# UTF-8 message body.
|
|
16
|
+
const :message, String
|
|
17
|
+
|
|
18
|
+
# Optional application-specific domain (version ≥ 1 only).
|
|
19
|
+
const :application_domain, T.nilable(String), default: nil
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'rbnacl'
|
|
5
|
+
|
|
6
|
+
# Mirrors @solana/signers off-chain message signing.
|
|
7
|
+
require_relative 'offchain_messages/message'
|
|
8
|
+
require_relative 'offchain_messages/codec'
|
|
9
|
+
|
|
10
|
+
module Solana::Ruby::Kit
|
|
11
|
+
module OffchainMessages
|
|
12
|
+
# Re-export codec helpers at module level for convenience.
|
|
13
|
+
extend T::Sig
|
|
14
|
+
extend Codec
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module Options
|
|
6
|
+
extend T::Sig
|
|
7
|
+
# Mirrors Rust's `Option<T>` pattern from @solana/options.
|
|
8
|
+
#
|
|
9
|
+
# TypeScript represents absence as `T | null`, but that collapses nested
|
|
10
|
+
# options: `Option<Option<T>>` becomes indistinguishable from `Option<T>`.
|
|
11
|
+
# This explicit discriminated union preserves that distinction.
|
|
12
|
+
|
|
13
|
+
# Represents the presence of a value — mirrors TypeScript's `Some<T>`.
|
|
14
|
+
class Some
|
|
15
|
+
extend T::Sig
|
|
16
|
+
extend T::Generic
|
|
17
|
+
|
|
18
|
+
Elem = type_member
|
|
19
|
+
|
|
20
|
+
sig { returns(Elem) }
|
|
21
|
+
attr_reader :value
|
|
22
|
+
|
|
23
|
+
sig { params(value: Elem).void }
|
|
24
|
+
def initialize(value)
|
|
25
|
+
@value = T.let(value, Elem)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
sig { returns(String) }
|
|
29
|
+
def inspect = "Some(#{T.unsafe(@value).inspect})"
|
|
30
|
+
|
|
31
|
+
sig { params(other: T.untyped).returns(T::Boolean) }
|
|
32
|
+
def ==(other)
|
|
33
|
+
!!(other.is_a?(Some) && T.unsafe(value) == T.unsafe(other.value))
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Represents the absence of a value — mirrors TypeScript's `None`.
|
|
38
|
+
class None
|
|
39
|
+
extend T::Sig
|
|
40
|
+
|
|
41
|
+
INSTANCE = T.let(new, None)
|
|
42
|
+
|
|
43
|
+
sig { returns(String) }
|
|
44
|
+
def inspect = 'None'
|
|
45
|
+
|
|
46
|
+
sig { params(other: T.untyped).returns(T::Boolean) }
|
|
47
|
+
def ==(other) = other.is_a?(None)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Sorbet type alias: Option<T> is either Some or None.
|
|
51
|
+
# Because Sorbet generics on non-class types are limited, we use T.untyped
|
|
52
|
+
# for the contained value at the type-alias level; callers rely on Some's
|
|
53
|
+
# generic parameter for per-use type safety.
|
|
54
|
+
Option = T.type_alias { T.any(Solana::Ruby::Kit::Options::Some[T.untyped], None) }
|
|
55
|
+
|
|
56
|
+
# OptionOrNullable mirrors TypeScript's `OptionOrNullable<T>`:
|
|
57
|
+
# accepts Option<T>, T, or nil — useful for codec input.
|
|
58
|
+
OptionOrNullable = T.type_alias { T.untyped }
|
|
59
|
+
|
|
60
|
+
module_function
|
|
61
|
+
|
|
62
|
+
# Wraps a value in Some. Mirrors `some<T>(value)`.
|
|
63
|
+
sig { params(value: T.untyped).returns(Solana::Ruby::Kit::Options::Some[T.untyped]) }
|
|
64
|
+
def some(value)
|
|
65
|
+
Some.new(value)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Returns the singleton None instance. Mirrors `none<T>()`.
|
|
69
|
+
sig { returns(None) }
|
|
70
|
+
def none
|
|
71
|
+
None::INSTANCE
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Returns true if the value is an Option (Some or None).
|
|
75
|
+
# Mirrors `isOption()`.
|
|
76
|
+
sig { params(input: T.untyped).returns(T::Boolean) }
|
|
77
|
+
def option?(input)
|
|
78
|
+
input.is_a?(Some) || input.is_a?(None)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Returns true if the option contains a value.
|
|
82
|
+
# Mirrors `isSome()`.
|
|
83
|
+
sig { params(opt: T.untyped).returns(T::Boolean) }
|
|
84
|
+
def some?(opt)
|
|
85
|
+
opt.is_a?(Some)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Returns true if the option is empty.
|
|
89
|
+
# Mirrors `isNone()`.
|
|
90
|
+
sig { params(opt: T.untyped).returns(T::Boolean) }
|
|
91
|
+
def none?(opt)
|
|
92
|
+
opt.is_a?(None)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Extracts the contained value, or returns a fallback.
|
|
96
|
+
# Mirrors `unwrapOption(option, fallback?)`.
|
|
97
|
+
#
|
|
98
|
+
# @param opt [Some, None]
|
|
99
|
+
# @param fallback [Proc, nil] called when opt is None; returns nil if omitted
|
|
100
|
+
sig { params(opt: T.untyped, fallback: T.nilable(T.proc.returns(T.untyped))).returns(T.untyped) }
|
|
101
|
+
def unwrap_option(opt, fallback = nil)
|
|
102
|
+
return opt.value if opt.is_a?(Some)
|
|
103
|
+
|
|
104
|
+
fallback ? fallback.call : nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Wraps a nullable (nil-able) value into an Option.
|
|
108
|
+
# Mirrors `wrapNullable()`.
|
|
109
|
+
sig { params(nullable: T.untyped).returns(T.any(Solana::Ruby::Kit::Options::Some[T.untyped], None)) }
|
|
110
|
+
def wrap_nullable(nullable)
|
|
111
|
+
nullable.nil? ? none : some(nullable)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Recursively unwraps nested Options within objects, arrays, and hashes.
|
|
115
|
+
# Mirrors `unwrapOptionRecursively()`.
|
|
116
|
+
#
|
|
117
|
+
# Primitives (Integer, Float, String, Symbol, true, false) and
|
|
118
|
+
# binary/typed-array equivalents are returned as-is.
|
|
119
|
+
sig { params(input: T.untyped, fallback: T.nilable(T.proc.returns(T.untyped))).returns(T.untyped) }
|
|
120
|
+
def unwrap_option_recursively(input, fallback = nil)
|
|
121
|
+
nxt = ->(x) { unwrap_option_recursively(x, fallback) }
|
|
122
|
+
|
|
123
|
+
case input
|
|
124
|
+
when Some then nxt.call(input.value)
|
|
125
|
+
when None then fallback ? fallback.call : nil
|
|
126
|
+
when Array then input.map { |el| nxt.call(el) }
|
|
127
|
+
when Hash then input.transform_values { |v| nxt.call(v) }
|
|
128
|
+
else input # primitives and opaque objects pass through
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Mirrors @solana/rpc-types plugin-core pattern (createEmptyClient / use).
|
|
5
|
+
#
|
|
6
|
+
# A PluginClient starts empty and is extended incrementally via #use.
|
|
7
|
+
# Each plugin is a callable that receives the current client and returns a
|
|
8
|
+
# Hash of { method_name => callable } which is merged into the client.
|
|
9
|
+
#
|
|
10
|
+
# client = Solana::Ruby::Kit::PluginCore.create_client
|
|
11
|
+
# .use(Solana::Ruby::Kit::Rpc::Api::ALL_METHODS)
|
|
12
|
+
#
|
|
13
|
+
module Solana::Ruby::Kit
|
|
14
|
+
class PluginClient
|
|
15
|
+
extend T::Sig
|
|
16
|
+
|
|
17
|
+
sig { void }
|
|
18
|
+
def initialize
|
|
19
|
+
@methods = T.let({}, T::Hash[Symbol, T.proc.params(args: T.untyped).returns(T.untyped)])
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Apply +plugin+ and return a new PluginClient with the additional methods.
|
|
23
|
+
# +plugin+ may be:
|
|
24
|
+
# - a Hash of { symbol => callable }
|
|
25
|
+
# - a callable that receives self and returns such a Hash
|
|
26
|
+
sig { params(plugin: T.untyped).returns(PluginClient) }
|
|
27
|
+
def use(plugin)
|
|
28
|
+
new_methods = plugin.respond_to?(:call) ? plugin.call(self) : plugin
|
|
29
|
+
extended = PluginClient.new
|
|
30
|
+
extended.instance_variable_set(:@methods, @methods.merge(new_methods.transform_keys(&:to_sym)))
|
|
31
|
+
extended
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
sig { params(name: Symbol, args: T.untyped, block: T.untyped).returns(T.untyped) }
|
|
35
|
+
def method_missing(name, *args, &block)
|
|
36
|
+
m = @methods[name]
|
|
37
|
+
return super unless m
|
|
38
|
+
|
|
39
|
+
T.unsafe(m).call(*args, &block)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
sig { params(name: Symbol, include_private: T::Boolean).returns(T::Boolean) }
|
|
43
|
+
def respond_to_missing?(name, include_private = false)
|
|
44
|
+
@methods.key?(name) || super
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
module PluginCore
|
|
49
|
+
extend T::Sig
|
|
50
|
+
|
|
51
|
+
module_function
|
|
52
|
+
|
|
53
|
+
sig { returns(PluginClient) }
|
|
54
|
+
def create_client
|
|
55
|
+
PluginClient.new
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Mirrors @solana/programs — helpers for inspecting custom program errors
|
|
5
|
+
# returned in Solana transaction failures.
|
|
6
|
+
#
|
|
7
|
+
# A program error in a transaction result looks like:
|
|
8
|
+
# { "InstructionError" => [0, { "Custom" => 1234 }] }
|
|
9
|
+
module Solana::Ruby::Kit
|
|
10
|
+
module Programs
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# Returns true when +err+ is a custom program error, optionally matching
|
|
16
|
+
# a specific error code.
|
|
17
|
+
sig { params(err: T.untyped, expected_code: T.nilable(Integer)).returns(T::Boolean) }
|
|
18
|
+
def program_error?(err, expected_code: nil)
|
|
19
|
+
code = get_program_error_code(err)
|
|
20
|
+
return false if code.nil?
|
|
21
|
+
|
|
22
|
+
expected_code ? code == expected_code : true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Extract the custom program error code from a transaction error hash.
|
|
26
|
+
# Returns nil when the error is not a custom program error.
|
|
27
|
+
sig { params(err: T.untyped).returns(T.nilable(Integer)) }
|
|
28
|
+
def get_program_error_code(err)
|
|
29
|
+
return nil unless err.is_a?(Hash)
|
|
30
|
+
|
|
31
|
+
instruction_error = err['InstructionError']
|
|
32
|
+
return nil unless instruction_error.is_a?(Array) && instruction_error.length == 2
|
|
33
|
+
|
|
34
|
+
inner = instruction_error[1]
|
|
35
|
+
return nil unless inner.is_a?(Hash) && inner.key?('Custom')
|
|
36
|
+
|
|
37
|
+
Kernel.Integer(inner['Custom'])
|
|
38
|
+
rescue TypeError, ArgumentError
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'timeout'
|
|
5
|
+
|
|
6
|
+
# Mirrors @solana/promises.
|
|
7
|
+
# Provides thread-safe race and timeout helpers used by the RPC subscription
|
|
8
|
+
# transport layer. Ruby threads replace JavaScript Promises throughout.
|
|
9
|
+
module Solana::Ruby::Kit
|
|
10
|
+
module Promises
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# Run each callable in a dedicated thread and return the first result.
|
|
16
|
+
# All losing threads are killed to avoid memory leaks — analogous to the
|
|
17
|
+
# JS safeRace() that cancels losing promises via AbortSignal.
|
|
18
|
+
#
|
|
19
|
+
# @param callables [Array<#call>] lambdas / procs to race
|
|
20
|
+
# @param timeout_secs [Float, nil] optional wall-clock limit
|
|
21
|
+
# @return the return value of whichever callable finishes first
|
|
22
|
+
# @raise [Timeout::Error] if timeout_secs is reached before any finishes
|
|
23
|
+
# @raise propagates any exception thrown by the winning callable
|
|
24
|
+
sig do
|
|
25
|
+
params(
|
|
26
|
+
callables: T::Array[T.proc.returns(T.untyped)],
|
|
27
|
+
timeout_secs: T.nilable(Float)
|
|
28
|
+
).returns(T.untyped)
|
|
29
|
+
end
|
|
30
|
+
def safe_race(callables, timeout_secs: nil)
|
|
31
|
+
result_q = Queue.new
|
|
32
|
+
threads = callables.map do |callable|
|
|
33
|
+
Thread.new do
|
|
34
|
+
value = callable.call
|
|
35
|
+
result_q.push([:ok, value])
|
|
36
|
+
rescue StandardError => e
|
|
37
|
+
result_q.push([:err, e])
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
begin
|
|
42
|
+
kind, payload = if timeout_secs
|
|
43
|
+
Timeout.timeout(timeout_secs) { result_q.pop }
|
|
44
|
+
else
|
|
45
|
+
result_q.pop
|
|
46
|
+
end
|
|
47
|
+
ensure
|
|
48
|
+
threads.each(&:kill)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
Kernel.raise payload if kind == :err
|
|
52
|
+
|
|
53
|
+
payload
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Execute +block+ with an optional wall-clock deadline.
|
|
57
|
+
# When +secs+ is nil the block runs without any deadline.
|
|
58
|
+
#
|
|
59
|
+
# @param secs [Float, nil]
|
|
60
|
+
# @raise [Timeout::Error] on deadline exceeded
|
|
61
|
+
sig do
|
|
62
|
+
type_parameters(:R)
|
|
63
|
+
.params(secs: T.nilable(Float), block: T.proc.returns(T.type_parameter(:R)))
|
|
64
|
+
.returns(T.type_parameter(:R))
|
|
65
|
+
end
|
|
66
|
+
def with_timeout(secs, &block)
|
|
67
|
+
if secs
|
|
68
|
+
Timeout.timeout(secs) { block.call }
|
|
69
|
+
else
|
|
70
|
+
yield
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Returns a lambda that, when called, raises Timeout::Error.
|
|
75
|
+
# Useful for constructing an "abort signal" from a deadline.
|
|
76
|
+
sig { params(secs: Float).returns(T.proc.void) }
|
|
77
|
+
def make_abort_signal(secs)
|
|
78
|
+
start = T.let(Time.now, Time)
|
|
79
|
+
Kernel.lambda do
|
|
80
|
+
elapsed = Time.now - start
|
|
81
|
+
Kernel.raise Timeout::Error, "Aborted after #{elapsed.round(2)}s" if elapsed >= secs
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# typed: ignore
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
class Railtie < Rails::Railtie
|
|
6
|
+
config.solana_ruby_kit = ActiveSupport::OrderedOptions.new
|
|
7
|
+
|
|
8
|
+
initializer 'solana_ruby_kit.configure' do |app|
|
|
9
|
+
opts = app.config.solana_ruby_kit
|
|
10
|
+
Solana::Ruby::Kit.configure do |c|
|
|
11
|
+
c.rpc_url = opts.rpc_url if opts.rpc_url
|
|
12
|
+
c.ws_url = opts.ws_url if opts.ws_url
|
|
13
|
+
c.commitment = opts.commitment if opts.commitment
|
|
14
|
+
c.timeout = opts.timeout if opts.timeout
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'base64'
|
|
5
|
+
require_relative '../../rpc_types/account_info'
|
|
6
|
+
|
|
7
|
+
module Solana::Ruby::Kit
|
|
8
|
+
module Rpc
|
|
9
|
+
module Api
|
|
10
|
+
# Fetches all stored information for an account at the given address.
|
|
11
|
+
# Mirrors TypeScript's `GetAccountInfoApi.getAccountInfo(address, config?)`.
|
|
12
|
+
#
|
|
13
|
+
# Returns a `RpcContextualValue` with:
|
|
14
|
+
# .slot — context slot
|
|
15
|
+
# .value — AccountInfoWithBase64Data | AccountInfoWithJsonData | nil
|
|
16
|
+
# (nil when the account does not exist)
|
|
17
|
+
module GetAccountInfo
|
|
18
|
+
extend T::Sig
|
|
19
|
+
|
|
20
|
+
SUPPORTED_ENCODINGS = T.let(%w[base64 jsonParsed base64+zstd].freeze, T::Array[String])
|
|
21
|
+
|
|
22
|
+
sig do
|
|
23
|
+
params(
|
|
24
|
+
address: String,
|
|
25
|
+
encoding: String,
|
|
26
|
+
commitment: T.nilable(Symbol),
|
|
27
|
+
min_context_slot: T.nilable(Integer),
|
|
28
|
+
data_slice: T.nilable(T::Hash[String, Integer])
|
|
29
|
+
).returns(RpcTypes::RpcContextualValue)
|
|
30
|
+
end
|
|
31
|
+
def get_account_info(
|
|
32
|
+
address,
|
|
33
|
+
encoding: 'base64',
|
|
34
|
+
commitment: nil,
|
|
35
|
+
min_context_slot: nil,
|
|
36
|
+
data_slice: nil
|
|
37
|
+
)
|
|
38
|
+
config = { 'encoding' => encoding }
|
|
39
|
+
config['commitment'] = commitment.to_s if commitment
|
|
40
|
+
config['minContextSlot'] = min_context_slot if min_context_slot
|
|
41
|
+
config['dataSlice'] = data_slice if data_slice
|
|
42
|
+
|
|
43
|
+
result = transport.request('getAccountInfo', [address, config])
|
|
44
|
+
|
|
45
|
+
slot = Kernel.Integer(result['context']['slot'])
|
|
46
|
+
raw = result['value']
|
|
47
|
+
|
|
48
|
+
value =
|
|
49
|
+
if raw.nil?
|
|
50
|
+
nil
|
|
51
|
+
elsif encoding == 'jsonParsed'
|
|
52
|
+
RpcTypes::AccountInfoWithJsonData.new(
|
|
53
|
+
executable: raw['executable'],
|
|
54
|
+
lamports: Kernel.Integer(raw['lamports']),
|
|
55
|
+
owner: raw['owner'],
|
|
56
|
+
space: Kernel.Integer(raw.fetch('space', 0)),
|
|
57
|
+
rent_epoch: Kernel.Integer(raw.fetch('rentEpoch', 0)),
|
|
58
|
+
data: raw['data']
|
|
59
|
+
)
|
|
60
|
+
else
|
|
61
|
+
RpcTypes::AccountInfoWithBase64Data.new(
|
|
62
|
+
executable: raw['executable'],
|
|
63
|
+
lamports: Kernel.Integer(raw['lamports']),
|
|
64
|
+
owner: raw['owner'],
|
|
65
|
+
space: Kernel.Integer(raw.fetch('space', 0)),
|
|
66
|
+
rent_epoch: Kernel.Integer(raw.fetch('rentEpoch', 0)),
|
|
67
|
+
data: Kernel.Array(raw['data'])
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
RpcTypes::RpcContextualValue.new(slot: slot, value: value)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../../rpc_types/account_info'
|
|
5
|
+
|
|
6
|
+
module Solana::Ruby::Kit
|
|
7
|
+
module Rpc
|
|
8
|
+
module Api
|
|
9
|
+
# Returns the lamport balance of an account.
|
|
10
|
+
# Mirrors TypeScript's `GetBalanceApi.getBalance(address, config?)`.
|
|
11
|
+
#
|
|
12
|
+
# Returns a `RpcContextualValue` with:
|
|
13
|
+
# .slot — the slot at which the balance was read
|
|
14
|
+
# .value — Integer (lamports)
|
|
15
|
+
module GetBalance
|
|
16
|
+
extend T::Sig
|
|
17
|
+
|
|
18
|
+
sig do
|
|
19
|
+
params(
|
|
20
|
+
address: String,
|
|
21
|
+
commitment: T.nilable(Symbol),
|
|
22
|
+
min_context_slot: T.nilable(Integer)
|
|
23
|
+
).returns(RpcTypes::RpcContextualValue)
|
|
24
|
+
end
|
|
25
|
+
def get_balance(address, commitment: nil, min_context_slot: nil)
|
|
26
|
+
config = {}
|
|
27
|
+
config['commitment'] = commitment.to_s if commitment
|
|
28
|
+
config['minContextSlot'] = min_context_slot if min_context_slot
|
|
29
|
+
|
|
30
|
+
params = config.empty? ? [address] : [address, config]
|
|
31
|
+
result = transport.request('getBalance', params)
|
|
32
|
+
|
|
33
|
+
RpcTypes::RpcContextualValue.new(
|
|
34
|
+
slot: Kernel.Integer(result['context']['slot']),
|
|
35
|
+
value: Kernel.Integer(result['value'])
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module Rpc
|
|
6
|
+
module Api
|
|
7
|
+
# Returns the current block height of the node.
|
|
8
|
+
# Mirrors TypeScript's `GetBlockHeightApi.getBlockHeight(config?)`.
|
|
9
|
+
module GetBlockHeight
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig do
|
|
13
|
+
params(
|
|
14
|
+
commitment: T.nilable(Symbol),
|
|
15
|
+
min_context_slot: T.nilable(Integer)
|
|
16
|
+
).returns(Integer)
|
|
17
|
+
end
|
|
18
|
+
def get_block_height(commitment: nil, min_context_slot: nil)
|
|
19
|
+
config = {}
|
|
20
|
+
config['commitment'] = commitment.to_s if commitment
|
|
21
|
+
config['minContextSlot'] = min_context_slot if min_context_slot
|
|
22
|
+
|
|
23
|
+
params = config.empty? ? [] : [config]
|
|
24
|
+
Kernel.Integer(transport.request('getBlockHeight', params))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module Rpc
|
|
6
|
+
module Api
|
|
7
|
+
# Struct returned by get_epoch_info.
|
|
8
|
+
EpochInfo = T.let(
|
|
9
|
+
Struct.new(
|
|
10
|
+
:absolute_slot, # Integer — current slot
|
|
11
|
+
:block_height, # Integer
|
|
12
|
+
:epoch, # Integer — current epoch
|
|
13
|
+
:slot_index, # Integer — slot within current epoch
|
|
14
|
+
:slots_in_epoch, # Integer — total slots in epoch
|
|
15
|
+
:transaction_count, # Integer | nil
|
|
16
|
+
keyword_init: true
|
|
17
|
+
),
|
|
18
|
+
T.untyped
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Fetch information about the current epoch.
|
|
22
|
+
# Mirrors TypeScript's GetEpochInfoApi.getEpochInfo.
|
|
23
|
+
module GetEpochInfo
|
|
24
|
+
extend T::Sig
|
|
25
|
+
|
|
26
|
+
sig do
|
|
27
|
+
params(commitment: T.nilable(Symbol)).returns(T.untyped)
|
|
28
|
+
end
|
|
29
|
+
def get_epoch_info(commitment: nil)
|
|
30
|
+
config = {}
|
|
31
|
+
config['commitment'] = commitment.to_s if commitment
|
|
32
|
+
|
|
33
|
+
raw = transport.request('getEpochInfo', config.empty? ? [] : [config])
|
|
34
|
+
|
|
35
|
+
EpochInfo.new(
|
|
36
|
+
absolute_slot: Kernel.Integer(raw['absoluteSlot']),
|
|
37
|
+
block_height: Kernel.Integer(raw['blockHeight']),
|
|
38
|
+
epoch: Kernel.Integer(raw['epoch']),
|
|
39
|
+
slot_index: Kernel.Integer(raw['slotIndex']),
|
|
40
|
+
slots_in_epoch: Kernel.Integer(raw['slotsInEpoch']),
|
|
41
|
+
transaction_count: raw['transactionCount'] ? Kernel.Integer(raw['transactionCount']) : nil
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../../rpc_types/account_info'
|
|
5
|
+
|
|
6
|
+
module Solana::Ruby::Kit
|
|
7
|
+
module Rpc
|
|
8
|
+
module Api
|
|
9
|
+
# The payload returned by getLatestBlockhash.
|
|
10
|
+
# Mirrors TypeScript's `GetLatestBlockhashApiResponse`.
|
|
11
|
+
class LatestBlockhash < T::Struct
|
|
12
|
+
const :blockhash, String # base58-encoded 32 bytes
|
|
13
|
+
const :last_valid_block_height, Integer # TypeScript bigint → Ruby Integer
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Returns the latest blockhash and its expiry block height.
|
|
17
|
+
# Mirrors TypeScript's `GetLatestBlockhashApi.getLatestBlockhash(config?)`.
|
|
18
|
+
#
|
|
19
|
+
# Returns a `RpcContextualValue` with:
|
|
20
|
+
# .slot — context slot
|
|
21
|
+
# .value — LatestBlockhash
|
|
22
|
+
module GetLatestBlockhash
|
|
23
|
+
extend T::Sig
|
|
24
|
+
|
|
25
|
+
sig do
|
|
26
|
+
params(
|
|
27
|
+
commitment: T.nilable(Symbol),
|
|
28
|
+
min_context_slot: T.nilable(Integer)
|
|
29
|
+
).returns(RpcTypes::RpcContextualValue)
|
|
30
|
+
end
|
|
31
|
+
def get_latest_blockhash(commitment: nil, min_context_slot: nil)
|
|
32
|
+
config = {}
|
|
33
|
+
config['commitment'] = commitment.to_s if commitment
|
|
34
|
+
config['minContextSlot'] = min_context_slot if min_context_slot
|
|
35
|
+
|
|
36
|
+
params = config.empty? ? [] : [config]
|
|
37
|
+
result = transport.request('getLatestBlockhash', params)
|
|
38
|
+
|
|
39
|
+
value = LatestBlockhash.new(
|
|
40
|
+
blockhash: result['value']['blockhash'],
|
|
41
|
+
last_valid_block_height: Kernel.Integer(result['value']['lastValidBlockHeight'])
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
RpcTypes::RpcContextualValue.new(
|
|
45
|
+
slot: Kernel.Integer(result['context']['slot']),
|
|
46
|
+
value: value
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module Rpc
|
|
6
|
+
module Api
|
|
7
|
+
# Returns the minimum balance required to keep an account rent-exempt
|
|
8
|
+
# given its data size in bytes.
|
|
9
|
+
# Mirrors `GetMinimumBalanceForRentExemptionApi.getMinimumBalanceForRentExemption()`.
|
|
10
|
+
module GetMinimumBalanceForRentExemption
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
sig do
|
|
14
|
+
params(
|
|
15
|
+
data_size: Integer,
|
|
16
|
+
commitment: T.nilable(Symbol)
|
|
17
|
+
).returns(Integer)
|
|
18
|
+
end
|
|
19
|
+
def get_minimum_balance_for_rent_exemption(data_size, commitment: nil)
|
|
20
|
+
config = {}
|
|
21
|
+
config['commitment'] = commitment.to_s if commitment
|
|
22
|
+
|
|
23
|
+
params = config.empty? ? [data_size] : [data_size, config]
|
|
24
|
+
Kernel.Integer(transport.request('getMinimumBalanceForRentExemption', params))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|