solace 0.1.7 → 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 +156 -0
- data/lib/solace/composers/address_lookup_table_program_create_composer.rb +95 -0
- data/lib/solace/composers/address_lookup_table_program_extend_composer.rb +87 -0
- data/lib/solace/composers/opaque_instruction_composer.rb +84 -0
- data/lib/solace/connection.rb +10 -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/instructions/address_lookup_table_program/create_lookup_table_instruction.rb +80 -0
- data/lib/solace/instructions/address_lookup_table_program/extend_lookup_table_instruction.rb +69 -0
- data/lib/solace/transaction_composer.rb +195 -17
- data/lib/solace/transaction_decomposer.rb +250 -0
- data/lib/solace/utils/account_context.rb +73 -37
- data/lib/solace/utils/compute_budget.rb +85 -0
- data/lib/solace/version.rb +1 -1
- data/lib/solace.rb +9 -1
- metadata +12 -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
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Solace
|
|
4
|
+
# On-chain account models — the account state living at an address, as opposed
|
|
5
|
+
# to the wire-level structures that reference it. Conventions for these types
|
|
6
|
+
# (fetching, deserializing, deriving) are expected to grow here.
|
|
7
|
+
module Accounts
|
|
8
|
+
# Models an on-chain Address Lookup Table account: its address, the metadata
|
|
9
|
+
# the program stores (authority, activation slots), and the full, ordered
|
|
10
|
+
# list of addresses it holds.
|
|
11
|
+
#
|
|
12
|
+
# This is distinct from {Solace::AddressLookupTable}, which is the *reference*
|
|
13
|
+
# a v0 message carries (a table account plus the writable/readonly index
|
|
14
|
+
# positions into it). A composer decides which of this account's addresses
|
|
15
|
+
# load — a v0 concern that needs the account context — and this object owns
|
|
16
|
+
# the table-local part: turning the addresses it contributes into that
|
|
17
|
+
# message reference.
|
|
18
|
+
#
|
|
19
|
+
# @example Register a table on a composer
|
|
20
|
+
# table = Solace::Accounts::AddressLookupTable.new(account: address, addresses: on_chain_addresses)
|
|
21
|
+
# table.reference(loaded_writable, loaded_readonly) # => Solace::AddressLookupTable or nil
|
|
22
|
+
#
|
|
23
|
+
# @example Read a table's on-chain state
|
|
24
|
+
# table = Solace::Accounts::AddressLookupTable.fetch(address, connection: connection)
|
|
25
|
+
# table&.addresses # => the stored addresses, or nil if the chain holds no such account
|
|
26
|
+
#
|
|
27
|
+
# @see Solace::AddressLookupTable
|
|
28
|
+
# @see Solace::TransactionComposer
|
|
29
|
+
# @since 0.1.8
|
|
30
|
+
class AddressLookupTable
|
|
31
|
+
# Byte offset at which the stored addresses begin — the program reserves a
|
|
32
|
+
# fixed-size metadata region ahead of them regardless of its contents.
|
|
33
|
+
META_SIZE = 56
|
|
34
|
+
|
|
35
|
+
# @!attribute [r] account
|
|
36
|
+
# @return [String, nil] The lookup table's on-chain address
|
|
37
|
+
attr_reader :account
|
|
38
|
+
|
|
39
|
+
# @!attribute [r] addresses
|
|
40
|
+
# @return [Array<String>] The full, ordered list of addresses stored in the table
|
|
41
|
+
attr_reader :addresses
|
|
42
|
+
|
|
43
|
+
# @!attribute [r] authority
|
|
44
|
+
# @return [String, nil] The authority allowed to extend/close the table
|
|
45
|
+
attr_reader :authority
|
|
46
|
+
|
|
47
|
+
# @!attribute [r] deactivation_slot
|
|
48
|
+
# @return [Integer, nil] The slot the table was deactivated in (max while active)
|
|
49
|
+
attr_reader :deactivation_slot
|
|
50
|
+
|
|
51
|
+
# @!attribute [r] last_extended_slot
|
|
52
|
+
# @return [Integer, nil] The slot the table was last extended in
|
|
53
|
+
attr_reader :last_extended_slot
|
|
54
|
+
|
|
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
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Initialize a lookup table account
|
|
113
|
+
#
|
|
114
|
+
# @param account [#to_s, PublicKey, nil] The lookup table's on-chain address
|
|
115
|
+
# @param addresses [Array<#to_s>, nil] The full, ordered address list; may be
|
|
116
|
+
# omitted (the composer then loads nothing through this table)
|
|
117
|
+
# @param authority [String, nil] The table authority
|
|
118
|
+
# @param deactivation_slot [Integer, nil] The deactivation slot
|
|
119
|
+
# @param last_extended_slot [Integer, nil] The last extended slot
|
|
120
|
+
def initialize(account: nil, addresses: nil, authority: nil, deactivation_slot: nil, last_extended_slot: nil)
|
|
121
|
+
@account = account&.to_s
|
|
122
|
+
@addresses = Array(addresses).map(&:to_s)
|
|
123
|
+
@authority = authority
|
|
124
|
+
@deactivation_slot = deactivation_slot
|
|
125
|
+
@last_extended_slot = last_extended_slot
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Build the v0 message reference for the addresses this table contributes
|
|
129
|
+
#
|
|
130
|
+
# @param writable [Array<String>] Loaded writable pubkeys drawn from this table
|
|
131
|
+
# @param readonly [Array<String>] Loaded readonly pubkeys drawn from this table
|
|
132
|
+
# @return [Solace::AddressLookupTable, nil] The reference, or nil if it loads nothing
|
|
133
|
+
def reference(writable, readonly)
|
|
134
|
+
writable_indexes = positions_of(writable)
|
|
135
|
+
readonly_indexes = positions_of(readonly)
|
|
136
|
+
return if writable_indexes.empty? && readonly_indexes.empty?
|
|
137
|
+
|
|
138
|
+
Solace::AddressLookupTable.new.tap do |reference|
|
|
139
|
+
reference.account = account
|
|
140
|
+
reference.writable_indexes = writable_indexes
|
|
141
|
+
reference.readonly_indexes = readonly_indexes
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
# Positions of the given pubkeys within this table's address list
|
|
148
|
+
#
|
|
149
|
+
# @param pubkeys [Array<String>] The pubkeys to locate
|
|
150
|
+
# @return [Array<Integer>] Their positions in {#addresses}
|
|
151
|
+
def positions_of(pubkeys)
|
|
152
|
+
pubkeys.filter_map { |pubkey| addresses.index(pubkey) }
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Solace
|
|
4
|
+
module Composers
|
|
5
|
+
# Composer for creating an address lookup table.
|
|
6
|
+
#
|
|
7
|
+
# Resolves and orders the accounts for a `CreateLookupTable` instruction and
|
|
8
|
+
# delegates construction to
|
|
9
|
+
# `Instructions::AddressLookupTableProgram::CreateLookupTableInstruction`.
|
|
10
|
+
#
|
|
11
|
+
# The table address is a program-derived address of `[authority, recent_slot]`;
|
|
12
|
+
# derive it (and its bump) with {Solace::Utils::PDA} and pass both in.
|
|
13
|
+
#
|
|
14
|
+
# Required accounts:
|
|
15
|
+
# - **Table**: the uninitialized table account (writable, non-signer)
|
|
16
|
+
# - **Authority**: controls the table (readonly, signer)
|
|
17
|
+
# - **Payer**: funds the table's rent (writable, signer)
|
|
18
|
+
# - **System program** (readonly, non-signer)
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# composer = AddressLookupTableProgramCreateComposer.new(
|
|
22
|
+
# table: table_address,
|
|
23
|
+
# authority: authority,
|
|
24
|
+
# payer: payer,
|
|
25
|
+
# recent_slot: recent_slot,
|
|
26
|
+
# bump: bump
|
|
27
|
+
# )
|
|
28
|
+
#
|
|
29
|
+
# @see Instructions::AddressLookupTableProgram::CreateLookupTableInstruction
|
|
30
|
+
# @since 0.1.8
|
|
31
|
+
class AddressLookupTableProgramCreateComposer < Base
|
|
32
|
+
# @return [String] The table's on-chain address
|
|
33
|
+
def table
|
|
34
|
+
params[:table].to_s
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [String] The table authority
|
|
38
|
+
def authority
|
|
39
|
+
params[:authority].to_s
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [String] The rent payer
|
|
43
|
+
def payer
|
|
44
|
+
params[:payer].to_s
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [String] The system program id
|
|
48
|
+
def system_program
|
|
49
|
+
Solace::Constants::SYSTEM_PROGRAM_ID.to_s
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [String] The address lookup table program id
|
|
53
|
+
def lookup_table_program
|
|
54
|
+
Solace::Constants::ADDRESS_LOOKUP_TABLE_PROGRAM_ID.to_s
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [Integer] The slot used to derive the table address
|
|
58
|
+
def recent_slot
|
|
59
|
+
params[:recent_slot]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [Integer] The bump seed for the table's program-derived address
|
|
63
|
+
def bump
|
|
64
|
+
params[:bump]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Setup accounts required for the create lookup table instruction
|
|
68
|
+
#
|
|
69
|
+
# @return [void]
|
|
70
|
+
def setup_accounts
|
|
71
|
+
account_context.add_writable_nonsigner(table)
|
|
72
|
+
account_context.add_readonly_signer(authority)
|
|
73
|
+
account_context.add_writable_signer(payer)
|
|
74
|
+
account_context.add_readonly_nonsigner(system_program)
|
|
75
|
+
account_context.add_readonly_nonsigner(lookup_table_program)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Build instruction with resolved account indices
|
|
79
|
+
#
|
|
80
|
+
# @param account_context [Utils::AccountContext] The account context
|
|
81
|
+
# @return [Solace::Instruction]
|
|
82
|
+
def build_instruction(account_context)
|
|
83
|
+
Solace::Instructions::AddressLookupTableProgram::CreateLookupTableInstruction.build(
|
|
84
|
+
recent_slot: recent_slot,
|
|
85
|
+
bump: bump,
|
|
86
|
+
program_index: account_context.index_of(lookup_table_program),
|
|
87
|
+
table_index: account_context.index_of(table),
|
|
88
|
+
authority_index: account_context.index_of(authority),
|
|
89
|
+
payer_index: account_context.index_of(payer),
|
|
90
|
+
system_program_index: account_context.index_of(system_program)
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Solace
|
|
4
|
+
module Composers
|
|
5
|
+
# Composer for extending an address lookup table with new addresses.
|
|
6
|
+
#
|
|
7
|
+
# Resolves and orders the accounts for an `ExtendLookupTable` instruction and
|
|
8
|
+
# delegates construction to
|
|
9
|
+
# `Instructions::AddressLookupTableProgram::ExtendLookupTableInstruction`.
|
|
10
|
+
#
|
|
11
|
+
# Addresses appended here become usable one slot after the extend lands.
|
|
12
|
+
#
|
|
13
|
+
# Required accounts:
|
|
14
|
+
# - **Table**: the table account being extended (writable, non-signer)
|
|
15
|
+
# - **Authority**: controls the table (readonly, signer)
|
|
16
|
+
# - **Payer**: funds any additional rent (writable, signer)
|
|
17
|
+
# - **System program** (readonly, non-signer)
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
# composer = AddressLookupTableProgramExtendComposer.new(
|
|
21
|
+
# table: table_address,
|
|
22
|
+
# authority: authority,
|
|
23
|
+
# payer: payer,
|
|
24
|
+
# addresses: [recipient1, recipient2]
|
|
25
|
+
# )
|
|
26
|
+
#
|
|
27
|
+
# @see Instructions::AddressLookupTableProgram::ExtendLookupTableInstruction
|
|
28
|
+
# @since 0.1.8
|
|
29
|
+
class AddressLookupTableProgramExtendComposer < Base
|
|
30
|
+
# @return [String] The table's on-chain address
|
|
31
|
+
def table
|
|
32
|
+
params[:table].to_s
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [String] The table authority
|
|
36
|
+
def authority
|
|
37
|
+
params[:authority].to_s
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [String] The rent payer
|
|
41
|
+
def payer
|
|
42
|
+
params[:payer].to_s
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [String] The system program id
|
|
46
|
+
def system_program
|
|
47
|
+
Solace::Constants::SYSTEM_PROGRAM_ID.to_s
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [String] The address lookup table program id
|
|
51
|
+
def lookup_table_program
|
|
52
|
+
Solace::Constants::ADDRESS_LOOKUP_TABLE_PROGRAM_ID.to_s
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @return [Array<String>] The addresses to append to the table
|
|
56
|
+
def addresses
|
|
57
|
+
params[:addresses].map(&:to_s)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Setup accounts required for the extend lookup table instruction
|
|
61
|
+
#
|
|
62
|
+
# @return [void]
|
|
63
|
+
def setup_accounts
|
|
64
|
+
account_context.add_writable_nonsigner(table)
|
|
65
|
+
account_context.add_readonly_signer(authority)
|
|
66
|
+
account_context.add_writable_signer(payer)
|
|
67
|
+
account_context.add_readonly_nonsigner(system_program)
|
|
68
|
+
account_context.add_readonly_nonsigner(lookup_table_program)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Build instruction with resolved account indices
|
|
72
|
+
#
|
|
73
|
+
# @param account_context [Utils::AccountContext] The account context
|
|
74
|
+
# @return [Solace::Instruction]
|
|
75
|
+
def build_instruction(account_context)
|
|
76
|
+
Solace::Instructions::AddressLookupTableProgram::ExtendLookupTableInstruction.build(
|
|
77
|
+
addresses: addresses,
|
|
78
|
+
program_index: account_context.index_of(lookup_table_program),
|
|
79
|
+
table_index: account_context.index_of(table),
|
|
80
|
+
authority_index: account_context.index_of(authority),
|
|
81
|
+
payer_index: account_context.index_of(payer),
|
|
82
|
+
system_program_index: account_context.index_of(system_program)
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -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
|
data/lib/solace/connection.rb
CHANGED
|
@@ -168,6 +168,16 @@ module Solace
|
|
|
168
168
|
@rpc_client.rpc_request('getBlockHeight', [{ commitment: commitment }])['result']
|
|
169
169
|
end
|
|
170
170
|
|
|
171
|
+
# Get the current slot from the Solana node
|
|
172
|
+
#
|
|
173
|
+
# @param commitment [String] The commitment level for the request
|
|
174
|
+
# @return [Integer] The current slot
|
|
175
|
+
#
|
|
176
|
+
# @since 0.1.8
|
|
177
|
+
def get_slot(commitment: default_options[:commitment])
|
|
178
|
+
@rpc_client.rpc_request('getSlot', [{ commitment: commitment }])['result']
|
|
179
|
+
end
|
|
180
|
+
|
|
171
181
|
# Get the minimum required lamports for rent exemption
|
|
172
182
|
#
|
|
173
183
|
# @param space [Integer] Number of bytes to allocate for the account
|
|
@@ -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
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Solace
|
|
4
|
+
module Instructions
|
|
5
|
+
# The AddressLookupTableProgram module contains instruction builders for the
|
|
6
|
+
# Address Lookup Table program.
|
|
7
|
+
#
|
|
8
|
+
# Address lookup tables let a versioned (v0) transaction reference accounts by
|
|
9
|
+
# a compact table index instead of a full 32-byte key, so a single transaction
|
|
10
|
+
# can touch far more accounts than the legacy format allows.
|
|
11
|
+
#
|
|
12
|
+
# @see https://docs.solana.com/developing/lookup-tables
|
|
13
|
+
# @since 0.1.8
|
|
14
|
+
module AddressLookupTableProgram
|
|
15
|
+
# Instruction for creating a new (uninitialized) address lookup table.
|
|
16
|
+
#
|
|
17
|
+
# The table's address is a program-derived address of `[authority, recent_slot]`;
|
|
18
|
+
# the caller supplies the derived address and its bump seed.
|
|
19
|
+
#
|
|
20
|
+
# @example Build a CreateLookupTable instruction
|
|
21
|
+
# instruction = Solace::Instructions::AddressLookupTableProgram::CreateLookupTableInstruction.build(
|
|
22
|
+
# recent_slot: 123,
|
|
23
|
+
# bump: 254,
|
|
24
|
+
# program_index: 4,
|
|
25
|
+
# table_index: 1,
|
|
26
|
+
# authority_index: 0,
|
|
27
|
+
# payer_index: 0,
|
|
28
|
+
# system_program_index: 3
|
|
29
|
+
# )
|
|
30
|
+
#
|
|
31
|
+
# @since 0.1.8
|
|
32
|
+
class CreateLookupTableInstruction
|
|
33
|
+
# Instruction discriminator for CreateLookupTable (u32 LE)
|
|
34
|
+
INSTRUCTION_ID = [0, 0, 0, 0].freeze
|
|
35
|
+
|
|
36
|
+
# Builds a Solace::Instruction for creating a lookup table
|
|
37
|
+
#
|
|
38
|
+
# @param recent_slot [Integer] The slot used to derive the table address
|
|
39
|
+
# @param bump [Integer] The bump seed for the table's program-derived address
|
|
40
|
+
# @param program_index [Integer] Index of the lookup table program
|
|
41
|
+
# @param table_index [Integer] Index of the (uninitialized) table account
|
|
42
|
+
# @param authority_index [Integer] Index of the table authority (signer)
|
|
43
|
+
# @param payer_index [Integer] Index of the rent payer (writable signer)
|
|
44
|
+
# @param system_program_index [Integer] Index of the system program
|
|
45
|
+
# @return [Solace::Instruction]
|
|
46
|
+
def self.build(
|
|
47
|
+
recent_slot:,
|
|
48
|
+
bump:,
|
|
49
|
+
program_index:,
|
|
50
|
+
table_index:,
|
|
51
|
+
authority_index:,
|
|
52
|
+
payer_index:,
|
|
53
|
+
system_program_index:
|
|
54
|
+
)
|
|
55
|
+
Instruction.new.tap do |ix|
|
|
56
|
+
ix.program_index = program_index
|
|
57
|
+
ix.accounts = [table_index, authority_index, payer_index, system_program_index]
|
|
58
|
+
ix.data = data(recent_slot, bump)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Instruction data for a create lookup table instruction
|
|
63
|
+
#
|
|
64
|
+
# The BufferLayout is:
|
|
65
|
+
# - [Instruction discriminator (4 bytes, u32 LE)]
|
|
66
|
+
# - [Recent slot (8 bytes, u64 LE)]
|
|
67
|
+
# - [Bump seed (1 byte)]
|
|
68
|
+
#
|
|
69
|
+
# @param recent_slot [Integer] The slot used to derive the table address
|
|
70
|
+
# @param bump [Integer] The bump seed
|
|
71
|
+
# @return [Array<Integer>]
|
|
72
|
+
def self.data(recent_slot, bump)
|
|
73
|
+
INSTRUCTION_ID +
|
|
74
|
+
Utils::Codecs.encode_le_u64(recent_slot).bytes +
|
|
75
|
+
[bump]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|