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
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ RESEND_API_KEY=replace_with_resend_key
# File uploads / Vercel Blob
BLOB_READ_WRITE_TOKEN=replace_with_local_or_preview_blob_token

# Stellar Testnet
# Stellar Testnet (server-side public identifiers and URLs only; never add private keys)
# Mock mode accepts these safe placeholders. Set ENABLE_MOCK_STELLAR=false only
# after replacing issuer, distribution, and contract values with deployed testnet IDs.
STELLAR_NETWORK=testnet
STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org
STELLAR_RPC_URL=https://soroban-testnet.stellar.org
Expand Down
30 changes: 15 additions & 15 deletions app/Providers.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
"use client"
import type { FC, ReactNode } from "react"
import { PrivyProvider } from "@/lib/privy/react-auth"
import { embeddedWalletProviderConfig } from "@/lib/wallet/config"
"use client"

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

export const Providers: FC<{ children: ReactNode }> = ({ children }) => {
return (
<PrivyProvider
appId={privyAppId || ""}
config={embeddedWalletProviderConfig}
>
{children}
</PrivyProvider>
)
}
<PrivyProvider
appId={privyAppId || ""}
config={embeddedWalletProviderConfig}
>
{children}
</PrivyProvider>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { render } from "@testing-library/react"
import { describe, expect, it } from "vitest"

import { StellarActivityPanel } from "@/components/dashboard/investor-overview/stellar-activity-panel"
import { buildStellarReferenceUrl } from "@/lib/stellar/config"
import { buildStellarReferenceUrl } from "@/lib/stellar/display-config"

