xrpl-ruby 0.5.0 → 0.6.0

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: 6c84782686e7934255929a23850a5f514d0435ff79e47cde2739ed1e940a0235
4
- data.tar.gz: b6b1c24d77a63365e8bf45f8a8b702332315057a8a7ec820ef830264e3ba446e
3
+ metadata.gz: a26ce8102c50640eec9433b64a9296c7d92635d66c42f2995456d913fff60d75
4
+ data.tar.gz: 60d3c026f1378a1d7cb79adcd6bbf830d518bb554b35286fe980010a14f4f11a
5
5
  SHA512:
6
- metadata.gz: 9cdee47951edf9ac0a804e8f8335aea13625f0cf61f6262f229f1c7e8e569892821a201b80916f0f274ac8ed9c2de677a77e32c5ed113bfc5102d7dca7f0cafa
7
- data.tar.gz: 6c6e8309646053ab8039c7eb9bac814d08c9984f8dba4a64ae290ce349ac5e8c739b7a81e1ecf6305d93623f2ca9ce5015e1435757b6aa9a3bf6e74e2c1a6759
6
+ metadata.gz: 975f1af5db2890d44ba6ac7266c2a927dc5e8bf4d3000a8e70bd864734dbba1e90495718b48300df5ca183077076de844b00c2844aa87c050d61263d5e43a12a
7
+ data.tar.gz: 85a70a5ebc31bde02941394182c93aeb1c09a57987fbadfcbac26efdd16e1b30fe786d1aefdefd2ee1fa3cf742b18dbde2edbb2983052869b8165411b6207fd5
data/CHANGELOG.md ADDED
@@ -0,0 +1,31 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.6.0] - 2026-08-05
9
+
10
+ ### Added
11
+ - **Connection readiness**: `Client#connect!` (and `connect(wait: true)`) block until the
12
+ WebSocket connection is open; `#open?` and `#wait_until_open` expose the state, so requests
13
+ no longer race with connection setup.
14
+ - **Faucet helper**: `XRPL.fund_wallet(client)` / `XRPL::Faucet` creates and funds a wallet on
15
+ the Testnet and waits until the account is funded on the ledger.
16
+ - **Transaction lifecycle** on the client (client-centric design): `Client#autofill`,
17
+ `Client#submit`, and `Client#submit_and_wait` (reliable submission that polls until the
18
+ transaction is included in a validated ledger).
19
+ - **Optional logger**: `Client.new(url, logger:)` — the library is silent by default and only
20
+ emits diagnostics through an injected logger.
21
+ - Example scripts for funding a wallet, querying account info, and sending a payment.
22
+
23
+ ### Changed
24
+ - The library no longer writes to `stdout` on its own; connection messages go through the
25
+ optional logger instead.
26
+
27
+ ## [0.5.2]
28
+
29
+ ### Added
30
+ - Binary codec, address codec, key pairs (secp256k1 / ed25519), wallet, and a WebSocket
31
+ client with account and ledger public API wrappers.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Alexander Busse
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # XRPL-Ruby
2
+
3
+ A pure-Ruby library to interact with the [XRP Ledger](https://xrpl.org) (XRPL) blockchain.
4
+
5
+ ## Features
6
+
7
+ - Key and wallet management (secp256k1 and ed25519)
8
+ - Address codec and binary (transaction) codec
9
+ - WebSocket client for the XRP Ledger public API
10
+ - Testnet faucet helper to create and fund wallets
11
+ - Transaction lifecycle: autofill, sign, submit, and reliable "submit and wait"
12
+
13
+ ## Requirements
14
+
15
+ - Ruby 3.0 or later
16
+
17
+ ## Installation
18
+
19
+ Install the gem:
20
+
21
+ ```sh
22
+ gem install xrpl-ruby
23
+ ```
24
+
25
+ Or add it to your `Gemfile`:
26
+
27
+ ```ruby
28
+ gem 'xrpl-ruby'
29
+ ```
30
+
31
+ ## Quick start
32
+
33
+ ```ruby
34
+ require 'xrpl-ruby'
35
+
36
+ # 1. Connect to the Testnet (blocks until the connection is ready)
37
+ client = XRPL::Client.new(:testnet)
38
+ client.connect!
39
+
40
+ # 2. Create and fund a wallet using the Testnet faucet
41
+ wallet = XRPL.fund_wallet(client)[:wallet]
42
+ puts wallet.classic_address
43
+
44
+ # 3. Look up the account on the ledger
45
+ info = client.account_info_response(
46
+ account: wallet.classic_address,
47
+ ledger_index: 'validated'
48
+ )
49
+ puts info.dig('result', 'account_data', 'Balance')
50
+
51
+ # 4. Send 1 XRP (1,000,000 drops) and wait for validation
52
+ receiver = XRPL.fund_wallet(client)[:wallet]
53
+ payment = {
54
+ 'TransactionType' => 'Payment',
55
+ 'Account' => wallet.classic_address,
56
+ 'Destination' => receiver.classic_address,
57
+ 'Amount' => '1000000'
58
+ }
59
+ result = client.submit_and_wait(payment, wallet: wallet)
60
+ puts result.dig('result', 'meta', 'TransactionResult') # => "tesSUCCESS"
61
+
62
+ client.disconnect
63
+ ```
64
+
65
+ The client is silent by default. To see diagnostic output, pass a logger:
66
+
67
+ ```ruby
68
+ require 'logger'
69
+ client = XRPL::Client.new(:testnet, logger: Logger.new($stdout))
70
+ ```
71
+
72
+ More runnable examples are in the [`examples/`](examples) directory.
73
+
74
+ ## Running the tests
75
+
76
+ ```sh
77
+ bundle install
78
+ bundle exec rspec
79
+ ```
80
+
81
+ Integration tests talk to the real Testnet and are skipped by default. Enable them explicitly:
82
+
83
+ ```sh
84
+ XRPL_NETWORK=1 bundle exec rspec spec/integration
85
+ ```
86
+
87
+ ## Contributing
88
+
89
+ Bug reports and pull requests are welcome on GitHub at
90
+ <https://github.com/AlexanderBuzz/xrpl-ruby>.
91
+
92
+ ## License
93
+
94
+ Released under the [MIT License](LICENSE).
@@ -25,7 +25,9 @@ module BinaryCodec
25
25
  def binary_to_json(hex)
26
26
  parser = make_parser(hex)
27
27
  st_object = SerializedType.get_type_by_name('STObject')
28
- JSON.parse(st_object.from_parser(parser).to_json)
28
+ result = st_object.from_parser(parser).to_json
29
+ result = JSON.generate(result) unless result.is_a?(String)
30
+ JSON.parse(result)
29
31
  end
30
32
 
31
33
  # Converts a JSON object to its binary representation.
@@ -31,10 +31,12 @@ module BinaryCodec
31
31
  is_signing_field: field[1]['isSigningField'],
32
32
  type: field[1]['type']
33
33
  )
