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.
@@ -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,13 +73,33 @@ 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); see {#set_version}
82
+ attr_reader :version
83
+
84
+ # @!attribute compute_budget
85
+ # The compute budget set on the composer (see {Utils::ComputeBudget})
86
+ attr_reader :compute_budget
87
+
88
+ # @!attribute blockhash
89
+ # The blockhash set on the composer, or nil when the latest is fetched at compose time
90
+ attr_reader :blockhash
91
+
76
92
  # Initialize the composer
77
93
  #
78
94
  # @param connection [Solace::Connection] The connection to the Solana cluster
79
95
  def initialize(connection:)
80
96
  @connection = connection
97
+ @blockhash = nil
81
98
  @instruction_composers = []
82
99
  @context = Utils::AccountContext.new
100
+ @address_lookup_tables = []
101
+ @version = nil
102
+ @compute_budget = Utils::ComputeBudget.new
83
103
  end
84
104
 
85
105
  # Add an instruction composer to the transaction
@@ -87,7 +107,7 @@ module Solace
87
107
  # @param composer [Composers::Base] The instruction composer
88
108
  # @return [TransactionComposer] Self for chaining
89
109
  def add_instruction(composer)
90
- merge_accounts(composer.account_context)
110
+ context.merge_from(composer.account_context)
91
111
  instruction_composers << composer
92
112
  self
93
113
  end
@@ -99,7 +119,7 @@ module Solace
99
119
  #
100
120
  # @since 0.1.0
101
121
  def prepend_instruction(composer)
102
- merge_accounts(composer.account_context)
122
+ context.merge_from(composer.account_context)
103
123
  instruction_composers.unshift(composer)
104
124
  self
105
125
  end
@@ -112,7 +132,7 @@ module Solace
112
132
  #
113
133
  # @since 0.1.0
114
134
  def insert_instruction(index, composer)
115
- merge_accounts(composer.account_context)
135
+ context.merge_from(composer.account_context)
116
136
  instruction_composers.insert(index, composer)
117
137
  self
118
138
  end
@@ -126,7 +146,7 @@ module Solace
126
146
  #
127
147
  # @since 0.1.0
128
148
  def merge(other, placement: :add, index: nil)
129
- merge_accounts(other.context)
149
+ absorb(other)
130
150
 
131
151
  case placement
132
152
  when :add
@@ -154,36 +174,194 @@ module Solace
154
174
  self
155
175
  end
156
176
 
177
+ # Set the transaction version
178
+ #
179
+ # Registering a lookup table opts into v0 on its own; this is for a v0
180
+ # transaction that loads nothing through a table, which is still v0 on the
181
+ # wire. nil composes a legacy message.
182
+ #
183
+ # @param version [Integer, nil] 0 for v0, nil for legacy
184
+ # @return [TransactionComposer] Self for chaining
185
+ # @raise [ArgumentError] For a version other than 0 or nil
186
+ def set_version(version)
187
+ raise ArgumentError, "Unsupported transaction version: #{version.inspect}" unless [nil, 0].include?(version)
188
+
189
+ @version = version
190
+ self
191
+ end
192
+
193
+ # Set the blockhash to compose against
194
+ #
195
+ # Beside {#set_fee_payer}: a caller re-composing a transaction it was handed
196
+ # wants the blockhash that transaction carried, so the expiry its origin
197
+ # reports stays true. With none set, {#compose_transaction} fetches the
198
+ # latest from the connection. nil clears it.
199
+ #
200
+ # @example
201
+ # composer.set_blockhash(original.message.recent_blockhash)
202
+ #
203
+ # @param blockhash [#to_s, nil] The blockhash (base58)
204
+ # @return [TransactionComposer] Self for chaining
205
+ def set_blockhash(blockhash)
206
+ @blockhash = blockhash&.to_s
207
+ self
208
+ end
209
+
210
+ # Set the compute budget for the transaction
211
+ #
212
+ # The composer owns the budget as a setting: when it composes, the
213
+ # ComputeBudget instructions are written first, and any
214
+ # ComputeBudget composer of the same kind that was added as a plain
215
+ # instruction is left out, so the budget can never be declared twice
216
+ # (a duplicate `SetComputeUnitLimit` fails on chain). Each keyword
217
+ # replaces that setting; nil clears it. Read it back through {#compute_budget}.
218
+ #
219
+ # @example
220
+ # composer.set_compute_budget(units: 200_000, micro_lamports: 50_000)
221
+ #
222
+ # @param units [Integer, nil] The compute unit limit
223
+ # @param micro_lamports [Integer, nil] The compute unit price in micro-lamports
224
+ # @return [TransactionComposer] Self for chaining
225
+ def set_compute_budget(units: nil, micro_lamports: nil)
226
+ apply_compute_budget(Utils::ComputeBudget.new(units: units, micro_lamports: micro_lamports))
227
+ end
228
+
229
+ # Make an address lookup table available to the transaction
230
+ #
231
+ # Registering a table opts the transaction into the v0 format: every
232
+ # compiled account found in a table that is allowed to load (a non-signer
233
+ # that is not the fee payer and not an invoked program id) is referenced by
234
+ # table index instead of occupying a static account slot. Adding the same
235
+ # table (by account) twice is a no-op.
236
+ #
237
+ # @example
238
+ # composer.add_address_lookup_table(account: table_address, addresses: on_chain_addresses)
239
+ #
240
+ # @param account [#to_s, PublicKey] The lookup table's on-chain address
241
+ # @param addresses [Array<#to_s>, nil] The full, ordered list of addresses stored in the table
242
+ # @return [TransactionComposer] Self for chaining
243
+ #
244
+ # @since 0.1.8
245
+ def add_address_lookup_table(account:, addresses: nil)
246
+ account = account.to_s
247
+ set_version(0) # Lookup tables require a v0 transaction
248
+
249
+ unless address_lookup_tables.any? { |table| table.account == account }
250
+ address_lookup_tables << Solace::Accounts::AddressLookupTable.new(account: account, addresses: addresses)
251
+ end
252
+
253
+ self
254
+ end
255
+
157
256
  # Compose the final transaction
158
257
  #
258
+ # Emits a message at the composer's {#version} — legacy by default, or v0
259
+ # once a lookup table has been added — loading eligible accounts through any
260
+ # registered tables.
261
+ #
262
+ # Composes against the {#blockhash} set with {#set_blockhash}, or the latest
263
+ # fetched from the connection when none is set.
264
+ #
159
265
  # @return [Transaction] The composed transaction (unsigned)
160
266
  def compose_transaction
161
267
  context.compile
162
268
 
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
- )
269
+ writable, readonly, references = resolve_address_lookup_tables
270
+ context.compile(loaded_accounts: writable + readonly)
169
271
 
