Skip to content

Repository files navigation

SmartHoldem Cosmic Sandbox

A decentralized 3D block-explorer for the SmartHoldem blockchain. Every wallet becomes a living, procedural planet inside a WebGL universe.

Live demo · Templates · Russian version


SmartHoldem Blockchain Tiny Planets

1. Stack

Layer Technology
UI framework Vue 3.5 (Composition API + <script setup lang="ts">)
Bundler Vite 8
Language TypeScript
Styles TailwindCSS 3.4 + JetBrains Mono / Unbounded / Outfit
State Pinia
Routing vue-router 4 (history mode)
3D engine TresJS (@tresjs/core 5 + @tresjs/cientos) on top of Three.js 0.184
Procedural noise fastnoise-lite (OpenSimplex2, FBm)
Network graph 3d-force-graph
Binary buffers b4a
QR codes qrcode
HTTP axios
Icons @lucide/vue
PWA manifest + service worker (network-first navigation, cache-first static)

There is no backend in this project. All data is read directly from public SmartHoldem nodes.


2. Directory layout

/smartholdem-cosmic-sandbox/
├── public/
│   ├── manifest.webmanifest       # PWA manifest
│   ├── sw.js                       # service worker
│   ├── og-default.jpg             # 1200x630 default OG card
│   └── icons/                     # 192/512/maskable + apple-touch
├── src/
│   ├── App.vue                    # router resolver + global HUD mount
│   ├── main.ts                    # createApp + SW registration
│   ├── style.css                  # Tailwind + design tokens
│   ├── router/index.ts            # 4 routes + catch-all > /
│   ├── pages/
│   │   ├── SandboxPage.vue        # planet view + photo mode + energon
│   │   └── GalaxyPage.vue         # 3d-force-graph network map
│   ├── core/                      # WebGL/THREE logic (markRaw'd from Vue)
│   │   ├── dna.ts                 # SHA-256(address) > PlanetDNA
│   │   ├── biomes.ts              # 5 biome palettes
│   │   ├── planetBuilder.ts       # IcosahedronGeometry + FBm + assets
│   │   ├── boar.ts                # Veipr fauna (1–3 boars)
│   │   ├── flyers.ts              # birds / drones / bats
│   │   ├── energon.ts             # feeding + raycast deploy
│   │   ├── ships.ts               # Rocket / UFO / Asteroid
│   │   ├── spaceBackdrop.ts       # nebula + starfield
│   │   └── audio.ts               # Web Audio (ambient / ping / boom)
│   ├── stores/cosmos.ts           # Pinia: focus, dna, stats, polling
│   ├── services/
│   │   ├── sth.ts                 # SmartHoldem nodes (failover, /api/wallets)
│   │   └── wallet.ts              # Prime Wallet (window.smartholdem)
│   ├── composables/
│   │   ├── useShare.ts            # native share / clipboard + capture
│   │   └── usePwaInstall.ts       # beforeinstallprompt interceptor
│   ├── components/hud/            # every HTML overlay (TopBar, MobileDock, …)
│   └── utils/
│       ├── capture.ts             # photo mode + 1080x1920 stories poster
│       └── format.ts              # K/M balance formatter
├── scripts/
│   └── fork-chain.mjs             # CLI installer for multi-chain adapters
└── index.html                     # PWA + OG meta + crawler injection hook

/smartholdem-cosmic-sandbox/templates/services/           # drop-in blockchain adapter templates
├── eth.ts.template                # any EVM chain
├── sol.ts.template                # Solana
├── tron.ts.template               # TRON
└── README.md                      # adapter contract documentation

3. Running locally

cd smartholdem-cosmic-sandbox
yarn install
yarn start   # vite --port 3000 --host 0.0.0.0

Production build

yarn build
# dist/ - static bundle, drop behind any Nginx / CDN

Environment variables

Variable Description Default
VITE_STH_NODES CSV list of SmartHoldem nodes (failover) https://node1.smartholdem.io,https://node2.smartholdem.io

