Files
sure/app/controllers/coinstats_items_controller.rb
Ethan 3b4ab735b0 Add (beta) CoinStats Crypto Wallet Integration with Balance and Transaction Syncing (#512)
* Feat(CoinStats): Scaffold implementation, not yet functional

* Feat(CoinStats): Implement crypto wallet balance and transactions

* Feat(CoinStats): Add tests, Minor improvements

* Feat(CoinStats): Utilize bulk fetch API endpoints

* Feat(CoinStats): Migrate strings to i8n

* Feat(CoinStats): Fix error handling in wallet link modal

* Feat(CoinStats): Implement hourly provider sync job

* Feat(CoinStats): Generate docstrings

* Fix(CoinStats): Validate API Key on provider update

* Fix(Providers): Safely handle race condition in merchance creation

* Fix(CoinStats): Don't catch system signals in account processor

* Fix(CoinStats): Preload before iterating accounts

* Fix(CoinStats): Add no opener / referrer to API dashboard link

* Fix(CoinStats): Use strict matching for symbols

* Fix(CoinStats): Remove dead code in transactions importer

* Fix(CoinStats): Avoid transaction fallback ID collisions

* Fix(CoinStats): Improve Blockchains fetch error handling

* Fix(CoinStats): Enforce NOT NULL constraint for API Key schema

* Fix(CoinStats): Migrate sync status strings to i8n

* Fix(CoinStats): Use class name rather than hardcoded string

* Fix(CoinStats): Use account currency rather than hardcoded USD

* Fix(CoinStats): Migrate from standalone to Provider class

* Fix(CoinStats): Fix test failures due to string changes
2026-01-07 15:59:04 +01:00

170 lines
5.5 KiB
Ruby

class CoinstatsItemsController < ApplicationController
before_action :set_coinstats_item, only: [ :show, :edit, :update, :destroy, :sync ]
def index
@coinstats_items = Current.family.coinstats_items.ordered
end
def show
end
def new
@coinstats_item = Current.family.coinstats_items.build
@coinstats_items = Current.family.coinstats_items.where.not(api_key: nil)
@blockchains = fetch_blockchain_options(@coinstats_items.first)
end
def create
@coinstats_item = Current.family.coinstats_items.build(coinstats_item_params)
@coinstats_item.name ||= t(".default_name")
# Validate API key before saving
unless validate_api_key(@coinstats_item.api_key)
return render_error_response(@coinstats_item.errors.full_messages.join(", "))
end
if @coinstats_item.save
render_success_response(".success")
else
render_error_response(@coinstats_item.errors.full_messages.join(", "))
end
end
def edit
end
def update
# Validate API key if it's being changed
unless validate_api_key(coinstats_item_params[:api_key])
return render_error_response(@coinstats_item.errors.full_messages.join(", "))
end
if @coinstats_item.update(coinstats_item_params)
render_success_response(".success")
else
render_error_response(@coinstats_item.errors.full_messages.join(", "))
end
end
def destroy
@coinstats_item.destroy_later
redirect_to settings_providers_path, notice: t(".success"), status: :see_other
end
def sync
unless @coinstats_item.syncing?
@coinstats_item.sync_later
end
respond_to do |format|
format.html { redirect_back_or_to accounts_path }
format.json { head :ok }
end
end
def link_wallet
coinstats_item_id = params[:coinstats_item_id].presence
@address = params[:address]&.to_s&.strip.presence
@blockchain = params[:blockchain]&.to_s&.strip.presence
unless coinstats_item_id && @address && @blockchain
return render_link_wallet_error(t(".missing_params"))
end
@coinstats_item = Current.family.coinstats_items.find(coinstats_item_id)
result = CoinstatsItem::WalletLinker.new(@coinstats_item, address: @address, blockchain: @blockchain).link
if result.success?
redirect_to accounts_path, notice: t(".success", count: result.created_count), status: :see_other
else
error_msg = result.errors.join("; ").presence || t(".failed")
render_link_wallet_error(error_msg)
end
rescue Provider::Coinstats::Error => e
render_link_wallet_error(t(".error", message: e.message))
rescue => e
Rails.logger.error("CoinStats link wallet error: #{e.class} - #{e.message}")
render_link_wallet_error(t(".error", message: e.message))
end
private
def set_coinstats_item
@coinstats_item = Current.family.coinstats_items.find(params[:id])
end
def coinstats_item_params
params.require(:coinstats_item).permit(
:name,
:sync_start_date,
:api_key
)
end
def validate_api_key(api_key)
return true if api_key.blank?
response = Provider::Coinstats.new(api_key).get_blockchains
if response.success?
true
else
@coinstats_item.errors.add(:api_key, t("coinstats_items.create.errors.validation_failed", message: response.error&.message))
false
end
rescue => e
@coinstats_item.errors.add(:api_key, t("coinstats_items.create.errors.validation_failed", message: e.message))
false
end
def render_error_response(error_message)
if turbo_frame_request?
render turbo_stream: turbo_stream.replace(
"coinstats-providers-panel",
partial: "settings/providers/coinstats_panel",
locals: { error_message: error_message }
), status: :unprocessable_entity
else
redirect_to settings_providers_path, alert: error_message, status: :unprocessable_entity
end
end
def render_success_response(notice_key)
if turbo_frame_request?
flash.now[:notice] = t(notice_key, default: notice_key.to_s.humanize)
@coinstats_items = Current.family.coinstats_items.ordered
render turbo_stream: [
turbo_stream.replace(
"coinstats-providers-panel",
partial: "settings/providers/coinstats_panel",
locals: { coinstats_items: @coinstats_items }
),
*flash_notification_stream_items
]
else
redirect_to settings_providers_path, notice: t(notice_key), status: :see_other
end
end
def render_link_wallet_error(error_message)
@error_message = error_message
@coinstats_items = Current.family.coinstats_items.where.not(api_key: nil)
@blockchains = fetch_blockchain_options(@coinstats_items.first)
render :new, status: :unprocessable_entity
end
def fetch_blockchain_options(coinstats_item)
return [] unless coinstats_item&.api_key.present?
Provider::Coinstats.new(coinstats_item.api_key).blockchain_options
rescue Provider::Coinstats::Error => e
Rails.logger.error("CoinStats blockchain fetch failed: item_id=#{coinstats_item.id} error=#{e.class} message=#{e.message}")
flash.now[:alert] = t("coinstats_items.new.blockchain_fetch_error")
[]
rescue StandardError => e
Rails.logger.error("CoinStats blockchain fetch failed: item_id=#{coinstats_item.id} error=#{e.class} message=#{e.message}")
flash.now[:alert] = t("coinstats_items.new.blockchain_fetch_error")
[]
end
end