solace 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ab3415933c7875971ad410ecc1d8d7487233a7eb58e9c3701f23babdaae9fbd5
4
- data.tar.gz: a9ef2bbb03d7988487ae48bff51122dc45670e5ad6ef8c8d6154fa386d0a1281
3
+ metadata.gz: 4e9ca7ab89db7412f9eea6ba3fc36e7a126fddf403fa639bb1d40fdf5a338a61
4
+ data.tar.gz: d35705a7acb46f5da7a4d421d32f5be1fb3cfbcdc293623799ba6be8467a3f2d
5
5
  SHA512:
6
- metadata.gz: 869108bf70f8a9143e28dae425a5e843135aedfd78642c39683fd214ab4a3d1c847125e0e2f0fc60b1766ac45970dc8f14bfc512861db95b89cdaaedc593226d
7
- data.tar.gz: 8ff9bcac1403007eb3dcc340587c76f4acb5dff266390621f16708589153644db16a16cdb0f08936f3d3a690c2943d78edd570735e9f0692eac6ffb62ee3c969
6
+ metadata.gz: a4621dc7f860419c640ad856ad602acb4c15dde56cc89a44130f4b505baf9856ac20e9227ffd88e7797c4df9aa58d3e9f0485e6f4e52acbd40a0de1d73b5feca
7
+ data.tar.gz: 305310534a9a2373ddb8df5e0208f2ab5723d9ba6b7c4be4d21533cc07f7faf94819f6744be7564ec0f20a82fc255230390b68f2a0b67cf69bc7009a05d5d60a
@@ -0,0 +1,134 @@
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
+ # data = Base64.decode64(connection.get_account_info(address)['data'][0])
25
+ # table = Solace::Accounts::AddressLookupTable.deserialize(StringIO.new(data))
26
+ # table.addresses # => the stored addresses
27
+ #
28
+ # @see Solace::AddressLookupTable
29
+ # @see Solace::TransactionComposer
30
+ # @since 0.1.8
31
+ class AddressLookupTable
32
+ # Byte offset at which the stored addresses begin — the program reserves a
33
+ # fixed-size metadata region ahead of them regardless of its contents.
34
+ META_SIZE = 56
35
+
36
+ # @!attribute [r] account
37
+ # @return [String, nil] The lookup table's on-chain address
38
+ attr_reader :account
39
+
40
+ # @!attribute [r] addresses
41
+ # @return [Array<String>] The full, ordered list of addresses stored in the table
42
+ attr_reader :addresses
43
+
44
+ # @!attribute [r] authority
45
+ # @return [String, nil] The authority allowed to extend/close the table
46
+ attr_reader :authority
47
+
48
+ # @!attribute [r] deactivation_slot
49
+ # @return [Integer, nil] The slot the table was deactivated in (max while active)
50
+ attr_reader :deactivation_slot
51
+
52
+ # @!attribute [r] last_extended_slot
53
+ # @return [Integer, nil] The slot the table was last extended in
54
+ attr_reader :last_extended_slot
55
+
56
+ # Deserialize an on-chain lookup table account
57
+ #
58
+ # The BufferLayout is:
59
+ # - [State type (4 bytes, u32 LE)]
60
+ # - [Deactivation slot (8 bytes, u64 LE)]
61
+ # - [Last extended slot (8 bytes, u64 LE)]
62
+ # - [Last extended start index (1 byte)]
63
+ # - [Authority (Borsh Option<Pubkey>)]
64
+ # - [Padding, up to {META_SIZE}]
65
+ # - [Addresses (32 bytes each, to end of data)]
66
+ #
67
+ # @param io [IO, StringIO] The account data to read from
68
+ # @return [AddressLookupTable] The parsed table
69
+ def self.deserialize(io)
70
+ Utils::Codecs.decode_le_u32(io) # state type (1 = lookup table); positional
71
+ deactivation_slot = Utils::Codecs.decode_le_u64(io)
72
+ last_extended_slot = Utils::Codecs.decode_le_u64(io)
73
+
74
+ Utils::Codecs.decode_u8(io) # last extended start index; positional
75
+ authority = Utils::Codecs.decode_option_pubkey(io)
76
+
77
+ io.seek(META_SIZE) # addresses begin after the fixed-size metadata region
78
+
79
+ addresses = []
80
+ addresses << Utils::Codecs.decode_pubkey(io) until io.eof?
81
+
82
+ new(
83
+ deactivation_slot: deactivation_slot,
84
+ last_extended_slot: last_extended_slot,
85
+ authority: authority,
86
+ addresses: addresses
87
+ )
88
+ end
89
+
90
+ # Initialize a lookup table account
91
+ #
92
+ # @param account [#to_s, PublicKey, nil] The lookup table's on-chain address
93
+ # @param addresses [Array<#to_s>, nil] The full, ordered address list; may be
94
+ # omitted (the composer then loads nothing through this table)
95
+ # @param authority [String, nil] The table authority
96
+ # @param deactivation_slot [Integer, nil] The deactivation slot
97
+ # @param last_extended_slot [Integer, nil] The last extended slot
98
+ def initialize(account: nil, addresses: nil, authority: nil, deactivation_slot: nil, last_extended_slot: nil)
99
+ @account = account&.to_s
100
+ @addresses = Array(addresses).map(&:to_s)
101
+ @authority = authority
102
+ @deactivation_slot = deactivation_slot
103
+ @last_extended_slot = last_extended_slot
104
+ end
105
+
106
+ # Build the v0 message reference for the addresses this table contributes
107
+ #
108
+ # @param writable [Array<String>] Loaded writable pubkeys drawn from this table
109
+ # @param readonly [Array<String>] Loaded readonly pubkeys drawn from this table
110
+ # @return [Solace::AddressLookupTable, nil] The reference, or nil if it loads nothing
111
+ def reference(writable, readonly)
112
+ writable_indexes = positions_of(writable)
113
+ readonly_indexes = positions_of(readonly)
114
+ return if writable_indexes.empty? && readonly_indexes.empty?
115
+
116
+ Solace::AddressLookupTable.new.tap do |reference|
117
+ reference.account = account
118
+ reference.writable_indexes = writable_indexes
119
+ reference.readonly_indexes = readonly_indexes
120
+ end
121
+ end
122
+
123
+ private
124
+
125
+ # Positions of the given pubkeys within this table's address list
126
+ #
127
+ # @param pubkeys [Array<String>] The pubkeys to locate
128
+ # @return [Array<Integer>] Their positions in {#addresses}
129
+ def positions_of(pubkeys)
130
+ pubkeys.filter_map { |pubkey| addresses.index(pubkey) }
131
+ end
132
+ end
133
+ end
134
+ 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
@@ -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,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
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solace
4
+ module Instructions
5
+ module AddressLookupTableProgram
6
+ # Instruction for appending addresses to an existing address lookup table.
7
+ #
8
+ # Newly added addresses become usable one slot after the extend lands.
9
+ #
10
+ # @example Build an ExtendLookupTable instruction
11
+ # instruction = Solace::Instructions::AddressLookupTableProgram::ExtendLookupTableInstruction.build(
12
+ # addresses: [recipient1, recipient2],
13
+ # program_index: 4,
14
+ # table_index: 1,
15
+ # authority_index: 0,
16
+ # payer_index: 0,
17
+ # system_program_index: 3
18
+ # )
19
+ #
20
+ # @since 0.1.8
21
+ class ExtendLookupTableInstruction
22
+ # Instruction discriminator for ExtendLookupTable (u32 LE)
23
+ INSTRUCTION_ID = [2, 0, 0, 0].freeze
24
+
25
+ # Builds a Solace::Instruction for extending a lookup table
26
+ #
27
+ # @param addresses [Array<#to_s>] The addresses to append to the table
28
+ # @param program_index [Integer] Index of the lookup table program
29
+ # @param table_index [Integer] Index of the table account (writable)
30
+ # @param authority_index [Integer] Index of the table authority (signer)
31
+ # @param payer_index [Integer] Index of the rent payer (writable signer)
32
+ # @param system_program_index [Integer] Index of the system program
33
+ # @return [Solace::Instruction]
34
+ def self.build(
35
+ addresses:,
36
+ program_index:,
37
+ table_index:,
38
+ authority_index:,
39
+ payer_index:,
40
+ system_program_index:
41
+ )
42
+ Instruction.new.tap do |ix|
43
+ ix.program_index = program_index
44
+ ix.accounts = [table_index, authority_index, payer_index, system_program_index]
45
+ ix.data = data(addresses)
46
+ end
47
+ end
48
+
49
+ # Instruction data for an extend lookup table instruction
50
+ #
51
+ # The BufferLayout is:
52
+ # - [Instruction discriminator (4 bytes, u32 LE)]
53
+ # - [Number of addresses (8 bytes, u64 LE)]
54
+ # - [Addresses (32 bytes each)]
55
+ #
56
+ # The address vector uses a u64 length prefix (bincode), not the u32
57
+ # Borsh prefix of {Utils::Codecs.encode_vec_pubkeys}.
58
+ #
59
+ # @param addresses [Array<#to_s>] The addresses to append
60
+ # @return [Array<Integer>]
61
+ def self.data(addresses)
62
+ INSTRUCTION_ID +
63
+ Utils::Codecs.encode_le_u64(addresses.length).bytes +
64
+ addresses.flat_map { |address| Utils::Codecs.encode_pubkey(address) }
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -73,6 +73,14 @@ module Solace
73
73
  # The instruction composers