34
- field_header = FieldHeader.new(type: @type_ordinals[field_info.type], nth: field_info.nth)
34
+ type_ordinal = @type_ordinals[field_info.type]
35
+ field_header = FieldHeader.new(type: type_ordinal, nth: field_info.nth)
35
36
 
36
37
  @field_info_map[field_name] = field_info
37
- @field_id_name_map[Digest::MD5.hexdigest(Marshal.dump(field_header))] = field_name
38
+ key = (type_ordinal << 16) | field_info.nth
39
+ @field_id_name_map[key] = field_name
38
40
  @field_header_map[field_name] = field_header
39
41
  end
40
42
 
@@ -62,8 +64,8 @@ module BinaryCodec
62
64
  # @param field_header [FieldHeader] The field header.
63
65
  # @return [String] The name of the field.
64
66
  def get_field_name_from_header(field_header)
65
- @field_id_name_map[Digest::MD5.hexdigest(Marshal.dump(field_header))]
66
- end
67
+ @field_id_name_map[(field_header.type << 16) | field_header.nth]
68
+ end
67
69
 
68
70
  # Returns a FieldInstance for a given field name.
69
71
  # @param field_name [String] The name of the field.
@@ -22,7 +22,7 @@ module BinaryCodec
22
22
  header.push(type << 4, nth)