170
- Solace::Transaction.new(message: message)
272
+ Solace::Transaction.new(message: build_message(references))
171
273
  end
172
274
 
173
275
  private
174
276
 
175
- # Build all instructions with resolved indices
277
+ # Fold the registered tables into the accounts they load and the references
278
+ # the message carries — a single pass over the tables (the order that
279
+ # defines the v0 combined account space).
280
+ #
281
+ # First table wins when tables share an address; within a table the on-chain
282
+ # address order is preserved, so the accumulated writable/readonly segments
283
+ # match the runtime's loaded-address order once concatenated.
284
+ #
285
+ # @return [Array(Array<String>, Array<String>, Array<Solace::AddressLookupTable>)]
286
+ # The loaded writable pubkeys, loaded readonly pubkeys, and table references
287
+ def resolve_address_lookup_tables
288
+ loadable = loadable_accounts
289
+ writable = []
290
+ readonly = []
291
+
292
+ references = address_lookup_tables.filter_map do |table|
293
+ loaded = table.addresses & loadable
294
+ loadable -= loaded
295
+ on, off = loaded.partition { |pubkey| context.writable?(pubkey) }
296
+ writable.concat(on)
297
+ readonly.concat(off)
298
+ table.reference(on, off)
299
+ end
300
+
301
+ [writable, readonly, references]
302
+ end
303
+
304
+ # Accounts eligible to load through a table: referenced by the transaction,
305
+ # not a signer (which covers the fee payer), and not an invoked program id.
306
+ #
307
+ # @return [Array<String>] The loadable account pubkeys
308
+ def loadable_accounts
309
+ programs = program_ids
310
+ context.accounts.reject { |pubkey| context.signer?(pubkey) || programs.include?(pubkey) }
311
+ end
312
+
313
+ # Program ids invoked by the built instructions — these must stay static
314
+ #
315
+ # @return [Array<String>] The invoked program id pubkeys
316
+ def program_ids
317
+ build_instructions.map { context.accounts[_1.program_index] }.uniq
318
+ end
319
+
320
+ # Build the composed message at the composer's version (legacy or v0)
321
+ #
322
+ # @param references [Array<Solace::AddressLookupTable>] The table references
323
+ # @return [Solace::Message] The composed message
324
+ def build_message(references)
325
+ Solace::Message.new(
326
+ version: version,
327
+ header: context.header,
328
+ accounts: context.accounts,
329
+ instructions: build_instructions,
330
+ recent_blockhash: blockhash || connection.get_latest_blockhash[0],
331
+ address_lookup_tables: references
332
+ )
333
+ end
334
+
335
+ # Build all instructions with resolved indices: the budget first, then the
336
+ # added composers minus any ComputeBudget composer the budget supersedes
176
337
  #
