Skip to content
Open
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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,11 @@ npm run dev
- `PAYSTACK_SECRET_KEY` is required for wallet funding flows.
- `PAYSTACK_DVA_PREFERRED_BANK` optionally overrides the bank slug used when provisioning Paystack dedicated virtual accounts. For test keys, the app defaults to `test-bank`.
- `RESEND_API_KEY` is only needed for email features.
- Stellar variables are not required by the current code yet. The expected integration pass should introduce explicit values such as `STELLAR_NETWORK`, `STELLAR_HORIZON_URL`, `STELLAR_RPC_URL`, issuer account public keys, distribution account public keys, and contract IDs.
- Stellar configuration is centralized in `lib/stellar/config.ts`. For local UI development, set `ENABLE_MOCK_STELLAR=true`; missing or placeholder deployment identifiers are then accepted.
- With mock mode disabled, `STELLAR_ISSUER_PUBLIC_KEY`, `STELLAR_DISTRIBUTION_PUBLIC_KEY`, and `STELLAR_CONTRACT_ID` are required and validated. `STELLAR_NETWORK` accepts `testnet` or `mainnet`.
- Testnet defaults are `https://horizon-testnet.stellar.org`, `https://soroban-testnet.stellar.org`, and the `CMOVE` asset code. These can be overridden with the variables in `.env.example`.
- This configuration contains public keys and service URLs only. Stellar private/secret keys must never be added to environment examples, committed, or required by this layer.
- Future mainnet deployments use the same variables with `STELLAR_NETWORK=mainnet`; mainnet Horizon, RPC, and network passphrase defaults are selected automatically.

## Driver Dedicated Repayment Accounts

Expand Down
4 changes: 2 additions & 2 deletions app/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { liskSepolia } from "viem/chains"

import { PrivyProvider } from "@/lib/privy/react-auth"
import { WalletProvider } from "@/contexts/wallet-context"
import { getStellarConfig } from "@/lib/stellar/config"
import { getStellarDisplayConfig } from "@/lib/stellar/display-config"
import type { WalletNetwork } from "@/types/wallet"

const privyAppId = process.env.NEXT_PUBLIC_PRIVY_APP_ID

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

Expand Down
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
26 changes: 26 additions & 0 deletions lib/stellar/client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, expect, it } from "vitest"

import type { StellarConfig } from "./config"
import { getHorizonUrl, getStellarNetworkPassphrase, getStellarRpcUrl } from "./client"

const config: StellarConfig = {
network: "testnet",
horizonUrl: "https://example.com/horizon",
rpcUrl: "https://example.com/rpc",
assetCode: "CMOVE",
issuerPublicKey: "",
distributionPublicKey: "",
contractId: "",
mock: true,
}

describe("Stellar client utilities", () => {
it("returns configured endpoints", () => {
expect(getHorizonUrl(config)).toBe("https://example.com/horizon")
expect(getStellarRpcUrl(config)).toBe("https://example.com/rpc")
})

it("returns the network passphrase", () => {
expect(getStellarNetworkPassphrase(config)).toBe("Test SDF Network ; September 2015")
})
})
20 changes: 20 additions & 0 deletions lib/stellar/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { getStellarConfig, type StellarConfig, type StellarNetwork } from "./config"

const NETWORK_PASSPHRASES: Record<StellarNetwork, string> = {
testnet: "Test SDF Network ; September 2015",
mainnet: "Public Global Stellar Network ; September 2015",
}

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

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

export function getStellarNetworkPassphrase(
config: Pick<StellarConfig, "network"> = getStellarConfig(),
): string {
return NETWORK_PASSPHRASES[config.network]
}
119 changes: 38 additions & 81 deletions lib/stellar/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,104 +1,61 @@
import { describe, expect, it, beforeEach, afterEach } from "vitest"
import { getStellarConfig } from "./config"

describe("getStellarConfig", () => {
const originalEnv = { ...process.env }

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",
"CHAINMOVE_CA",
"ENABLE_MOCK_STELLAR",
]
keysToRemove.forEach((key) => {
delete process.env[key]
})
})
import { describe, expect, it } from "vitest"

afterEach(() => {
// Restore original env vars after each test
process.env = { ...originalEnv }
})
import { getStellarConfig } 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 defaults without deployment identifiers", () => {
expect(getStellarConfig({ ENABLE_MOCK_STELLAR: "true" })).toMatchObject({
network: "testnet",
horizonUrl: "https://horizon-testnet.stellar.org",
rpcUrl: "https://soroban-testnet.stellar.org",
assetCode: "CMOVE",
issuerPublicKey: "",
distributionPublicKey: "",
contractId: "",
mock: false,
mock: true,
})
})

it("should return configuration with 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..."
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...",
mock: false,
})
})

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()

expect(config.rpcUrl).toBe("https://fallback-rpc.stellar.org")
expect(config.contractId).toBe("CC_FALLBACK_123")
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 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("returns configured testnet 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://stellar-rpc.org")
expect(config.contractId).toBe("C_STELLAR_1")
expect(config.horizonUrl).toBe("https://example.com/horizon")
expect(config.rpcUrl).toBe("https://example.com/rpc")
})

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"')
})

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