Module: Solana::Network

Defined in:
lib/solana/network.rb

Overview

Cluster identity — the one place that knows what "devnet" IS.

A Solana cluster has three names that must agree, and nothing in the protocol makes them agree for you:

1. The operator's name for it   — "devnet"          (SOLANA_NETWORK)
2. The chain's own fingerprint  — genesis hash      (getGenesisHash)
3. The wallet's name for it     — "solana:devnet"   (Wallet Standard)

Every mis-alignment bug in this ecosystem is two of those three disagreeing: a mainnet program ID against a devnet RPC (caught at boot by the host's alignment check), or an app on devnet against a wallet on mainnet (caught at sign-in, by handing the wallet name #3 and letting it object).

This module is deliberately Rails-free and RPC-free — it is a lookup table with opinions. Fetching the live genesis hash is the HOST's job — the RPC call is getGenesisHash, which this gem's Solana::Client does not wrap today (turf monster adds it in an initializer); deciding what to DO about a mismatch is the host's too.

Constant Summary collapse

MAINNET =

Canonical cluster names, as Solana's own tooling spells them. Note "mainnet-beta" — the hyphenated form is the real one; "mainnet" is an alias people type, normalized by .canonical.

"mainnet-beta"
DEVNET =
"devnet"
TESTNET =
"testnet"
LOCALNET =
"localnet"
CLUSTERS =
[MAINNET, DEVNET, TESTNET, LOCALNET].freeze
GENESIS_HASHES =

The genesis block hash of each public cluster — a chain's immutable fingerprint, and the only cluster identifier that cannot be misconfigured. An RPC URL can lie (a proxy, a fork, a typo'd host); its genesis hash cannot. localnet is absent on purpose: a fresh validator generates a new genesis every time it boots, so there is no constant to pin.

{
  MAINNET => "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d",
  DEVNET  => "EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG",
  TESTNET => "4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY"
}.freeze
WALLET_STANDARD_CHAINS =

Wallet Standard chain identifiers (CAIP-2 namespace "solana"), the form a browser wallet speaks. Phantom and every Wallet-Standard wallet advertise these in wallet.chains and accept one as the SIWS chainId.

Mind the asymmetry: the Wallet Standard spells mainnet "solana:mainnet", WITHOUT the -beta suffix the operator name carries. Passing "solana:mainnet-beta" is not a near-miss the wallet forgives — it is an unrecognized chain.

{
  MAINNET  => "solana:mainnet",
  DEVNET   => "solana:devnet",
  TESTNET  => "solana:testnet",
  # localnet is a valid Wallet Standard chain, but NOT a valid SIWS chainId —
  # no wallet can verify a sign-in against a chain only your machine has. It is
  # here for completeness of the chain table; do not send it as a chainId.
  LOCALNET => "solana:localnet"
}.freeze
LABELS =

Human labels for UI. Anything unrecognized is deliberately NOT given a friendly name — an unknown cluster is a fact worth showing the operator, not something to paper over.

{
  MAINNET  => "Mainnet",
  DEVNET   => "Devnet",
  TESTNET  => "Testnet",
  LOCALNET => "Localnet"
}.freeze
ALIASES =
{
  "mainnet"      => MAINNET,
  "mainnet-beta" => MAINNET,
  "main"         => MAINNET,
  "dev"          => DEVNET,
  "local"        => LOCALNET,
  "localhost"    => LOCALNET
}.freeze

Class Method Summary collapse

Class Method Details

.alignment(cluster:, genesis_hash:) ⇒ Object

Does a live genesis hash confirm the declared cluster?

Three outcomes, and the middle one matters: :aligned (the chain is who it says), :mismatched (it is a DIFFERENT known chain — the dangerous case), and :unverifiable (no pinned hash to compare, i.e. localnet or an unknown cluster name). Callers must not collapse :unverifiable into either of the other two — refusing to boot on localnet is as wrong as trusting a mainnet RPC that answered with a devnet genesis.