23
23
  end
24
24
  elsif nth < 16
25
- header.push(nth,type)
25
+ header.push(nth, type)
26
26
  else
27
27
  header.push(0, type, nth)
28
28
  end
@@ -111,25 +111,19 @@ module BinaryCodec
111
111
  # Reads a field header from the stream.
112
112
  # @return [FieldHeader] The field header.
113
113
  def read_field_header
114
- type = read_uint8
115
- nth = type & 15
116
- type >>= 4
114
+ first_byte = read_uint8
115
+ type = first_byte >> 4
116
+ nth = first_byte & 15
117
117
 
118
118
  if type == 0
119
119
  type = read_uint8
120
- if type == 0 || type < 16
121
- raise StandardError.new("Cannot read FieldOrdinal, type_code #{type} out of range")
122
- end
123
120
  end
124
121
 
125
122
  if nth == 0
126
123
  nth = read_uint8
127
- if nth == 0 || nth < 16
128
- raise StandardError.new("Cannot read FieldOrdinal, field_code #{nth} out of range")
129
- end
130
124
  end
131
125
 
132
- FieldHeader.new(type: type, nth: nth) # (type << 16) | nth for read_field_ordinal
126
+ FieldHeader.new(type: type, nth: nth)
133
127
  end
134
128
 
135
129
  # Reads a field instance from the stream.
@@ -137,6 +131,8 @@ module BinaryCodec
137
131
  def read_field
138
132
  field_header = read_field_header
139
133
  field_name = @definitions.get_field_name_from_header(field_header)
134
+
135
+ raise "Unknown field for header: type=#{field_header.type}, nth=#{field_header.nth}" if field_name.nil?
140
136
 
141
137
  @definitions.get_field_instance(field_name)
142
138
  end
@@ -37,6 +37,15 @@ module BinaryCodec
37
37
  # @param value [Object] The value of the field.
38
38
  # @param is_unl_modify_workaround [Boolean] Whether to apply the UNLModify workaround.
39
39
  def write_field_and_value(field, value, is_unl_modify_workaround = false)
40
+ # Special case for Blob fields that are empty (e.g., SigningPubKey = "")
41
+ # In Ruby, Blob.from("") returns an empty Blob.
42
+ # If we want to force 0x00 length prefix, we handle it here.
43
+ if field.type == 'Blob' && (value == "" || (value.is_a?(Array) && value.empty?))
44
+ @sink.put(field.header.to_bytes)
45
+ @sink.put([0]) # length 0
46
+ return
47
+ end
48
+
40
49
  field_header = field.header
41
50
  associated_value = field.associated_type.from(value)
42
51
 
@@ -46,6 +55,9 @@ module BinaryCodec
46
55
  write_length_encoded(associated_value, is_unl_modify_workaround)
47
56
  else
48
57
  associated_value.to_byte_sink(@sink)
58
+ if field.type == 'STObject'
59
+ @sink.put([0xE1]) # ObjectEndMarker
60
+ end
49
61
  end
50
62
  end
51
63
 
@@ -16,10 +16,10 @@ module BinaryCodec
16
16
  end
17
17
 
18
18
  # Adds bytes to the list.
19
- # @param bytes_arg [Array<Integer>] The bytes to add.
19
+ # @param bytes_arg [Array<Integer>, Integer] The bytes to add.
20
20
  # @return [BytesList] self for chaining.
21
21
  def put(bytes_arg)
22
- bytes = bytes_arg.dup
22
+ bytes = bytes_arg.is_a?(Integer) ? [bytes_arg] : bytes_arg.dup
23
23
  @bytes_array << bytes
24
24
  self # Allow chaining
25
25
  end
@@ -16,6 +16,8 @@ module BinaryCodec
16
16
 
17
17
  MAX_DROPS = BigDecimal("1e17")
18
18
  MIN_XRP = BigDecimal("1e-6")