File: //.env. No secrets - only public URLs.

Supervisor (production preview)

[program:cosmos]
command=yarn start
directory=/
autostart=true
autorestart=true

Hot reload works out of the box. A restart is only required when .env changes or new dependencies are installed: sudo supervisorctl restart cosmos.


4. Key modules

4.1 Planet DNA - src/core/dna.ts

PlanetDNA is a deterministic object derived from an address via SHA-256:

import b4a from 'b4a'

export async function deriveDNA(seed: string): Promise<PlanetDNA> {
  const data = b4a.from(seed, 'utf-8')
  const hash = await crypto.subtle.digest('SHA-256', data)
  const bytes = new Uint8Array(hash)
  return {
    biome:       bytes[0] % 5,                          // 0..4
    noiseSeed:  (bytes[1] << 8) | bytes[2],             // uint16
    hue:         bytes[3] / 255,                        // 0..1
    oceanLevel:  0.3 + (bytes[4] / 255) * 0.25,
    amplitude:   0.1 + (bytes[5] / 255) * 0.14,
    frequency:   1.1 + (bytes[6] / 255) * 1.7,
    ringTilt:   ((bytes[7] / 255) * 0.6 - 0.3) + 0.4,
    cloudiness:  bytes[8] / 255,
    bytes,
  }
}

One address = one planet on every device. Reproducible for sharing.

4.2 Procedural terrain - src/core/planetBuilder.ts

function makeNoise(dna: PlanetDNA) {
  const noise = new FastNoiseLite(dna.noiseSeed)
  noise.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2)
  noise.SetFractalType(FastNoiseLite.FractalType.FBm)
  noise.SetFractalOctaves(5)
  noise.SetFractalLacunarity(2.1)
  noise.SetFractalGain(0.52)
  const f = dna.frequency
  return (x: number, y: number, z: number) => noise.GetNoise(x * f, y * f, z * f)
}

Geometry: IcosahedronGeometry(1, detail).toNonIndexed() - non-indexed is mandatory so adjacent triangles get their own vertex colours (facet-style low-poly look). LOD slider 4..7 = subdivision 20..64.

4.3 Store (Pinia) - src/stores/cosmos.ts

State: focusSeed, dna, stats, transactions, neighbors, liveTxs, photoMode, evolving, and more.

Main actions:

  • init() - restore a Prime Wallet session or bootstrap the guest genesis.
  • randomizeGuest() - random WILD-XXXX seed with mocked stats.
  • setAddress(address) - fetch wallet + tx + DNA, start polling.
  • connectWallet() - window.smartholdem.getAccount() (single approval).
  • startPolling() / pollOnce() - every 10 s, delta by knownTxIds.

4.4 THREE object cache

THREE instances must never be reactive (Vue's proxy breaks the scene graph). Everything goes through markRaw():

const planetCache = new Map<string, BuiltPlanet>()

export function getOrBuildPlanet(dna, stats, opts): BuiltPlanet {
  const key = `${dna.seedHex}|${Math.round(stats.balanceSTH)}|${stats.txCount}|${opts.detail ?? 42}`
  let built = planetCache.get(key)
  if (!built) {
    built = markRaw(buildPlanet(dna, stats, opts))
    planetCache.set(key, built)
    if (planetCache.size > 40) planetCache.delete(planetCache.keys().next().value)
  }
  return built
}

LRU capped at 40 planets - a balance between memory and back-navigation speed.


5. SmartHoldem API

Every call is a direct fetch from the browser (CORS is open on the nodes):

Endpoint Description
GET /api/wallets/{address} balance, nonce, publicKey
GET /api/wallets/{address}/transactions?page=1&limit=N&orderBy=timestamp:desc history
GET /api/wallets/{address}/transactions?orderBy=timestamp:asc&limit=1 first transaction (hold-time)

fetchWallet, fetchTransactions, fetchFirstTxTimestamp all fail over between nodes via nodeGet<T>(path). Timeout: 12 s.

Address validation

export function isValidSthAddress(addr: string): boolean {
  return /^S[1-9A-HJ-NP-Za-km-z]{33}$/.test(addr.trim())
}

Length 34, prefix S, Base58 without ambiguous characters (0, O, I, l).


6. Routing

routes: [
  { path: '/',                 name: 'sandbox', component: SandboxPage },
  { path: '/planet/:address',  name: 'planet',  component: SandboxPage },
  { path: '/galaxy',           name: 'galaxy',  component: GalaxyPage },
  { path: '/:pathMatch(.*)*',  redirect: '/' },                          // catch-all
]

Resolver in App.vue (resolvePlanetRoute):

  1. Valid :address > store.setAddress() immediately on mount (no guest flash).
  2. Invalid address - fall back to the Prime Wallet session > genesis init, router.replace('/'), notice.
  3. The TresJS loop is guarded by !store.dna || store.loading in SandboxPage.vue - it never crashes on null.

7. PWA

Manifest

public/manifest.webmanifest - display: fullscreen with a standalone fallback, icons 192 / 512 / 512-maskable + apple-touch.

Service worker

public/sw.js - network-first for navigations (with an offline shell), cache-first for static assets. Registered in main.ts:

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js').catch(() => {})
  })
}

