Skip to content

Commit ae7aa0d

Browse files
moviendomeclaude
andcommitted
Simplify: single network at boot via SOLANA_NETWORK env var
Remove runtime network switching. The app runs on one network configured at boot (SOLANA_NETWORK=devnet|mainnet|testnet). - Remove NetworksController and route - Remove dropdown_controller.js - Network badge is now display-only in nav - All services read from SolanaConfig (no network params) - Explorer links use SolanaConfig.explorer_base/explorer_cluster - WalletPortfolioService and TokenMetadataService simplified (no network arg) - Cleaner cache keys (no network prefix needed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b3358d9 commit ae7aa0d

16 files changed

Lines changed: 69 additions & 171 deletions

‎app/controllers/application_controller.rb‎

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ class ApplicationController < ActionController::Base
33

44
before_action :authenticate!
55

6-
helper_method :current_user, :logged_in?, :current_network, :current_rpc_url
6+
helper_method :current_user, :logged_in?
77

88
private
99

@@ -18,12 +18,4 @@ def logged_in?
1818
def authenticate!
1919
redirect_to login_path unless logged_in?
2020
end
21-
22-
def current_network
23-
session[:network] || "mainnet"
24-
end
25-
26-
def current_rpc_url
27-
SolanaConfig.rpc_url(current_network)
28-
end
2921
end

‎app/controllers/dashboard_controller.rb‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,13 @@ class DashboardController < ApplicationController
22
def show
33
@wallet_address = current_user.wallet_address
44
@short_address = "#{@wallet_address[0..3]}...#{@wallet_address[-4..]}"
5-
@network = current_network
65

7-
portfolio = WalletPortfolioService.new(@wallet_address, network: @network)
6+
portfolio = WalletPortfolioService.new(@wallet_address)
87
@tokens = portfolio.tokens
98
@total_usd = portfolio.total_usd_value
109
@transactions = portfolio.recent_transactions
1110

12-
# Start background monitoring for new transactions
13-
WalletMonitorJob.perform_later(@wallet_address, network: @network) unless monitoring?(@wallet_address)
11+
WalletMonitorJob.perform_later(@wallet_address) unless monitoring?(@wallet_address)
1412
end
1513

1614
private

‎app/controllers/networks_controller.rb‎

Lines changed: 0 additions & 19 deletions
This file was deleted.

‎app/controllers/transfers_controller.rb‎

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
class TransfersController < ApplicationController
2-
# GET /transfers/new — Send SOL form
32
def new
43
@wallet_address = current_user.wallet_address
5-
@network = current_network
6-
@balance = SolanaClient.new(rpc_url: current_rpc_url).get_balance(@wallet_address)
4+
@balance = SolanaClient.new.get_balance(@wallet_address)
75
end
86

9-
# POST /transfers — Validate and return transaction params for JS to build
107
def create
118
recipient = params[:recipient]&.strip
129
amount_sol = params[:amount_sol].to_f
@@ -25,7 +22,7 @@ def create
2522

2623
amount_lamports = (amount_sol * 1_000_000_000).to_i
2724

28-
client = SolanaClient.new(rpc_url: current_rpc_url)
25+
client = SolanaClient.new
2926
balance = client.get_balance(current_user.wallet_address)
3027

3128
if balance.nil? || (balance * 1_000_000_000).to_i < amount_lamports + 5000
@@ -41,7 +38,7 @@ def create
4138
recipient: recipient,
4239
amount_lamports: amount_lamports,
4340
amount_sol: amount_sol,
44-
network: current_network,
41+
network: SolanaConfig.network,
4542
status: "pending"
4643
)
4744

@@ -51,11 +48,10 @@ def create
5148
recipient: recipient,
5249
amount_lamports: amount_lamports,
5350
blockhash: blockhash,
54-
rpc_url: current_rpc_url
51+
rpc_url: SolanaConfig.rpc_url
5552
}
5653
end
5754

58-
# PATCH /transfers/:id — Update with signature after wallet signs
5955
def update
6056
transfer = current_user.transfers.find(params[:id])
6157
signature = params[:signature]
@@ -70,13 +66,10 @@ def update
7066
render json: { success: true, transfer_id: transfer.id }
7167
end
7268

73-
# GET /transfers/:id — Show transfer detail
7469
def show
7570
@transfer = current_user.transfers.find(params[:id])
76-
@network = current_network
7771
end
7872