19
+ MIN_XRP_DROPS = 1
20
+ MAX_XRP_DROPS = 10**17
19
21
 
20
22
  def initialize(bytes = nil)
21
23
  if bytes.nil?
@@ -35,60 +37,74 @@ module BinaryCodec
35
37
  def self.from(value)
36
38
  return value if value.is_a?(Amount)
37
39
 
38
- amount = Array.new(8, 0) # Equivalent to a Uint8Array of 8 zeros
39
-
40
40
  if value.is_a?(String)
41
41
  Amount.assert_xrp_is_valid(value)
42
-
43
- number = value.to_i # Use to_i for equivalent BigInt handling
44
-
45
- int_buf = [Array.new(4, 0), Array.new(4, 0)]
46
- BinaryCodec.write_uint32be(int_buf[0], (number >> 32) & 0xFFFFFFFF, 0)
47
- BinaryCodec.write_uint32be(int_buf[1], number & 0xFFFFFFFF, 0)
48
-
49
- amount = int_buf.flatten
50
-
51
- amount[0] |= 0x40
52
-
53
- return Amount.new(amount)
42
+ number = value.to_i
43
+ amount_bytes = int_to_bytes(number, 8)
44
+ amount_bytes[0] |= 0x40
45
+ return Amount.new(amount_bytes)
54
46
  end
55
47
 
56
- if is_amount_object_iou?(value)
57
- number = BigDecimal(value[:value])
58
- self.assert_iou_is_valid(number)
48
+ if value.respond_to?(:key?)
49
+ val = value[:value] || value['value']
50
+ cur = value[:currency] || value['currency']
51
+ iss = value[:issuer] || value['issuer']
59
52
 
60
- if number.zero?
61
- amount[0] |= 0x80
62
- else
63
- scale = number.frac.to_s('F').split('.').last.size
64
- unscaled_value = (number * (10**scale)).to_i
65
- int_string = unscaled_value.abs.to_s.ljust(16, '0')
66
- num = int_string.to_i
53
+ if val && cur && iss
54
+ number = BigDecimal(val.to_s)
55
+
56
+ if number.precision > MAX_IOU_PRECISION
57
+ raise ArgumentError, 'Decimal precision out of range'
58
+ end
67
59
 
68
- int_buf = [Array.new(4, 0), Array.new(4, 0)]
69
- BinaryCodec.write_uint32be(int_buf[0], (num >> 32) & 0xFFFFFFFF)
70
- BinaryCodec.write_uint32be(int_buf[1], num & 0xFFFFFFFF)
60
+ currency_inst = Currency.from(cur)
61
+ issuer_inst = AccountId.from(iss)
71
62
 
72
- amount = int_buf.flatten
63
+ if number.zero?
64
+ iou_bytes = [0x80, 0, 0, 0, 0, 0, 0, 0]
65
+ return Amount.new(iou_bytes + currency_inst.to_bytes + issuer_inst.to_bytes)
66
+ end
73
67
 
74
- amount[0] |= 0x80
68
+ is_positive = number >= 0
69
+ abs_value = number.abs
70
+
71
+ exponent = (Math.log10(abs_value.to_f).floor) - 15
72
+ mantissa = (abs_value / (BigDecimal(10)**exponent)).to_i
75
73
 
76
- if number > 0
77
- amount[0] |= 0x40
74
+ while mantissa < 1000000000000000
75
+ mantissa *= 10
76
+ exponent -= 1
77
+ end
78
+ while mantissa > 9999999999999999
79
+ mantissa /= 10
80
+ exponent += 1
78
81
  end
79
82
 