Manual install interceptor

The composables/usePwaInstall.ts composable catches beforeinstallprompt, hides the Chrome mini-infobar and exposes a button with data-testid="pwa-install-btn" (mobile) / pwa-install-desktop-btn (desktop). On accepted it shows a success toast and removes the button.


8. SEO / OG cards

Static base

index.html ships a full Open Graph + Twitter Card set. The default og:image is /og-default.jpg (1200×630, neon planet + logo).

Dynamic injection (server-side)

For /planet/:address an external edge worker is expected (outside the frontend) that:

  1. Intercepts requests by User-Agent: TelegramBot, Twitterbot, facebookexternalhit, Discordbot, Slackbot, Pinterestbot, etc.
  2. Validates the address with ^S[A-HJ-NP-Za-km-z1-9]{33}$.
  3. Fans out to https://node2.smartholdem.io/api/wallets/{address} with a 1.5 s timeout.
  4. String-replaces the markers OG:TITLE, OG:DESCRIPTION, OG:IMAGE, OG:URL (see inline docs in index.html).
  5. Caches the result for 5 min per address.

Without the server hook, crawlers see the default OG tags - that is still valid for the home page and /galaxy.


9. Photo Mode and viral formats

utils/capture.ts exports:

Function Format Purpose
captureWithWatermark() Canvas aspect ratio, JPEG Classic share image with a corner QR
watermarkedBlob() Same, Blob Web Share API on mobile
captureStoriesPoster() 1080×1920 JPEG Instagram / Telegram / Twitter Stories
storiesPosterBlob() Same, Blob Native share with a vertical poster

Every temporary canvas is released via width = height = 0 after toDataURL / toBlob.

Text auto-fit

Title and seed strings go through c.measureText() and the font shrinks down to 55 % of the base until the text fits to the left of the QR. On portrait canvases the · WILD WORLD suffix is dropped entirely.


10. Prime Wallet (Web3)

