8000 feat: Add crypto currency support with global toggle by davidpatters0n · Pull Request #401 · Shopify/money · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat: Add crypto currency support with global toggle #401

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
shopify-money (3.0.3)
shopify-money (3.1.0)

GEM
remote: https://rubygems.org/
Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,14 @@ Money.new(money.value * exchange_rate, "JPY")
money.convert_currency(exchange_rate, "JPY")
```

### Crypto Currencies
To enable support for currencies listed in `crypto.yml` use
``` ruby
Money.configure do |config|
config.experimental_crypto_currencies = true
end
```

## Money column

Since money internally uses BigDecimal it's logical to use a `decimal` column
Expand Down Expand Up @@ -237,4 +245,3 @@ To release a new version of the gem, follow these steps:

Copyright (c) 2011 Shopify. See LICENSE.txt for
further details.

15 changes: 15 additions & 0 deletions config/crypto.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
usdc:
priority: 100
iso_code: "USDC"
name: "USD Coin"
symbol: "USDC"
disambiguate_symbol: USDC
alternate_symbols: []
subunit: Cent
subunit_to_unit: 100
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why two decimal places see; #398 (comment) for more context. In addition if there is a need to override we can do as mentioned on the same comment.

symbol_first: false
html_entity: "$"
decimal_mark: "."
thousands_separator: ","
iso_numeric: ''
smallest_denomination: 1
3 changes: 2 additions & 1 deletion lib/money/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

class Money
class Config
attr_accessor :default_currency, :legacy_json_format, :legacy_deprecations
attr_accessor :default_currency, :legacy_json_format, :legacy_deprecations, :experimental_crypto_currencies

def legacy_default_currency!
@default_currency ||= Money::NULL_CURRENCY
Expand All @@ -20,6 +20,7 @@ def initialize
@default_currency = nil
@legacy_json_format = false
@legacy_deprecations = false
@experimental_crypto_currencies = false
end

def without_legacy_deprecations(&block)
Expand Down
5 changes: 5 additions & 0 deletions lib/money/currency.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ def find(currency_iso)
def currencies
@@currencies ||= Loader.load_currencies
end

def crypto_currencies
@@crypto_currencies ||= Loader.load_crypto_currencies
end
end

attr_reader :iso_code,
Expand All @@ -41,6 +45,7 @@ def currencies

def initialize(currency_iso)
data = self.class.currencies[currency_iso]
data = self.class.crypto_currencies[currency_iso] if data.nil? && Money.config.experimental_crypto_currencies
raise UnknownCurrency, "Invalid iso4217 currency '#{currency_iso}'" unless data
@symbol = data['symbol']
@disambiguate_symbol = data['disambiguate_symbol'] || data['symbol']
Expand Down
14 changes: 10 additions & 4 deletions lib/money/currency/loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,20 @@
class Money
class Currency
module Loader
CURRENCY_DATA_PATH = File.expand_path("../../../config", __dir__)

class << self
def load_currencies
currency_data_path = File.expand_path("../../../../config", __FILE__)
currencies = {}
currencies.merge!(YAML.load_file("#{CURRENCY_DATA_PATH}/currency_historic.yml"))
currencies.merge!(YAML.load_file("#{CURRENCY_DATA_PATH}/currency_non_iso.yml"))
currencies.merge!(YAML.load_file("#{CURRENCY_DATA_PATH}/currency_iso.yml"))
deep_deduplicate!(currencies)
end

def load_crypto_currencies
currencies = {}
currencies.merge!(YAML.load_file("#{currency_data_path}/currency_historic.yml"))
currencies.merge!(YAML.load_file("#{currency_data_path}/currency_non_iso.yml"))
currencies.merge!(YAML.load_file("#{currency_data_path}/currency_iso.yml"))
currencies.merge!(YAML.load_file("#{CURRENCY_DATA_PATH}/crypto.yml"))
deep_deduplicate!(currencies)
end

Expand Down
2 changes: 1 addition & 1 deletion lib/money/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

class Money
VERSION = "3.0.3"
VERSION = "3.1.0"
end
12 changes: 12 additions & 0 deletions spec/config_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,16 @@
end
end
end

describe 'experimental_crypto_currencies' do
it 'defaults to false' do
expect(Money::Config.new.experimental_crypto_currencies).to eq(false)
end

it 'can be set to true' do
config = Money::Config.new
config.experimental_crypto_currencies = true
expect(config.experimental_crypto_currencies).to eq(true)
end
end
end
18 changes: 18 additions & 0 deletions spec/currency/loader_spec.rb
F438
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,22 @@
expect(subject.load_currencies['eek']['iso_code']).to eq('EEK')
end
end

describe 'load_crypto_currencies' do
it 'loads the crypto currency file' do
expect(subject.load_crypto_currencies['usdc']['iso_code']).to eq('USDC')
expect(subject.load_crypto_currencies['usdc']['name']).to eq('USD Coin')
expect(subject.load_crypto_currencies['usdc']['symbol']).to eq('USDC')
expect(subject.load_crypto_currencies['usdc']['disambiguate_symbol']).to eq('USDC')
expect(subject.load_crypto_currencies['usdc']['subunit_to_unit']).to eq(100)
expect(subject.load_crypto_currencies['usdc']['smallest_denomination']).to eq(1)
end

it 'returns frozen and deduplicated data' do
currencies = subject.load_crypto_currencies
expect(currencies).to be_frozen
expect(currencies['usdc']).to be_frozen
expect(currencies['usdc']['iso_code']).to be_frozen
end
end
end
64 changes: 64 additions & 0 deletions spec/currency_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@

let(:currency) { Money::Currency.new('usd') }

let(:mock_crypto_currency) do
{
"usdc" => {
"iso_code" => "USDC",
"name" => "USD Coin",
"symbol" => "USDC",
"disambiguate_symbol" => "USDC",
"subunit_to_unit" => 100,
"smallest_denomination" => 1,
"decimal_mark" => "."
}
}
end

describe ".new" do
it "is constructable with a uppercase string" do
expect(Money::Currency.new('USD').iso_code).to eq('USD')
Expand All @@ -37,6 +51,24 @@
it "raises when the currency is nil" do
expect { Money::Currency.new(nil) }.to raise_error(Money::Currency::UnknownCurrency)
end

it "looks up crypto currencies when enabled" do

allow(Money::Currency).to receive(:currencies).and_return({})
allow(Money::Currency).to receive(:crypto_currencies).and_return(mock_crypto_currency)

configure(experimental_crypto_currencies: true) do
currency = Money::Currency.new('USDC')
expect(currency.iso_code).to eq('USDC')
expect(currency.symbol).to eq('USDC')
end
end

it "doesn't look up crypto currencies when disabled" do
configure(experimental_crypto_currencies: false) do
expect(Money::Currency.find("USDC")).to be_nil
end
end
end

describe ".find" do
Expand All @@ -47,6 +79,23 @@
it "returns a valid currency" do
expect(Money::Currency.find('usd')).to eq(Money::Currency.new('usd'))
end

it "returns a crypto currency when enabled" do
allow(Money::Currency).to receive(:currencies).and_return({})
allow(Money::Currency).to receive(:crypto_currencies).and_return(mock_crypto_currency)

configure(experimental_crypto_currencies: true) do
expect(Money::Currency.find('USDC')).not_to eq(nil)
expect(Money::Currency.find('USDC').symbol).to eq("USDC")
end
end

it "returns nil for crypto currency when disabled" do
configure(experimental_crypto_currencies: false) do
expect(Money.config.experimental_crypto_currencies).to eq(false)
expect(Money::Currency.find('USDC')).to eq(nil)
end
end
end

describe ".find!" do
Expand All @@ -59,6 +108,21 @@
end
end

describe ".crypto_currencies" do
it "loads crypto currencies from the loader" do
old_currencies = Money::Currency.class_variable_get(:@@crypto_currencies) rescue nil
Money::Currency.class_variable_set(:@@crypto_currencies, nil)

allow(Money::Currency::Loader).to receive(:load_crypto_currencies).and_return(mock_crypto_currency)

expect(Money::Currency.crypto_currencies).to eq(mock_crypto_currency)
expect(Money::Currency.crypto_currencies).to eq(mock_crypto_currency) # Second call to verify caching
expect(Money::Currency::Loader).to have_received(:load_crypto_currencies).once

Money::Currency.class_variable_set(:@@crypto_currencies, old_currencies) if old_currencies
end
end

CURRENCY_DATA.each do |attribute, value|
describe "##{attribute}" do
it 'returns the correct value' do
Expand Down
6 changes: 5 additions & 1 deletion spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,20 @@ def missing_methods
end


def configure(default_currency: nil, legacy_json_format: nil, legacy_deprecations: nil, legacy_default_currency: nil)
def configure(default_currency: nil, legacy_json_format: nil, legacy_deprecations: nil, legacy_default_currency: nil, experimental_crypto_currencies: nil)
old_currencies = Money::Currency.class_variable_get(:@@loaded_currencies) rescue {}
Money::Currency.class_variable_set(:@@loaded_currencies, {})
old_config = Money.config
Money.config = Money::Config.new.tap do |config|
config.default_currency = default_currency if default_currency
config.legacy_json_format! if legacy_json_format
config.legacy_deprecations! if legacy_deprecations
config.legacy_default_currency! if legacy_default_currency
config.experimental_crypto_currencies = experimental_crypto_currencies unless experimental_crypto_currencies.nil?
end
yield
ensure
Money::Currency.class_variable_set(:@@loaded_currencies, old_currencies)
Money.config = old_config
end

Expand Down
0