mamrhein/money.rs

Create custom currencies (support for crypto like $BTC)

Opened this issue · 11 comments

You library + quantities is exactly what I was looking for long time ago, even the support for 18 decimals amount without rounding using integer for math operations, the only thing is I need to work with custom currencies, something like:

    let btc_currency = Currency("BTC", 7);
    let eth_currency = Currency("ETH", 18);

Is this an option on your library?

Can you define your custom currencies statically or do you need to create them at runtime?

One option I can do is to have a repository with all currencies known today (or whitelisted ones) and then update the list with PR
Another option is to be able to create currencies on runtime (new crypto currencies are created daily).

I'm trying to implement the runtime option based on this library but it's getting messy as you force to implement the Copy type class (why is that by the way?) and this force me to add lifespans everywhere.

Looking forward to reading your thoughts about both options.

I have been looking for solutions for the dynamic extension of enums, but these seem too complex to me. ISO 4217 currencies are limited in number, rarely change and there is a universally recognized authority that maintains them. I would therefore like to stick with a static solution for these.
Cryptocurrencies are - in comparison - very numerous (over 25,000 according to Wikipedia) and there is no controlling authority. That's why a static solution seems inappropriate to me.
I will try to keep the static definition of ISO currencies and combine it with the possibility to create additional currencies at runtime via Currency::new (to be activated via a feature flag).

I agree with you and that was the point I was trying to made, in crypto you can create a new currency in seconds and there are actually way more than 25k so the only way to manage is at runtime.

Still all the rules for a currency should apply, for example to change one amount in currency X to an amount on currency Y you need a exchange rate X/Y, I just need this algebra without the static currency types.

Thanks a lot for looking into this I really appreciate the work you put on this crate.

After some experiments I find that providing the possibility to create additional currencies at runtime would need several breaking changes to the underlying quantities crate.
What I can imagine as possible with a manageable amount of changes (and effort) at the moment is to define Currency as wrapper around two enums, namely ISO_Currency and a generic one which can then be provided by the down-stream code.
Something like:

trait MoneyUnit { ... }
enum ISO_Currency { ... }
enum Currency<T: MoneyUnit> {
  ISO(ISO_Currency),
  Custom(T),
}
struct Money<T> {
  amount: AmountT,
  unit: Currency<T>,
}

And then down-stream:

enum CustomCurrency { ... }
impl MoneyUnit for CustomCurrency { ... } // maybe simplified via macro
type Currency = moneta::Currency<CustomCurrency>
type Money = moneta::Money<CustomCurrency>

This would still force you to define the crypto currencies you need statically, but without any need to have a new version of the crate moneta.

I'd appreciate your comments.
☺︎

I think that would work but I don't want to overcomplicate your library just for one use case.
Is there an option to give just an example of how can I build one currency on runtime and still leverage your library?

Something like this:

struct CryptoCurrency { symbol: String }

impl CryptoCurrency {
  fn new ...
}

let btc = CryptoCurrency::new("BTC");
let eth = CryptoCurrency::new("ETH");

let btc_eth_ratio = 
...

I think supporting crypto currencies is definitely a relevant use case.
But that means supporting Money and ExchangeRate, which in turn requires Currency to implement Unit.
Unit needs to be able to iterate over all its variants. This currently works because all variants are statically defined. That's why my first proposal is to define CustomCurrency as Enum.
Making the creation of Currency instances fully dynamic would require to maintain a dynamic list of the defined quantities and their properties. I tried to replace the static array of Currency variants by a dynamic structure like Vec or HashMap, guarded by a RwLock. But then it's no longer possible to get static references to Curreny instances or their symbol / name. Thus, some changes to the quantity API would be needed.
I will continue to explore both approaches in order to be able to weigh up the pros and cons.

I think the best is to separate in two different domains (Currency and CryptoCurrency) as a first approach.

If at some point we can find the convergence without affecting current API then merge with the Enum approach.

I say that because in crypto things are really more complex, for example the symbol can change with time, same symbol can be used by two different projects, the same currency exists in different networks (for example ETH has testnet networks where the value is virtual, and then what is called mainnet network with the "real value"). You have native currencies like ETH and tokens like ERC20 (where you should have the network + smart contract address to identify the token), it's really a domain in itself.

Finally, I managed to redesign the impl of Currency to enable the creation of new currencies.
Please, have a look at the branch custom-currencies-support.
The documentation is not yet updated, but there's a test module test_custom_currency showing the new API.
Important to note:
All currencies are registered in a global static database. ISO currencies are still available as constants and are registered in the database "lazily". Custom currencies must be created by calling Currency::new. This can be done only once w/o error, so it should be done in the main thread. Later (maybe in another thread) an instance can be obtained by calling Currency::from_symbol.
Thanks in advance for your feedback!

Thanks for the effort, it looks really good I'll test in my project and let you know.

Only one question, I understand I can't have 2 currencies with the same symbol is that right?

Yes, the symbol is mapped to an u64, which is used as unique identifying key. The mapping is done by stripping all whitespace and eliminaing all non-ascii characters.