Cryptocurrency Account Management#
Sure models crypto holdings as a Crypto accountable type — one of the 9 types registered in the Accountable concern — classified as an asset . The core model is app/models/crypto.rb.
Subtypes: Wallet vs. Exchange#
Crypto accounts have exactly two subtypes :
| Subtype | Display Name | Use Case |
|---|---|---|
wallet | Crypto Wallet | Self-custody wallets; synced via CoinStats or similar providers |
exchange | Crypto Exchange | Centralized exchanges (Coinbase, Kraken, Binance) with trade history |
The key behavioral difference is supports_trades?: only exchange accounts return true, enabling manual trade entry and investment activity labels. wallet accounts are sync-only — they receive transaction data from the provider but do not expose a trade entry UI.
Account#supports_trades? delegates this check to the Crypto accountable, treating exchange crypto accounts the same as all Investment subtypes for trade-related features.
Account#manual_crypto_exchange? identifies the specific case of a manually-managed exchange account (no live provider attached), which affects sync and balance calculation behavior.
Provider Integration (Linked Accounts)#
Crypto accounts are connected to external providers at the Account level. Factory methods create exchange accounts with cash_balance: 0 (all value is in holdings) and tax_treatment: :taxable by default .
Supported providers with dedicated factory methods:
- Coinbase —
Account.create_from_coinbase_account: createssubtype: "exchange" - Kraken —
Account.create_from_kraken_account: delegates to the sharedcreate_from_crypto_exchange_accountprivate method - Binance —
Account.create_from_binance_account: same pattern, plus sets an opening anchor balance of 0 - CoinStats — syncs wallet and portfolio data via
CoinstatsEntry::Processor
All provider-created accounts skip the initial sync; the provider sync itself handles balance and holdings creation once the correct currency is known .
CoinStats Sync (Wallet & Portfolio)#
CoinstatsEntry::Processor processes individual CoinStats API transactions into local records. Its branching logic is the key point of differentiation:
- Exchange trades (
buy,sell,swap,trade,convert,filltypes) on non-fiat assets → callsimport_adapter.import_trade(...)to create aTradeentry . - All other transactions (receives, sends, rewards, fees, swaps on fiat accounts) → calls
import_adapter.import_transaction(...)with aninvestment_activity_labelderived from the transaction type .
The exchange_trade? predicate guards trade creation: it requires exchange_source?, a non-fiat asset, nonzero quantity/price, and a recognized trade type.
Transaction metadata stored in extra includes: transaction_hash, explorer_url, transaction_type, coin symbol/count, profit/loss, and fee details . A legacy migration path exists: if a prior Transaction entry exists for the same external_id, the processor either migrates it to a Trade or skips it (preserving user modifications) .
Trade Entry (Exchange Accounts Only)#
Manual trade entry for exchange accounts flows through Trade::CreateForm, which handles six entry types :
| Type | Result |
|---|---|
buy / sell | Trade entry; qty sign determines direction; amount = signed_qty × price + fee |
dividend | Trade with qty: 0, price: 0; security required |
interest | Trade with qty: 0, price: 0; falls back to a synthetic cash security |
deposit / withdrawal | Transfer (if transfer_account_id provided) or unlinked Transaction |
For buy/sell, security is resolved via Security::Resolver from a combobox ID (SYMBOL|EXCHANGE|PROVIDER format) or a manual ticker. The investment_activity_label is set to "Buy" or "Sell" automatically on save . After saving, lock_saved_attributes! prevents provider syncs from overwriting the manual entry, and account.sync_later queues a holdings/balance recalculation .
Activity Labels & Spending Categorization#
Exchange crypto accounts use investment activity labels instead of standard expense categories . The full label set is shared with investment accounts via Trade::ACTIVITY_LABELS = Transaction::ACTIVITY_LABELS.dup.freeze .
Key labels for crypto:
Exchange— classified asINTERNAL_MOVEMENT_LABELS; excluded from income/expense totalsBuy/Sell— assigned automatically; never reach expense budgets becauseTrade#excluded_from_budget?always returnstrueInterest/Dividend— income labels; read-only in the quick-edit badge UI
For CoinStats-synced wallet transactions, the processor maps raw transaction types to labels: received/deposit → "Transfer", reward/interest → "Interest", fee → "Fee", etc. .
Portfolio & Cost Basis#
Both crypto subtypes share the balance_type: :investment path , meaning balance calculations include both cash and holdings components.
For exchange accounts with trade history, cost basis is materialized on sync via Holding::Materializer. Manual accounts use Holding::ForwardCalculator (weighted average from buy trades); linked/synced accounts use Holding::ReverseCalculator . Unrealized and realized gain/loss are computed in Trade#unrealized_gain_loss and Trade#realized_gain_loss respectively.
Crypto is taxable by default (tax_treatment: :taxable), though the model supports tax_deferred and tax_exempt for edge cases like self-directed IRAs .
Key Source Files#
| File | Purpose |
|---|---|
app/models/crypto.rb | Subtype definitions, supports_trades?, tax treatment |
app/models/account.rb | supports_trades?, manual_crypto_exchange?, provider factory methods |
app/models/trade/create_form.rb | Manual trade entry logic for all 6 trade types |
app/models/trade.rb | Trade model: validations, buy?/sell?, gain/loss computation, budget exclusion |
app/models/coinstats_entry/processor.rb | CoinStats transaction → Trade or Transaction mapping |
app/views/trades/_form.html.erb | Trade entry UI form |