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 +4 -4
- data/lib/solace/accounts/address_lookup_table.rb +134 -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/connection.rb +10 -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 +110 -7
- data/lib/solace/utils/account_context.rb +39 -15
- data/lib/solace/version.rb +1 -1
- data/lib/solace.rb +4 -0
- 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: 4e9ca7ab89db7412f9eea6ba3fc36e7a126fddf403fa639bb1d40fdf5a338a61
|
|
4
|
+
data.tar.gz: d35705a7acb46f5da7a4d421d32f5be1fb3cfbcdc293623799ba6be8467a3f2d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
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,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
|
-
|
|
164
|
-
|
|
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:
|
|
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
|
-
#
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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
|
|
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
|
-
#
|
|
220
|
-
|
|
221
|
-
|
|
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
|
data/lib/solace/version.rb
CHANGED
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.
|
|
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-
|
|
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
|