74
74
  attr_reader :instruction_composers
75
75
 
76
+ # @!attribute address_lookup_tables
77
+ # The registered address lookup tables
78
+ attr_reader :address_lookup_tables
79
+
80
+ # @!attribute version
81
+ # The transaction version (nil for legacy, 0 for v0)
82
+ attr_reader :version
83
+
76
84
  # Initialize the composer
77
85
  #
78
86
  # @param connection [Solace::Connection] The connection to the Solana cluster
@@ -80,6 +88,8 @@ module Solace
80
88
  @connection = connection
81
89
  @instruction_composers = []
82
90
  @context = Utils::AccountContext.new
91
+ @address_lookup_tables = []
92
+ @version = nil
83
93
  end
84
94
 
85
95
  # Add an instruction composer to the transaction
@@ -127,6 +137,7 @@ module Solace
127
137
  # @since 0.1.0
128
138
  def merge(other, placement: :add, index: nil)
129
139
  merge_accounts(other.context)
140
+ merge_address_lookup_tables(other.address_lookup_tables)
130
141
 
131
142
  case placement
132
143
  when :add
@@ -154,24 +165,109 @@ module Solace
154
165
  self
155
166
  end
156
167
 
168
+ # Make an address lookup table available to the transaction
169
+ #
170
+ # Registering a table opts the transaction into the v0 format: every
171
+ # compiled account found in a table that is allowed to load (a non-signer
172
+ # that is not the fee payer and not an invoked program id) is referenced by
173
+ # table index instead of occupying a static account slot. Adding the same
174
+ # table (by account) twice is a no-op.
175
+ #
176
+ # @example
177
+ # composer.add_address_lookup_table(account: table_address, addresses: on_chain_addresses)
178
+ #
179
+ # @param account [#to_s, PublicKey] The lookup table's on-chain address
180
+ # @param addresses [Array<#to_s>, nil] The full, ordered list of addresses stored in the table
181
+ # @return [TransactionComposer] Self for chaining
182
+ #
183
+ # @since 0.1.8
184
+ def add_address_lookup_table(account:, addresses: nil)
185
+ account = account.to_s
186
+ @version = 0 # Lookup tables require a v0 transaction
187
+
188
+ unless address_lookup_tables.any? { |table| table.account == account }
189
+ address_lookup_tables << Solace::Accounts::AddressLookupTable.new(account: account, addresses: addresses)
190
+ end
191
+
192
+ self
193
+ end
194
+
157
195
  # Compose the final transaction
