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 +4 -4
- data/CHANGELOG.md +31 -0
- data/LICENSE +21 -0
- data/README.md +94 -0
- data/lib/binary-codec/binary_codec.rb +3 -1
- data/lib/binary-codec/enums/definitions.rb +6 -4
- data/lib/binary-codec/enums/fields.rb +1 -1
- data/lib/binary-codec/serdes/binary_parser.rb +6 -10
- data/lib/binary-codec/serdes/binary_serializer.rb +12 -0
- data/lib/binary-codec/serdes/bytes_list.rb +2 -2
- data/lib/binary-codec/types/amount.rb +81 -55
- data/lib/binary-codec/types/blob.rb +1 -1
- data/lib/binary-codec/types/currency.rb +1 -1
- data/lib/binary-codec/types/hash.rb +1 -1
- data/lib/binary-codec/types/issue.rb +8 -11
- data/lib/binary-codec/types/path_set.rb +1 -1
- data/lib/binary-codec/types/st_array.rb +53 -18
- data/lib/binary-codec/types/st_object.rb +56 -17
- data/lib/binary-codec/types/uint.rb +51 -1
- data/lib/binary-codec/types/xchain_bridge.rb +5 -5
- data/lib/key-pairs/ed25519.rb +7 -2
- data/lib/key-pairs/secp256k1.rb +86 -33
- data/lib/wallet/wallet.rb +69 -7
- data/lib/xrpl/client.rb +498 -0
- data/lib/xrpl/faucet.rb +138 -0
- data/lib/xrpl/version.rb +5 -0
- data/lib/xrpl-ruby.rb +6 -1
- metadata +56 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a26ce8102c50640eec9433b64a9296c7d92635d66c42f2995456d913fff60d75
|
|
4
|
+
data.tar.gz: 60d3c026f1378a1d7cb79adcd6bbf830d518bb554b35286fe980010a14f4f11a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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[
|
|
66
|
-
|
|
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.
|
|
@@ -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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
BinaryCodec.write_uint32be(int_buf[1], num & 0xFFFFFFFF)
|
|
60
|
+
currency_inst = Currency.from(cur)
|
|
61
|
+
issuer_inst = AccountId.from(iss)
|
|
71
62
|
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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" =>
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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 =
|
|
239
|
+
decimal = amount.to_i
|
|
216
240
|
unless decimal.zero?
|
|
217
|
-
if decimal <
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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)
|
|
@@ -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,
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|