The extension injects window.smartholdem. The wrapper in services/wallet.ts waits for the provider (smartholdem#initialized event) with a ~600 ms timeout. A single getAccount() call > one Prime Wallet modal > signed challenge > session stored in localStorage.

Important: never auto-call getAccount() on mount - otherwise the extension opens the overlay on every load and the UX becomes spammy.

If the extension is missing the app happily runs in Guest Mode on a random seed.


11. Performance

  • InstancedMesh for every asset (trees / crystals / spikes / mesas / towers / asteroids in the ring).
  • LRU cache for planets (40 slots).
  • LOD slider 4..7 (icosahedron subdivision 20..64).
  • DPR clamp: <TresCanvas :dpr="[1, 2]" />.
  • preserve-drawing-buffer enabled only for Photo Mode (otherwise a canvas-captured buffer would be empty).
  • shallowRef for THREE objects in Vue (no deep reactivity).
  • Poll throttle: 10 s, delta by knownTxIds, early break when focusSeed !== address.

12. Testing

data-testid attributes are present on every interactive element:

app-root, planet-sandbox-page, search-address-input, search-submit-btn,
connect-wallet-btn, connected-wallet-btn, disconnect-wallet-btn,
pwa-install-btn, pwa-install-desktop-btn, pwa-install-toast,
share-btn, share-toast, photo-mode-btn, capture-photo-btn,
capture-stories-btn, exit-photo-btn, audio-toggle-btn,
replay-evolution-btn, regenerate-planet-btn, energon-btn,
dock-planet-btn, dock-galaxy-btn, dock-photo-btn, dock-share-btn,
dock-audio-btn, mobile-info-fab, mobile-dock, ...

E2E: Playwright. No backend tests (there is no backend).

Test address: SSU6TvycebBMTvc8WHiKTfG9xmX1QSniZu.


13. Deployment

Production deploys are done manually on the server, no CI/CD automation:

cd smartholdem-cosmic-sandbox
yarn build           # > dist/
rsync -avz dist/ user@server:/var/www/sandbox/

Nginx must:

  • Serve dist/ statically.
  • Fall back all unknown routes to /index.html (SPA).
  • Serve sw.js, manifest.webmanifest, icons/*, og-default.jpg with the correct Content-Type.
  • (optional) Run the crawler-injection edge worker described in §8.

14. Design tokens

The palette is a cyberpunk dashboard over low-poly 3D dioramas:

--space-base:    #0B0F19
--space-panel:   #10182a
--neon-cyan:     #00F0FF
--neon-green:    #00FF66
--neon-gold:     #F2A900
--steel-light:   #b3c5d6
--steel-muted:   #6c7a87

Fonts: Unbounded (display), Outfit (body), JetBrains Mono (HUD / seed / coords).


15. Roadmap

P1: Pet Lair feeding, camera transition animations between worlds P2: smart-contract integrations, dynamic OG renderer endpoint, iOS A2HS guide


SmartHoldem Blockchain Tiny Planets

16. Adapting to other blockchains

The Cosmic Sandbox architecture isolates everything chain-specific in one file - src/services/sth.ts. All the other code (DNA, terrain, biomes, boars, photo mode, PWA) is pure WebGL/Vue and has no idea which address it has been handed.

🚀 Starter kit: yarn fork:chain <chain>

Ready-to-use adapters for the three biggest ecosystems ship in the repo under /templates/services/{eth,sol,tron}.ts.template. A single command turns Cosmic Sandbox into a visualiser for the network of your choice:

cd smartholdem-cosmic-sandbox
yarn fork:chain ethereum   # or: polygon / arbitrum / base / bsc / optimism
yarn fork:chain solana
yarn fork:chain tron

The scripts/fork-chain.mjs script:

  1. backs up the current src/services/sth.ts > sth.ts.bak;
  2. drops the matching template in its place (fully interface-compatible);
  3. appends the required VITE_* variables to smartholdem-cosmic-sandbox/.env.local (idempotently);
  4. prints a demo address for a quick smoke test (e.g. vitalik.eth for EVM).

After that just fill in a real RPC key and run yarn dev - planets, ships, photo mode and Stories export keep working without a single change to the 3D code.

*.ts.template file layout

Templates live in /templates/services/ (outside smartholdem-cosmic-sandbox/src so tsc does not parse them):

File Chain Native unit Address format RPC / API
eth.ts.template Any EVM (ETH, Polygon, Arbitrum, Base, BSC, Optimism) wei (1e18) 0x + 40 hex JSON-RPC eth_getBalance / eth_getTransactionCount + Etherscan-compatible txlist
sol.ts.template Solana lamport (1e9) Base58, 32–44 chars Pure JSON-RPC (getBalance, getSignaturesForAddress, getTransaction) - no SDK
tron.ts.template TRON SUN (1e6) Base58Check, T… 34 chars TronGrid v1 REST (/v1/accounts/{addr}, /transactions)

Every template:

  1. Strictly matches the sth.ts contract - same export names, same types, same error-handling style (return null / empty array instead of throwing). The DNA engine reads a template one-to-one.
  2. Has no heavy dependencies - only axios (already in the project). No ethers / @solana/web3.js / tronweb; everything is raw JSON-RPC / REST so the bundle stays small.
  3. Documents itself in the file header: which VITE_* variables to read, where to get an RPC key, what the native unit is, links to free public endpoints.
  4. Keeps the balanceSTH / amountSTH / SMARTOSHI names for interop with the rest of the codebase - these values are interpreted as “native units” (ETH, SOL, TRX) without renaming anything downstream.

smartholdem-cosmic-sandbox/scripts/fork-chain.mjs is a thin CLI installer:

  • accepts aliases (polygon / arbitrum / base / bsc / optimism > ethereum, sol > solana, trx > tron);
  • takes a one-time backup of sth.ts > sth.ts.bak (subsequent runs never clobber the original);
  • appends a VITE_* block to .env.local with the marker # cosmic-sandbox:<chain> - a second run will not duplicate it (idempotent);
  • prints a demo address for a smoke test (vitalik.eth, a well-known Solana whale, a well-known TRX contract).

Want a new chain (Cosmos, Aptos, Sui, TON, Bitcoin)? Copy any of the three templates, rename the file, swap out the RPC logic and the isValidSthAddress() regex, then add an entry to the CHAINS object inside fork-chain.mjs. Nothing else in the project needs to change.

The manual path

For an exotic chain (Cosmos, Aptos, Sui, TON…) without going through a template:

  1. Replace services/sth.ts with services/<chain>.ts implementing the same contract.
  2. Tune isValidSthAddress() to match the target chain's address format.
  3. (optional) Rebalance RING_THRESHOLD, MOON_THRESHOLD, FLYER_THRESHOLD for the new network's economics.
  4. Replace the wallet wrapper in services/wallet.ts (Prime Wallet > MetaMask / Phantom).

Service contract

Any adapter must export:

export interface WalletInfo { address: string; balanceSTH: number; nonce: number }
export interface TxInfo { id: string; sender: string; recipient: string; amountSTH: number; timestamp: number }

export function isValidSthAddress(addr: string): boolean
export async function fetchWallet(address: string): Promise<WalletInfo | null>
export async function fetchTransactions(address: string, limit?: number): Promise<{ txs: TxInfo[]; totalCount: number }>
export async function fetchFirstTxTimestamp(address: string): Promise<number>
export function extractCounterparties(txs: TxInfo[], focus: string, max?: number): string[]

Keep the balanceSTH field name as is for compatibility with the rest of the code (it is just a number in native units - ETH, SOL, and so on). The same goes for amountSTH.


16.1 Ethereum (ethers v6)

yarn add ethers

src/services/eth.ts:

import { JsonRpcProvider, formatEther, isAddress } from 'ethers'

const RPCS: string[] = (import.meta.env.VITE_ETH_RPCS || '')
  .split(',').map(s => s.trim()).filter(Boolean)

const provider = new JsonRpcProvider(RPCS[0]) // (failover via FallbackProvider)

export interface WalletInfo { address: string; balanceSTH: number; nonce: number }
export interface TxInfo { id: string; sender: string; recipient: string; amountSTH: number; timestamp: number }

export function isValidSthAddress(addr: string): boolean {
  return isAddress(addr.trim()) // EIP-55 checksum-tolerant
}

export async function fetchWallet(address: string): Promise<WalletInfo | null> {
  try {
    const [balance, nonce] = await Promise.all([
      provider.getBalance(address),
      provider.getTransactionCount(address),
    ])
    return {
      address,
      balanceSTH: Number(formatEther(balance)),
      nonce,
    }
  } catch { return null }
}

// Ethereum RPC does not expose per-address history without an indexer.
// Use the Etherscan API, Alchemy `alchemy_getAssetTransfers` or Covalent.
export async function fetchTransactions(
  address: string,
  limit = 30,
): Promise<{ txs: TxInfo[]; totalCount: number }> {
  const key = import.meta.env.VITE_ETHERSCAN_KEY
  const url = `https://api.etherscan.io/api?module=account&action=txlist&address=${address}` +
              `&startblock=0&endblock=99999999&page=1&offset=${limit}&sort=desc&apikey=${key}`
  try {
    const res = await fetch(url).then(r => r.json())
    const txs: TxInfo[] = (res.result || []).map((t: any) => ({
      id: t.hash,
      sender: t.from,
      recipient: t.to,
      amountSTH: Number(t.value) / 1e18,
      timestamp: Number(t.timeStamp),
    }))
    return { txs, totalCount: txs.length }
  } catch {
    return { txs: [], totalCount: 0 }
  }
}

export async function fetchFirstTxTimestamp(address: string): Promise<number> {
  const key = import.meta.env.VITE_ETHERSCAN_KEY
  const url = `https://api.etherscan.io/api?module=account&action=txlist&address=${address}` +
              `&page=1&offset=1&sort=asc&apikey=${key}`
  try {
    const res = await fetch(url).then(r => r.json())
    return Number(res.result?.[0]?.timeStamp || 0)
  } catch { return 0 }
}

export function extractCounterparties(txs: TxInfo[], focus: string, max = 10): string[] {
  const set = new Set<string>()
  const f = focus.toLowerCase()
  for (const tx of txs) {
    const other = (tx.sender.toLowerCase() === f ? tx.recipient : tx.sender).toLowerCase()
    if (other && other !== f) set.add(other)
    if (set.size >= max) break
  }
  return [...set]
}

export const SMARTOSHI = 1e18 // wei > ETH

Key differences from SmartHoldem:

  • EVM address - 0x + 40 hex characters; use isAddress() from ethers (it accepts both checksummed and lowercase).
  • wei > ETH - the divisor is 1e18, not 1e8.
  • No native per-address history - you need an Etherscan / Alchemy / Covalent key. This changes UX: add rate-limit handling.
  • Wallet wrapper: replace awaitPrime() with the standard window.ethereum.request({ method: 'eth_requestAccounts' }).

.env additions:

VITE_ETH_RPCS=https://eth.llamarpc.com,https://rpc.ankr.com/eth
VITE_ETHERSCAN_KEY=your_key_here

16.2 Solana (@solana/web3.js)

yarn add @solana/web3.js

src/services/sol.ts:

import { Connection, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js'

const RPCS: string[] = (import.meta.env.VITE_SOL_RPCS || '')
  .split(',').map(s => s.trim()).filter(Boolean)

const conn = new Connection(RPCS[0], 'confirmed')

export interface WalletInfo { address: string; balanceSTH: number; nonce: number }
export interface TxInfo { id: string; sender: string; recipient: string; amountSTH: number; timestamp: number }

export function isValidSthAddress(addr: string): boolean {
  try {
    const pk = new PublicKey(addr.trim())
    return PublicKey.isOnCurve(pk.toBytes()) // rejects PDAs, keeps real wallets
  } catch { return false }
}

export async function fetchWallet(address: string): Promise<WalletInfo | null> {
  try {
    const pk = new PublicKey(address)
    const lamports = await conn.getBalance(pk)
    return {
      address,
      balanceSTH: lamports / LAMPORTS_PER_SOL,
      nonce: 0, // Solana has no EVM-style nonce counter
    }
  } catch { return null }
}

export async function fetchTransactions(
  address: string,
  limit = 30,
): Promise<{ txs: TxInfo[]; totalCount: number }> {
  try {
    const pk = new PublicKey(address)
    const sigs = await conn.getSignaturesForAddress(pk, { limit })
    // Fetch full tx and parse SystemProgram transfer instructions.
    const txs: TxInfo[] = []
    for (const s of sigs) {
      const tx = await conn.getParsedTransaction(s.signature, { maxSupportedTransactionVersion: 0 })
      if (!tx) continue
      for (const ix of tx.transaction.message.instructions as any[]) {
        if (ix.program === 'system' && ix.parsed?.type === 'transfer') {
          txs.push({
            id: s.signature,
            sender: ix.parsed.info.source,
            recipient: ix.parsed.info.destination,
            amountSTH: Number(ix.parsed.info.lamports) / LAMPORTS_PER_SOL,
            timestamp: s.blockTime ?? 0,
          })
          break
        }
      }
    }
    return { txs, totalCount: sigs.length }
  } catch {
    return { txs: [], totalCount: 0 }
  }
}

export async function fetchFirstTxTimestamp(address: string): Promise<number> {
  try {
    const pk = new PublicKey(address)
    // Paginate to the earliest signature (`before` cursor).
    let before: string | undefined
    let earliest = 0
    for (let i = 0; i < 10; i++) {
      const sigs = await conn.getSignaturesForAddress(pk, { limit: 1000, before })
      if (!sigs.length) break
      const last = sigs[sigs.length - 1]
      earliest = last.blockTime ?? earliest
      if (sigs.length < 1000) break
      before = last.signature
    }
    return earliest
  } catch { return 0 }
}

export function extractCounterparties(txs: TxInfo[], focus: string, max = 10): string[] {
  const set = new Set<string>()
  for (const tx of txs) {
    const other = tx.sender === focus ? tx.recipient : tx.sender
    if (other && other !== focus) set.add(other)
    if (set.size >= max) break
  }
  return [...set]
}

export const SMARTOSHI = LAMPORTS_PER_SOL // 1e9

Key differences from SmartHoldem:

  • Base58 address 32–44 chars - validated through new PublicKey(). The isOnCurve() filter cuts off PDA accounts (program-derived addresses) that have no private key and should not be visualised as “wallets”.
  • lamports > SOL - the divisor is LAMPORTS_PER_SOL (1e9).
  • No nonce - Solana uses a different transaction model; set it to 0 or map it to slot.
  • Transaction history - getSignaturesForAddress() + getParsedTransaction(). That is more RPC-expensive; batches or a Helius / QuickNode indexer will help.
  • Wallet wrapper: swap awaitPrime() for window.phantom?.solana or the Solana Wallet Adapter (@solana/wallet-adapter-react - a Vue fork exists).

.env:

VITE_SOL_RPCS=https://api.mainnet-beta.solana.com,https://solana-mainnet.g.alchemy.com/v2/YOUR_KEY

16.3 What is reused as is

Module Changes
core/dna.ts 0 - SHA-256 works for any address string
core/biomes.ts 0 - biomes can be renamed to match the network's lore
core/planetBuilder.ts 0
core/boar.ts / flyers.ts / energon.ts 0
core/ships.ts possibly retune the Rocket/UFO/Asteroid thresholds
utils/capture.ts change the “SMARTHOLDEM · COSMIC SANDBOX” brand string
composables/usePwaInstall.ts 0
Routing optionally change the /planet/:address regex constraint

A full port to Ethereum or Solana takes one working day for a single developer. That is the main promise of this architecture: the 3D visualisation is not tied to any particular chain.


17. License / contributing

This is a reference implementation for the open SmartHoldem blockchain. Pull requests are welcome. Bug reports via issues; for UX proposals please attach a screenshot or a screen recording.

  • SmartHoldem Cosmic Sandbox · 2026
  • Powered by TechnoL0g

💸 Support / Donate

If this project helped you, feel free to buy me a coffee:

Donate BTC Donate LTC Donate STH

  • Bitcoin (BTC): bc1qcvyh4884hq0g0lfttmxtpk5l4ep459wkclwnkp
  • Litecoin (LTC): M8GJBdUDNivr9eodU7iCX7rk2Zi392RUCb
  • SmartHoldem (STH): SeZLuyhhYf2qxs4ArPJ71oEu3x8EsVw51C

SmartHoldem Blockchain Tiny Planets

Releases

Packages

Contributors

Languages