158
196
  #
197
+ # Emits a message at the composer's {#version} — legacy by default, or v0
198
+ # once a lookup table has been added — loading eligible accounts through any
199
+ # registered tables.
200
+ #
159
201
  # @return [Transaction] The composed transaction (unsigned)
160
202
  def compose_transaction
161
203
  context.compile
162
204
 
163
- message = Solace::Message.new(
164
- header: context.header,
165
- accounts: context.accounts,
166
- instructions: build_instructions,
167
- recent_blockhash: connection.get_latest_blockhash[0]
168
- )
205
+ writable, readonly, references = resolve_address_lookup_tables
206
+ context.compile(loaded_accounts: writable + readonly)
169
207
 
170
- Solace::Transaction.new(message: message)
208
+ Solace::Transaction.new(message: build_message(references))
171
209
  end
172
210
 
173
211
  private
174
212
 
213
+ # Fold the registered tables into the accounts they load and the references
214
+ # the message carries — a single pass over the tables (the order that
215
+ # defines the v0 combined account space).
216
+ #
217
+ # First table wins when tables share an address; within a table the on-chain
218
+ # address order is preserved, so the accumulated writable/readonly segments
219
+ # match the runtime's loaded-address order once concatenated.
220
+ #
221
+ # @return [Array(Array<String>, Array<String>, Array<Solace::AddressLookupTable>)]
222
+ # The loaded writable pubkeys, loaded readonly pubkeys, and table references
223
+ def resolve_address_lookup_tables
224
+ loadable = loadable_accounts
225
+ writable = []
226
+ readonly = []
227
+
228
+ references = address_lookup_tables.filter_map do |table|
229
+ loaded = table.addresses & loadable
230
+ loadable -= loaded
231
+ on, off = loaded.partition { |pubkey| context.writable?(pubkey) }
232
+ writable.concat(on)
233
+ readonly.concat(off)
234
+ table.reference(on, off)
235
+ end
236
+
237
+ [writable, readonly, references]
238
+ end
239
+
240
+ # Accounts eligible to load through a table: referenced by the transaction,
241
+ # not a signer (which covers the fee payer), and not an invoked program id.
242
+ #
243
+ # @return [Array<String>] The loadable account pubkeys
244
+ def loadable_accounts
245
+ programs = program_ids
246
+ context.accounts.reject { |pubkey| context.signer?(pubkey) || programs.include?(pubkey) }
247
+ end
248
+
249
+ # Program ids invoked by the built instructions — these must stay static
250
+ #
251
+ # @return [Array<String>] The invoked program id pubkeys
252
+ def program_ids
253
+ build_instructions.map { context.accounts[_1.program_index] }.uniq
254
+ end
255
+
256
+ # Build the composed message at the composer's version (legacy or v0)
257
+ #
258
+ # @param references [Array<Solace::AddressLookupTable>] The table references
259
+ # @return [Solace::Message] The composed message
260
+ def build_message(references)
261
+ Solace::Message.new(
262
+ version: version,
263
+ header: context.header,
264
+ accounts: context.accounts,
265
+ instructions: build_instructions,
266
+ recent_blockhash: connection.get_latest_blockhash[0],
267
+ address_lookup_tables: references
268
+ )
269
+ end
270
+
175
271
  # Build all instructions with resolved indices
