mirror of
https://github.com/we-promise/sure.git
synced 2026-05-12 15:15:01 +00:00
Add Binance support, heavily inspired by the Coinbase one (#1317)
* feat: add Binance support (Items, Accounts, Importers, Processor, and Sync) * refactor: deduplicate 'stablecoins' constant and push stale_rate filter to SQL --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
This commit is contained in:
72
test/models/binance_account/holdings_processor_test.rb
Normal file
72
test/models/binance_account/holdings_processor_test.rb
Normal file
@@ -0,0 +1,72 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class BinanceAccount::HoldingsProcessorTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@family = families(:dylan_family)
|
||||
@family.update!(currency: "EUR")
|
||||
|
||||
@item = BinanceItem.create!(
|
||||
family: @family, name: "Binance", api_key: "k", api_secret: "s"
|
||||
)
|
||||
@ba = @item.binance_accounts.create!(
|
||||
name: "Binance",
|
||||
account_type: "combined",
|
||||
currency: "USD",
|
||||
current_balance: 1000,
|
||||
raw_payload: {
|
||||
"assets" => [ { "symbol" => "BTC", "total" => "0.5", "source" => "spot" } ]
|
||||
}
|
||||
)
|
||||
@account = Account.create!(
|
||||
family: @family,
|
||||
name: "Binance",
|
||||
balance: 0,
|
||||
currency: "EUR",
|
||||
accountable: Crypto.create!(subtype: "exchange")
|
||||
)
|
||||
AccountProvider.create!(account: @account, provider: @ba)
|
||||
end
|
||||
|
||||
test "converts holding amount to family currency when exact rate exists" do
|
||||
ExchangeRate.create!(from_currency: "USD", to_currency: "EUR",
|
||||
date: Date.current, rate: 0.92)
|
||||
|
||||
Security.find_or_create_by!(ticker: "CRYPTO:BTC") do |s|
|
||||
s.name = "BTC"
|
||||
s.exchange_operating_mic = "XBNC"
|
||||
end
|
||||
|
||||
BinanceAccount::HoldingsProcessor.any_instance
|
||||
.stubs(:fetch_price).with("BTC").returns(60_000.0)
|
||||
|
||||
import_adapter = mock
|
||||
import_adapter.expects(:import_holding).with(
|
||||
has_entries(currency: "EUR", amount: 27_600.0)
|
||||
)
|
||||
Account::ProviderImportAdapter.stubs(:new).returns(import_adapter)
|
||||
|
||||
BinanceAccount::HoldingsProcessor.new(@ba).process
|
||||
end
|
||||
|
||||
test "uses raw USD amount when no rate is available" do
|
||||
ExchangeRate.stubs(:find_or_fetch_rate).returns(nil)
|
||||
|
||||
Security.find_or_create_by!(ticker: "CRYPTO:BTC") do |s|
|
||||
s.name = "BTC"
|
||||
s.exchange_operating_mic = "XBNC"
|
||||
end
|
||||
|
||||
BinanceAccount::HoldingsProcessor.any_instance
|
||||
.stubs(:fetch_price).with("BTC").returns(60_000.0)
|
||||
|
||||
import_adapter = mock
|
||||
import_adapter.expects(:import_holding).with(
|
||||
has_entries(currency: "EUR", amount: 30_000.0)
|
||||
)
|
||||
Account::ProviderImportAdapter.stubs(:new).returns(import_adapter)
|
||||
|
||||
BinanceAccount::HoldingsProcessor.new(@ba).process
|
||||
end
|
||||
end
|
||||
89
test/models/binance_account/processor_test.rb
Normal file
89
test/models/binance_account/processor_test.rb
Normal file
@@ -0,0 +1,89 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class BinanceAccount::ProcessorTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@family = families(:dylan_family)
|
||||
@family.update!(currency: "EUR")
|
||||
|
||||
@item = BinanceItem.create!(
|
||||
family: @family, name: "Binance", api_key: "k", api_secret: "s"
|
||||
)
|
||||
@ba = @item.binance_accounts.create!(
|
||||
name: "Binance", account_type: "combined", currency: "USD", current_balance: 1000
|
||||
)
|
||||
@account = Account.create!(
|
||||
family: @family,
|
||||
name: "Binance",
|
||||
balance: 0,
|
||||
currency: "EUR",
|
||||
accountable: Crypto.create!(subtype: "exchange")
|
||||
)
|
||||
AccountProvider.create!(account: @account, provider: @ba)
|
||||
|
||||
BinanceAccount::HoldingsProcessor.any_instance.stubs(:process).returns(nil)
|
||||
@ba.stubs(:binance_item).returns(
|
||||
stub(binance_provider: nil, family: @family)
|
||||
)
|
||||
end
|
||||
|
||||
test "converts USD balance to family currency when exact rate exists" do
|
||||
ExchangeRate.create!(from_currency: "USD", to_currency: "EUR",
|
||||
date: Date.current, rate: 0.92)
|
||||
|
||||
BinanceAccount::Processor.new(@ba).process
|
||||
|
||||
@account.reload
|
||||
@ba.reload
|
||||
assert_equal "EUR", @account.currency
|
||||
assert_in_delta 920.0, @account.balance, 0.01
|
||||
assert_equal false, @ba.extra.dig("binance", "stale_rate")
|
||||
end
|
||||
|
||||
test "uses nearest rate and sets stale flag when exact rate missing" do
|
||||
ExchangeRate.create!(from_currency: "USD", to_currency: "EUR",
|
||||
date: Date.current - 3, rate: 0.90)
|
||||
|
||||
BinanceAccount::Processor.new(@ba).process
|
||||
|
||||
@account.reload
|
||||
@ba.reload
|
||||
assert_equal "EUR", @account.currency
|
||||
assert_in_delta 900.0, @account.balance, 0.01
|
||||
assert_equal true, @ba.extra.dig("binance", "stale_rate")
|
||||
end
|
||||
|
||||
test "falls back to USD amount and sets stale flag when no rate available" do
|
||||
ExchangeRate.expects(:find_or_fetch_rate).returns(nil)
|
||||
|
||||
BinanceAccount::Processor.new(@ba).process
|
||||
|
||||
@account.reload
|
||||
@ba.reload
|
||||
assert_in_delta 1000.0, @account.balance, 0.01
|
||||
assert_equal true, @ba.extra.dig("binance", "stale_rate")
|
||||
end
|
||||
|
||||
test "clears stale flag on subsequent sync when exact rate found" do
|
||||
@ba.update!(extra: { "binance" => { "stale_rate" => true } })
|
||||
ExchangeRate.create!(from_currency: "USD", to_currency: "EUR",
|
||||
date: Date.current, rate: 0.92)
|
||||
|
||||
BinanceAccount::Processor.new(@ba).process
|
||||
|
||||
@account.reload
|
||||
@ba.reload
|
||||
assert_equal false, @ba.extra.dig("binance", "stale_rate")
|
||||
end
|
||||
|
||||
test "does not convert when family uses USD" do
|
||||
@family.update!(currency: "USD")
|
||||
|
||||
BinanceAccount::Processor.new(@ba).process
|
||||
|
||||
@account.reload
|
||||
assert_equal "USD", @account.currency
|
||||
assert_in_delta 1000.0, @account.balance, 0.01
|
||||
end
|
||||
end
|
||||
75
test/models/binance_account/usd_converter_test.rb
Normal file
75
test/models/binance_account/usd_converter_test.rb
Normal file
@@ -0,0 +1,75 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class BinanceAccount::UsdConverterTest < ActiveSupport::TestCase
|
||||
# A minimal host class that includes the concern so we can test it in isolation
|
||||
class Host
|
||||
include BinanceAccount::UsdConverter
|
||||
|
||||
def initialize(family_currency)
|
||||
@family_currency = family_currency
|
||||
end
|
||||
|
||||
def target_currency
|
||||
@family_currency
|
||||
end
|
||||
end
|
||||
|
||||
test "returns original amount unchanged when target is USD" do
|
||||
host = Host.new("USD")
|
||||
amount, stale, rate_date = host.send(:convert_from_usd, 1000.0, date: Date.current)
|
||||
assert_equal 1000.0, amount
|
||||
assert_equal false, stale
|
||||
assert_nil rate_date
|
||||
end
|
||||
|
||||
test "returns converted amount when exact rate exists" do
|
||||
date = Date.new(2026, 3, 28)
|
||||
ExchangeRate.create!(from_currency: "USD", to_currency: "EUR", date: date, rate: 0.92)
|
||||
|
||||
host = Host.new("EUR")
|
||||
amount, stale, rate_date = host.send(:convert_from_usd, 1000.0, date: date)
|
||||
|
||||
assert_in_delta 920.0, amount, 0.01
|
||||
assert_equal false, stale
|
||||
assert_nil rate_date
|
||||
end
|
||||
|
||||
test "marks stale and returns converted amount when nearest rate used" do
|
||||
old_date = Date.new(2026, 3, 25)
|
||||
ExchangeRate.create!(from_currency: "USD", to_currency: "EUR", date: old_date, rate: 0.91)
|
||||
|
||||
host = Host.new("EUR")
|
||||
amount, stale, rate_date = host.send(:convert_from_usd, 1000.0, date: Date.new(2026, 3, 28))
|
||||
|
||||
assert_in_delta 910.0, amount, 0.01
|
||||
assert_equal true, stale
|
||||
assert_equal old_date, rate_date
|
||||
end
|
||||
|
||||
test "returns raw USD amount with stale flag when no rate available" do
|
||||
host = Host.new("EUR")
|
||||
ExchangeRate.expects(:find_or_fetch_rate).returns(nil)
|
||||
|
||||
amount, stale, rate_date = host.send(:convert_from_usd, 1000.0, date: Date.new(2026, 3, 28))
|
||||
|
||||
assert_equal 1000.0, amount
|
||||
assert_equal true, stale
|
||||
assert_nil rate_date
|
||||
end
|
||||
|
||||
test "build_stale_extra returns correct hash when stale" do
|
||||
host = Host.new("EUR")
|
||||
result = host.send(:build_stale_extra, true, Date.new(2026, 3, 25), Date.new(2026, 3, 28))
|
||||
|
||||
assert_equal({ "binance" => { "stale_rate" => true, "rate_date_used" => "2026-03-25", "rate_target_date" => "2026-03-28" } }, result)
|
||||
end
|
||||
|
||||
test "build_stale_extra returns cleared hash when not stale" do
|
||||
host = Host.new("EUR")
|
||||
result = host.send(:build_stale_extra, false, nil, Date.new(2026, 3, 28))
|
||||
|
||||
assert_equal({ "binance" => { "stale_rate" => false } }, result)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user