Skip to content

Commit

Permalink
feat: Add CryptoAmount and FiatAmount types
Browse files Browse the repository at this point in the history
This adds a CryptoAmount types that provides a wrapper for the
underlying CryptoAmount object and can provide helpers for converting
from atomic to whole amounts.

This provides a FiatAmount type as well to wrap our fiat amount +
currency pairings in a consistent manner.
  • Loading branch information
alex-stone committed Nov 22, 2024
1 parent 1e4a621 commit 6dc6256
Show file tree
Hide file tree
Showing 4 changed files with 264 additions and 0 deletions.
2 changes: 2 additions & 0 deletions lib/coinbase.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
require_relative 'coinbase/constants'
require_relative 'coinbase/contract_event'
require_relative 'coinbase/contract_invocation'
require_relative 'coinbase/crypto_amount'
require_relative 'coinbase/destination'
require_relative 'coinbase/errors'
require_relative 'coinbase/faucet_transaction'
require_relative 'coinbase/fiat_amount'
require_relative 'coinbase/middleware'
require_relative 'coinbase/network'
require_relative 'coinbase/pagination'
Expand Down
62 changes: 62 additions & 0 deletions lib/coinbase/crypto_amount.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# frozen_string_literal: true

module Coinbase
# A representation of a CryptoAmount that includes the amount and asset.
class CryptoAmount
# Converts a Coinbase::Client::CryptoAmount model to a Coinbase::CryptoAmount
# @param amount_model [Coinbase::Client::CryptoAmount] The crypto amount from the API.
# @return [CryptoAmount] The converted CryptoAmount object.
def self.from_model(amount_model)
asset = Coinbase::Asset.from_model(amount_model.asset)

new(amount: asset.from_atomic_amount(amount_model.amount), asset: asset)
end

# Converts a Coinbase::Client::CryptoAmount model and asset ID to a Coinbase::CryptoAmount
# This can be used to specify a non-primary denomination that we want the amount
# to be converted to.
# @param amount_model [Coinbase::Client::CryptoAmount] The crypto amount from the API.
# @param asset_id [Symbol] The Asset ID of the denomination we want returned.
# @return [CryptoAmount] The converted CryptoAmount object.
def self.from_model_and_asset_id(amount_model, asset_id)
asset = Coinbase::Asset.from_model(amount_model.asset, asset_id: asset_id)

new(
amount: asset.from_atomic_amount(amount_model.amount),
asset: asset,
asset_id: asset_id
)
end

# Returns a new CryptoAmount object. Do not use this method.
# Instead, use CryptoAmount.from_model or CryptoAmount.from_model_and_asset_id.
# @param amount [BigDecimal] The amount of the Asset
# @param asset [Coinbase::Asset] The Asset
# @param asset_id [Symbol] The Asset ID
def initialize(amount:, asset:, asset_id: nil)
@amount = amount
@asset = asset
@asset_id = asset_id || asset.asset_id
end

attr_reader :amount, :asset, :asset_id

# Returns the amount in atomic units.
# @return [BigDecimal] the amount in atomic units
def to_atomic_amount
asset.to_atomic_amount(amount)
end

# Returns a string representation of the CryptoAmount.
# @return [String] a string representation of the CryptoAmount
def to_s
Coinbase.pretty_print_object(self.class, amount: amount.to_i, asset_id: asset_id)
end

# Same as to_s.
# @return [String] a string representation of the CryptoAmount
def inspect
to_s
end
end
end
37 changes: 37 additions & 0 deletions lib/coinbase/fiat_amount.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

module Coinbase
# A representation of a FiatAmount that includes the amount and fiat.
class FiatAmount
# Converts a Coinbase::Client::FiatAmount model to a Coinbase::FiatAmount
# @param amount_model [Coinbase::Client::FiatAmount] The crypto amount from the API.
# @return [FiatAmount] The converted FiatAmount object.
def self.from_model(amount_model)
new(amount: amount_model.amount, currency: currency)
end

# Returns a new CryptoAmount object. Do not use this method.
# Instead, use CryptoAmount.from_model or CryptoAmount.from_model_and_asset_id.
# @param amount [BigDecimal] The amount of the Asset
# @param asset [Coinbase::Asset] The Asset
# @param asset_id [Symbol] The Asset ID
def initialize(amount:, currency:)
@amount = BigDecimal(amount)
@currency = currency
end

attr_reader :amount, :currency

# Returns a string representation of the CryptoAmount.
# @return [String] a string representation of the CryptoAmount
def to_s
Coinbase.pretty_print_object(self.class, amount: amount.to_i, currency: currency)
end

# Same as to_s.
# @return [String] a string representation of the CryptoAmount
def inspect
to_s
end
end
end
163 changes: 163 additions & 0 deletions spec/unit/coinbase/crypto_amount_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# frozen_string_literal: true

