mirror of
https://github.com/we-promise/sure.git
synced 2026-04-09 15:24:48 +00:00
* 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
158 lines
5.5 KiB
Ruby
158 lines
5.5 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# Links a cryptocurrency wallet to CoinStats by fetching token balances
|
|
# and creating corresponding accounts for each token found.
|
|
class CoinstatsItem::WalletLinker
|
|
attr_reader :coinstats_item, :address, :blockchain
|
|
|
|
Result = Struct.new(:success?, :created_count, :errors, keyword_init: true)
|
|
|
|
# @param coinstats_item [CoinstatsItem] Parent item with API credentials
|
|
# @param address [String] Wallet address to link
|
|
# @param blockchain [String] Blockchain network identifier
|
|
def initialize(coinstats_item, address:, blockchain:)
|
|
@coinstats_item = coinstats_item
|
|
@address = address
|
|
@blockchain = blockchain
|
|
end
|
|
|
|
# Fetches wallet balances and creates accounts for each token.
|
|
# @return [Result] Success status, created count, and any errors
|
|
def link
|
|
balance_data = fetch_balance_data
|
|
tokens = normalize_tokens(balance_data)
|
|
|
|
return Result.new(success?: false, created_count: 0, errors: [ "No tokens found for wallet" ]) if tokens.empty?
|
|
|
|
created_count = 0
|
|
errors = []
|
|
|
|
tokens.each do |token_data|
|
|
result = create_account_from_token(token_data)
|
|
if result[:success]
|
|
created_count += 1
|
|
else
|
|
errors << result[:error]
|
|
end
|
|
end
|
|
|
|
# Trigger a sync if we created any accounts
|
|
coinstats_item.sync_later if created_count > 0
|
|
|
|
Result.new(success?: created_count > 0, created_count: created_count, errors: errors)
|
|
end
|
|
|
|
private
|
|
|
|
# Fetches balance data for this wallet from CoinStats API.
|
|
# @return [Array<Hash>] Token balances for the wallet
|
|
def fetch_balance_data
|
|
provider = Provider::Coinstats.new(coinstats_item.api_key)
|
|
wallets_param = "#{blockchain}:#{address}"
|
|
response = provider.get_wallet_balances(wallets_param)
|
|
|
|
return [] unless response.success?
|
|
|
|
provider.extract_wallet_balance(response.data, address, blockchain)
|
|
end
|
|
|
|
# Normalizes various balance data formats to an array of tokens.
|
|
# @param balance_data [Array, Hash, Object] Raw balance response
|
|
# @return [Array<Hash>] Normalized array of token data
|
|
def normalize_tokens(balance_data)
|
|
if balance_data.is_a?(Array)
|
|
balance_data
|
|
elsif balance_data.is_a?(Hash)
|
|
balance_data[:result] || balance_data[:tokens] || [ balance_data ]
|
|
elsif balance_data.present?
|
|
[ balance_data ]
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
# Creates a CoinstatsAccount and linked Account for a token.
|
|
# @param token_data [Hash] Token balance data from API
|
|
# @return [Hash] Result with :success and optional :error
|
|
def create_account_from_token(token_data)
|
|
token = token_data.with_indifferent_access
|
|
account_name = build_account_name(token)
|
|
current_balance = calculate_balance(token)
|
|
token_id = (token[:coinId] || token[:id])&.to_s
|
|
|
|
ActiveRecord::Base.transaction do
|
|
coinstats_account = coinstats_item.coinstats_accounts.create!(
|
|
name: account_name,
|
|
currency: "USD",
|
|
current_balance: current_balance,
|
|
account_id: token_id
|
|
)
|
|
|
|
# Store wallet metadata for future syncs
|
|
snapshot = build_snapshot(token, current_balance)
|
|
coinstats_account.upsert_coinstats_snapshot!(snapshot)
|
|
|
|
account = coinstats_item.family.accounts.create!(
|
|
accountable: Crypto.new,
|
|
name: account_name,
|
|
balance: current_balance,
|
|
cash_balance: current_balance,
|
|
currency: coinstats_account.currency,
|
|
status: "active"
|
|
)
|
|
|
|
AccountProvider.create!(account: account, provider: coinstats_account)
|
|
|
|
{ success: true }
|
|
end
|
|
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
|
|
Rails.logger.error("CoinstatsItem::WalletLinker - Failed to create account: #{e.message}")
|
|
{ success: false, error: "Failed to create #{account_name || 'account'}: #{e.message}" }
|
|
rescue => e
|
|
Rails.logger.error("CoinstatsItem::WalletLinker - Unexpected error: #{e.class} - #{e.message}")
|
|
{ success: false, error: "Unexpected error: #{e.message}" }
|
|
end
|
|
|
|
# Builds a display name for the account from token and address.
|
|
# @param token [Hash] Token data with :name
|
|
# @return [String] Human-readable account name
|
|
def build_account_name(token)
|
|
token_name = token[:name].to_s.strip
|
|
truncated_address = address.present? ? "#{address.first(4)}...#{address.last(4)}" : nil
|
|
|
|
if token_name.present? && truncated_address.present?
|
|
"#{token_name} (#{truncated_address})"
|
|
elsif token_name.present?
|
|
token_name
|
|
elsif truncated_address.present?
|
|
"#{blockchain.capitalize} (#{truncated_address})"
|
|
else
|
|
"Crypto Wallet"
|
|
end
|
|
end
|
|
|
|
# Calculates USD balance from token amount and price.
|
|
# @param token [Hash] Token data with :amount/:balance and :price
|
|
# @return [Float] Balance in USD
|
|
def calculate_balance(token)
|
|
amount = token[:amount] || token[:balance] || token[:current_balance] || 0
|
|
price = token[:price] || 0
|
|
(amount.to_f * price.to_f)
|
|
end
|
|
|
|
# Builds snapshot hash for storing in CoinstatsAccount.
|
|
# @param token [Hash] Token data from API
|
|
# @param current_balance [Float] Calculated USD balance
|
|
# @return [Hash] Snapshot with balance, address, and metadata
|
|
def build_snapshot(token, current_balance)
|
|
token.to_h.merge(
|
|
id: (token[:coinId] || token[:id])&.to_s,
|
|
balance: current_balance,
|
|
currency: "USD",
|
|
address: address,
|
|
blockchain: blockchain,
|
|
institution_logo: token[:imgUrl]
|
|
)
|
|
end
|
|
end
|