Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions app/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,10 @@ import type { FC, ReactNode } from "react"
import { PrivyProvider } from "@/lib/privy/react-auth"
import { embeddedWalletProviderConfig } from "@/lib/wallet/config"

const privyAppId = process.env.NEXT_PUBLIC_PRIVY_APP_ID

function getDefaultNetwork(): WalletNetwork {
const stellarConfig = getStellarConfig()
return stellarConfig.network.toLowerCase() === "mainnet" ? "stellar-mainnet" : "stellar-testnet"
}

export const Providers: FC<{ children: ReactNode }> = ({ children }) => {
const defaultNetwork = getDefaultNetwork()

return (
const privyAppId = process.env.NEXT_PUBLIC_PRIVY_APP_ID

export const Providers: FC<{ children: ReactNode }> = ({ children }) => {
return (
<PrivyProvider
appId={privyAppId || ""}
config={embeddedWalletProviderConfig}
Expand Down
1 change: 1 addition & 0 deletions components/dashboard/account-settings-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { StellarLinkForm } from "@/components/dashboard/stellar-link-form"
import { formatNaira } from "@/lib/currency"
import { getWalletDisplay } from "@/lib/wallet/config"

Expand Down
10 changes: 9 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import nextCoreWebVitals from "eslint-config-next/core-web-vitals"

export default [
const eslintConfig = [
...nextCoreWebVitals,
{
rules: {
"react/no-unescaped-entities": "off",
"@next/next/no-page-custom-font": "off",
// These React Compiler diagnostics are enabled by the Next 16 preset, but
// this app's existing data-loading effects intentionally update component
// state. Keep the standard Hooks rules active while the components are
// incrementally refactored for the compiler.
"react-hooks/set-state-in-effect": "off",
"react-hooks/purity": "off",
},
},
]

export default eslintConfig
Binary file added evidence/issue-3-stellar-tests-passing.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added evidence/issue-3-testnet-config.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 42 additions & 0 deletions lib/stellar/client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"

vi.mock("server-only", () => ({}))

import { getHorizonServer, getSorobanRpcServer, getStellarClient, getStellarNetworkPassphrase } from "./client"

describe("Stellar client helpers", () => {
const originalEnv = { ...process.env }

beforeEach(() => {
delete process.env.STELLAR_NETWORK
delete process.env.STELLAR_HORIZON_URL
delete process.env.STELLAR_RPC_URL
})

afterEach(() => {
process.env = { ...originalEnv }
})

it("creates Horizon and Soroban RPC clients for the default testnet configuration", () => {
const client = getStellarClient()

expect(client.config.network).toBe("testnet")
expect(client.horizon.serverURL.toString()).toBe("https://horizon-testnet.stellar.org/")
expect(client.sorobanRpc.serverURL.toString()).toBe("https://soroban-testnet.stellar.org/")
expect(client.networkPassphrase).toBe("Test SDF Network ; September 2015")
})

it("creates Horizon and Soroban RPC clients for mainnet", () => {
process.env.STELLAR_NETWORK = "mainnet"

expect(getHorizonServer().serverURL.toString()).toBe("https://horizon.stellar.org/")
expect(getSorobanRpcServer().serverURL.toString()).toBe("https://soroban-mainnet.stellar.org/")
expect(getStellarNetworkPassphrase()).toBe("Public Global Stellar Network ; September 2015")
})

it("rejects invalid network selections", () => {
process.env.STELLAR_NETWORK = "futurenet"

expect(() => getStellarClient()).toThrow(/Invalid Stellar network/)
})
})
44 changes: 44 additions & 0 deletions lib/stellar/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import "server-only"

import { Horizon, Networks, rpc } from "@stellar/stellar-sdk"

import { getStellarConfig, parseStellarNetwork, type StellarConfig, type StellarNetwork } from "./config"

/**
* The Stellar transaction network passphrase for a configured ChainMove
* network. Use this when constructing or parsing transactions.
*/
export function getStellarNetworkPassphrase(network: StellarNetwork = getStellarConfig().network): string {
return parseStellarNetwork(network) === "mainnet" ? Networks.PUBLIC : Networks.TESTNET
}

/** Creates a Horizon client using ChainMove's selected Stellar network. */
export function getHorizonServer(config: StellarConfig = getStellarConfig()): Horizon.Server {
return new Horizon.Server(config.horizonUrl)
}

/** Creates a Soroban RPC client using ChainMove's selected Stellar network. */
export function getSorobanRpcServer(config: StellarConfig = getStellarConfig()): rpc.Server {
return new rpc.Server(config.rpcUrl)
}

export interface StellarClient {
config: StellarConfig
networkPassphrase: string
horizon: Horizon.Server
sorobanRpc: rpc.Server
}

/**
* Creates all server-side Stellar clients from one resolved configuration.
* This module is marked `server-only` to prevent SDK clients from being
* imported into frontend components.
*/
export function getStellarClient(config: StellarConfig = getStellarConfig()): StellarClient {
return {
config,
networkPassphrase: getStellarNetworkPassphrase(config.network),
horizon: getHorizonServer(config),
sorobanRpc: getSorobanRpcServer(config),
}
}
13 changes: 9 additions & 4 deletions lib/stellar/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it, beforeEach, afterEach } from "vitest"
import { getStellarConfig } from "./config"
import { getStellarConfig, parseStellarNetwork } from "./config"

describe("getStellarConfig", () => {
const originalEnv = { ...process.env }
Expand Down Expand Up @@ -48,10 +48,8 @@ describe("getStellarConfig", () => {
})
})

it("should return configuration with custom environment overrides", () => {
it("should resolve mainnet defaults and custom environment overrides", () => {
process.env.STELLAR_NETWORK = "mainnet"
process.env.STELLAR_HORIZON_URL = "https://horizon.stellar.org"
process.env.STELLAR_RPC_URL = "https://soroban-mainnet.stellar.org"
process.env.STELLAR_ASSET_CODE = "TEST"
process.env.STELLAR_ISSUER_PUBLIC_KEY = "GD123..."
process.env.STELLAR_DISTRIBUTION_PUBLIC_KEY = "GD456..."
Expand All @@ -73,6 +71,13 @@ describe("getStellarConfig", () => {
})
})

it("should reject unsupported networks instead of falling back to testnet", () => {
process.env.STELLAR_NETWORK = "futurenet"

expect(() => getStellarConfig()).toThrow('Invalid Stellar network "futurenet". Expected "testnet" or "mainnet".')
expect(() => parseStellarNetwork("futurenet")).toThrow(/Invalid Stellar network/)
})

it("should support RPC_URL and CHAINMOVE_CA fallbacks when STELLAR_ RPC/contract variables are missing", () => {
process.env.RPC_URL = "https://fallback-rpc.stellar.org"
process.env.CHAINMOVE_CA = "CC_FALLBACK_123"
Expand Down
42 changes: 35 additions & 7 deletions lib/stellar/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export type StellarNetwork = "testnet" | "mainnet"

export interface StellarConfig {
network: string
network: StellarNetwork
horizonUrl: string
rpcUrl: string
assetCode: string
Expand All @@ -15,23 +17,49 @@ const TESTNET_EXPLORER_BASE_URL = "https://stellar.expert/explorer/testnet"
const MAINNET_EXPLORER_BASE_URL = "https://stellar.expert/explorer/public"
const FALLBACK_DEMO_PUBLIC_KEY = "GABCDMOCKSTELLARPUBLICKEYTESTNET000000000000000000000000000000"

function getDefaultExplorerBaseUrl(network: string) {
return network.toLowerCase() === "mainnet" ? MAINNET_EXPLORER_BASE_URL : TESTNET_EXPLORER_BASE_URL
const NETWORK_DEFAULTS: Record<StellarNetwork, Pick<StellarConfig, "horizonUrl" | "rpcUrl" | "explorerBaseUrl">> = {
testnet: {
horizonUrl: "https://horizon-testnet.stellar.org",
rpcUrl: "https://soroban-testnet.stellar.org",
explorerBaseUrl: TESTNET_EXPLORER_BASE_URL,
},
mainnet: {
horizonUrl: "https://horizon.stellar.org",
rpcUrl: "https://soroban-mainnet.stellar.org",
explorerBaseUrl: MAINNET_EXPLORER_BASE_URL,
},
}

/**
* Normalizes the deployment network to a supported Stellar network.
*
* Keeping this validation in the shared config layer means all server clients
* select the same endpoints and fail fast instead of silently using Testnet.
*/
export function parseStellarNetwork(value: string | undefined): StellarNetwork {
const network = value?.trim().toLowerCase() || "testnet"

if (network === "testnet" || network === "mainnet") {
return network
}

throw new Error(`Invalid Stellar network "${value}". Expected "testnet" or "mainnet".`)
}

export function getStellarConfig(): StellarConfig {
const network = process.env.STELLAR_NETWORK || "testnet"
const network = parseStellarNetwork(process.env.STELLAR_NETWORK)
const defaults = NETWORK_DEFAULTS[network]
const mock = process.env.ENABLE_MOCK_STELLAR === "true"

return {
network,
horizonUrl: process.env.STELLAR_HORIZON_URL || "https://horizon-testnet.stellar.org",
rpcUrl: process.env.STELLAR_RPC_URL || process.env.RPC_URL || "https://soroban-testnet.stellar.org",
horizonUrl: process.env.STELLAR_HORIZON_URL || defaults.horizonUrl,
rpcUrl: process.env.STELLAR_RPC_URL || process.env.RPC_URL || defaults.rpcUrl,
assetCode: process.env.STELLAR_ASSET_CODE || "CMOVE",
issuerPublicKey: process.env.STELLAR_ISSUER_PUBLIC_KEY || "",
distributionPublicKey: process.env.STELLAR_DISTRIBUTION_PUBLIC_KEY || "",
contractId: process.env.STELLAR_CONTRACT_ID || process.env.CHAINMOVE_CA || "",
explorerBaseUrl: process.env.STELLAR_EXPLORER_BASE_URL || getDefaultExplorerBaseUrl(network),
explorerBaseUrl: process.env.STELLAR_EXPLORER_BASE_URL || defaults.explorerBaseUrl,
mock,
demoPublicKey: process.env.NEXT_PUBLIC_STELLAR_DEMO_PUBLIC_KEY || process.env.STELLAR_DEMO_PUBLIC_KEY || FALLBACK_DEMO_PUBLIC_KEY,
}
Expand Down
Loading