177
338
  # @return [Array<Solace::Instruction>] The built instructions
178
339
  def build_instructions
179
- instruction_composers.map { _1.build_instruction(context) }.flatten
340
+ kept = instruction_composers.reject { |composer| compute_budget.supersedes?(composer) }
341
+
342
+ (compute_budget.composers + kept).map { _1.build_instruction(context) }.flatten
180
343
  end
181
344
 
182
- # Merge all accounts from another AccountContext into this one
345
+ # Replace the compute budget, bringing its program into the account context
183
346
  #
184
- # @param account_context [AccountContext] The other context to merge from
185
- def merge_accounts(account_context)
186
- context.merge_from(account_context)
347
+ # @param budget [Utils::ComputeBudget] The budget to hold
348
+ # @return [TransactionComposer] Self for chaining
349
+ def apply_compute_budget(budget)
350
+ @compute_budget = budget
351
+ budget.composers.each { |composer| context.merge_from(composer.account_context) }
352
+ self
353
+ end
354
+
355
+ # Take on another composer's accounts, tables (deduped by account) and
356
+ # budget (where it has one set)
357
+ #
358
+ # @param other [TransactionComposer] The other composer
359
+ def absorb(other)
360
+ context.merge_from(other.context)
361
+ other.address_lookup_tables.each do |table|
362
+ add_address_lookup_table(account: table.account, addresses: table.addresses)
363
+ end
364
+ apply_compute_budget(compute_budget.merge(other.compute_budget))
187
365
  end
188
366
  end