176
272
  #
177
273
  # @return [Array<Solace::Instruction>] The built instructions
@@ -185,5 +281,12 @@ module Solace
185
281
  def merge_accounts(account_context)
186
282
  context.merge_from(account_context)
187
283
  end
284
+
285
+ # Merge registered tables from another composer, deduped by account
286
+ #
287
+ # @param tables [Array<Solace::Accounts::AddressLookupTable>] The other composer's tables
288
+ def merge_address_lookup_tables(tables)
289
+ tables.each { |table| add_address_lookup_table(account: table.account, addresses: table.addresses) }
290
+ end
188
291
  end
189
292
  end
@@ -62,6 +62,7 @@ module Solace
62
62
  def initialize
63
63
  @header = []
64
64
  @accounts = []
65
+ @loaded_accounts = []
65
66
  @pubkey_account_map = {}
66
67
  end
67
68
 
@@ -173,26 +174,40 @@ module Solace
173
174
  # - Then writable accounts
174
175
  # - Then readonly accounts
175
176
  #
176
- # @return [Hash] The compiled accounts and header
177
- def compile
178
- self.header = calculate_header
179
- self.accounts = order_accounts
177
+ # For a v0 (versioned) transaction, accounts resolved through lookup tables
178
+ # leave the static account list entirely: the message carries only the
179
+ # static keys, and the runtime rebuilds the combined space
180
+ # [static..., loaded...] at execution time. Passing them keeps {#index_of}
181
+ # resolving against that combined space (so instructions index correctly)
182
+ # while dropping them from the static accounts and the header. They must be
183
+ # ordered writable-first to match the runtime's combined space. With no
184
+ # loaded accounts (the default) this is the legacy compilation, unchanged.
185
+ #
186
+ # @param loaded_accounts [Array<String>] Pubkeys resolved through lookup tables
187
+ # @return [AccountContext] Self
188
+ def compile(loaded_accounts: [])
189
+ @loaded_accounts = loaded_accounts
190
+
191
+ self.header = calculate_header(loaded_accounts)
192
+ self.accounts = order_accounts(loaded_accounts)
180
193
  self
181
194
  end
182
195
 
183
- # Index of a pubkey in the accounts array
196
+ # Index of a pubkey in the combined account space
184
197
  #
185
198
  # @param pubkey_str [String] The public key of the account
186
- # @return [Integer] The index of the pubkey in the accounts array or -1 if not found
199
+ # @return [Integer] The index in the combined space, or -1 if not found
187
200
  def index_of(pubkey_str)
188
201
  indices[pubkey_str] || -1
189
202
  end
190
203
 
191
- # Get map of indicies for pubkeys in accounts array
204
+ # Map of pubkey => index across the combined space (static keys followed by
205
+ # any loaded accounts), so instruction indices resolve the same way the
206
+ # runtime does once lookup tables are expanded.
192
207
  #
