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,90 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module Subscribable
|
|
6
|
+
# Thread-safe pub/sub hub — mirrors @solana/subscribable DataPublisher.
|
|
7
|
+
#
|
|
8
|
+
# Channels are arbitrary symbols/strings. Subscribers are blocks called
|
|
9
|
+
# synchronously from #publish (in the calling thread). An optional
|
|
10
|
+
# +signal+ lambda is checked before each dispatch; if it raises the
|
|
11
|
+
# subscriber is automatically removed.
|
|
12
|
+
#
|
|
13
|
+
# Returns an unsubscribe lambda from #on.
|
|
14
|
+
class DataPublisher
|
|
15
|
+
extend T::Sig
|
|
16
|
+
|
|
17
|
+
ERROR_CHANNEL = T.let(:error, Symbol)
|
|
18
|
+
CLOSE_CHANNEL = T.let(:close, Symbol)
|
|
19
|
+
|
|
20
|
+
sig { void }
|
|
21
|
+
def initialize
|
|
22
|
+
# channel_name → [[subscriber_proc, signal_lambda_or_nil], ...]
|
|
23
|
+
@subscribers = T.let(
|
|
24
|
+
Hash.new { |h, k| h[k] = [] },
|
|
25
|
+
T::Hash[T.untyped, T::Array[[T.proc.params(data: T.untyped).void, T.nilable(T.proc.void)]]]
|
|
26
|
+
)
|
|
27
|
+
@mutex = T.let(Mutex.new, Mutex)
|
|
28
|
+
@closed = T.let(false, T::Boolean)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Register +block+ as a subscriber on +channel_name+.
|
|
32
|
+
# +signal+ is an optional lambda that is called before each dispatch;
|
|
33
|
+
# if it raises (e.g. a Timeout::Error or custom AbortError) the
|
|
34
|
+
# subscription is removed automatically.
|
|
35
|
+
# Returns a lambda that removes the subscription when called.
|
|
36
|
+
sig do
|
|
37
|
+
params(
|
|
38
|
+
channel_name: T.untyped,
|
|
39
|
+
signal: T.nilable(T.proc.void),
|
|
40
|
+
block: T.proc.params(data: T.untyped).void
|
|
41
|
+
).returns(T.proc.void)
|
|
42
|
+
end
|
|
43
|
+
def on(channel_name, signal: nil, &block)
|
|
44
|
+
entry = [block, signal]
|
|
45
|
+
@mutex.synchronize { T.must(@subscribers[channel_name]) << entry }
|
|
46
|
+
|
|
47
|
+
# Return unsubscribe lambda
|
|
48
|
+
lambda do
|
|
49
|
+
@mutex.synchronize { T.must(@subscribers[channel_name]).delete(entry) }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Deliver +data+ to all subscribers on +channel_name+.
|
|
54
|
+
# Subscribers whose signal has fired are pruned automatically.
|
|
55
|
+
sig { params(channel_name: T.untyped, data: T.untyped).void }
|
|
56
|
+
def publish(channel_name, data)
|
|
57
|
+
entries = @mutex.synchronize { (@subscribers[channel_name] || []).dup }
|
|
58
|
+
entries.each do |subscriber, signal|
|
|
59
|
+
begin
|
|
60
|
+
signal&.call
|
|
61
|
+
rescue StandardError
|
|
62
|
+
# Signal fired — remove this subscriber and skip dispatch
|
|
63
|
+
@mutex.synchronize { T.must(@subscribers[channel_name]).delete([subscriber, signal]) }
|
|
64
|
+
next
|
|
65
|
+
end
|
|
66
|
+
begin
|
|
67
|
+
subscriber.call(data)
|
|
68
|
+
rescue StandardError => e
|
|
69
|
+
# Dispatch errors are re-published on the error channel (not raised)
|
|
70
|
+
publish(ERROR_CHANNEL, e) unless channel_name == ERROR_CHANNEL
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Close the publisher: emit a final event on the close channel and remove
|
|
76
|
+
# all subscribers.
|
|
77
|
+
sig { void }
|
|
78
|
+
def close
|
|
79
|
+
publish(CLOSE_CHANNEL, nil)
|
|
80
|
+
@mutex.synchronize do
|
|
81
|
+
@subscribers.clear
|
|
82
|
+
@closed = true
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
sig { returns(T::Boolean) }
|
|
87
|
+
def closed? = @closed
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'timeout'
|
|
5
|
+
|
|
6
|
+
# Mirrors @solana/subscribable.
|
|
7
|
+
require_relative 'subscribable/data_publisher'
|
|
8
|
+
require_relative 'subscribable/async_iterable'
|
|
9
|
+
|
|
10
|
+
module Solana::Ruby::Kit
|
|
11
|
+
module Subscribable
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module Sysvars
|
|
6
|
+
# Well-known on-chain addresses for all Solana sysvar accounts.
|
|
7
|
+
SYSVAR_CLOCK_ADDRESS = T.let('SysvarC1ock11111111111111111111111111111111', String)
|
|
8
|
+
SYSVAR_RENT_ADDRESS = T.let('SysvarRent111111111111111111111111111111111', String)
|
|
9
|
+
SYSVAR_EPOCH_SCHEDULE_ADDRESS = T.let('SysvarEpochSchedu1e111111111111111111111111', String)
|
|
10
|
+
SYSVAR_FEES_ADDRESS = T.let('SysvarFees111111111111111111111111111111111', String)
|
|
11
|
+
SYSVAR_RECENT_BLOCKHASHES_ADDRESS = T.let('SysvarRecentB1ockHashes11111111111111111111', String)
|
|
12
|
+
SYSVAR_SLOT_HASHES_ADDRESS = T.let('SysvarS1otHashes111111111111111111111111111', String)
|
|
13
|
+
SYSVAR_SLOT_HISTORY_ADDRESS = T.let('SysvarS1otHistory11111111111111111111111111', String)
|
|
14
|
+
SYSVAR_STAKE_HISTORY_ADDRESS = T.let('SysvarStakeHistory1111111111111111111111111', String)
|
|
15
|
+
SYSVAR_INSTRUCTIONS_ADDRESS = T.let('Sysvar1nstructions1111111111111111111111111', String)
|
|
16
|
+
SYSVAR_LAST_RESTART_SLOT_ADDRESS = T.let('SysvarLastRestartS1ot1111111111111111111111', String)
|
|
17
|
+
SYSVAR_EPOCH_REWARDS_ADDRESS = T.let('SysvarEpochRewards1111111111111111111111111', String)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'base64'
|
|
5
|
+
|
|
6
|
+
module Solana::Ruby::Kit
|
|
7
|
+
module Sysvars
|
|
8
|
+
# Clock sysvar — 40 bytes little-endian layout:
|
|
9
|
+
# slot(u64) + epoch_start_timestamp(i64) + epoch(u64) + leader_schedule_epoch(u64) + unix_timestamp(i64)
|
|
10
|
+
class SysvarClock < T::Struct
|
|
11
|
+
const :slot, Integer
|
|
12
|
+
const :epoch_start_timestamp, Integer # Unix seconds (i64)
|
|
13
|
+
const :epoch, Integer
|
|
14
|
+
const :leader_schedule_epoch, Integer
|
|
15
|
+
const :unix_timestamp, Integer # Unix seconds (i64)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
CLOCK_SIZE = T.let(40, Integer)
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
sig { params(rpc: Rpc::Client).returns(SysvarClock) }
|
|
23
|
+
def fetch_sysvar_clock(rpc)
|
|
24
|
+
res = rpc.get_account_info(SYSVAR_CLOCK_ADDRESS, encoding: 'base64')
|
|
25
|
+
data = _decode_account_data(res)
|
|
26
|
+
|
|
27
|
+
unpacked = T.cast(T.unsafe(data).unpack('Q<q<Q<Q<q<'), T::Array[Integer])
|
|
28
|
+
SysvarClock.new(
|
|
29
|
+
slot: T.must(unpacked[0]),
|
|
30
|
+
epoch_start_timestamp: T.must(unpacked[1]),
|
|
31
|
+
epoch: T.must(unpacked[2]),
|
|
32
|
+
leader_schedule_epoch: T.must(unpacked[3]),
|
|
33
|
+
unix_timestamp: T.must(unpacked[4])
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module Sysvars
|
|
6
|
+
# EpochSchedule sysvar — 33 bytes:
|
|
7
|
+
# slots_per_epoch(u64) + leader_schedule_slot_offset(u64)
|
|
8
|
+
# + warmup(bool/u8) + first_normal_epoch(u64) + first_normal_slot(u64)
|
|
9
|
+
class SysvarEpochSchedule < T::Struct
|
|
10
|
+
const :slots_per_epoch, Integer
|
|
11
|
+
const :leader_schedule_slot_offset, Integer
|
|
12
|
+
const :warmup, T::Boolean
|
|
13
|
+
const :first_normal_epoch, Integer
|
|
14
|
+
const :first_normal_slot, Integer
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
sig { params(rpc: Rpc::Client).returns(SysvarEpochSchedule) }
|
|
20
|
+
def fetch_sysvar_epoch_schedule(rpc)
|
|
21
|
+
res = rpc.get_account_info(SYSVAR_EPOCH_SCHEDULE_ADDRESS, encoding: 'base64')
|
|
22
|
+
data = _decode_account_data(res)
|
|
23
|
+
|
|
24
|
+
unpacked = T.unsafe(data).unpack('Q<Q<CQ<Q<')
|
|
25
|
+
SysvarEpochSchedule.new(
|
|
26
|
+
slots_per_epoch: unpacked[0],
|
|
27
|
+
leader_schedule_slot_offset: unpacked[1],
|
|
28
|
+
warmup: unpacked[2] == 1,
|
|
29
|
+
first_normal_epoch: unpacked[3],
|
|
30
|
+
first_normal_slot: unpacked[4]
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module Sysvars
|
|
6
|
+
# LastRestartSlot sysvar — 8 bytes: last_restart_slot(u64 LE)
|
|
7
|
+
class SysvarLastRestartSlot < T::Struct
|
|
8
|
+
const :last_restart_slot, Integer
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
sig { params(rpc: Rpc::Client).returns(SysvarLastRestartSlot) }
|
|
14
|
+
def fetch_sysvar_last_restart_slot(rpc)
|
|
15
|
+
res = rpc.get_account_info(SYSVAR_LAST_RESTART_SLOT_ADDRESS, encoding: 'base64')
|
|
16
|
+
data = _decode_account_data(res)
|
|
17
|
+
|
|
18
|
+
slot = data.unpack1('Q<')
|
|
19
|
+
SysvarLastRestartSlot.new(last_restart_slot: slot)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module Sysvars
|
|
6
|
+
# Rent sysvar — 17 bytes:
|
|
7
|
+
# lamports_per_byte_year(u64) + exemption_threshold(f64) + burn_percent(u8)
|
|
8
|
+
class SysvarRent < T::Struct
|
|
9
|
+
const :lamports_per_byte_year, Integer
|
|
10
|
+
const :exemption_threshold, Float
|
|
11
|
+
const :burn_percent, Integer
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
sig { params(rpc: Rpc::Client).returns(SysvarRent) }
|
|
17
|
+
def fetch_sysvar_rent(rpc)
|
|
18
|
+
res = rpc.get_account_info(SYSVAR_RENT_ADDRESS, encoding: 'base64')
|
|
19
|
+
data = _decode_account_data(res)
|
|
20
|
+
|
|
21
|
+
unpacked = T.unsafe(data).unpack('Q<EC')
|
|
22
|
+
SysvarRent.new(
|
|
23
|
+
lamports_per_byte_year: unpacked[0],
|
|
24
|
+
exemption_threshold: unpacked[1],
|
|
25
|
+
burn_percent: unpacked[2]
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'base64'
|
|
5
|
+
|
|
6
|
+
# Mirrors @solana/sysvars.
|
|
7
|
+
# Provides addresses and fetch helpers for Solana sysvar accounts.
|
|
8
|
+
|
|
9
|
+
# Set up the module (extend T::Sig) before requiring sub-files that use `sig`.
|
|
10
|
+
module Solana::Ruby::Kit
|
|
11
|
+
module Sysvars
|
|
12
|
+
extend T::Sig
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# Decode base64 account data from an RpcContextualValue returned by
|
|
17
|
+
# get_account_info(encoding: 'base64').
|
|
18
|
+
sig { params(res: RpcTypes::RpcContextualValue).returns(String) }
|
|
19
|
+
def _decode_account_data(res)
|
|
20
|
+
info = res.value
|
|
21
|
+
Kernel.raise ArgumentError, 'Account not found' if info.nil?
|
|
22
|
+
|
|
23
|
+
raw = T.cast(info, RpcTypes::AccountInfoWithBase64Data)
|
|
24
|
+
Base64.decode64(raw.data.first.to_s).b
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
require_relative 'sysvars/addresses'
|
|
30
|
+
require_relative 'sysvars/clock'
|
|
31
|
+
require_relative 'sysvars/rent'
|
|
32
|
+
require_relative 'sysvars/epoch_schedule'
|
|
33
|
+
require_relative 'sysvars/last_restart_slot'
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'timeout'
|
|
5
|
+
|
|
6
|
+
# Mirrors @solana/transaction-confirmation.
|
|
7
|
+
# Provides synchronous polling strategies that wait for a transaction to reach
|
|
8
|
+
# a desired commitment level.
|
|
9
|
+
module Solana::Ruby::Kit
|
|
10
|
+
module TransactionConfirmation
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# Poll getSignatureStatuses until +signature+ reaches +commitment+ or
|
|
16
|
+
# the blockheight/timeout deadline is exceeded.
|
|
17
|
+
#
|
|
18
|
+
# @param rpc [Rpc::Client]
|
|
19
|
+
# @param signature [Keys::Signature, String]
|
|
20
|
+
# @param commitment [:processed, :confirmed, :finalized]
|
|
21
|
+
# @param timeout_secs [Integer]
|
|
22
|
+
# @param poll_interval [Float]
|
|
23
|
+
# @return [Rpc::Api::SignatureStatus]
|
|
24
|
+
# @raise [Timeout::Error] if the deadline is reached
|
|
25
|
+
# @raise [Solana::Ruby::Kit::SolanaError] if the transaction failed
|
|
26
|
+
sig do
|
|
27
|
+
params(
|
|
28
|
+
rpc: Rpc::Client,
|
|
29
|
+
signature: T.any(Keys::Signature, String),
|
|
30
|
+
commitment: Symbol,
|
|
31
|
+
timeout_secs: Integer,
|
|
32
|
+
poll_interval: Float
|
|
33
|
+
).returns(T.untyped) # Rpc::Api::SignatureStatus
|
|
34
|
+
end
|
|
35
|
+
def wait_for_confirmation(
|
|
36
|
+
rpc,
|
|
37
|
+
signature,
|
|
38
|
+
commitment: :confirmed,
|
|
39
|
+
timeout_secs: 30,
|
|
40
|
+
poll_interval: 0.5
|
|
41
|
+
)
|
|
42
|
+
sig_str = signature.respond_to?(:value) ? T.cast(signature, Keys::Signature).value : signature.to_s
|
|
43
|
+
commitment_order = %i[processed confirmed finalized]
|
|
44
|
+
min_order = commitment_order.index(commitment) || 1
|
|
45
|
+
|
|
46
|
+
Timeout.timeout(timeout_secs) do
|
|
47
|
+
Kernel.loop do
|
|
48
|
+
res = rpc.get_signature_statuses([sig_str], search_transaction_history: false)
|
|
49
|
+
status = res.value.first
|
|
50
|
+
next Kernel.sleep(poll_interval) if status.nil?
|
|
51
|
+
|
|
52
|
+
err = status.respond_to?(:err) ? status.err : status[:err]
|
|
53
|
+
Kernel.raise SolanaError.new(SolanaError::TRANSACTIONS__FAILED_TRANSACTION_PLAN, err: err.inspect) if err
|
|
54
|
+
|
|
55
|
+
current_commitment = status.respond_to?(:confirmation_status) ? status.confirmation_status : status[:confirmation_status]
|
|
56
|
+
current_order = commitment_order.index(current_commitment) || 0
|
|
57
|
+
|
|
58
|
+
return status if current_order >= min_order
|
|
59
|
+
|
|
60
|
+
Kernel.sleep(poll_interval)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Wait until the transaction is confirmed or the blockhash lifetime expires
|
|
66
|
+
# (i.e. the current block height exceeds +last_valid_block_height+).
|
|
67
|
+
sig do
|
|
68
|
+
params(
|
|
69
|
+
rpc: Rpc::Client,
|
|
70
|
+
signature: T.any(Keys::Signature, String),
|
|
71
|
+
last_valid_block_height: Integer,
|
|
72
|
+
commitment: Symbol,
|
|
73
|
+
poll_interval: Float
|
|
74
|
+
).returns(T.untyped)
|
|
75
|
+
end
|
|
76
|
+
def wait_for_blockheight_lifetime(
|
|
77
|
+
rpc,
|
|
78
|
+
signature,
|
|
79
|
+
last_valid_block_height:,
|
|
80
|
+
commitment: :confirmed,
|
|
81
|
+
poll_interval: 0.5
|
|
82
|
+
)
|
|
83
|
+
sig_str = signature.respond_to?(:value) ? T.cast(signature, Keys::Signature).value : signature.to_s
|
|
84
|
+
commitment_order = %i[processed confirmed finalized]
|
|
85
|
+
min_order = commitment_order.index(commitment) || 1
|
|
86
|
+
|
|
87
|
+
Kernel.loop do
|
|
88
|
+
# Check if blockheight has been exceeded
|
|
89
|
+
current_height = rpc.get_block_height
|
|
90
|
+
Kernel.raise Timeout::Error, 'Transaction lifetime expired (blockheight)' if current_height > last_valid_block_height
|
|
91
|
+
|
|
92
|
+
res = rpc.get_signature_statuses([sig_str], search_transaction_history: false)
|
|
93
|
+
status = res.value.first
|
|
94
|
+
next Kernel.sleep(poll_interval) if status.nil?
|
|
95
|
+
|
|
96
|
+
err = status.respond_to?(:err) ? status.err : status[:err]
|
|
97
|
+
Kernel.raise SolanaError.new(SolanaError::TRANSACTIONS__FAILED_TRANSACTION_PLAN, err: err.inspect) if err
|
|
98
|
+
|
|
99
|
+
current_commitment = status.respond_to?(:confirmation_status) ? status.confirmation_status : status[:confirmation_status]
|
|
100
|
+
current_order = commitment_order.index(current_commitment) || 0
|
|
101
|
+
|
|
102
|
+
return status if current_order >= min_order
|
|
103
|
+
|
|
104
|
+
Kernel.sleep(poll_interval)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Wait for confirmation using a durable nonce strategy.
|
|
109
|
+
# The transaction is considered expired once the nonce account's blockhash
|
|
110
|
+
# changes (indicating the nonce was advanced by someone else or the TX landed).
|
|
111
|
+
sig do
|
|
112
|
+
params(
|
|
113
|
+
rpc: Rpc::Client,
|
|
114
|
+
signature: T.any(Keys::Signature, String),
|
|
115
|
+
nonce_account: String,
|
|
116
|
+
nonce: String,
|
|
117
|
+
commitment: Symbol,
|
|
118
|
+
timeout_secs: Integer,
|
|
119
|
+
poll_interval: Float
|
|
120
|
+
).returns(T.untyped)
|
|
121
|
+
end
|
|
122
|
+
def wait_for_nonce_invalidation(
|
|
123
|
+
rpc,
|
|
124
|
+
signature,
|
|
125
|
+
nonce_account:,
|
|
126
|
+
nonce:,
|
|
127
|
+
commitment: :confirmed,
|
|
128
|
+
timeout_secs: 120,
|
|
129
|
+
poll_interval: 1.0
|
|
130
|
+
)
|
|
131
|
+
sig_str = signature.respond_to?(:value) ? T.cast(signature, Keys::Signature).value : signature.to_s
|
|
132
|
+
commitment_order = %i[processed confirmed finalized]
|
|
133
|
+
min_order = commitment_order.index(commitment) || 1
|
|
134
|
+
|
|
135
|
+
Timeout.timeout(timeout_secs) do
|
|
136
|
+
Kernel.loop do
|
|
137
|
+
# First check if transaction already confirmed
|
|
138
|
+
res = rpc.get_signature_statuses([sig_str], search_transaction_history: false)
|
|
139
|
+
status = res.value.first
|
|
140
|
+
unless status.nil?
|
|
141
|
+
err = status.respond_to?(:err) ? status.err : status[:err]
|
|
142
|
+
Kernel.raise SolanaError.new(SolanaError::TRANSACTIONS__FAILED_TRANSACTION_PLAN, err: err.inspect) if err
|
|
143
|
+
|
|
144
|
+
current_commitment = status.respond_to?(:confirmation_status) ? status.confirmation_status : status[:confirmation_status]
|
|
145
|
+
current_order = commitment_order.index(current_commitment) || 0
|
|
146
|
+
return status if current_order >= min_order
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Check if nonce has advanced (transaction expired without confirming)
|
|
150
|
+
nonce_res = rpc.get_account_info(nonce_account, encoding: 'jsonParsed')
|
|
151
|
+
current_nonce_hash = nonce_res.value&.respond_to?(:data) ? nonce_res.value.data&.dig('parsed', 'info', 'blockhash') : nil
|
|
152
|
+
Kernel.raise Timeout::Error, 'Nonce advanced — transaction expired' if current_nonce_hash && current_nonce_hash != nonce
|
|
153
|
+
|
|
154
|
+
Kernel.sleep(poll_interval)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../addresses/address'
|
|
5
|
+
require_relative '../errors'
|
|
6
|
+
require_relative '../instructions/instruction'
|
|
7
|
+
|
|
8
|
+
module Solana::Ruby::Kit
|
|
9
|
+
module TransactionMessages
|
|
10
|
+
extend T::Sig
|
|
11
|
+
# Supported transaction versions.
|
|
12
|
+
# Mirrors TypeScript's `TransactionVersion = 'legacy' | 0 | 1`.
|
|
13
|
+
# Version 1 is defined but not yet supported by most tooling.
|
|
14
|
+
TransactionVersion = T.type_alias { T.any(Symbol, Integer) }
|
|
15
|
+
|
|
16
|
+
LEGACY_VERSION = T.let(:legacy, Symbol)
|
|
17
|
+
V0_VERSION = T.let(0, Integer)
|
|
18
|
+
V1_VERSION = T.let(1, Integer)
|
|
19
|
+
|
|
20
|
+
MAX_SUPPORTED_TRANSACTION_VERSION = T.let(1, Integer)
|
|
21
|
+
|
|
22
|
+
# A blockhash-based lifetime constraint.
|
|
23
|
+
# Mirrors TypeScript's `BlockhashLifetimeConstraint`.
|
|
24
|
+
class BlockhashLifetimeConstraint < T::Struct
|
|
25
|
+
# A recent blockhash string (base58-encoded 32 bytes).
|
|
26
|
+
const :blockhash, String
|
|
27
|
+
# Block height after which the blockhash is considered expired.
|
|
28
|
+
# TypeScript uses bigint; Ruby uses Integer.
|
|
29
|
+
const :last_valid_block_height, Integer
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# A durable-nonce lifetime constraint.
|
|
33
|
+
# Mirrors TypeScript's `DurableNonceTransactionMessageLifetimeConstraint`.
|
|
34
|
+
class DurableNonceLifetimeConstraint < T::Struct
|
|
35
|
+
const :nonce, String # base58 nonce value
|
|
36
|
+
const :nonce_account_address, Addresses::Address
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Lifetime can be a blockhash constraint, a durable-nonce constraint, or nil.
|
|
40
|
+
Lifetime = T.type_alias { T.nilable(T.any(BlockhashLifetimeConstraint, DurableNonceLifetimeConstraint)) }
|
|
41
|
+
|
|
42
|
+
# The core transaction message structure.
|
|
43
|
+
# Mirrors TypeScript's `TransactionMessage` (legacy | V0).
|
|
44
|
+
#
|
|
45
|
+
# TypeScript uses compile-time type narrowing to differentiate versions;
|
|
46
|
+
# Ruby uses the `version` field at runtime.
|
|
47
|
+
class TransactionMessage < T::Struct
|
|
48
|
+
# :legacy or 0 (V1 reserved)
|
|
49
|
+
const :version, T.untyped # Symbol or Integer
|
|
50
|
+
# Ordered list of instructions.
|
|
51
|
+
const :instructions, T::Array[Instructions::Instruction]
|
|
52
|
+
# The account that pays the transaction fee (nil on a freshly created message).
|
|
53
|
+
const :fee_payer, T.nilable(Addresses::Address)
|
|
54
|
+
# Lifetime constraint (nil until explicitly set).
|
|
55
|
+
const :lifetime_constraint, Lifetime
|
|
56
|
+
# V0 only: lookup table addresses mapped to the accounts they hold.
|
|
57
|
+
const :address_table_lookups, T.nilable(T::Hash[String, T::Array[Integer]])
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
module_function
|
|
61
|
+
|
|
62
|
+
# Creates an empty transaction message at the given version.
|
|
63
|
+
# Mirrors `createTransactionMessage({ version })`.
|
|
64
|
+
sig { params(version: T.untyped).returns(TransactionMessage) }
|
|
65
|
+
def create_transaction_message(version:)
|
|
66
|
+
TransactionMessage.new(
|
|
67
|
+
version: version,
|
|
68
|
+
instructions: [],
|
|
69
|
+
fee_payer: nil,
|
|
70
|
+
lifetime_constraint: nil,
|
|
71
|
+
address_table_lookups: nil
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Sets the fee payer on a transaction message.
|
|
76
|
+
# Mirrors `setTransactionMessageFeePayer(feePayer, transactionMessage)`.
|
|
77
|
+
sig { params(fee_payer: Addresses::Address, message: TransactionMessage).returns(TransactionMessage) }
|
|
78
|
+
def set_fee_payer(fee_payer, message)
|
|
79
|
+
return message if message.fee_payer == fee_payer
|
|
80
|
+
|
|
81
|
+
TransactionMessage.new(
|
|
82
|
+
version: message.version,
|
|
83
|
+
instructions: message.instructions,
|
|
84
|
+
fee_payer: fee_payer,
|
|
85
|
+
lifetime_constraint: message.lifetime_constraint,
|
|
86
|
+
address_table_lookups: message.address_table_lookups
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Sets a blockhash-based lifetime constraint on a transaction message.
|
|
91
|
+
# Mirrors `setTransactionMessageLifetimeUsingBlockhash(constraint, message)`.
|
|
92
|
+
sig { params(constraint: BlockhashLifetimeConstraint, message: TransactionMessage).returns(TransactionMessage) }
|
|
93
|
+
def set_blockhash_lifetime(constraint, message)
|
|
94
|
+
existing = message.lifetime_constraint
|
|
95
|
+
if existing.is_a?(BlockhashLifetimeConstraint) &&
|
|
96
|
+
existing.blockhash == constraint.blockhash &&
|
|
97
|
+
existing.last_valid_block_height == constraint.last_valid_block_height
|
|
98
|
+
return message
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
TransactionMessage.new(
|
|
102
|
+
version: message.version,
|
|
103
|
+
instructions: message.instructions,
|
|
104
|
+
fee_payer: message.fee_payer,
|
|
105
|
+
lifetime_constraint: constraint,
|
|
106
|
+
address_table_lookups: message.address_table_lookups
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Sets a durable-nonce lifetime constraint on a transaction message.
|
|
111
|
+
# Mirrors `setTransactionMessageLifetimeUsingDurableNonce(constraint, message)`.
|
|
112
|
+
sig { params(constraint: DurableNonceLifetimeConstraint, message: TransactionMessage).returns(TransactionMessage) }
|
|
113
|
+
def set_durable_nonce_lifetime(constraint, message)
|
|
114
|
+
TransactionMessage.new(
|
|
115
|
+
version: message.version,
|
|
116
|
+
instructions: message.instructions,
|
|
117
|
+
fee_payer: message.fee_payer,
|
|
118
|
+
lifetime_constraint: constraint,
|
|
119
|
+
address_table_lookups: message.address_table_lookups
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Appends one or more instructions to a transaction message.
|
|
124
|
+
# Mirrors `appendTransactionMessageInstruction(instruction, message)` and
|
|
125
|
+
# `appendTransactionMessageInstructions(instructions, message)`.
|
|
126
|
+
sig { params(message: TransactionMessage, instructions: T::Array[Instructions::Instruction]).returns(TransactionMessage) }
|
|
127
|
+
def append_instructions(message, instructions)
|
|
128
|
+
TransactionMessage.new(
|
|
129
|
+
version: message.version,
|
|
130
|
+
instructions: message.instructions + instructions,
|
|
131
|
+
fee_payer: message.fee_payer,
|
|
132
|
+
lifetime_constraint: message.lifetime_constraint,
|
|
133
|
+
address_table_lookups: message.address_table_lookups
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Prepends one or more instructions to a transaction message.
|
|
138
|
+
# Mirrors `prependTransactionMessageInstruction(instruction, message)`.
|
|
139
|
+
sig { params(message: TransactionMessage, instructions: T::Array[Instructions::Instruction]).returns(TransactionMessage) }
|
|
140
|
+
def prepend_instructions(message, instructions)
|
|
141
|
+
TransactionMessage.new(
|
|
142
|
+
version: message.version,
|
|
143
|
+
instructions: instructions + message.instructions,
|
|
144
|
+
fee_payer: message.fee_payer,
|
|
145
|
+
lifetime_constraint: message.lifetime_constraint,
|
|
146
|
+
address_table_lookups: message.address_table_lookups
|
|
147
|
+
)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Returns true if the message has a blockhash-based lifetime.
|
|
151
|
+
sig { params(message: TransactionMessage).returns(T::Boolean) }
|
|
152
|
+
def blockhash_lifetime?(message)
|
|
153
|
+
message.lifetime_constraint.is_a?(BlockhashLifetimeConstraint)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Returns true if the message has a durable-nonce lifetime.
|
|
157
|
+
sig { params(message: TransactionMessage).returns(T::Boolean) }
|
|
158
|
+
def durable_nonce_lifetime?(message)
|
|
159
|
+
message.lifetime_constraint.is_a?(DurableNonceLifetimeConstraint)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Raises SolanaError unless the message has a blockhash lifetime.
|
|
163
|
+
sig { params(message: TransactionMessage).void }
|
|
164
|
+
def assert_blockhash_lifetime!(message)
|
|
165
|
+
Kernel.raise SolanaError.new(:SOLANA_ERROR__TRANSACTION__EXPECTED_BLOCKHASH_LIFETIME) unless blockhash_lifetime?(message)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|