amounts 0.0.1

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.
data/lib/amount.rb ADDED
@@ -0,0 +1,494 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+ require "forwardable"
5
+
6
+ require_relative "amount/version"
7
+ require_relative "amount/registry"
8
+ require_relative "amount/display"
9
+ require_relative "amount/parser"
10
+ require_relative "amount/serializer"
11
+
12
+ # Represents a precise quantity of a registered fungible type.
13
+ #
14
+ # `Amount` stores its value as an arbitrary-precision atomic `Integer` in the
15
+ # smallest unit configured for the registered symbol. UI values are parsed from
16
+ # strings or decimals, while integer inputs are treated as atomic counts unless
17
+ # `from:` overrides inference.
18
+ #
19
+ # @example Constructing from a UI value
20
+ # Amount.register :USDC, decimals: 6
21
+ #
22
+ # Amount.usdc("1.50").atomic
23
+ # # => 1500000
24
+ #
25
+ # @example Constructing from an atomic value
26
+ # Amount.usdc(1_500_000, from: :atomic).decimal.to_s("F")
27
+ # # => "1.5"
28
+ class Amount
29
+ include Comparable
30
+ extend Forwardable
31
+
32
+ class Error < StandardError; end
33
+ class TypeMismatch < Error; end
34
+ class InvalidInput < Error; end
35
+ class UnregisteredType < Error; end
36
+
37
+ class << self
38
+ # @return [Amount::Registry]
39
+ # @example Accessing the shared registry
40
+ # Amount.registry.locked?
41
+ # # => false
42
+ def registry
43
+ Amount.instance_variable_get(:@registry) ||
44
+ replace_registry(Registry.new)
45
+ end
46
+
47
+ # @param symbol [Symbol, String]
48
+ # @param opts [Hash]
49
+ # @return [void]
50
+ # @example Registering a type
51
+ # Amount.register :USDC,
52
+ # decimals: 6,
53
+ # display_symbol: "$",
54
+ # display_position: :prefix,
55
+ # ui_decimals: 2
56
+ def register(symbol, **opts)
57
+ registry.register(symbol, **opts)
58
+ end
59
+
60
+ # @param from [Symbol, String]
61
+ # @param to [Symbol, String]
62
+ # @param rate [String, Numeric, BigDecimal]
63
+ # @return [void]
64
+ # @example Registering a directional default rate
65
+ # Amount.register_default_rate :USD, :USDC, "1"
66
+ def register_default_rate(from, to, rate)
67
+ registry.register_default_rate(from, to, rate)
68
+ end
69
+
70
+ # Parses the compact client-facing string representation.
71
+ #
72
+ # Accepts either the default form `SYMBOL|amount` or the explicit versioned
73
+ # form `v1:SYMBOL|amount`.
74
+ #
75
+ # @param str [String]
76
+ # @return [Amount]
77
+ # @raise [InvalidInput]
78
+ # @example Parsing the default compact format
79
+ # Amount.parse("USDC|1.50")
80
+ #
81
+ # @example Parsing the explicit versioned compact format
82
+ # Amount.parse("v1:USDC|1.50")
83
+ def parse(str)
84
+ Parser.new(str).parse
85
+ end
86
+
87
+ # Temporarily swaps the global registry. Intended for tests.
88
+ #
89
+ # @param registry [Amount::Registry]
90
+ # @yield
91
+ # @return [Object]
92
+ # @example Using a temporary registry
93
+ # test_registry = Amount::Registry.new
94
+ # Amount.with_registry(test_registry) do
95
+ # Amount.register :TEST, decimals: 2
96
+ # end
97
+ def with_registry(registry)
98
+ original = Amount.instance_variable_get(:@registry)
99
+ replace_registry(registry)
100
+ yield
101
+ ensure
102
+ replace_registry(original)
103
+ end
104
+
105
+ private
106
+
107
+ def replace_registry(registry)
108
+ current = Amount.instance_variable_get(:@registry)
109
+ current&.remove_generated_methods!
110
+ Amount.instance_variable_set(:@registry, registry)
111
+ registry&.activate_generated_methods!
112
+ registry
113
+ end
114
+ end
115
+
116
+ attr_reader :atomic, :symbol
117
+
118
+ # Creates an amount for a registered symbol.
119
+ #
120
+ # Input inference rules:
121
+ # - `Integer` => atomic units
122
+ # - `String` => UI decimal value
123
+ # - `Float`, `BigDecimal`, `Rational` => UI decimal value
124
+ # - `from:` overrides inference explicitly
125
+ #
126
+ # @param value [Integer, String, Float, BigDecimal, Rational]
127
+ # @param symbol [Symbol, String] registered type identifier
128
+ # @param from [Symbol, nil] one of `:atomic`, `:ui`, or `:float`
129
+ # @raise [Amount::Registry::UnknownType] if the symbol is not registered
130
+ # @raise [InvalidInput] if the value cannot be interpreted for the symbol
131
+ # @example Integer inputs are atomic by default
132
+ # Amount.new(1_500_000, :USDC).decimal.to_s("F")
133
+ # # => "1.5"
134
+ #
135
+ # @example String inputs are UI values by default
136
+ # Amount.new("1.50", :USDC).atomic
137
+ # # => 1500000
138
+ def initialize(value, symbol, from: nil)
139
+ @symbol = symbol.to_sym
140
+ @entry = self.class.registry.lookup(@symbol)
141
+
142
+ if @entry.amount_class != self.class && @entry.amount_class != Amount
143
+ raise InvalidInput, "use #{@entry.amount_class}.new for #{@symbol}" unless instance_of?(@entry.amount_class)
144
+ end
145
+
146
+ @atomic = infer_value(from, value)
147
+ end
148
+
149
+ # @return [Amount::Registry::Entry]
150
+ # @example Accessing display configuration for this amount
151
+ # Amount.usdc("1").registry_entry.ui_decimals
152
+ # # => 2
153
+ def registry_entry
154
+ @entry
155
+ end
156
+
157
+ # @return [Integer]
158
+ # @example Reading the registered storage precision
159
+ # Amount.usdc("1").decimals
160
+ # # => 6
161
+ def decimals
162
+ @entry.decimals
163
+ end
164
+
165
+ # @return [BigDecimal]
166
+ # @example Converting the atomic value back to a decimal quantity
167
+ # Amount.usdc(1_500_000, from: :atomic).decimal.to_s("F")
168
+ # # => "1.5"
169
+ def decimal
170
+ BigDecimal(@atomic) / (BigDecimal(10)**decimals)
171
+ end
172
+
173
+ # @return [Amount::Display]
174
+ # @example Delegating formatting concerns
175
+ # Amount.usdc("1.50").display.ui
176
+ # # => "$1.50"
177
+ def display
178
+ @display ||= Display.new(self)
179
+ end
180
+
181
+ def_delegators :display, :formatted, :ui, :to_s, :in_unit
182
+
183
+ # @return [String]
184
+ # @example Console-friendly inspection
185
+ # Amount.usdc("1.50").inspect
186
+ # # => "#<Amount USDC $1.50>"
187
+ def inspect
188
+ "#<#{self.class} #{symbol} #{ui}>"
189
+ end
190
+
191
+ # @return [Boolean]
192
+ # @example
193
+ # Amount.usdc(0, from: :atomic).zero?
194
+ # # => true
195
+ def zero? = @atomic.zero?
196
+
197
+ # @return [Boolean]
198
+ # @example
199
+ # Amount.usdc("1").positive?
200
+ # # => true
201
+ def positive? = @atomic.positive?
202
+
203
+ # @return [Boolean]
204
+ # @example
205
+ # Amount.usdc("-1").negative?
206
+ # # => true
207
+ def negative? = @atomic.negative?
208
+
209
+ # @param other [Object]
210
+ # @return [Boolean]
211
+ # @example
212
+ # Amount.usdc("1").same_type?(Amount.usdc("2"))
213
+ # # => true
214
+ def same_type?(other)
215
+ other.is_a?(Amount) && other.symbol == symbol
216
+ end
217
+
218
+ # @return [Amount]
219
+ # @example
220
+ # Amount.usdc("-1").abs
221
+ # # => #<Amount USDC $1.00>
222
+ def abs
223
+ build(@atomic.abs)
224
+ end
225
+
226
+ # @return [Amount]
227
+ # @example
228
+ # -Amount.usdc("1")
229
+ # # => #<Amount USDC -$1.00>
230
+ def -@
231
+ build(-@atomic)
232
+ end
233
+
234
+ # @param other [Amount]
235
+ # @return [Amount]
236
+ # @raise [TypeMismatch]
237
+ # @example Same-type addition
238
+ # Amount.usdc("1.50") + Amount.usdc("0.50")
239
+ #
240
+ # @example Cross-type addition using a registered directional rate
241
+ # Amount.register_default_rate :USD, :USDC, "1"
242
+ # Amount.usdc("10.00") + Amount.new("5.00", :USD)
243
+ def +(other)
244
+ rhs = coerce_other_to_self_type!(other)
245
+ build(@atomic + rhs.atomic)
246
+ end
247
+
248
+ # @param other [Amount]
249
+ # @return [Amount]
250
+ # @raise [TypeMismatch]
251
+ # @example
252
+ # Amount.usdc("2.00") - Amount.usdc("0.50")
253
+ def -(other)
254
+ rhs = coerce_other_to_self_type!(other)
255
+ build(@atomic - rhs.atomic)
256
+ end
257
+
258
+ # @param scalar [Numeric]
259
+ # @return [Amount]
260
+ # @raise [TypeMismatch]
261
+ # @example
262
+ # Amount.usdc("1.25") * 2
263
+ def *(scalar)
264
+ ensure_scalar!(scalar)
265
+ build((BigDecimal(@atomic) * BigDecimal(scalar.to_s)).to_i)
266
+ end
267
+
268
+ # @param other [Amount, Numeric]
269
+ # @return [Amount, BigDecimal]
270
+ # @raise [TypeMismatch, ZeroDivisionError]
271
+ # @example Dividing by a scalar returns an amount
272
+ # Amount.usdc("1.00") / 2
273
+ #
274
+ # @example Dividing by an amount returns a ratio
275
+ # Amount.usdc("10.00") / Amount.usdc("2.00")
276
+ def /(other)
277
+ if other.is_a?(Amount)
278
+ ensure_same_type!(other)
279
+ raise ZeroDivisionError if other.zero?
280
+
281
+ BigDecimal(@atomic) / BigDecimal(other.atomic)
282
+ else
283
+ ensure_scalar!(other)
284
+ raise ZeroDivisionError if other.zero?
285
+
286
+ build((BigDecimal(@atomic) / BigDecimal(other.to_s)).to_i)
287
+ end
288
+ end
289
+
290
+ # Splits into equal parts and returns the leftover explicitly.
291
+ #
292
+ # @param n [Integer]
293
+ # @return [Array<(Array<Amount>, Amount)>]
294
+ # @raise [ArgumentError] if `n` is not a positive integer
295
+ # @example
296
+ # parts, remainder = Amount.new(10, :LOGS).split(3)
297
+ # parts.map(&:atomic)
298
+ # # => [3, 3, 3]
299
+ # remainder.atomic
300
+ # # => 1
301
+ def split(n)
302
+ raise ArgumentError, "n must be positive" unless n.is_a?(Integer) && n.positive?
303
+
304
+ sign = atomic_sign
305
+ base, remainder = @atomic.abs.divmod(n)
306
+ parts = Array.new(n) { build(sign * base) }
307
+
308
+ [parts, build(sign * remainder)]
309
+ end
310
+
311
+ # Allocates proportionally by integer weights and returns the leftover explicitly.
312
+ #
313
+ # @param weights [Array<Integer>]
314
+ # @return [Array<(Array<Amount>, Amount)>]
315
+ # @raise [ArgumentError] if weights are empty, negative, or sum to zero
316
+ # @example
317
+ # parts, remainder = Amount.new(10, :LOGS).allocate([1, 1, 2])
318
+ # parts.map(&:atomic)
319
+ # # => [2, 2, 5]
320
+ # remainder.atomic
321
+ # # => 1
322
+ def allocate(weights)
323
+ raise ArgumentError, "weights must be non-empty" if weights.empty?
324
+ raise ArgumentError, "weights must be non-negative integers" unless weights.all? { |weight| weight.is_a?(Integer) && weight >= 0 }
325
+
326
+ total = weights.sum
327
+ raise ArgumentError, "weights must sum to positive value" unless total.positive?
328
+
329
+ sign = atomic_sign
330
+ absolute_atomic = @atomic.abs
331
+ allocations = weights.map { |weight| absolute_atomic * weight / total }
332
+ remainder = absolute_atomic - allocations.sum
333
+
334
+ parts = allocations.map { |allocation| build(sign * allocation) }
335
+ [parts, build(sign * remainder)]
336
+ end
337
+
338
+ # @param other [Object]
339
+ # @return [-1, 0, 1, nil]
340
+ # @example
341
+ # Amount.usdc("1") <=> Amount.usdc("2")
342
+ # # => -1
343
+ def <=>(other)
344
+ return nil unless other.is_a?(Amount)
345
+
346
+ comparable = coerce_other_to_self_type(other)
347
+ return nil unless comparable
348
+
349
+ @atomic <=> comparable.atomic
350
+ end
351
+
352
+ # @param other [Object]
353
+ # @return [Boolean]
354
+ # @example
355
+ # Amount.usdc("1.50") == Amount.usdc("1.50")
356
+ # # => true
357
+ def ==(other)
358
+ same_type?(other) && @atomic == other.atomic
359
+ end
360
+
361
+ # @param other [Object]
362
+ # @return [Boolean]
363
+ # @example Hash-key equality keeps class and symbol identity
364
+ # Amount.usdc("1").eql?(Amount.usdc("1"))
365
+ # # => true
366
+ def eql?(other)
367
+ other.class == self.class && symbol == other.symbol && @atomic == other.atomic
368
+ end
369
+
370
+ # @return [Integer]
371
+ # @example
372
+ # { Amount.usdc("1") => :ok }[Amount.usdc("1")]
373
+ # # => :ok
374
+ def hash
375
+ [self.class, symbol, @atomic].hash
376
+ end
377
+
378
+ # @param target_symbol [Symbol, String]
379
+ # @param rate [String, Numeric, BigDecimal, nil]
380
+ # @return [Amount]
381
+ # @raise [Amount::Registry::NoDefaultRate] if no explicit or registered rate is available
382
+ # @example Using an explicit one-off rate
383
+ # Amount.usdc("100").to(:GOLD, rate: "0.00042")
384
+ #
385
+ # @example Using a registered default rate
386
+ # Amount.register_default_rate :USDC, :USD, "1"
387
+ # Amount.usdc("1.50").to(:USD)
388
+ def to(target_symbol, rate: nil)
389
+ target_symbol = target_symbol.to_sym
390
+ return self.class.new(@atomic, symbol, from: :atomic) if target_symbol == symbol
391
+
392
+ rate = resolve_rate(target_symbol, rate)
393
+ target_entry = self.class.registry.lookup(target_symbol)
394
+
395
+ decimal_result = decimal * BigDecimal(rate.to_s)
396
+ atomic_result = (decimal_result * (BigDecimal(10)**target_entry.decimals)).to_i
397
+
398
+ target_entry.amount_class.new(atomic_result, target_symbol, from: :atomic)
399
+ end
400
+
401
+ # @return [Hash]
402
+ # @example
403
+ # Amount.usdc("1.50").to_h
404
+ # # => { v: 1, atomic: "1500000", symbol: "USDC" }
405
+ def to_h
406
+ Serializer.dump(self)
407
+ end
408
+
409
+ # @param hash [Hash]
410
+ # @return [Amount]
411
+ # @raise [InvalidInput] for unsupported serialized versions
412
+ # @example Loading the current versioned payload
413
+ # Amount.load(v: 1, atomic: "1500000", symbol: "USDC")
414
+ #
415
+ # @example Loading the legacy unversioned payload
416
+ # Amount.load(atomic: 1500000, symbol: :USDC)
417
+ def self.load(hash)
418
+ Serializer.load(hash)
419
+ end
420
+
421
+ private
422
+
423
+ def build(atomic_value)
424
+ self.class.new(atomic_value, symbol, from: :atomic)
425
+ end
426
+
427
+ def atomic_sign
428
+ return 1 if @atomic.positive?
429
+ return(-1) if @atomic.negative?
430
+
431
+ 0
432
+ end
433
+
434
+ def ensure_same_type!(other)
435
+ return if same_type?(other)
436
+
437
+ raise TypeMismatch, "type mismatch: #{symbol} vs #{other.is_a?(Amount) ? other.symbol : other.class}"
438
+ end
439
+
440
+ def ensure_scalar!(value)
441
+ return if value.is_a?(Integer) || value.is_a?(Float) ||
442
+ value.is_a?(BigDecimal) || value.is_a?(Rational)
443
+
444
+ raise TypeMismatch, "expected scalar, got #{value.class}"
445
+ end
446
+
447
+ def resolve_rate(target, provided)
448
+ return provided if provided
449
+
450
+ self.class.registry.default_rate(symbol, target)
451
+ end
452
+
453
+ def infer_value(from, value)
454
+ case from || infer_type(value)
455
+ when :atomic then value.to_i
456
+ when :ui then ui_to_atomic(value)
457
+ when :float then ui_to_atomic(value.to_s)
458
+ else
459
+ raise InvalidInput, "unknown amount format: #{value.inspect}"
460
+ end
461
+ end
462
+
463
+ def infer_type(value)
464
+ case value
465
+ when Integer then :atomic
466
+ when String then :ui
467
+ when Float, BigDecimal, Rational then :float
468
+ else
469
+ raise InvalidInput, "cannot infer type for #{value.class}"
470
+ end
471
+ end
472
+
473
+ def ui_to_atomic(value)
474
+ (BigDecimal(value.to_s) * (BigDecimal(10)**decimals)).to_i
475
+ rescue ArgumentError
476
+ raise InvalidInput, "cannot parse #{value.inspect} as #{symbol}"
477
+ end
478
+
479
+ def coerce_other_to_self_type!(other)
480
+ coerce_other_to_self_type(other) || raise(
481
+ TypeMismatch,
482
+ "type mismatch: #{symbol} vs #{other.is_a?(Amount) ? other.symbol : other.class}"
483
+ )
484
+ end
485
+
486
+ def coerce_other_to_self_type(other)
487
+ return other if same_type?(other)
488
+ return unless other.is_a?(Amount)
489
+
490
+ other.to(symbol)
491
+ rescue Registry::NoDefaultRate
492
+ nil
493
+ end
494
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Holding < ActiveRecord::Base
4
+ has_amount :amount
5
+ has_amount :fee, symbol: :SOL
6
+ has_amount :reserve
7
+ end
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../../Gemfile", __dir__)
5
+ ENV["RAILS_ENV"] ||= "test"
6
+
7
+ require "bundler/setup"
8
+ require "rails/commands"
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+ require "active_model/railtie"
5
+ require "active_record/railtie"
6
+
7
+ module Dummy
8
+ class Application < Rails::Application
9
+ config.root = File.expand_path("..", __dir__)
10
+ config.eager_load = false
11
+ config.secret_key_base = "amounts-test-secret-key-base"
12
+ config.hosts.clear
13
+ config.active_support.deprecation = :stderr
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ default: &default
2
+ adapter: postgresql
3
+ encoding: unicode
4
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>
5
+ url: <%= ENV.fetch("AMOUNTS_POSTGRES_URL", "postgresql://localhost/amounts_test") %>
6
+
7
+ development:
8
+ <<: *default
9
+
10
+ test:
11
+ <<: *default
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__)
4
+
5
+ require_relative "application"
6
+ Dummy::Application.initialize!
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActiveRecord::Schema.define(version: 1) do
4
+ create_table :holdings, force: true do |t|
5
+ t.amount :amount, null: true
6
+ t.amount :fee, symbol: :SOL, null: true
7
+ t.amount :reserve, precision: 40, default: "USDC|1.25", null: false
8
+ end
9
+ end
File without changes
File without changes
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ begin
6
+ require "pg"
7
+ rescue LoadError
8
+ # Handled in setup.
9
+ end
10
+
11
+ ENV["RAILS_ENV"] = "test"
12
+
13
+ require_relative "dummy/config/environment"
14
+ require "active_record/tasks/database_tasks"
15
+
16
+ class PostgreSQLIntegrationTest < Minitest::Test
17
+ def setup
18
+ skip "pg gem is not installed" unless defined?(PG)
19
+ skip "set AMOUNTS_POSTGRES_URL to run PostgreSQL integration tests" unless ENV["AMOUNTS_POSTGRES_URL"]
20
+
21
+ AmountTestSupport.register_active_record_types!
22
+ configure_database_tasks
23
+ recreate_schema!
24
+ Holding.delete_all
25
+ rescue PG::Error, ActiveRecord::ConnectionNotEstablished => error
26
+ skip "PostgreSQL is unavailable: #{error.message}"
27
+ end
28
+
29
+ def teardown
30
+ Amount.registry.clear!
31
+ end
32
+
33
+ def test_postgresql_round_trips_large_atomic_values_exactly
34
+ huge_atomic = 10**30
35
+ holding = Holding.create!(amount: Amount.new(huge_atomic, :SOL, from: :atomic))
36
+
37
+ assert_equal huge_atomic, holding.reload.amount.atomic
38
+ end
39
+
40
+ def test_postgresql_uses_numeric_precision_for_amount_columns
41
+ columns = Holding.columns_hash
42
+
43
+ assert_equal 78, columns.fetch("amount_atomic").precision
44
+ assert_equal 0, columns.fetch("amount_atomic").scale
45
+ assert_equal 40, columns.fetch("reserve_atomic").precision
46
+ assert_equal 0, columns.fetch("reserve_atomic").scale
47
+ end
48
+
49
+ def test_postgresql_where_and_group_queries_work_with_amount_columns
50
+ usdc = Holding.create!(amount: "USDC|1.50")
51
+ Holding.create!(amount: "USD|3.00")
52
+
53
+ assert_equal [usdc.id], Holding.where_amount("USDC|1.50").pluck(:id)
54
+ assert_equal({ "USDC" => 1_500_000, "USD" => 300 }, Holding.group(:amount_symbol).sum(:amount_atomic))
55
+ end
56
+
57
+ private
58
+
59
+ def configure_database_tasks
60
+ ActiveRecord::Tasks::DatabaseTasks.database_configuration = Rails.application.config.database_configuration
61
+ ActiveRecord::Tasks::DatabaseTasks.db_dir = File.expand_path("dummy/db", __dir__)
62
+ ActiveRecord::Tasks::DatabaseTasks.env = "test"
63
+ ActiveRecord::Tasks::DatabaseTasks.root = File.expand_path("dummy", __dir__)
64
+ end
65
+
66
+ def recreate_schema!
67
+ ActiveRecord::Base.establish_connection(:test)
68
+ load File.expand_path("dummy/db/schema.rb", __dir__)
69
+ Holding.reset_column_information
70
+ end
71
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+ require_relative "../../lib/amount"
5
+
6
+ module AmountTestSupport
7
+ module_function
8
+
9
+ def register_default_types!
10
+ Amount.registry.clear!
11
+ Amount.register :USDC, decimals: 6, display_symbol: "$",
12
+ display_position: :prefix, ui_decimals: 2
13
+ Amount.register :USD, decimals: 2, display_symbol: "$",
14
+ display_position: :prefix, ui_decimals: 2
15
+ Amount.register :SOL, decimals: 9, display_symbol: "SOL",
16
+ display_position: :suffix, ui_decimals: 4
17
+ Amount.register :GOLD, decimals: 8, display_symbol: "oz t",
18
+ display_position: :suffix, ui_decimals: 4,
19
+ display_units: {
20
+ oz_t: { scale: 1, symbol: "oz t", ui_decimals: 4 },
21
+ gram: { scale: "31.1035", symbol: "g", ui_decimals: 2 },
22
+ kg: { scale: "0.0311035", symbol: "kg", ui_decimals: 5 }
23
+ },
24
+ default_display: :oz_t
25
+ Amount.register :LOGS, decimals: 0, display_symbol: "logs",
26
+ display_position: :suffix, ui_decimals: 0
27
+ end
28
+
29
+ def register_active_record_types!
30
+ Amount.registry.clear!
31
+ Amount.register :USDC, decimals: 6, display_symbol: "$",
32
+ display_position: :prefix, ui_decimals: 2
33
+ Amount.register :USD, decimals: 2, display_symbol: "$",
34
+ display_position: :prefix, ui_decimals: 2
35
+ Amount.register :SOL, decimals: 9, display_symbol: "SOL",
36
+ display_position: :suffix, ui_decimals: 4
37
+ end
38
+ end