193
- # @return [Hash{String => Integer}] The indices of the pubkeys in the accounts array
208
+ # @return [Hash{String => Integer}] The indices of the pubkeys
194
209
  def indices
195
- accounts.each_with_index.to_h
210
+ (accounts + @loaded_accounts).each_with_index.to_h
196
211
  end
197
212
 
198
213
  private
@@ -214,11 +229,15 @@ module Solace
214
229
  self
215
230
  end
216
231
 
217
- # Order accounts by signer, writable, readonly signer, readonly
232
+ # Order the static accounts by signer, writable, readonly signer, readonly
218
233
  #
219
- # @return [Array<String>] The ordered accounts
220
- def order_accounts
221
- @pubkey_account_map.keys.sort_by do |pubkey|
234
+ # Loaded accounts are excluded — they leave the static account list and are
235
+ # resolved through lookup tables at runtime.
236
+ #
237
+ # @param loaded_accounts [Array<String>] Pubkeys resolved through lookup tables
238
+ # @return [Array<String>] The ordered static accounts
239
+ def order_accounts(loaded_accounts)
240
+ (@pubkey_account_map.keys - loaded_accounts).sort_by do |pubkey|
222
241
  if fee_payer?(pubkey) then 0
223
242
  elsif writable_signer?(pubkey) then 1
224
243
  elsif readonly_signer?(pubkey) then 2
@@ -237,9 +256,14 @@ module Solace
237
256
  # - The number of readonly signers
238
257
  # - The number of readonly unsigned accounts
239
258
  #
259
+ # Loaded accounts no longer occupy the static account list, so they drop
260
+ # out of the header. They are never signers, so only the readonly-unsigned
261
+ # count changes.
262
+ #
263
+ # @param loaded_accounts [Array<String>] Pubkeys resolved through lookup tables
240
264
  # @return [Array] The header for the transaction
241
- def calculate_header
242
- @pubkey_account_map.keys.each_with_object([0, 0, 0]) do |pubkey, acc|
265
+ def calculate_header(loaded_accounts)
266
+ (@pubkey_account_map.keys - loaded_accounts).each_with_object([0, 0, 0]) do |pubkey, acc|
243
267
  acc[0] += 1 if signer?(pubkey)
244
268
 
245
269
  if readonly_signer?(pubkey) then acc[1] += 1
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Solace
4
4
  # Latest version of the Solace gem.
5
- VERSION = '0.1.7'
5
+ VERSION = '0.1.8'
6
6
  end
data/lib/solace.rb CHANGED
@@ -55,6 +55,10 @@ require_relative 'solace/transaction'
55
55
  require_relative 'solace/message'
56
56
  require_relative 'solace/instruction'
57
57
  require_relative 'solace/address_lookup_table'
58
+
59
+ # Accounts (on-chain account models)
60
+ require_relative 'solace/accounts/address_lookup_table'
61
+
58
62
  require_relative 'solace/transaction_composer'
59
63
 
60
64
  # Base Classes (Abstract classes)
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.7
4
+ version: 0.1.8
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-06 00:00:00.000000000 Z
11
+ date: 2026-07-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base58
@@ -202,7 +202,10 @@ extensions: []
202
202
  extra_rdoc_files: []
203
203
  files:
204
204
  - lib/solace.rb
205
+ - lib/solace/accounts/address_lookup_table.rb
205
206
  - lib/solace/address_lookup_table.rb
207
+ - lib/solace/composers/address_lookup_table_program_create_composer.rb
208
+ - lib/solace/composers/address_lookup_table_program_extend_composer.rb
206
209
  - lib/solace/composers/associated_token_account_program_create_account_composer.rb
207
210
  - lib/solace/composers/associated_token_account_program_create_idempotent_account_composer.rb
208
211
  - lib/solace/composers/base.rb
@@ -231,6 +234,8 @@ files:
231
234
  - lib/solace/errors/parse_error.rb
232
235
  - lib/solace/errors/rpc_error.rb
233
236
  - lib/solace/instruction.rb
237
+ - lib/solace/instructions/address_lookup_table_program/create_lookup_table_instruction.rb
238
+ - lib/solace/instructions/address_lookup_table_program/extend_lookup_table_instruction.rb
234
239
  - lib/solace/instructions/associated_token_account/create_account_instruction.rb
235
240
  - lib/solace/instructions/associated_token_account/create_idempotent_account_instruction.rb
236
241
  - lib/solace/instructions/compute_budget/set_compute_unit_limit_instruction.rb