describe("StellarActivityPanel", () => {
it("shows the empty state when no Stellar account is linked", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Skeleton } from "@/components/ui/skeleton"
import { cn } from "@/lib/utils"
import { getStellarConfig, getStellarNetworkLabel } from "@/lib/stellar/config"
import { getStellarDisplayConfig, getStellarNetworkLabel } from "@/lib/stellar/display-config"
import { createMockStellarActivityFeed, type StellarActivityItem } from "@/lib/stellar/mock-activity"

export interface StellarActivityPanelData {
Expand Down Expand Up @@ -112,7 +112,7 @@ export function StellarActivityPanel({
isRefreshing = false,
className,
}: StellarActivityPanelProps) {
const config = useMemo(() => getStellarConfig(), [])
const config = useMemo(() => getStellarDisplayConfig(), [])
const networkLabel = getStellarNetworkLabel(config.network)

return (
Expand Down Expand Up @@ -266,7 +266,7 @@ export function StellarActivityPanel({
}

export function InvestorStellarActivityPanel({ className }: { className?: string }) {
const config = useMemo(() => getStellarConfig(), [])
const config = useMemo(() => getStellarDisplayConfig(), [])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [data, setData] = useState<StellarActivityPanelData | null>(null)
Expand Down
14 changes: 12 additions & 2 deletions lib/stellar/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"

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

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

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

beforeEach(() => {
process.env.ENABLE_MOCK_STELLAR = "true"
delete process.env.STELLAR_NETWORK
delete process.env.STELLAR_HORIZON_URL
delete process.env.STELLAR_RPC_URL
Expand All @@ -23,6 +31,8 @@ describe("Stellar client helpers", () => {
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(getHorizonUrl(client.config)).toBe("https://horizon-testnet.stellar.org")
expect(getStellarRpcUrl(client.config)).toBe("https://soroban-testnet.stellar.org")
expect(client.networkPassphrase).toBe("Test SDF Network ; September 2015")
})

Expand All @@ -37,6 +47,6 @@ describe("Stellar client helpers", () => {
it("rejects invalid network selections", () => {
process.env.STELLAR_NETWORK = "futurenet"

expect(() => getStellarClient()).toThrow(/Invalid Stellar network/)
expect(() => getStellarClient()).toThrow(/Unsupported STELLAR_NETWORK/)
})
})
8 changes: 8 additions & 0 deletions lib/stellar/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ export function getStellarNetworkPassphrase(network: StellarNetwork = getStellar
return parseStellarNetwork(network) === "mainnet" ? Networks.PUBLIC : Networks.TESTNET
}

export function getHorizonUrl(config: StellarConfig = getStellarConfig()): string {
return config.horizonUrl
}

export function getStellarRpcUrl(config: StellarConfig = getStellarConfig()): string {
return config.rpcUrl
}

/** Creates a Horizon client using ChainMove's selected Stellar network. */
export function getHorizonServer(config: StellarConfig = getStellarConfig()): Horizon.Server {
return new Horizon.Server(config.horizonUrl)
Expand Down
136 changes: 46 additions & 90 deletions lib/stellar/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,116 +1,72 @@
import { describe, expect, it, beforeEach, afterEach } from "vitest"
import { getStellarConfig, parseStellarNetwork } from "./config"

describe("getStellarConfig", () => {
const originalEnv = { ...process.env }
import { describe, expect, it } from "vitest"

beforeEach(() => {
// Clear out related env vars to test defaults and ensure test isolation
const keysToRemove = [
"STELLAR_NETWORK",
"STELLAR_HORIZON_URL",
"STELLAR_RPC_URL",
"RPC_URL",
"STELLAR_ASSET_CODE",
"STELLAR_ISSUER_PUBLIC_KEY",
"STELLAR_DISTRIBUTION_PUBLIC_KEY",
"STELLAR_CONTRACT_ID",
"STELLAR_EXPLORER_BASE_URL",
"NEXT_PUBLIC_STELLAR_DEMO_PUBLIC_KEY",
"STELLAR_DEMO_PUBLIC_KEY",
"CHAINMOVE_CA",
"ENABLE_MOCK_STELLAR",
]
keysToRemove.forEach((key) => {
delete process.env[key]
})
})

afterEach(() => {
// Restore original env vars after each test
process.env = { ...originalEnv }
})
import { getStellarConfig, parseStellarNetwork } from "./config"

it("should return default testnet configuration when no env override is present", () => {
const config = getStellarConfig()
const VALID_PUBLIC_KEY = "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H"
const VALID_CONTRACT_SHAPE = `C${"A".repeat(55)}`

expect(config).toEqual({
describe("getStellarConfig", () => {
it("lets mock mode use testnet defaults without deployment identifiers", () => {
expect(getStellarConfig({ ENABLE_MOCK_STELLAR: "true" })).toEqual({
network: "testnet",
horizonUrl: "https://horizon-testnet.stellar.org",
rpcUrl: "https://soroban-testnet.stellar.org",
assetCode: "CMOVE",
issuerPublicKey: "",
distributionPublicKey: "",
contractId: "",
explorerBaseUrl: "https://stellar.expert/explorer/testnet",
mock: false,
demoPublicKey: "GABCDMOCKSTELLARPUBLICKEYTESTNET000000000000000000000000000000",
mock: true,
})
})

it("should resolve mainnet defaults and custom environment overrides", () => {
process.env.STELLAR_NETWORK = "mainnet"
process.env.STELLAR_ASSET_CODE = "TEST"
process.env.STELLAR_ISSUER_PUBLIC_KEY = "GD123..."
process.env.STELLAR_DISTRIBUTION_PUBLIC_KEY = "GD456..."
process.env.STELLAR_CONTRACT_ID = "C123..."

const config = getStellarConfig()

expect(config).toEqual({
network: "mainnet",
horizonUrl: "https://horizon.stellar.org",
rpcUrl: "https://soroban-mainnet.stellar.org",
assetCode: "TEST",
issuerPublicKey: "GD123...",
distributionPublicKey: "GD456...",
contractId: "C123...",
explorerBaseUrl: "https://stellar.expert/explorer/public",
mock: false,
demoPublicKey: "GABCDMOCKSTELLARPUBLICKEYTESTNET000000000000000000000000000000",
})
})

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("names a missing required field when mock mode is disabled", () => {
expect(() => getStellarConfig({ ENABLE_MOCK_STELLAR: "false" })).toThrow(
"Missing required Stellar configuration: STELLAR_ISSUER_PUBLIC_KEY",
)
})

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"

const config = getStellarConfig()
it("returns configured URLs for a valid live configuration", () => {
const config = getStellarConfig({
STELLAR_NETWORK: "testnet",
STELLAR_HORIZON_URL: "https://example.com/horizon",
STELLAR_RPC_URL: "https://example.com/rpc",
STELLAR_ISSUER_PUBLIC_KEY: VALID_PUBLIC_KEY,
STELLAR_DISTRIBUTION_PUBLIC_KEY: VALID_PUBLIC_KEY,
STELLAR_CONTRACT_ID: VALID_CONTRACT_SHAPE,
ENABLE_MOCK_STELLAR: "false",
})

expect(config.rpcUrl).toBe("https://fallback-rpc.stellar.org")
expect(config.contractId).toBe("CC_FALLBACK_123")
expect(config.horizonUrl).toBe("https://example.com/horizon")
expect(config.rpcUrl).toBe("https://example.com/rpc")
})

it("should prioritize STELLAR_ environment variables over their fallback counterparts", () => {
process.env.STELLAR_RPC_URL = "https://stellar-rpc.org"
process.env.RPC_URL = "https://fallback-rpc.org"

process.env.STELLAR_CONTRACT_ID = "C_STELLAR_1"
process.env.CHAINMOVE_CA = "C_FALLBACK_1"

const config = getStellarConfig()
it("selects mainnet endpoint defaults", () => {
const config = getStellarConfig({
STELLAR_NETWORK: "mainnet",
ENABLE_MOCK_STELLAR: "true",
})

expect(config.rpcUrl).toBe("https://stellar-rpc.org")
expect(config.contractId).toBe("C_STELLAR_1")
expect(config.horizonUrl).toBe("https://horizon.stellar.org")
expect(config.rpcUrl).toBe("https://soroban-mainnet.stellar.org")
})

it("should support mock mode when ENABLE_MOCK_STELLAR is 'true'", () => {
process.env.ENABLE_MOCK_STELLAR = "true"
expect(getStellarConfig().mock).toBe(true)
it("rejects unsupported networks even in mock mode", () => {
expect(() =>
getStellarConfig({ STELLAR_NETWORK: "futurenet", ENABLE_MOCK_STELLAR: "true" }),
).toThrow('Unsupported STELLAR_NETWORK: "futurenet"')
expect(() => parseStellarNetwork("futurenet")).toThrow(/Unsupported STELLAR_NETWORK/)
})

it("should not enable mock mode when ENABLE_MOCK_STELLAR is not 'true'", () => {
process.env.ENABLE_MOCK_STELLAR = "false"
expect(getStellarConfig().mock).toBe(false)
it("allows placeholders only in mock mode", () => {
const placeholders = {
STELLAR_ISSUER_PUBLIC_KEY: "replace_with_public_key",
STELLAR_DISTRIBUTION_PUBLIC_KEY: "replace_with_public_key",
STELLAR_CONTRACT_ID: "replace_after_deployment",
}

delete process.env.ENABLE_MOCK_STELLAR
expect(getStellarConfig().mock).toBe(false)
expect(getStellarConfig({ ...placeholders, ENABLE_MOCK_STELLAR: "true" }).mock).toBe(true)
expect(() => getStellarConfig({ ...placeholders, ENABLE_MOCK_STELLAR: "false" })).toThrow(
"STELLAR_ISSUER_PUBLIC_KEY",
)
})
})
Loading
Loading