80
- exponent = number.exponent - 16
81
- exponent_byte = 97 + exponent
82
- amount[0] |= exponent_byte >> 2
83
- amount[1] |= (exponent_byte & 0x03) << 6
83
+ exponent_byte = exponent + 97
84
+ b1 = (is_positive ? 0x40 : 0) | 0x80 | (exponent_byte >> 2)
85
+ b2 = ((exponent_byte & 0x03) << 6) | (mantissa >> 48)
86
+
87
+ iou_bytes = [
88
+ b1, b2,
89
+ (mantissa >> 40) & 0xff,
90
+ (mantissa >> 32) & 0xff,
91
+ (mantissa >> 24) & 0xff,
92
+ (mantissa >> 16) & 0xff,
93
+ (mantissa >> 8) & 0xff,
94
+ mantissa & 0xff
95
+ ]
96
+ return Amount.new(iou_bytes + currency_inst.to_bytes + issuer_inst.to_bytes)
84
97
  end
98
+ end
85
99
 
86
- currency = Currency.from(value[:currency]).to_bytes
87
- issuer = AccountId.from(value[:issuer]).to_bytes
88
-
89
- return Amount.new(amount + currency + issuer)
100
+ if value.is_a?(Integer)
101
+ Amount.assert_xrp_is_valid(value.to_s)
102
+ amount_bytes = int_to_bytes(value, 8)
103
+ amount_bytes[0] |= 0x40
104
+ return Amount.new(amount_bytes)
90
105
  end
91
106
 
107
+ raise ArgumentError, "Cannot construct Amount from the value given"
92
108
  end
93
109
 
94
110
  # Read an amount from a BinaryParser
@@ -140,7 +156,6 @@ module BinaryCodec
140
156
  b2 = mantissa_bytes[1]
141
157
 
142
158
  is_positive = (b1 & 0x40) != 0
143
- sign = is_positive ? '' : '-'
144
159
  exponent = ((b1 & 0x3f) << 2) + ((b2 & 0xff) >> 6) - 97
145
160
 
146
161
  mantissa_bytes[0] = 0
@@ -149,12 +164,15 @@ module BinaryCodec
149
164
  # Convert mantissa bytes to integer
150
165
  mantissa_int = mantissa_bytes.reduce(0) { |acc, b| (acc << 8) + b }
151
166
 
167
+ # value = mantissa * 10^exponent
152
168
  value = BigDecimal(mantissa_int) * (BigDecimal(10)**exponent)
153
169
  value = -value unless is_positive
154
- self.class.assert_iou_is_valid(value)
170
+
171
+ # Format the value string to match xrpl.js (stripping trailing .0)
172
+ formatted_value = value.to_s('F').sub(/\.0$/, '')
155
173
 
156
174
  return {
157
- "value" => value.to_s('F').sub(/\.0$/, ''),
175
+ "value" => formatted_value,
158
176
  "currency" => currency.to_json,
159
177
  "issuer" => issuer.to_json
160
178
  }
@@ -186,17 +204,23 @@ module BinaryCodec
186
204
 
187
205
  # Type guard for AmountObjectIOU
188
206
  def self.is_amount_object_iou?(arg)
189
- keys = arg.transform_keys(&:to_s).keys.sort
190
-
191
- keys.length == 3 &&
192
- keys[0] == 'currency' &&
193
- keys[1] == 'issuer' &&
194
- keys[2] == 'value'
207
+ return false unless arg.is_a?(::Hash)
208
+
209
+ # Handle both string and symbol keys
210
+ processed = arg.transform_keys(&:to_s)
211
+
212
+ # Log for debugging
213
+ # puts "DEBUG: Checking if #{processed.keys.inspect} is IOU"
214
+
215
+ processed.key?('currency') &&
216
+ processed.key?('issuer') &&
217
+ processed.key?('value')
195
218
  end
196
219
 
197
220
  # Type guard for AmountObjectMPT
198
221
  def self.is_amount_object_mpt?(arg)
199
- keys = arg.keys.sort
222
+ return false unless arg.is_a?(::Hash)
223
+ keys = arg.transform_keys(&:to_s).keys.sort
200
224
 
201
225
  keys.length == 2 &&
202
226
  keys[0] == 'mpt_issuance_id' &&
@@ -212,9 +236,9 @@ module BinaryCodec
212
236
  raise "#{amount} is an illegal amount"
213
237
  end
214
238
 
215
- decimal = BigDecimal(amount)
239
+ decimal = amount.to_i
216
240
  unless decimal.zero?
