Class: BinaryCodec::Amount

Inherits:
SerializedType show all
Defined in:
lib/binary-codec/types/amount.rb

Constant Summary collapse

DEFAULT_AMOUNT_HEX =
"4000000000000000".freeze
ZERO_CURRENCY_AMOUNT_HEX =
"8000000000000000".freeze
NATIVE_AMOUNT_BYTE_LENGTH =
8
CURRENCY_AMOUNT_BYTE_LENGTH =
48
MAX_IOU_PRECISION =
16
MIN_IOU_EXPONENT =
-96
MAX_IOU_EXPONENT =
80
MAX_DROPS =
BigDecimal("1e17")
MIN_XRP =
BigDecimal("1e-6")

Instance Attribute Summary

Attributes inherited from SerializedType

#bytes

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from SerializedType

from_bytes, from_hex, from_json, get_type_by_name, #to_byte_sink, #to_bytes, #to_hex, #value_of

Constructor Details

#initialize(bytes = nil) ⇒ Amount

Returns a new instance of Amount.



20
21
22
23
24
25
26
# File 'lib/binary-codec/types/amount.rb', line 20

def initialize(bytes = nil)
  if bytes.nil?
    bytes = hex_to_bytes(DEFAULT_AMOUNT_HEX)
  end

  @bytes = bytes
end

Class Method Details

.assert_iou_is_valid(decimal) ⇒ Object

Validate IOU.value amount

Parameters:

  • decimal (BigDecimal)

    object representing IOU.value

Raises:

  • (ArgumentError)

    if the amount is invalid



227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/binary-codec/types/amount.rb', line 227

def self.assert_iou_is_valid(decimal)
  return if decimal.zero?

  p = decimal.precision
  e = (decimal.exponent || 0) - 15

  if p > MAX_IOU_PRECISION || e > MAX_IOU_EXPONENT || e < MIN_IOU_EXPONENT
    raise ArgumentError, 'Decimal precision out of range'
  end

  verify_no_decimal(decimal)
end

.assert_mpt_is_valid(amount) ⇒ void

This method returns an undefined value.

Validate MPT.value amount

Parameters:

  • amount (String)

    representing MPT.value



244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/binary-codec/types/amount.rb', line 244

def self.assert_mpt_is_valid(amount)
  if amount.include?('.')
    raise "#{amount} is an illegal amount"
  end

  decimal = BigDecimal(amount)
  unless decimal.zero?
    if decimal < BigDecimal("0")
      raise "#{amount} is an illegal amount"
    end

    if (amount.to_i & mpt_mask) != 0
      raise "#{amount} is an illegal amount"
    end
  end
end

.assert_xrp_is_valid(amount) ⇒ void

This method returns an undefined value.

Validate XRP amount

Parameters:

  • amount (String)

    representing XRP amount



210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/binary-codec/types/amount.rb', line 210

def self.assert_xrp_is_valid(amount)
  if amount.include?('.')
    raise "#{amount} is an illegal amount"
  end

  decimal = BigDecimal(amount)
  unless decimal.zero?
    if decimal < MIN_XRP || decimal > MAX_DROPS
      raise "#{amount} is an illegal amount"
    end
  end
end

.from(value) ⇒ Amount

Construct an amount from an IOU, MPT, or string amount

Creates a new Amount instance from a value.

Parameters:

  • value (Amount, Hash, String)

    representing the amount

  • value (Amount, String, Hash, Integer)

    The value to convert.

Returns:

  • (Amount)

    an Amount object

  • (Amount)

    The created instance.



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/binary-codec/types/amount.rb', line 35

def self.from(value)
  return value if value.is_a?(Amount)

  amount = Array.new(8, 0) # Equivalent to a Uint8Array of 8 zeros

  if value.is_a?(String)
    Amount.assert_xrp_is_valid(value)

    number = value.to_i # Use to_i for equivalent BigInt handling

    int_buf = [Array.new(4, 0), Array.new(4, 0)]
    BinaryCodec.write_uint32be(int_buf[0], (number >> 32) & 0xFFFFFFFF, 0)
    BinaryCodec.write_uint32be(int_buf[1], number & 0xFFFFFFFF, 0)

    amount = int_buf.flatten

    amount[0] |= 0x40

    return Amount.new(amount)
  end

  if is_amount_object_iou?(value)
    number = BigDecimal(value[:value])
    self.assert_iou_is_valid(number)

    if number.zero?
      amount[0] |= 0x80
    else
      scale = number.frac.to_s('F').split('.').last.size
      unscaled_value = (number * (10**scale)).to_i
      int_string = unscaled_value.abs.to_s.ljust(16, '0')
      num = int_string.to_i

      int_buf = [Array.new(4, 0), Array.new(4, 0)]
      BinaryCodec.write_uint32be(int_buf[0], (num >> 32) & 0xFFFFFFFF)
      BinaryCodec.write_uint32be(int_buf[1], num & 0xFFFFFFFF)

      amount = int_buf.flatten

      amount[0] |= 0x80

      if number > 0
        amount[0] |= 0x40
      end

      exponent = number.exponent - 16
      exponent_byte = 97 + exponent
      amount[0] |= exponent_byte >> 2
      amount[1] |= (exponent_byte & 0x03) << 6
    end

    currency = Currency.from(value[:currency]).to_bytes
    issuer = AccountId.from(value[:issuer]).to_bytes

    return Amount.new(amount + currency + issuer)
  end

end