189
367
  end
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'composers/base'
4
+ require_relative 'composers/opaque_instruction_composer'
5
+ require_relative 'instructions/compute_budget/set_compute_unit_limit_instruction'
6
+ require_relative 'instructions/compute_budget/set_compute_unit_price_instruction'
7
+
8
+ module Solace
9
+ # Takes a transaction apart into the composer that composes it again
10
+ #
11
+ # The reverse of {TransactionComposer}: given a transaction (or its base64),
12
+ # answers a {TransactionComposer} holding one
13
+ # {Composers::OpaqueInstructionComposer} per instruction, in order, each
14
+ # carrying the program it invokes, its accounts with the signer and writable
15
+ # flags the message header gave them, and its data untouched. The lookup
16
+ # tables a v0 message references are read from chain once and registered, and
17
+ # the version is kept, so the composer composes as v0 again (with or without
18
+ # tables); a `SetComputeUnitLimit` or
19
+ # `SetComputeUnitPrice` the transaction carried becomes the composer's
20
+ # {TransactionComposer#compute_budget}; and the blockhash it was composed
21
+ # against is set as the composer's {TransactionComposer#blockhash}.
22
+ #
23
+ # The Solana facts applied:
24
+ #
25
+ # - Static keys take their flags from the header ordering: signers first,
26
+ # writable before read-only, then non-signers the same way.
27
+ # - A v0 message's instruction indexes address the combined space: the
28
+ # static keys, then every table's writable entries in table order, then
29
+ # every table's read-only entries. A loaded address never signs and is
30
+ # writable by the segment it sits in.
31
+ # - A table that does not exist on chain is an error, not an empty list.
32
+ #
33
+ # For a transaction a {TransactionComposer} built, the composer answered here
34
+ # composes the same bytes again.
35
+ #
36
+ # @example
37
+ # composer = Solace::TransactionDecomposer.new(connection: connection).decompose_transaction(transaction)
38
+ # composer.instruction_composers # => the recovered instructions
39
+ # composer.compose_transaction # => the transaction again
40
+ #
41
+ # @see Solace::TransactionComposer
42
+ # @since 0.1.9
43
+ class TransactionDecomposer
44
+ # The discriminators of the two directives the budget setting holds
45
+ LIMIT_INDEX = Instructions::ComputeBudget::SetComputeUnitLimitInstruction::INSTRUCTION_INDEX.first
46
+ PRICE_INDEX = Instructions::ComputeBudget::SetComputeUnitPriceInstruction::INSTRUCTION_INDEX.first
47
+ private_constant :LIMIT_INDEX, :PRICE_INDEX
48
+
49
+ # What one call reads off a message: the message, the tables it references
50
+ # (by account, read from chain), the combined account space in index
51
+ # order, and a context declaring every account's role
52
+ Reading = Struct.new(:message, :tables, :accounts, :roles, keyword_init: true)
53
+ private_constant :Reading
54
+
55
+ # @!attribute connection
56
+ # The connection lookup tables are read through
57
+ attr_reader :connection
58
+
59
+ # @param connection [Solace::Connection] The connection to read lookup tables through
60
+ def initialize(connection:)
61
+ @connection = connection
62
+ end
63
+
64
+ # Take a transaction apart
65
+ #
66
+ # @param transaction [Solace::Transaction, String] The transaction, or its base64
67
+ # @return [Solace::TransactionComposer] A composer that composes it again
68
+ # @raise [Solace::Errors::AddressLookupTableNotFound] When a referenced table is not on chain
69
+ def decompose_transaction(transaction)
70
+ message = message_of(transaction)
71
+ reading = read(message)
72
+ composer = TransactionComposer.new(connection: connection)
73
+ .set_version(message.version)
74
+ .set_blockhash(message.recent_blockhash)
75
+
76
+ seed(composer, reading)
77
+ fill(composer, reading)
78
+ end
79
+
80
+ private
81
+
82
+ # Read a message: its tables from chain, its combined account space, and
83
+ # the role of every account in it
84
+ #
85
+ # @param message [Solace::Message]
86
+ # @return [Reading]
87
+ def read(message)
88
+ tables = fetch_tables(message)
89
+ writable = loaded(message, tables, :writable_indexes)
90
+ readonly = loaded(message, tables, :readonly_indexes)
91
+ roles = Utils::AccountContext.new
92
+
93
+ declare_static(roles, message)
94
+ writable.each { |pubkey| roles.merge_account(pubkey, signer: false, writable: true) }
95
+ readonly.each { |pubkey| roles.merge_account(pubkey, signer: false, writable: false) }
96
+
97
+ accounts = message.accounts + writable + readonly
98
+
99
+ Reading.new(message: message, tables: tables, accounts: accounts, roles: roles)
100
+ end
101
+
102
+ # Declare the static keys with the roles their header positions give them:
103
+ # signers first, writable before read-only, then non-signers the same way
104
+ #
105
+ # @param roles [Utils::AccountContext]
106
+ # @param message [Solace::Message]
107
+ # @return [void]
108
+ def declare_static(roles, message)
109
+ signers, readonly_signed, readonly_unsigned = message.header
110
+
111
+ message.accounts.each_with_index do |pubkey, index|
112
+ is_signer = index < signers
113
+ readonly_from = is_signer ? signers - readonly_signed : message.accounts.size - readonly_unsigned
114
+ is_writable = index < readonly_from
115
+
116
+ roles.merge_account(pubkey, signer: is_signer, writable: is_writable)
117
+ end
118
+ end
119
+
120
+ # Seed the composer's accounts in message order and set the fee payer, so
121
+ # it composes the same account order again
122
+ #
123
+ # @param composer [Solace::TransactionComposer]
124
+ # @param reading [Reading]
125
+ # @return [void]
126
+ def seed(composer, reading)
127
+ composer.context.merge_from(reading.roles)
128
+ composer.set_fee_payer(reading.message.accounts.first)
129
+ end
130
+
131
+ # Add the instructions, register the tables, and set the budget
132
+ #
133
+ # @param composer [Solace::TransactionComposer]
134
+ # @param reading [Reading]
135
+ # @return [Solace::TransactionComposer] The composer
136
+ def fill(composer, reading)
137
+ instruction_composers(reading).each { |instruction| composer.add_instruction(instruction) }
138
+
139
+ reading.tables.each_value do |table|
140
+ composer.add_address_lookup_table(account: table.account, addresses: table.addresses)
141
+ end
142
+
143
+ budget = compute_budget(reading)
144
+ composer.set_compute_budget(units: budget.units, micro_lamports: budget.micro_lamports) if budget.set?
145
+
146
+ composer
147
+ end
148
+
149
+ # The message of a transaction given as the object or its base64
150
+ #
151
+ # @param transaction [Solace::Transaction, String]
152
+ # @return [Solace::Message]
153
+ def message_of(transaction)
154
+ transaction = Solace::Transaction.from(transaction) if transaction.is_a?(String)
155
+ transaction.message
156
+ end
157
+
158
+ # The tables the message references, read from chain once each, in message order
159
+ #
160
+ # @param message [Solace::Message]
161
+ # @return [Hash{String => Accounts::AddressLookupTable}] Keyed by table account
162
+ def fetch_tables(message)
163
+ references = Array(message.address_lookup_tables)
164
+ accounts = references.map(&:account).uniq
165
+
166
+ accounts.to_h do |account|
167
+ table = Accounts::AddressLookupTable.fetch(account, connection: connection)
168
+ raise Errors::AddressLookupTableNotFound, account unless table
169
+
170
+ [account, table]
171
+ end
172
+ end
173
+
174
+ # The pubkeys one segment loads, across every table in table order
175
+ #
176
+ # @param message [Solace::Message]
177
+ # @param tables [Hash{String => Accounts::AddressLookupTable}]
178
+ # @param kind [Symbol] :writable_indexes or :readonly_indexes
179
+ # @return [Array<String>]
180
+ def loaded(message, tables, kind)
181
+ references = Array(message.address_lookup_tables)
182
+
183
+ references.flat_map do |reference|
184
+ addresses = tables.fetch(reference.account).addresses
185
+ indexes = kind == :writable_indexes ? reference.writable_indexes : reference.readonly_indexes
186
+
187
+ indexes.map { |index| addresses.fetch(index) }
188
+ end
189
+ end
190
+
191
+ # The message's instructions as composers, the budget directives left out
192
+ #
193
+ # @param reading [Reading]
194
+ # @return [Array<Composers::OpaqueInstructionComposer>]
195
+ def instruction_composers(reading)
196
+ lifted = reading.message.instructions.reject { |instruction| budget?(instruction, reading) }
197
+
198
+ lifted.map do |instruction|
199
+ program_id = reading.accounts.fetch(instruction.program_index)
200
+ accounts = instruction.accounts.map { |index| reading.accounts.fetch(index) }
201
+
202
+ Composers::OpaqueInstructionComposer.new(
203
+ program_id: program_id,
204
+ accounts: accounts,
205
+ data: instruction.data,
206
+ roles: reading.roles
207
+ )
208
+ end
209
+ end
210
+
211
+ # Whether an instruction is a limit or price directive the budget setting holds
212
+ #
213
+ # @param instruction [Solace::Instruction]
214
+ # @param reading [Reading]
215
+ # @return [Boolean]
216
+ def budget?(instruction, reading)
217
+ program_id = reading.accounts.fetch(instruction.program_index)
218
+ discriminator = instruction.data.first
219
+
220
+ program_id == Constants::COMPUTE_BUDGET_PROGRAM_ID && [LIMIT_INDEX, PRICE_INDEX].include?(discriminator)
221
+ end
222
+
223
+ # The budget the message carried, decoded from its limit and price directives
224
+ #
225
+ # @param reading [Reading]
226
+ # @return [Utils::ComputeBudget] Unset when the message carried neither
227
+ def compute_budget(reading)
228
+ directives = reading.message.instructions.select { |instruction| budget?(instruction, reading) }
229
+
230
+ Utils::ComputeBudget.new(
231
+ units: decode(directives, LIMIT_INDEX, 'L<'),
232
+ micro_lamports: decode(directives, PRICE_INDEX, 'Q<')
233
+ )
234
+ end
235
+
236
+ # The value of the first directive with the given discriminator, or nil
237
+ #
238
+ # @param directives [Array<Solace::Instruction>] The budget directives
239
+ # @param discriminator [Integer] The directive's first data byte
240
+ # @param format [String] The pack format of the value that follows it
241
+ # @return [Integer, nil]
242
+ def decode(directives, discriminator, format)
243
+ directive = directives.find { |instruction| instruction.data.first == discriminator }
244
+ return unless directive
245
+
246
+ value_bytes = directive.data.drop(1)
247
+ value_bytes.pack('C*').unpack1(format)
248
+ end
249
+ end
250
+ end