217
- if decimal < MIN_XRP || decimal > MAX_DROPS
241
+ if decimal < MIN_XRP_DROPS || decimal > MAX_XRP_DROPS
218
242
  raise "#{amount} is an illegal amount"
219
243
  end
220
244
  end
@@ -267,10 +291,12 @@ module BinaryCodec
267
291
  # @raise [ArgumentError] if the value contains a decimal
268
292
  # @return [String] The decimal converted to a string without a decimal point
269
293
  def self.verify_no_decimal(decimal)
270
- exponent = -((decimal.exponent || 0) - 16)
271
- scaled_decimal = decimal * 10 ** exponent
272
-
273
- raise ArgumentError, 'Decimal place found in int_string' unless scaled_decimal.frac == 0
294
+ # p is the number of significant digits
295
+ # e is the power of 10 to multiply by the mantissa to get the number
296
+ # BigDecimal('1.1234567891234567').precision => 17
297
+ if decimal.precision > MAX_IOU_PRECISION
298
+ raise ArgumentError, 'Decimal precision out of range'
299
+ end
274
300
  end
275
301
 
276
302
  # Check if this amount is in units of Native Currency (XRP)
@@ -20,7 +20,7 @@ module BinaryCodec
20
20
  return Blob.new(hex_to_bytes(value))
21
21
  end
22
22
 
23
- if value.is_a?(Array)
23
+ if value.is_a?(::Array)
24
24
  return Blob.new(value)
25
25
  end
26
26
 
@@ -43,7 +43,7 @@ module BinaryCodec
43
43
  return new(bytes_from_representation(value))
44
44
  end
45
45
 
46
- if value.is_a?(Array)
46
+ if value.is_a?(::Array)
47
47
  return new(value)
48
48
  end
49
49
 
@@ -27,7 +27,7 @@ module BinaryCodec
27
27
  return new(hex_to_bytes(value))
28
28
  end
29
29
 
30
- if value.is_a?(Array)
30
+ if value.is_a?(::Array)
31
31
  return new(value)
32
32
  end
33
33
 
@@ -23,19 +23,13 @@ module BinaryCodec
23
23
  raise StandardError, "Cannot construct Issue from #{value.class}"
24
24
  end
25
25
 
26
- def self.from_parser(parser, _hint = nil)
26
+ def self.from_parser(parser, size_hint = nil)
27
27
  bytes = []
28
+ return Issue.new(bytes) if parser.end?
28
29
  bytes.concat(parser.read(20)) # Currency
29
- # If there are more bytes in this field, it might have an issuer?
30
- # Actually Issue is often fixed length 20 or 40.
31
- # For XChainBridge it uses Issue.
32
- # Let's see how much we should read.
33
- # Usually if it's an Issue in a field, we might know the size.
34
- # For now, let's assume it can be 20 or 40.
35
- # But wait, how does the parser know?
36
- # If it's not variable length, it must have a fixed width or be the rest of the object.
37
- # Definitions.json says Issue is type 24.
38
- bytes.concat(parser.read(20)) unless parser.end? # Try reading issuer
30
+ unless parser.end? || (size_hint && size_hint <= 20)
31
+ bytes.concat(parser.read(20))
32
+ end
39
33
  Issue.new(bytes)
40
34
  end
41
35
 
@@ -45,6 +39,9 @@ module BinaryCodec
45
39
  result['currency'] = Currency.from_parser(parser).to_json
46
40
  result['issuer'] = AccountId.from_parser(parser).to_json unless parser.end?
47
41
  result
42
+ rescue
43
+ # Fallback for partial/invalid binary
44
+ { 'currency' => Currency.new(to_bytes[0, 20]).to_json }
48
45
  end
49
46
  end
50
47
  end
@@ -20,7 +20,7 @@ module BinaryCodec
20
20
  return PathSet.new(hex_to_bytes(value))
21
21
  end
22
22
 
23
- if value.is_a?(Array)
23
+ if value.is_a?(::Array)
24
24
  bytes = []
25
25
  value.each_with_index do |path, index|
26
26
  path.each do |step|