79-
# GET /transfers/:id/status — Poll confirmation status
8073
def status
8174
transfer = current_user.transfers.find(params[:id])
8275
render json: {

‎app/javascript/controllers/dropdown_controller.js‎

Lines changed: 0 additions & 25 deletions
This file was deleted.

‎app/javascript/controllers/index.js‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@ application.register("auto-refresh", AutoRefreshController)
1010
import ClipboardController from "./clipboard_controller"
1111
application.register("clipboard", ClipboardController)
1212

13-
import DropdownController from "./dropdown_controller"
14-
application.register("dropdown", DropdownController)
15-
1613
import TransferController from "./transfer_controller"
1714
application.register("transfer", TransferController)
1815

‎app/jobs/wallet_monitor_job.rb‎

Lines changed: 18 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,60 @@
11
# Starts a WebSocket subscription to Solana for real-time account changes.
22
# Falls back to polling if WebSocket is unavailable.
3+
# Runs as long as the dashboard is open (re-triggered on page load).
34
class WalletMonitorJob < ApplicationJob
45
queue_as :default
56

67
MONITOR_DURATION = 10.minutes
78

8-
def perform(wallet_address, network: "mainnet")
9+
def perform(wallet_address)
910
cache_key = "wallet_monitor/#{wallet_address}/active"
10-
return if Rails.cache.read(cache_key) # Already monitoring
11+
return if Rails.cache.read(cache_key)
1112

1213
Rails.cache.write(cache_key, true, expires_in: MONITOR_DURATION)
1314

14-
ws_url = ws_url_for(network)
15-
15+
ws_url = SolanaConfig.ws_url
1616
if ws_url.present?
17-
monitor_via_websocket(wallet_address, network, ws_url)
17+
monitor_via_websocket(wallet_address)
1818
else
19-
monitor_via_polling(wallet_address, network)
19+
monitor_via_polling(wallet_address)
2020
end
2121
ensure
2222
Rails.cache.delete("wallet_monitor/#{wallet_address}/active")
2323
end
2424

2525
private
2626

27-
def monitor_via_websocket(wallet_address, network, ws_url)
28-
monitor = SolanaWebsocketMonitor.new(wallet_address, network: network)
27+
def monitor_via_websocket(wallet_address)
28+
monitor = SolanaWebsocketMonitor.new(wallet_address)
2929
monitor.start
30-
31-
# Run for MONITOR_DURATION then stop
3230
sleep MONITOR_DURATION
3331
monitor.stop
3432
end
3533

36-
def monitor_via_polling(wallet_address, network, started_at: Time.current)
37-
client = SolanaClient.new(rpc_url: SolanaConfig.rpc_url(network))
38-
34+
def monitor_via_polling(wallet_address, started_at: Time.current)
35+
client = SolanaClient.new
3936
cache_key = "wallet_monitor/#{wallet_address}/last_sig"
37+
4038
signatures = client.get_recent_signatures(wallet_address, limit: 1)
4139
latest_sig = signatures.first&.dig(:signature)
42-
4340
previous_sig = Rails.cache.read(cache_key)
4441

4542
if latest_sig && latest_sig != previous_sig
4643
Rails.cache.write(cache_key, latest_sig, expires_in: 1.hour)
47-
broadcast_update(wallet_address, network) if previous_sig
44+
broadcast_update(wallet_address) if previous_sig
4845
end
4946

5047
if Time.current - started_at < MONITOR_DURATION
5148
sleep 15
52-
monitor_via_polling(wallet_address, network, started_at: started_at)
49+
monitor_via_polling(wallet_address, started_at: started_at)
5350
end
5451
end
5552

56-
def broadcast_update(wallet_address, network)
57-
SolanaConfig::NETWORKS.each do |net|
58-
Rails.cache.delete("wallet/#{net}/#{wallet_address}/tokens_v2")
59-
Rails.cache.delete("wallet/#{net}/#{wallet_address}/recent_txs")
60-
end
53+
def broadcast_update(wallet_address)
54+
Rails.cache.delete("wallet/#{wallet_address}/tokens")
55+
Rails.cache.delete("wallet/#{wallet_address}/recent_txs")
6156

62-
portfolio = WalletPortfolioService.new(wallet_address, network: network)
57+
portfolio = WalletPortfolioService.new(wallet_address)
6358
stream = "wallet_#{wallet_address}"
6459

6560
Turbo::StreamsChannel.broadcast_replace_to(
@@ -74,17 +69,10 @@ def broadcast_update(wallet_address, network)
7469
locals: { tokens: portfolio.tokens }
7570
)
7671

77-
explorer_base = network == "mainnet" ? "https://solscan.io" : "https://explorer.solana.com"
78-
explorer_cluster = network == "mainnet" ? "" : "?cluster=#{network}"
79-
8072
Turbo::StreamsChannel.broadcast_replace_to(
8173
stream, target: "recent_activity",
8274
partial: "dashboard/recent_activity",
83-
locals: { transactions: portfolio.recent_transactions, wallet_address: wallet_address, explorer_base: explorer_base, explorer_cluster: explorer_cluster }
75+
locals: { transactions: portfolio.recent_transactions, wallet_address: wallet_address }
8476
)
8577
end
86-
87-
def ws_url_for(network)
88-
SolanaConfig.ws_url(network)
89-
end
9078
end

‎app/models/solana_config.rb‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,29 @@
11
# Single source of truth for Solana network configuration.
2-
# Reads from config/solana.yml which pulls from environment variables.
2+
# Network is set at boot via SOLANA_NETWORK env var (default: mainnet).
33
class SolanaConfig
4-
NETWORKS = %w[mainnet devnet testnet].freeze
5-
64
class << self
7-
def rpc_url(network = "mainnet")
5+
def network
6+
config["network"]
7+
end
8+
9+
def rpc_url
810
config.dig("networks", network, "rpc_url")
911
end
1012

11-
def ws_url(network = "mainnet")
13+
def ws_url
1214
config.dig("networks", network, "ws_url")
1315
end
1416

15-
def valid_network?(network)
16-
NETWORKS.include?(network)
17+
def mainnet?
18+
network == "mainnet"
19+
end
20+
21+
def explorer_base
22+
mainnet? ? "https://solscan.io" : "https://explorer.solana.com"
23+
end
24+
25+
def explorer_cluster
26+
mainnet? ? "" : "?cluster=#{network}"
1727
end
1828

1929
private

‎app/services/solana_websocket_monitor.rb‎

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22
require "websocket-client-simple"
33

44
# Subscribes to Solana WebSocket RPC for real-time account changes.
5-
# Runs as a background thread and broadcasts updates via Turbo Streams.
5+
# Broadcasts updates via Turbo Streams when the account balance changes.
66
class SolanaWebsocketMonitor
77
RECONNECT_DELAY = 5
88

9-
def initialize(wallet_address, network: "mainnet")
9+
def initialize(wallet_address)
1010
@wallet_address = wallet_address
11-
@network = network
12-
@ws_url = SolanaConfig.ws_url(network)
11+
@ws_url = SolanaConfig.ws_url
1312
@running = false
1413
end
1514

@@ -42,7 +41,7 @@ def connect_and_listen
4241
monitor = self
4342

4443
ws.on :open do
45-
Rails.logger.info("[SolanaWS] Connected for #{monitor.send(:@wallet_address)}")
44+
Rails.logger.info("[SolanaWS] Connected for #{monitor.send(:@wallet_address)} on #{SolanaConfig.network}")
4645
ws.send({
4746
jsonrpc: "2.0",
4847
id: 1,
@@ -86,12 +85,10 @@ def handle_message(data)
8685
end
8786

8887
def broadcast_update
89-
SolanaConfig::NETWORKS.each do |net|
90-
Rails.cache.delete("wallet/#{net}/#{@wallet_address}/tokens_v2")
91-
Rails.cache.delete("wallet/#{net}/#{@wallet_address}/recent_txs")
92-
end
88+
Rails.cache.delete("wallet/#{@wallet_address}/tokens")
89+
Rails.cache.delete("wallet/#{@wallet_address}/recent_txs")
9390

94-
portfolio = WalletPortfolioService.new(@wallet_address, network: @network)
91+
portfolio = WalletPortfolioService.new(@wallet_address)
9592
stream = "wallet_#{@wallet_address}"
9693

9794
Turbo::StreamsChannel.broadcast_replace_to(
@@ -106,13 +103,10 @@ def broadcast_update
106103
locals: { tokens: portfolio.tokens }
107104
)
108105

109-
explorer_base = @network == "mainnet" ? "https://solscan.io" : "https://explorer.solana.com"
110-
explorer_cluster = @network == "mainnet" ? "" : "?cluster=#{@network}"
111-
112106
Turbo::StreamsChannel.broadcast_replace_to(
113107
stream, target: "recent_activity",
114108
partial: "dashboard/recent_activity",
115-
locals: { transactions: portfolio.recent_transactions, wallet_address: @wallet_address, explorer_base: explorer_base, explorer_cluster: explorer_cluster }
109+
locals: { transactions: portfolio.recent_transactions, wallet_address: @wallet_address }
116110
)
117111
end
118112
end

‎app/services/token_metadata_service.rb‎

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,23 @@
22
class TokenMetadataService
33
SOL_MINT = "So11111111111111111111111111111111111111112"
44

5-
def initialize(network: "mainnet", rpc_url: nil)
6-
@network = network
7-
@client = SolanaClient.new(rpc_url: rpc_url)
8-
@mainnet = (network == "mainnet")
5+
def initialize
6+
@client = SolanaClient.new
97
end
108

11-
# Returns all tokens for a wallet with metadata and USD values.
129
def token_balances_for(wallet_address)
1310
sol_balance = @client.get_balance(wallet_address)
1411
accounts = @client.get_token_accounts(wallet_address)
1512

1613
all_mints = [ SOL_MINT ] + accounts.map { |a| a[:mint] }
1714

18-
# Metadata from DB (fetches from Jupiter only for new mints)
1915
token_records = Token.find_or_fetch_many(all_mints)
2016

2117
# Prices only available on mainnet
22-
prices = @mainnet ? JupiterClient.fetch_prices(all_mints) : {}
18+
prices = SolanaConfig.mainnet? ? JupiterClient.fetch_prices(all_mints) : {}
2319

2420
tokens = []
2521

26-
# SOL entry
2722
if sol_balance
2823
sol_token = token_records[SOL_MINT]
2924
sol_price = prices[SOL_MINT]
@@ -38,7 +33,6 @@ def token_balances_for(wallet_address)
3833
tokens << entry
3934
end
4035

41-
# SPL tokens
4236
accounts.each do |account|
4337
token = token_records[account[:mint]]
4438
price = prices[account[:mint]]

0 commit comments

Comments
 (0)