describe Coinbase::CryptoAmount do
let(:amount) { BigDecimal('123.0') }
let(:crypto_amount_model) { instance_double(Coinbase::Client::CryptoAmount, asset: asset, amount: amount) }
let(:eth_asset) { build(:asset_model) }

describe '.from_model' do
subject(:crypto_amount) { described_class.from_model(crypto_amount_model) }

context 'when the asset is :eth' do
let(:asset) { eth_asset }

it 'returns a CryptoAmount object' do
expect(crypto_amount).to be_a(described_class)
end

it 'sets the correct amount' do
expect(crypto_amount.amount).to eq(amount / BigDecimal(10).power(eth_asset.decimals))
end

it 'sets the correct asset_id' do
expect(crypto_amount.asset_id).to eq(:eth)
end
end

context 'when the asset is other' do
let(:decimals) { 9 }
let(:asset) { build(:asset_model, asset_id: 'other', decimals: decimals) }

it 'returns a CryptoAmount object' do
expect(crypto_amount).to be_a(described_class)
end

it 'sets the correct amount' do
expect(crypto_amount.amount).to eq(amount / BigDecimal(10).power(decimals))
end

it 'sets the correct asset_id' do
expect(crypto_amount.asset_id).to eq(:other)
end
end
end

describe '.from_model_and_asset_id' do
subject(:crypto_amount) { described_class.from_model_and_asset_id(crypto_amount_model, asset_id) }

context 'when the crypto_amount model asset is :eth' do
let(:asset) { eth_asset }

context 'when the specified asset_id is :eth' do
let(:asset_id) { :eth }

it 'returns a new CryptoAmount object with the correct amount' do
expect(crypto_amount.amount).to eq(amount / BigDecimal(10).power(eth_asset.decimals))
end

it 'returns a new CryptoAmount object with the correct asset_id' do
expect(crypto_amount.asset_id).to eq(asset_id)
end
end

context 'when the specified asset_id is :gwei' do
let(:asset_id) { :gwei }

it 'returns a new CryptoAmount object with the correct amount' do
expect(crypto_amount.amount).to eq(amount / BigDecimal(10).power(Coinbase::GWEI_DECIMALS))
end

it 'returns a new CryptoAmount object with the correct asset_id' do
expect(crypto_amount.asset_id).to eq(asset_id)
end
end

context 'when the specified asset_id is :wei' do
let(:asset_id) { :wei }

it 'returns a new CryptoAmount object with the correct amount' do
expect(crypto_amount.amount).to eq(amount)
end

it 'returns a new CryptoAmount object with the correct asset_id' do
expect(crypto_amount.asset_id).to eq(asset_id)
end
end

context 'when the specified asset_id is another asset type' do
let(:asset_id) { :other }

it 'raise an error' do
expect { crypto_amount }.to raise_error(ArgumentError)
end
end
end

context 'when the asset is not eth' do
let(:decimals) { 9 }
let(:asset_id) { :other }
let(:asset) { build(:asset_model, asset_id: 'other', decimals: decimals) }

it 'returns a new CryptoAmount object with the correct amount' do
expect(crypto_amount.amount).to eq(amount / BigDecimal(10).power(decimals))
end

it 'returns a new CryptoAmount object with the correct asset_id' do
expect(crypto_amount.asset_id).to eq(asset_id)
end

context 'when the asset ID does not match the asset' do
let(:asset_id) { :different }

it 'raises an error' do
expect { crypto_amount }.to raise_error(ArgumentError)
end
end
end
end

describe '#initialize' do
subject(:crypto_amount) { described_class.new(amount: amount, asset: asset) }

let(:amount) { BigDecimal('123.0') }
let(:asset) { Coinbase::Asset.from_model(eth_asset) }

it 'sets the amount' do
expect(crypto_amount.amount).to eq(amount)
end

it 'sets the asset' do
expect(crypto_amount.asset).to eq(asset)
end

it "sets the asset_id to the asset's ID" do
expect(crypto_amount.asset_id).to eq(:eth)
end
end

describe '#to_atomic_amount' do
subject(:crypto_amount) { described_class.new(amount: amount, asset: asset) }

let(:amount) { BigDecimal('123.0') }
let(:asset) { Coinbase::Asset.from_model(build(:asset_model, decimals: 3)) }

it 'returns the amount in atomic units' do
expect(crypto_amount.to_atomic_amount).to eq(123_000)
end
end

describe '#inspect' do
subject(:crypto_amount) { described_class.new(amount: amount, asset: asset) }

let(:amount) { BigDecimal('123.0') }
let(:asset) { Coinbase::Asset.from_model(eth_asset) }

it 'includes crypto_amount details' do
expect(crypto_amount.inspect).to include('123', 'eth')
end

it 'returns the same value as to_s' do
expect(crypto_amount.inspect).to eq(crypto_amount.to_s)
end
end
end

0 comments on commit 6dc6256

Please sign in to comment.