solace 0.1.8 → 0.1.9
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 +4 -4
- data/lib/rubocop/cop/solace/exploded_hash.rb +82 -0
- data/lib/solace/accounts/address_lookup_table.rb +57 -35
- data/lib/solace/composers/opaque_instruction_composer.rb +84 -0
- data/lib/solace/errors/address_lookup_table_not_found.rb +29 -0
- data/lib/solace/errors/rpc_error.rb +7 -1
- data/lib/solace/errors.rb +1 -0
- data/lib/solace/transaction_composer.rb +94 -19
- data/lib/solace/transaction_decomposer.rb +250 -0
- data/lib/solace/utils/account_context.rb +38 -26
- data/lib/solace/utils/compute_budget.rb +85 -0
- data/lib/solace/version.rb +1 -1
- data/lib/solace.rb +5 -1
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2f6a793f450df697c4f216ab4e5a483d7a331f57a3fccb05ecd7c76e788fa4ed
|
|
4
|
+
data.tar.gz: 479fcb0c38b8e5ea8d82636922ab327ec68ae18455550658c79b06ddd11b2bbd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ec35bdf08bf2ecdd235d25ec67f9c897a7a5c0a167c46d3f2dea8cfb21600b7c3523e236f9db644638fdb2aadd3b219a67296676d580d3ce2ef2ad1a9c8ebe00
|
|
7
|
+
data.tar.gz: 9647d86486a247042220f53b621591e2de5599868bb154350eb367a4d523301c60ac1735cc664c46ebb18e181f7095c9972748257d25373863d81c520ab7ba3a
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Solace
|
|
6
|
+
# Every pair in a braced hash starts on its own line.
|
|
7
|
+
#
|
|
8
|
+
# A hash is read as a table: a reader scanning for one key, or diffing two
|
|
9
|
+
# of them, works down the left edge. Collapsing pairs onto a shared line
|
|
10
|
+
# hides a key mid-line and makes a one-key change show up as a rewritten
|
|
11
|
+
# line. Length is not the test — a short hash collapses just as badly as a
|
|
12
|
+
# long one, which is why the built-in layout cops are not enough: they only
|
|
13
|
+
# act once a hash is already multi-line.
|
|
14
|
+
#
|
|
15
|
+
# Only a hash written on one line is this cop's business. Once a hash spans
|
|
16
|
+
# lines, Layout/FirstHashElementLineBreak and Layout/MultilineHashKeyLineBreaks
|
|
17
|
+
# already break every pair onto its own line, and a second cop inserting the
|
|
18
|
+
# same break in the same correction loop leaves a blank line behind.
|
|
19
|
+
#
|
|
20
|
+
# Two more restrictions keep it from reaching where it does not belong. A
|
|
21
|
+
# hash of one pair is left inline, since it is already the only thing on
|
|
22
|
+
# its line. And only BRACED hashes are checked: a keyword argument list is
|
|
23
|
+
# a hash node too, and exploding it would break up every multi-argument
|
|
24
|
+
# call in the codebase.
|
|
25
|
+
#
|
|
26
|
+
# @example
|
|
27
|
+
# # bad
|
|
28
|
+
# page_id: { type: 'string', format: 'uuid' }
|
|
29
|
+
#
|
|
30
|
+
# # good
|
|
31
|
+
# page_id: {
|
|
32
|
+
# type: 'string',
|
|
33
|
+
# format: 'uuid'
|
|
34
|
+
# }
|
|
35
|
+
#
|
|
36
|
+
# # good — one pair, and a keyword argument list
|
|
37
|
+
# { user: payload[:user_id] }
|
|
38
|
+
# create(:user, name: 'Ada', email: 'ada@example.com')
|
|
39
|
+
class ExplodedHash < Base
|
|
40
|
+
extend AutoCorrector
|
|
41
|
+
|
|
42
|
+
MSG = 'Each pair in a hash starts on its own line.'
|
|
43
|
+
|
|
44
|
+
def on_hash(node)
|
|
45
|
+
pairs = braced_pairs(node)
|
|
46
|
+
return unless pairs
|
|
47
|
+
|
|
48
|
+
pairs.each { |pair| flag(pair) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# Nil for anything this cop leaves alone: a hash already spanning lines,
|
|
54
|
+
# a keyword argument list, which carries no braces, and a hash of a
|
|
55
|
+
# single pair.
|
|
56
|
+
def braced_pairs(node)
|
|
57
|
+
return unless node.loc.begin
|
|
58
|
+
return if node.multiline?
|
|
59
|
+
|
|
60
|
+
pairs = node.pairs
|
|
61
|
+
pairs if pairs.size >= 2
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The break replaces the space in front of the pair rather than being
|
|
65
|
+
# inserted before it, or the old separator is left behind as trailing
|
|
66
|
+
# whitespace. Indentation is left to Layout/FirstHashElementIndentation
|
|
67
|
+
# and Layout/HashAlignment, which run in the same correction loop.
|
|
68
|
+
def flag(pair)
|
|
69
|
+
add_offense(pair) { |corrector| corrector.replace(leading_space(pair), "\n") }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def leading_space(pair)
|
|
73
|
+
range = pair.source_range
|
|
74
|
+
source = range.source_buffer.source
|
|
75
|
+
start = range.begin_pos
|
|
76
|
+
start -= 1 while start.positive? && [' ', "\t"].include?(source[start - 1])
|
|
77
|
+
Parser::Source::Range.new(range.source_buffer, start, range.begin_pos)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -21,9 +21,8 @@ module Solace
|
|
|
21
21
|
# table.reference(loaded_writable, loaded_readonly) # => Solace::AddressLookupTable or nil
|
|
22
22
|
#
|
|
23
23
|
# @example Read a table's on-chain state
|
|
24
|
-
#
|
|
25
|
-
# table
|
|
26
|
-
# table.addresses # => the stored addresses
|
|
24
|
+
# table = Solace::Accounts::AddressLookupTable.fetch(address, connection: connection)
|
|
25
|
+
# table&.addresses # => the stored addresses, or nil if the chain holds no such account
|
|
27
26
|
#
|
|
28
27
|
# @see Solace::AddressLookupTable
|
|
29
28
|
# @see Solace::TransactionComposer
|
|
@@ -53,38 +52,61 @@ module Solace
|
|
|
53
52
|
# @return [Integer, nil] The slot the table was last extended in
|
|
54
53
|
attr_reader :last_extended_slot
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
55
|
+
class << self
|
|
56
|
+
# Fetch a lookup table account from chain
|
|
57
|
+
#
|
|
58
|
+
# Like {Solace::Connection#get_account_info}, answers nil when the chain
|
|
59
|
+
# holds no account at the address; the caller decides whether that is an error.
|
|
60
|
+
#
|
|
61
|
+
# @param account [#to_s, PublicKey] The lookup table's on-chain address
|
|
62
|
+
# @param connection [Solace::Connection] The connection to read it through
|
|
63
|
+
# @return [AddressLookupTable, nil] The table with its address set, or nil
|
|
64
|
+
def fetch(account, connection:)
|
|
65
|
+
info = connection.get_account_info(account.to_s)
|
|
66
|
+
return unless info
|
|
67
|
+
|
|
68
|
+
base64, = info['data'] # the RPC answers [data, encoding]
|
|
69
|
+
deserialize(Utils::Codecs.base64_to_bytestream(base64), account: account)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Deserialize an on-chain lookup table account
|
|
73
|
+
#
|
|
74
|
+
# The BufferLayout is:
|
|
75
|
+
# - [State type (4 bytes, u32 LE)]
|
|
76
|
+
# - [Deactivation slot (8 bytes, u64 LE)]
|
|
77
|
+
# - [Last extended slot (8 bytes, u64 LE)]
|
|
78
|
+
# - [Last extended start index (1 byte)]
|
|
79
|
+
# - [Authority (Borsh Option<Pubkey>)]
|
|
80
|
+
# - [Padding, up to {META_SIZE}]
|
|
81
|
+
# - [Addresses (32 bytes each, to end of data)]
|
|
82
|
+
#
|
|
83
|
+
# The account data does not carry its own address, so pass it to have
|
|
84
|
+
# the table know where it lives.
|
|
85
|
+
#
|
|
86
|
+
# @param io [IO, StringIO] The account data to read from
|
|
87
|
+
# @param account [#to_s, PublicKey, nil] The lookup table's on-chain address
|
|
88
|
+
# @return [AddressLookupTable] The parsed table
|
|
89
|
+
def deserialize(io, account: nil)
|
|
90
|
+
Utils::Codecs.decode_le_u32(io) # state type (1 = lookup table); positional
|
|
91
|
+
deactivation_slot = Utils::Codecs.decode_le_u64(io)
|
|
92
|
+
last_extended_slot = Utils::Codecs.decode_le_u64(io)
|
|
93
|
+
|
|
94
|
+
Utils::Codecs.decode_u8(io) # last extended start index; positional
|
|
95
|
+
authority = Utils::Codecs.decode_option_pubkey(io)
|
|
96
|
+
|
|
97
|
+
io.seek(META_SIZE) # addresses begin after the fixed-size metadata region
|
|
98
|
+
|
|
99
|
+
addresses = []
|
|
100
|
+
addresses << Utils::Codecs.decode_pubkey(io) until io.eof?
|
|
101
|
+
|
|
102
|
+
new(
|
|
103
|
+
account: account,
|
|
104
|
+
deactivation_slot: deactivation_slot,
|
|
105
|
+
last_extended_slot: last_extended_slot,
|
|
106
|
+
authority: authority,
|
|
107
|
+
addresses: addresses
|
|
108
|
+
)
|
|
109
|
+
end
|
|
88
110
|
end
|
|
89
111
|
|
|
90
112
|
# Initialize a lookup table account
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Solace
|
|
4
|
+
module Composers
|
|
5
|
+
# Composer for an instruction it does not interpret.
|
|
6
|
+
#
|
|
7
|
+
# Where every other composer derives its accounts' roles from domain
|
|
8
|
+
# arguments, this one is handed the program id, the accounts in order, the
|
|
9
|
+
# data, and an {Utils::AccountContext} declaring each account's role, and
|
|
10
|
+
# rebuilds the instruction by index against whatever context it is
|
|
11
|
+
# composed into. It is what {Solace::TransactionDecomposer} answers for
|
|
12
|
+
# each instruction of a transaction taken apart, and what a caller reaches
|
|
13
|
+
# for to rebuild one of those with an account swapped out.
|
|
14
|
+
#
|
|
15
|
+
# @example Rebuild a recovered instruction with a different rent payer
|
|
16
|
+
# roles = Solace::Utils::AccountContext.new
|
|
17
|
+
# roles.merge_from(recovered.account_context)
|
|
18
|
+
# roles.add_writable_signer(sponsor)
|
|
19
|
+
#
|
|
20
|
+
# composer = Solace::Composers::OpaqueInstructionComposer.new(
|
|
21
|
+
# program_id: recovered.program_id,
|
|
22
|
+
# accounts: [sponsor, *recovered.accounts.drop(1)],
|
|
23
|
+
# data: recovered.data,
|
|
24
|
+
# roles: roles
|
|
25
|
+
# )
|
|
26
|
+
#
|
|
27
|
+
# @since 0.1.9
|
|
28
|
+
class OpaqueInstructionComposer < Base
|
|
29
|
+
# The program the instruction invokes
|
|
30
|
+
#
|
|
31
|
+
# @return [String] The program id
|
|
32
|
+
def program_id
|
|
33
|
+
params[:program_id].to_s
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The accounts the instruction touches, in order
|
|
37
|
+
#
|
|
38
|
+
# @return [Array<String>] The pubkeys
|
|
39
|
+
def accounts
|
|
40
|
+
@accounts ||= params[:accounts].map(&:to_s)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The instruction data, untouched
|
|
44
|
+
#
|
|
45
|
+
# @return [Array<Integer>] The data bytes
|
|
46
|
+
def data
|
|
47
|
+
params[:data]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The context declaring each account's role
|
|
51
|
+
#
|
|
52
|
+
# @return [Utils::AccountContext]
|
|
53
|
+
def roles
|
|
54
|
+
params[:roles]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Declare every account with the role given for it, plus the program
|
|
58
|
+
#
|
|
59
|
+
# @return [void]
|
|
60
|
+
# @raise [ArgumentError] When an account has no role declared
|
|
61
|
+
def setup_accounts
|
|
62
|
+
accounts.each do |pubkey|
|
|
63
|
+
raise ArgumentError, "No role declared for account #{pubkey}" unless roles.pubkey_account_map.key?(pubkey)
|
|
64
|
+
|
|
65
|
+
account_context.merge_account(pubkey, signer: roles.signer?(pubkey), writable: roles.writable?(pubkey))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
account_context.add_readonly_nonsigner(program_id)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Build the instruction with indices resolved against the given context
|
|
72
|
+
#
|
|
73
|
+
# @param account_context [Utils::AccountContext] The account context
|
|
74
|
+
# @return [Solace::Instruction]
|
|
75
|
+
def build_instruction(account_context)
|
|
76
|
+
Solace::Instruction.new.tap do |instruction|
|
|
77
|
+
instruction.program_index = account_context.index_of(program_id)
|
|
78
|
+
instruction.accounts = accounts.map { |pubkey| account_context.index_of(pubkey) }
|
|
79
|
+
instruction.data = data
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Solace
|
|
4
|
+
module Errors
|
|
5
|
+
# Raised when an address lookup table a transaction references does not
|
|
6
|
+
# exist on chain. A message that loads addresses through a table cannot be
|
|
7
|
+
# taken apart without the table's address list, so a missing table is an
|
|
8
|
+
# error rather than an empty list.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# begin
|
|
12
|
+
# Solace::TransactionDecomposer.new(connection: connection).decompose_transaction(transaction)
|
|
13
|
+
# rescue Solace::Errors::AddressLookupTableNotFound => e
|
|
14
|
+
# puts "missing table: #{e.account}"
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# @since 0.1.9
|
|
18
|
+
class AddressLookupTableNotFound < Error
|
|
19
|
+
# @return [String] The lookup table's on-chain address
|
|
20
|
+
attr_reader :account
|
|
21
|
+
|
|
22
|
+
# @param account [String] The lookup table's on-chain address
|
|
23
|
+
def initialize(account)
|
|
24
|
+
super("address lookup table #{account} not found on chain")
|
|
25
|
+
@account = account
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/solace/errors.rb
CHANGED
|
@@ -78,18 +78,28 @@ module Solace
|
|
|
78
78
|
attr_reader :address_lookup_tables
|
|
79
79
|
|
|
80
80
|
# @!attribute version
|
|
81
|
-
# The transaction version (nil for legacy, 0 for v0)
|
|
81
|
+
# The transaction version (nil for legacy, 0 for v0); see {#set_version}
|
|
82
82
|
attr_reader :version
|
|
83
83
|
|
|
84
|
+
# @!attribute compute_budget
|
|
85
|
+
# The compute budget set on the composer (see {Utils::ComputeBudget})
|
|
86
|
+
attr_reader :compute_budget
|
|
87
|
+
|
|
88
|
+
# @!attribute blockhash
|
|
89
|
+
# The blockhash set on the composer, or nil when the latest is fetched at compose time
|
|
90
|
+
attr_reader :blockhash
|
|
91
|
+
|
|
84
92
|
# Initialize the composer
|
|
85
93
|
#
|
|
86
94
|
# @param connection [Solace::Connection] The connection to the Solana cluster
|
|
87
95
|
def initialize(connection:)
|
|
88
96
|
@connection = connection
|
|
97
|
+
@blockhash = nil
|
|
89
98
|
@instruction_composers = []
|
|
90
99
|
@context = Utils::AccountContext.new
|
|
91
100
|
@address_lookup_tables = []
|
|
92
101
|
@version = nil
|
|
102
|
+
@compute_budget = Utils::ComputeBudget.new
|
|
93
103
|
end
|
|
94
104
|
|
|
95
105
|
# Add an instruction composer to the transaction
|
|
@@ -97,7 +107,7 @@ module Solace
|
|
|
97
107
|
# @param composer [Composers::Base] The instruction composer
|
|
98
108
|
# @return [TransactionComposer] Self for chaining
|
|
99
109
|
def add_instruction(composer)
|
|
100
|
-
|
|
110
|
+
context.merge_from(composer.account_context)
|
|
101
111
|
instruction_composers << composer
|
|
102
112
|
self
|
|
103
113
|
end
|
|
@@ -109,7 +119,7 @@ module Solace
|
|
|
109
119
|
#
|
|
110
120
|
# @since 0.1.0
|
|
111
121
|
def prepend_instruction(composer)
|
|
112
|
-
|
|
122
|
+
context.merge_from(composer.account_context)
|
|
113
123
|
instruction_composers.unshift(composer)
|
|
114
124
|
self
|
|
115
125
|
end
|
|
@@ -122,7 +132,7 @@ module Solace
|
|
|
122
132
|
#
|
|
123
133
|
# @since 0.1.0
|
|
124
134
|
def insert_instruction(index, composer)
|
|
125
|
-
|
|
135
|
+
context.merge_from(composer.account_context)
|
|
126
136
|
instruction_composers.insert(index, composer)
|
|
127
137
|
self
|
|
128
138
|
end
|
|
@@ -136,8 +146,7 @@ module Solace
|
|
|
136
146
|
#
|
|
137
147
|
# @since 0.1.0
|
|
138
148
|
def merge(other, placement: :add, index: nil)
|
|
139
|
-
|
|
140
|
-
merge_address_lookup_tables(other.address_lookup_tables)
|
|
149
|
+
absorb(other)
|
|
141
150
|
|
|
142
151
|
case placement
|
|
143
152
|
when :add
|
|
@@ -165,6 +174,58 @@ module Solace
|
|
|
165
174
|
self
|
|
166
175
|
end
|
|
167
176
|
|
|
177
|
+
# Set the transaction version
|
|
178
|
+
#
|
|
179
|
+
# Registering a lookup table opts into v0 on its own; this is for a v0
|
|
180
|
+
# transaction that loads nothing through a table, which is still v0 on the
|
|
181
|
+
# wire. nil composes a legacy message.
|
|
182
|
+
#
|
|
183
|
+
# @param version [Integer, nil] 0 for v0, nil for legacy
|
|
184
|
+
# @return [TransactionComposer] Self for chaining
|
|
185
|
+
# @raise [ArgumentError] For a version other than 0 or nil
|
|
186
|
+
def set_version(version)
|
|
187
|
+
raise ArgumentError, "Unsupported transaction version: #{version.inspect}" unless [nil, 0].include?(version)
|
|
188
|
+
|
|
189
|
+
@version = version
|
|
190
|
+
self
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Set the blockhash to compose against
|
|
194
|
+
#
|
|
195
|
+
# Beside {#set_fee_payer}: a caller re-composing a transaction it was handed
|
|
196
|
+
# wants the blockhash that transaction carried, so the expiry its origin
|
|
197
|
+
# reports stays true. With none set, {#compose_transaction} fetches the
|
|
198
|
+
# latest from the connection. nil clears it.
|
|
199
|
+
#
|
|
200
|
+
# @example
|
|
201
|
+
# composer.set_blockhash(original.message.recent_blockhash)
|
|
202
|
+
#
|
|
203
|
+
# @param blockhash [#to_s, nil] The blockhash (base58)
|
|
204
|
+
# @return [TransactionComposer] Self for chaining
|
|
205
|
+
def set_blockhash(blockhash)
|
|
206
|
+
@blockhash = blockhash&.to_s
|
|
207
|
+
self
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Set the compute budget for the transaction
|
|
211
|
+
#
|
|
212
|
+
# The composer owns the budget as a setting: when it composes, the
|
|
213
|
+
# ComputeBudget instructions are written first, and any
|
|
214
|
+
# ComputeBudget composer of the same kind that was added as a plain
|
|
215
|
+
# instruction is left out, so the budget can never be declared twice
|
|
216
|
+
# (a duplicate `SetComputeUnitLimit` fails on chain). Each keyword
|
|
217
|
+
# replaces that setting; nil clears it. Read it back through {#compute_budget}.
|
|
218
|
+
#
|
|
219
|
+
# @example
|
|
220
|
+
# composer.set_compute_budget(units: 200_000, micro_lamports: 50_000)
|
|
221
|
+
#
|
|
222
|
+
# @param units [Integer, nil] The compute unit limit
|
|
223
|
+
# @param micro_lamports [Integer, nil] The compute unit price in micro-lamports
|
|
224
|
+
# @return [TransactionComposer] Self for chaining
|
|
225
|
+
def set_compute_budget(units: nil, micro_lamports: nil)
|
|
226
|
+
apply_compute_budget(Utils::ComputeBudget.new(units: units, micro_lamports: micro_lamports))
|
|
227
|
+
end
|
|
228
|
+
|
|
168
229
|
# Make an address lookup table available to the transaction
|
|
169
230
|
#
|
|
170
231
|
# Registering a table opts the transaction into the v0 format: every
|
|
@@ -182,8 +243,8 @@ module Solace
|
|
|
182
243
|
#
|
|
183
244
|
# @since 0.1.8
|
|
184
245
|
def add_address_lookup_table(account:, addresses: nil)
|
|
185
|
-
account
|
|
186
|
-
|
|
246
|
+
account = account.to_s
|
|
247
|
+
set_version(0) # Lookup tables require a v0 transaction
|
|
187
248
|
|
|
188
249
|
unless address_lookup_tables.any? { |table| table.account == account }
|
|
189
250
|
address_lookup_tables << Solace::Accounts::AddressLookupTable.new(account: account, addresses: addresses)
|
|
@@ -198,6 +259,9 @@ module Solace
|
|
|
198
259
|
# once a lookup table has been added — loading eligible accounts through any
|
|
199
260
|
# registered tables.
|
|
200
261
|
#
|
|
262
|
+
# Composes against the {#blockhash} set with {#set_blockhash}, or the latest
|
|
263
|
+
# fetched from the connection when none is set.
|
|
264
|
+
#
|
|
201
265
|
# @return [Transaction] The composed transaction (unsigned)
|
|
202
266
|
def compose_transaction
|
|
203
267
|
context.compile
|
|
@@ -263,30 +327,41 @@ module Solace
|
|
|
263
327
|
header: context.header,
|
|
264
328
|
accounts: context.accounts,
|
|
265
329
|
instructions: build_instructions,
|
|
266
|
-
recent_blockhash: connection.get_latest_blockhash[0],
|
|
330
|
+
recent_blockhash: blockhash || connection.get_latest_blockhash[0],
|
|
267
331
|
address_lookup_tables: references
|
|
268
332
|
)
|
|
269
333
|
end
|
|
270
334
|
|
|
271
|
-
# Build all instructions with resolved indices
|
|
335
|
+
# Build all instructions with resolved indices: the budget first, then the
|
|
336
|
+
# added composers minus any ComputeBudget composer the budget supersedes
|
|
272
337
|
#
|
|
273
338
|
# @return [Array<Solace::Instruction>] The built instructions
|
|
274
339
|
def build_instructions
|
|
275
|
-
instruction_composers.
|
|
340
|
+
kept = instruction_composers.reject { |composer| compute_budget.supersedes?(composer) }
|
|
341
|
+
|
|
342
|
+
(compute_budget.composers + kept).map { _1.build_instruction(context) }.flatten
|
|
276
343
|
end
|
|
277
344
|
|
|
278
|
-
#
|
|
345
|
+
# Replace the compute budget, bringing its program into the account context
|
|
279
346
|
#
|
|
280
|
-
# @param
|
|
281
|
-
|
|
282
|
-
|
|
347
|
+
# @param budget [Utils::ComputeBudget] The budget to hold
|
|
348
|
+
# @return [TransactionComposer] Self for chaining
|
|
349
|
+
def apply_compute_budget(budget)
|
|
350
|
+
@compute_budget = budget
|
|
351
|
+
budget.composers.each { |composer| context.merge_from(composer.account_context) }
|
|
352
|
+
self
|
|
283
353
|
end
|
|
284
354
|
|
|
285
|
-
#
|
|
355
|
+
# Take on another composer's accounts, tables (deduped by account) and
|
|
356
|
+
# budget (where it has one set)
|
|
286
357
|
#
|
|
287
|
-
# @param
|
|
288
|
-
def
|
|
289
|
-
|
|
358
|
+
# @param other [TransactionComposer] The other composer
|
|
359
|
+
def absorb(other)
|
|
360
|
+
context.merge_from(other.context)
|
|
361
|
+
other.address_lookup_tables.each do |table|
|
|
362
|
+
add_address_lookup_table(account: table.account, addresses: table.addresses)
|
|
363
|
+
end
|
|
364
|
+
apply_compute_budget(compute_budget.merge(other.compute_budget))
|
|
290
365
|
end
|
|
291
366
|
end
|
|
292
367
|
end
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'composers/base'
|
|
4
|
+
require_relative 'composers/opaque_instruction_composer'
|
|
5
|
+
require_relative 'instructions/compute_budget/set_compute_unit_limit_instruction'
|
|
6
|
+
require_relative 'instructions/compute_budget/set_compute_unit_price_instruction'
|
|
7
|
+
|
|
8
|
+
module Solace
|
|
9
|
+
# Takes a transaction apart into the composer that composes it again
|
|
10
|
+
#
|
|
11
|
+
# The reverse of {TransactionComposer}: given a transaction (or its base64),
|
|
12
|
+
# answers a {TransactionComposer} holding one
|
|
13
|
+
# {Composers::OpaqueInstructionComposer} per instruction, in order, each
|
|
14
|
+
# carrying the program it invokes, its accounts with the signer and writable
|
|
15
|
+
# flags the message header gave them, and its data untouched. The lookup
|
|
16
|
+
# tables a v0 message references are read from chain once and registered, and
|
|
17
|
+
# the version is kept, so the composer composes as v0 again (with or without
|
|
18
|
+
# tables); a `SetComputeUnitLimit` or
|
|
19
|
+
# `SetComputeUnitPrice` the transaction carried becomes the composer's
|
|
20
|
+
# {TransactionComposer#compute_budget}; and the blockhash it was composed
|
|
21
|
+
# against is set as the composer's {TransactionComposer#blockhash}.
|
|
22
|
+
#
|
|
23
|
+
# The Solana facts applied:
|
|
24
|
+
#
|
|
25
|
+
# - Static keys take their flags from the header ordering: signers first,
|
|
26
|
+
# writable before read-only, then non-signers the same way.
|
|
27
|
+
# - A v0 message's instruction indexes address the combined space: the
|
|
28
|
+
# static keys, then every table's writable entries in table order, then
|
|
29
|
+
# every table's read-only entries. A loaded address never signs and is
|
|
30
|
+
# writable by the segment it sits in.
|
|
31
|
+
# - A table that does not exist on chain is an error, not an empty list.
|
|
32
|
+
#
|
|
33
|
+
# For a transaction a {TransactionComposer} built, the composer answered here
|
|
34
|
+
# composes the same bytes again.
|
|
35
|
+
#
|
|
36
|
+
# @example
|
|
37
|
+
# composer = Solace::TransactionDecomposer.new(connection: connection).decompose_transaction(transaction)
|
|
38
|
+
# composer.instruction_composers # => the recovered instructions
|
|
39
|
+
# composer.compose_transaction # => the transaction again
|
|
40
|
+
#
|
|
41
|
+
# @see Solace::TransactionComposer
|
|
42
|
+
# @since 0.1.9
|
|
43
|
+
class TransactionDecomposer
|
|
44
|
+
# The discriminators of the two directives the budget setting holds
|
|
45
|
+
LIMIT_INDEX = Instructions::ComputeBudget::SetComputeUnitLimitInstruction::INSTRUCTION_INDEX.first
|
|
46
|
+
PRICE_INDEX = Instructions::ComputeBudget::SetComputeUnitPriceInstruction::INSTRUCTION_INDEX.first
|
|
47
|
+
private_constant :LIMIT_INDEX, :PRICE_INDEX
|
|
48
|
+
|
|
49
|
+
# What one call reads off a message: the message, the tables it references
|
|
50
|
+
# (by account, read from chain), the combined account space in index
|
|
51
|
+
# order, and a context declaring every account's role
|
|
52
|
+
Reading = Struct.new(:message, :tables, :accounts, :roles, keyword_init: true)
|
|
53
|
+
private_constant :Reading
|
|
54
|
+
|
|
55
|
+
# @!attribute connection
|
|
56
|
+
# The connection lookup tables are read through
|
|
57
|
+
attr_reader :connection
|
|
58
|
+
|
|
59
|
+
# @param connection [Solace::Connection] The connection to read lookup tables through
|
|
60
|
+
def initialize(connection:)
|
|
61
|
+
@connection = connection
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Take a transaction apart
|
|
65
|
+
#
|
|
66
|
+
# @param transaction [Solace::Transaction, String] The transaction, or its base64
|
|
67
|
+
# @return [Solace::TransactionComposer] A composer that composes it again
|
|
68
|
+
# @raise [Solace::Errors::AddressLookupTableNotFound] When a referenced table is not on chain
|
|
69
|
+
def decompose_transaction(transaction)
|
|
70
|
+
message = message_of(transaction)
|
|
71
|
+
reading = read(message)
|
|
72
|
+
composer = TransactionComposer.new(connection: connection)
|
|
73
|
+
.set_version(message.version)
|
|
74
|
+
.set_blockhash(message.recent_blockhash)
|
|
75
|
+
|
|
76
|
+
seed(composer, reading)
|
|
77
|
+
fill(composer, reading)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# Read a message: its tables from chain, its combined account space, and
|
|
83
|
+
# the role of every account in it
|
|
84
|
+
#
|
|
85
|
+
# @param message [Solace::Message]
|
|
86
|
+
# @return [Reading]
|
|
87
|
+
def read(message)
|
|
88
|
+
tables = fetch_tables(message)
|
|
89
|
+
writable = loaded(message, tables, :writable_indexes)
|
|
90
|
+
readonly = loaded(message, tables, :readonly_indexes)
|
|
91
|
+
roles = Utils::AccountContext.new
|
|
92
|
+
|
|
93
|
+
declare_static(roles, message)
|
|
94
|
+
writable.each { |pubkey| roles.merge_account(pubkey, signer: false, writable: true) }
|
|
95
|
+
readonly.each { |pubkey| roles.merge_account(pubkey, signer: false, writable: false) }
|
|
96
|
+
|
|
97
|
+
accounts = message.accounts + writable + readonly
|
|
98
|
+
|
|
99
|
+
Reading.new(message: message, tables: tables, accounts: accounts, roles: roles)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Declare the static keys with the roles their header positions give them:
|
|
103
|
+
# signers first, writable before read-only, then non-signers the same way
|
|
104
|
+
#
|
|
105
|
+
# @param roles [Utils::AccountContext]
|
|
106
|
+
# @param message [Solace::Message]
|
|
107
|
+
# @return [void]
|
|
108
|
+
def declare_static(roles, message)
|
|
109
|
+
signers, readonly_signed, readonly_unsigned = message.header
|
|
110
|
+
|
|
111
|
+
message.accounts.each_with_index do |pubkey, index|
|
|
112
|
+
is_signer = index < signers
|
|
113
|
+
readonly_from = is_signer ? signers - readonly_signed : message.accounts.size - readonly_unsigned
|
|
114
|
+
is_writable = index < readonly_from
|
|
115
|
+
|
|
116
|
+
roles.merge_account(pubkey, signer: is_signer, writable: is_writable)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Seed the composer's accounts in message order and set the fee payer, so
|
|
121
|
+
# it composes the same account order again
|
|
122
|
+
#
|
|
123
|
+
# @param composer [Solace::TransactionComposer]
|
|
124
|
+
# @param reading [Reading]
|
|
125
|
+
# @return [void]
|
|
126
|
+
def seed(composer, reading)
|
|
127
|
+
composer.context.merge_from(reading.roles)
|
|
128
|
+
composer.set_fee_payer(reading.message.accounts.first)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Add the instructions, register the tables, and set the budget
|
|
132
|
+
#
|
|
133
|
+
# @param composer [Solace::TransactionComposer]
|
|
134
|
+
# @param reading [Reading]
|
|
135
|
+
# @return [Solace::TransactionComposer] The composer
|
|
136
|
+
def fill(composer, reading)
|
|
137
|
+
instruction_composers(reading).each { |instruction| composer.add_instruction(instruction) }
|
|
138
|
+
|
|
139
|
+
reading.tables.each_value do |table|
|
|
140
|
+
composer.add_address_lookup_table(account: table.account, addresses: table.addresses)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
budget = compute_budget(reading)
|
|
144
|
+
composer.set_compute_budget(units: budget.units, micro_lamports: budget.micro_lamports) if budget.set?
|
|
145
|
+
|
|
146
|
+
composer
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# The message of a transaction given as the object or its base64
|
|
150
|
+
#
|
|
151
|
+
# @param transaction [Solace::Transaction, String]
|
|
152
|
+
# @return [Solace::Message]
|
|
153
|
+
def message_of(transaction)
|
|
154
|
+
transaction = Solace::Transaction.from(transaction) if transaction.is_a?(String)
|
|
155
|
+
transaction.message
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# The tables the message references, read from chain once each, in message order
|
|
159
|
+
#
|
|
160
|
+
# @param message [Solace::Message]
|
|
161
|
+
# @return [Hash{String => Accounts::AddressLookupTable}] Keyed by table account
|
|
162
|
+
def fetch_tables(message)
|
|
163
|
+
references = Array(message.address_lookup_tables)
|
|
164
|
+
accounts = references.map(&:account).uniq
|
|
165
|
+
|
|
166
|
+
accounts.to_h do |account|
|
|
167
|
+
table = Accounts::AddressLookupTable.fetch(account, connection: connection)
|
|
168
|
+
raise Errors::AddressLookupTableNotFound, account unless table
|
|
169
|
+
|
|
170
|
+
[account, table]
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# The pubkeys one segment loads, across every table in table order
|
|
175
|
+
#
|
|
176
|
+
# @param message [Solace::Message]
|
|
177
|
+
# @param tables [Hash{String => Accounts::AddressLookupTable}]
|
|
178
|
+
# @param kind [Symbol] :writable_indexes or :readonly_indexes
|
|
179
|
+
# @return [Array<String>]
|
|
180
|
+
def loaded(message, tables, kind)
|
|
181
|
+
references = Array(message.address_lookup_tables)
|
|
182
|
+
|
|
183
|
+
references.flat_map do |reference|
|
|
184
|
+
addresses = tables.fetch(reference.account).addresses
|
|
185
|
+
indexes = kind == :writable_indexes ? reference.writable_indexes : reference.readonly_indexes
|
|
186
|
+
|
|
187
|
+
indexes.map { |index| addresses.fetch(index) }
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# The message's instructions as composers, the budget directives left out
|
|
192
|
+
#
|
|
193
|
+
# @param reading [Reading]
|
|
194
|
+
# @return [Array<Composers::OpaqueInstructionComposer>]
|
|
195
|
+
def instruction_composers(reading)
|
|
196
|
+
lifted = reading.message.instructions.reject { |instruction| budget?(instruction, reading) }
|
|
197
|
+
|
|
198
|
+
lifted.map do |instruction|
|
|
199
|
+
program_id = reading.accounts.fetch(instruction.program_index)
|
|
200
|
+
accounts = instruction.accounts.map { |index| reading.accounts.fetch(index) }
|
|
201
|
+
|
|
202
|
+
Composers::OpaqueInstructionComposer.new(
|
|
203
|
+
program_id: program_id,
|
|
204
|
+
accounts: accounts,
|
|
205
|
+
data: instruction.data,
|
|
206
|
+
roles: reading.roles
|
|
207
|
+
)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Whether an instruction is a limit or price directive the budget setting holds
|
|
212
|
+
#
|
|
213
|
+
# @param instruction [Solace::Instruction]
|
|
214
|
+
# @param reading [Reading]
|
|
215
|
+
# @return [Boolean]
|
|
216
|
+
def budget?(instruction, reading)
|
|
217
|
+
program_id = reading.accounts.fetch(instruction.program_index)
|
|
218
|
+
discriminator = instruction.data.first
|
|
219
|
+
|
|
220
|
+
program_id == Constants::COMPUTE_BUDGET_PROGRAM_ID && [LIMIT_INDEX, PRICE_INDEX].include?(discriminator)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# The budget the message carried, decoded from its limit and price directives
|
|
224
|
+
#
|
|
225
|
+
# @param reading [Reading]
|
|
226
|
+
# @return [Utils::ComputeBudget] Unset when the message carried neither
|
|
227
|
+
def compute_budget(reading)
|
|
228
|
+
directives = reading.message.instructions.select { |instruction| budget?(instruction, reading) }
|
|
229
|
+
|
|
230
|
+
Utils::ComputeBudget.new(
|
|
231
|
+
units: decode(directives, LIMIT_INDEX, 'L<'),
|
|
232
|
+
micro_lamports: decode(directives, PRICE_INDEX, 'Q<')
|
|
233
|
+
)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# The value of the first directive with the given discriminator, or nil
|
|
237
|
+
#
|
|
238
|
+
# @param directives [Array<Solace::Instruction>] The budget directives
|
|
239
|
+
# @param discriminator [Integer] The directive's first data byte
|
|
240
|
+
# @param format [String] The pack format of the value that follows it
|
|
241
|
+
# @return [Integer, nil]
|
|
242
|
+
def decode(directives, discriminator, format)
|
|
243
|
+
directive = directives.find { |instruction| instruction.data.first == discriminator }
|
|
244
|
+
return unless directive
|
|
245
|
+
|
|
246
|
+
value_bytes = directive.data.drop(1)
|
|
247
|
+
value_bytes.pack('C*').unpack1(format)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
@@ -101,6 +101,27 @@ module Solace
|
|
|
101
101
|
merge_account(pubkey, signer: false, writable: false)
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
+
# Add or merge an account into the context
|
|
105
|
+
#
|
|
106
|
+
# The primitive the +add_*+ methods above name a role for; call it
|
|
107
|
+
# directly when the flags are data in hand. Permissions only ever widen:
|
|
108
|
+
# an account added twice keeps the union of what it was given.
|
|
109
|
+
#
|
|
110
|
+
# @param pubkey [#to_s, PublicKey] The public key of the account
|
|
111
|
+
# @param signer [Boolean] Whether the account is a signer
|
|
112
|
+
# @param writable [Boolean] Whether the account is writable
|
|
113
|
+
# @param fee_payer [Boolean] Whether the account pays the fee (see {#set_fee_payer})
|
|
114
|
+
def merge_account(pubkey, signer:, writable:, fee_payer: false)
|
|
115
|
+
pubkey_str = pubkey.to_s
|
|
116
|
+
|
|
117
|
+
@pubkey_account_map[pubkey_str] ||= DEFAULT_ACCOUNT.dup
|
|
118
|
+
@pubkey_account_map[pubkey_str][:signer] ||= signer
|
|
119
|
+
@pubkey_account_map[pubkey_str][:writable] ||= writable
|
|
120
|
+
@pubkey_account_map[pubkey_str][:fee_payer] ||= fee_payer
|
|
121
|
+
|
|
122
|
+
self
|
|
123
|
+
end
|
|
124
|
+
|
|
104
125
|
# Predicate to check if an account is a fee payer
|
|
105
126
|
#
|
|
106
127
|
# @param pubkey [String] The pubkey of the account
|
|
@@ -212,23 +233,6 @@ module Solace
|
|
|
212
233
|
|
|
213
234
|
private
|
|
214
235
|
|
|
215
|
-
# Add or merge an account into the context
|
|
216
|
-
#
|
|
217
|
-
# @param pubkey [#to_s, PublicKey] The public key of the account
|
|
218
|
-
# @param signer [Boolean] Whether the account is a signer
|
|
219
|
-
# @param writable [Boolean] Whether the account is writable
|
|
220
|
-
# @param [Boolean] fee_payer
|
|
221
|
-
def merge_account(pubkey, signer:, writable:, fee_payer: false)
|
|
222
|
-
pubkey_str = pubkey.is_a?(String) ? pubkey : pubkey.address
|
|
223
|
-
|
|
224
|
-
@pubkey_account_map[pubkey_str] ||= DEFAULT_ACCOUNT.dup
|
|
225
|
-
@pubkey_account_map[pubkey_str][:signer] ||= signer
|
|
226
|
-
@pubkey_account_map[pubkey_str][:writable] ||= writable
|
|
227
|
-
@pubkey_account_map[pubkey_str][:fee_payer] ||= fee_payer
|
|
228
|
-
|
|
229
|
-
self
|
|
230
|
-
end
|
|
231
|
-
|
|
232
236
|
# Order the static accounts by signer, writable, readonly signer, readonly
|
|
233
237
|
#
|
|
234
238
|
# Loaded accounts are excluded — they leave the static account list and are
|
|
@@ -237,15 +241,23 @@ module Solace
|
|
|
237
241
|
# @param loaded_accounts [Array<String>] Pubkeys resolved through lookup tables
|
|
238
242
|
# @return [Array<String>] The ordered static accounts
|
|
239
243
|
def order_accounts(loaded_accounts)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
244
|
+
# Keyed on the position too: sort_by is not stable, and accounts of the
|
|
245
|
+
# same rank must keep the order they were added in
|
|
246
|
+
(@pubkey_account_map.keys - loaded_accounts).sort_by.with_index { |pubkey, index| [rank_of(pubkey), index] }
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# The position of an account's segment in the static account list
|
|
250
|
+
#
|
|
251
|
+
# @param pubkey [String] The pubkey of the account
|
|
252
|
+
# @return [Integer] The segment rank
|
|
253
|
+
def rank_of(pubkey)
|
|
254
|
+
if fee_payer?(pubkey) then 0
|
|
255
|
+
elsif writable_signer?(pubkey) then 1
|
|
256
|
+
elsif readonly_signer?(pubkey) then 2
|
|
257
|
+
elsif writable_nonsigner?(pubkey) then 3
|
|
258
|
+
elsif readonly_nonsigner?(pubkey) then 4
|
|
259
|
+
else
|
|
260
|
+
raise ArgumentError, "Unknown account type for pubkey: #{pubkey}"
|
|
249
261
|
end
|
|
250
262
|
end
|
|
251
263
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../composers/base'
|
|
4
|
+
require_relative '../composers/compute_budget_program_set_compute_unit_limit_composer'
|
|
5
|
+
require_relative '../composers/compute_budget_program_set_compute_unit_price_composer'
|
|
6
|
+
|
|
7
|
+
module Solace
|
|
8
|
+
module Utils
|
|
9
|
+
# The compute budget a transaction composer carries as a setting
|
|
10
|
+
#
|
|
11
|
+
# Holds the compute unit limit and price, answers the ComputeBudget composers
|
|
12
|
+
# they translate to (limit then price), and decides which directly added
|
|
13
|
+
# ComputeBudget composers the setting supersedes so the budget is never
|
|
14
|
+
# declared twice. Instances are immutable; {#merge} answers a new one.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# budget = Solace::Utils::ComputeBudget.new(units: 200_000, micro_lamports: 50_000)
|
|
18
|
+
# budget.composers # => [SetComputeUnitLimit composer, SetComputeUnitPrice composer]
|
|
19
|
+
#
|
|
20
|
+
# @see Solace::TransactionComposer#set_compute_budget
|
|
21
|
+
# @since 0.1.9
|
|
22
|
+
class ComputeBudget
|
|
23
|
+
# The composer a compute unit limit translates to
|
|
24
|
+
LIMIT_COMPOSER = Composers::ComputeBudgetProgramSetComputeUnitLimitComposer
|
|
25
|
+
private_constant :LIMIT_COMPOSER
|
|
26
|
+
|
|
27
|
+
# The composer a compute unit price translates to
|
|
28
|
+
PRICE_COMPOSER = Composers::ComputeBudgetProgramSetComputeUnitPriceComposer
|
|
29
|
+
private_constant :PRICE_COMPOSER
|
|
30
|
+
|
|
31
|
+
# @!attribute units
|
|
32
|
+
# The compute unit limit, or nil when none is set
|
|
33
|
+
attr_reader :units
|
|
34
|
+
|
|
35
|
+
# @!attribute micro_lamports
|
|
36
|
+
# The compute unit price in micro-lamports, or nil when none is set
|
|
37
|
+
attr_reader :micro_lamports
|
|
38
|
+
|
|
39
|
+
# Initialize the compute budget
|
|
40
|
+
#
|
|
41
|
+
# @param units [Integer, nil] The compute unit limit
|
|
42
|
+
# @param micro_lamports [Integer, nil] The compute unit price in micro-lamports
|
|
43
|
+
def initialize(units: nil, micro_lamports: nil)
|
|
44
|
+
@units = units
|
|
45
|
+
@micro_lamports = micro_lamports
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Whether any part of the budget is set
|
|
49
|
+
#
|
|
50
|
+
# @return [Boolean]
|
|
51
|
+
def set?
|
|
52
|
+
!(units.nil? && micro_lamports.nil?)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The ComputeBudget composers the budget translates to, limit then price
|
|
56
|
+
#
|
|
57
|
+
# @return [Array<Composers::Base>] The composers, empty when nothing is set
|
|
58
|
+
def composers
|
|
59
|
+
[
|
|
60
|
+
(LIMIT_COMPOSER.new(units: units) if units),
|
|
61
|
+
(PRICE_COMPOSER.new(micro_lamports: micro_lamports) if micro_lamports)
|
|
62
|
+
].compact
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Whether a directly added composer is replaced by a setting of the same kind
|
|
66
|
+
#
|
|
67
|
+
# @param composer [Composers::Base] The added composer
|
|
68
|
+
# @return [Boolean]
|
|
69
|
+
def supersedes?(composer)
|
|
70
|
+
(!units.nil? && composer.is_a?(LIMIT_COMPOSER)) || (!micro_lamports.nil? && composer.is_a?(PRICE_COMPOSER))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# A budget with the other's settings where it has them, and this one's otherwise
|
|
74
|
+
#
|
|
75
|
+
# @param other [ComputeBudget] The budget to fold in
|
|
76
|
+
# @return [ComputeBudget] The merged budget
|
|
77
|
+
def merge(other)
|
|
78
|
+
self.class.new(
|
|
79
|
+
units: other.units || units,
|
|
80
|
+
micro_lamports: other.micro_lamports || micro_lamports
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
data/lib/solace/version.rb
CHANGED
data/lib/solace.rb
CHANGED
|
@@ -35,11 +35,14 @@ require_relative 'solace/version'
|
|
|
35
35
|
require_relative 'solace/errors'
|
|
36
36
|
require_relative 'solace/constants'
|
|
37
37
|
require_relative 'solace/connection'
|
|
38
|
+
require_relative 'solace/concerns/binary_serializable'
|
|
39
|
+
|
|
40
|
+
# Utils
|
|
38
41
|
require_relative 'solace/utils/codecs'
|
|
39
42
|
require_relative 'solace/utils/pda'
|
|
40
43
|
require_relative 'solace/utils/account_context'
|
|
44
|
+
require_relative 'solace/utils/compute_budget'
|
|
41
45
|
require_relative 'solace/utils/curve25519_dalek'
|
|
42
|
-
require_relative 'solace/concerns/binary_serializable'
|
|
43
46
|
|
|
44
47
|
# Tokens
|
|
45
48
|
require_relative 'solace/tokens'
|
|
@@ -60,6 +63,7 @@ require_relative 'solace/address_lookup_table'
|
|
|
60
63
|
require_relative 'solace/accounts/address_lookup_table'
|
|
61
64
|
|
|
62
65
|
require_relative 'solace/transaction_composer'
|
|
66
|
+
require_relative 'solace/transaction_decomposer'
|
|
63
67
|
|
|
64
68
|
# Base Classes (Abstract classes)
|
|
65
69
|
require_relative 'solace/programs/base'
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solace
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sebastian Scholl
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07
|
|
11
|
+
date: 2026-10-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base58
|
|
@@ -201,6 +201,7 @@ executables: []
|
|
|
201
201
|
extensions: []
|
|
202
202
|
extra_rdoc_files: []
|
|
203
203
|
files:
|
|
204
|
+
- lib/rubocop/cop/solace/exploded_hash.rb
|
|
204
205
|
- lib/solace.rb
|
|
205
206
|
- lib/solace/accounts/address_lookup_table.rb
|
|
206
207
|
- lib/solace/address_lookup_table.rb
|
|
@@ -211,6 +212,7 @@ files:
|
|
|
211
212
|
- lib/solace/composers/base.rb
|
|
212
213
|
- lib/solace/composers/compute_budget_program_set_compute_unit_limit_composer.rb
|
|
213
214
|
- lib/solace/composers/compute_budget_program_set_compute_unit_price_composer.rb
|
|
215
|
+
- lib/solace/composers/opaque_instruction_composer.rb
|
|
214
216
|
- lib/solace/composers/spl_token_program_close_account_composer.rb
|
|
215
217
|
- lib/solace/composers/spl_token_program_initialize_mint_composer.rb
|
|
216
218
|
- lib/solace/composers/spl_token_program_mint_to_composer.rb
|
|
@@ -227,6 +229,7 @@ files:
|
|
|
227
229
|
- lib/solace/connection.rb
|
|
228
230
|
- lib/solace/constants.rb
|
|
229
231
|
- lib/solace/errors.rb
|
|
232
|
+
- lib/solace/errors/address_lookup_table_not_found.rb
|
|
230
233
|
- lib/solace/errors/confirmation_timeout.rb
|
|
231
234
|
- lib/solace/errors/connection_error.rb
|
|
232
235
|
- lib/solace/errors/error.rb
|
|
@@ -276,8 +279,10 @@ files:
|
|
|
276
279
|
- lib/solace/tokens/token.rb
|
|
277
280
|
- lib/solace/transaction.rb
|
|
278
281
|
- lib/solace/transaction_composer.rb
|
|
282
|
+
- lib/solace/transaction_decomposer.rb
|
|
279
283
|
- lib/solace/utils/account_context.rb
|
|
280
284
|
- lib/solace/utils/codecs.rb
|
|
285
|
+
- lib/solace/utils/compute_budget.rb
|
|
281
286
|
- lib/solace/utils/curve25519_dalek.rb
|
|
282
287
|
- lib/solace/utils/libcurve25519_dalek-linux-arm64/libcurve25519_dalek.so
|
|
283
288
|
- lib/solace/utils/libcurve25519_dalek-linux/libcurve25519_dalek.so
|