.from_parser(parser, _size_hint = nil) ⇒ Amount

Read an amount from a BinaryParser

Creates an Amount instance from a parser.

Parameters:

  • parser (BinaryParser)

    The BinaryParser to read the Amount from

  • parser (BinaryParser)

    The parser to read from.

  • _size_hint (Integer, nil) (defaults to: nil)

    Optional size hint (unused).

Returns:

  • (Amount)

    An Amount bundle exec rspec spec/binary-codec/types/st_object_spec.rb object

  • (Amount)

    The created instance.



102
103
104
105
106
107
108
109
110
# File 'lib/binary-codec/types/amount.rb', line 102

def self.from_parser(parser, _size_hint = nil)
  is_iou = parser.peek & 0x80 != 0
  return Amount.new(parser.read(48)) if is_iou

  # The amount can be either MPT or XRP at this point
  is_mpt = parser.peek & 0x20 != 0
  num_bytes = is_mpt ? 33 : 8
  Amount.new(parser.read(num_bytes))
end

.is_amount_object_iou?(arg) ⇒ Boolean

Type guard for AmountObjectIOU

Returns:

  • (Boolean)


188
189
190
191
192
193
194
195
# File 'lib/binary-codec/types/amount.rb', line 188

def self.is_amount_object_iou?(arg)
  keys = arg.transform_keys(&:to_s).keys.sort

  keys.length == 3 &&
    keys[0] == 'currency' &&
    keys[1] == 'issuer' &&
    keys[2] == 'value'
end

.is_amount_object_mpt?(arg) ⇒ Boolean

Type guard for AmountObjectMPT

Returns:

  • (Boolean)


198
199
200
201
202
203
204
# File 'lib/binary-codec/types/amount.rb', line 198

def self.is_amount_object_mpt?(arg)
  keys = arg.keys.sort

  keys.length == 2 &&
    keys[0] == 'mpt_issuance_id' &&
    keys[1] == 'value'
end

.verify_no_decimal(decimal) ⇒ String

Ensure that the value, after being multiplied by the exponent, does not contain a decimal. This function is typically used to validate numbers that need to be represented as precise integers after scaling, such as amounts in financial transactions. Example failure:1.1234567891234567

Parameters:

  • decimal (BigDecimal)

    A BigDecimal object

Returns:

  • (String)

    The decimal converted to a string without a decimal point

Raises:

  • (ArgumentError)

    if the value contains a decimal



269
270
271
272
273
274
# File 'lib/binary-codec/types/amount.rb', line 269

def self.verify_no_decimal(decimal)
  exponent = -((decimal.exponent || 0) - 16)
  scaled_decimal = decimal * 10 ** exponent

    raise ArgumentError, 'Decimal place found in int_string' unless scaled_decimal.frac == 0
end

Instance Method Details

#to_json(_definitions = nil, _field_name = nil) ⇒ Hash, String

The JSON representation of this Amount

Returns the JSON representation of the Amount.

Parameters:

  • _definitions (Definitions, nil) (defaults to: nil)

    Unused.

  • _field_name (String, nil) (defaults to: nil)

    Optional field name.

Returns:

  • (Hash, String)

    The JSON interpretation of this.bytes

  • (String, Hash)

    The JSON representation.



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/binary-codec/types/amount.rb', line 119

def to_json(_definitions = nil, _field_name = nil)
  if is_native?
    bytes = @bytes.dup
    is_positive = (bytes[0] & 0x40) != 0
    sign = is_positive ? '' : '-'
    bytes[0] &= 0x3f

    msb = BinaryCodec.read_uint32be(bytes[0, 4])
    lsb = BinaryCodec.read_uint32be(bytes[4, 4])
    num = (msb << 32) | lsb

    return "#{sign}#{num}"
  end

  if is_iou?
    parser = BinaryParser.new(to_hex)
    mantissa_bytes = parser.read(8)
    currency = Currency.from_parser(parser)
    issuer = AccountId.from_parser(parser)

    b1 = mantissa_bytes[0]
    b2 = mantissa_bytes[1]

    is_positive = (b1 & 0x40) != 0
    sign = is_positive ? '' : '-'
    exponent = ((b1 & 0x3f) << 2) + ((b2 & 0xff) >> 6) - 97

    mantissa_bytes[0] = 0
    mantissa_bytes[1] &= 0x3f
    
    # Convert mantissa bytes to integer
    mantissa_int = mantissa_bytes.reduce(0) { |acc, b| (acc << 8) + b }
    
    value = BigDecimal(mantissa_int) * (BigDecimal(10)**exponent)
    value = -value unless is_positive
    self.class.assert_iou_is_valid(value)

    return {
      "value" => value.to_s('F').sub(/\.0$/, ''),
      "currency" => currency.to_json,
      "issuer" => issuer.to_json
    }
  end

  if is_mpt?
    parser = BinaryParser.new(to_hex)
    leading_byte = parser.read(1)
    amount_bytes = parser.read(8)
    mpt_id = Hash192.from_parser(parser)

    is_positive = (leading_byte[0] & 0x40) != 0
    sign = is_positive ? '' : '-'

    msb = BinaryCodec.read_uint32be(amount_bytes[0, 4])
    lsb = BinaryCodec.read_uint32be(amount_bytes[4, 4])
    num = (msb << 32) | lsb

    return {
      "value" => "#{sign}#{num}",
      "mpt_issuance_id" => mpt_id.to_hex
    }
  end

  raise 'Invalid amount to construct JSON'
end