THERE IS DELIBERATELY NO aligned? BOOLEAN. One existed and was removed in review: it answered false for :unverifiable, so a host writing the obvious raise unless aligned? would refuse to boot against a local validator — precisely the mistake the paragraph above warns about, re-introduced by the convenience wrapper meant to save callers from it. Three states, three branches; the caller decides what :unverifiable means for them.



154
155
156
157
158
159
160
# File 'lib/solana/network.rb', line 154

def alignment(cluster:, genesis_hash:)
  expected = self.genesis_hash(cluster)
  return :unverifiable if expected.nil?
  return :unverifiable if genesis_hash.nil? || genesis_hash.to_s.strip.empty?

  genesis_hash.to_s.strip == expected ? :aligned : :mismatched
end

.canonical(cluster) ⇒ Object

Normalize an operator-supplied cluster name to its canonical spelling. Returns nil for anything unrecognized — callers decide whether an unknown cluster is a warning or a refusal, and they need to be able to tell.



87
88
89
90
91
92
93
94
95
# File 'lib/solana/network.rb', line 87

def canonical(cluster)
  return nil if cluster.nil?

  name = cluster.to_s.strip.downcase
  return nil if name.empty?
  return name if CLUSTERS.include?(name)

  ALIASES[name]
end

.cluster_for_genesis(hash) ⇒ Object

Reverse lookup: given a hash from getGenesisHash, which cluster is this? This is how you learn what an RPC URL ACTUALLY points at, as opposed to what its hostname claims. nil means "no public cluster we know" — which for a localnet validator is the correct and expected answer.



111
112
113
114
115
# File 'lib/solana/network.rb', line 111

def cluster_for_genesis(hash)
  return nil if hash.nil? || hash.to_s.strip.empty?

  GENESIS_HASHES.key(hash.to_s.strip)
end

.describe(cluster, environment: nil) ⇒ Object

Everything a UI needs to explain a network to a person, in one hash. Serialized straight into a data attribute by the host so the browser guard reads the same facts the server holds.



165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/solana/network.rb', line 165

def describe(cluster, environment: nil)
  canonical_name = canonical(cluster)
  {
    cluster: canonical_name,
    declared: cluster.to_s,
    known: !canonical_name.nil?,
    label: label(cluster),
    genesis_hash: genesis_hash(cluster),
    wallet_standard_chain: wallet_standard_chain(cluster),
    environment: environment&.to_s,
    expected_cluster: environment.nil? ? nil : expected_for_environment(environment)
  }
end

.expected_for_environment(environment) ⇒ Object

The cluster an environment is SUPPOSED to run against. Production means real money, so it means mainnet; everything else means devnet. A host that disagrees (a mainnet-facing staging rehearsal, say) passes its own map.



135
136
137
# File 'lib/solana/network.rb', line 135

def expected_for_environment(environment)
  environment.to_s == "production" ? MAINNET : DEVNET
end

.genesis_hash(cluster) ⇒ Object

The pinned genesis hash for a cluster, or nil when there isn't one (unknown cluster, or localnet — whose genesis is per-boot).



103
104
105
# File 'lib/solana/network.rb', line 103

def genesis_hash(cluster)
  GENESIS_HASHES[canonical(cluster)]
end

.known?(cluster) ⇒ Boolean

Returns:

  • (Boolean)


97
98
99
# File 'lib/solana/network.rb', line 97

def known?(cluster)
  !canonical(cluster).nil?
end

.label(cluster) ⇒ Object



128
129
130
# File 'lib/solana/network.rb', line 128

def label(cluster)
  LABELS[canonical(cluster)] || "Unknown Network"
end

.wallet_standard_chain(cluster) ⇒ Object

The Wallet Standard chain id for a cluster — the value to hand a browser wallet as the SIWS chainId so the wallet compares it against its OWN selected network and objects when they differ.

This is the ONLY pre-emptive mismatch signal available to a website: wallets do not expose their selected cluster for reading. You cannot ask; you can only assert and let the wallet contradict you.



124
125
126
# File 'lib/solana/network.rb', line 124

def wallet_standard_chain(cluster)
  WALLET_STANDARD_CHAINS[canonical(cluster)]
end