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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,23 @@ In practical terms:
- Stellar asset issuance, account flows, payout tracking, and Soroban contracts are the next chain layer
- README language, roadmap, and contribution guidance below are written for the Stellar path

### Wallet modes

Wallet and chain assumptions live in `lib/wallet/config.ts`; UI-facing wallet shapes live in
`lib/wallet/types.ts`.

- **Current mode — Privy embedded wallet:** Privy remains the authentication and embedded-wallet
provider. New users receive an EVM wallet on Lisk Sepolia, preserving the existing signup,
provider, and funding flows.
- **Planned mode — Stellar account:** a linked `stellarPublicKey` is displayed as a Stellar Testnet
account in wallet/profile surfaces. This display path is separate from authentication and does
not replace Privy.
- **No linked wallet:** public wallet display helpers return a neutral “Not linked” state instead
of leaking chain-specific fallback assumptions into components.

When Stellar signing is introduced, add it behind the planned mode rather than changing Privy
authentication or scattering network constants through UI components.

## Tech Stack

- Next.js 16 with the App Router
Expand Down
72 changes: 26 additions & 46 deletions app/Providers.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,26 @@
"use client"

import type { FC, ReactNode } from "react"
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 type { WalletNetwork } from "@/types/wallet"

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 (
<PrivyProvider
appId={privyAppId || ""}
config={{
loginMethods: ["email", "sms"],
supportedChains: [liskSepolia],
defaultChain: liskSepolia,
embeddedWallets: {
ethereum: {
createOnLogin: "all-users",
},
showWalletUIs: true,
},
appearance: {
theme: "light",
accentColor: "#F2780E",
logo: "/images/chainmovelogo.png",
},
}}
>
<WalletProvider defaultNetwork={defaultNetwork}>
{children}
</WalletProvider>
</PrivyProvider>
)
}
"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

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 (
<PrivyProvider
appId={privyAppId || ""}
config={embeddedWalletProviderConfig}
>
{children}
</PrivyProvider>
)
}
14 changes: 8 additions & 6 deletions app/dashboard/investor/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { useCallback, useEffect, useMemo, useState } from "react"
import { useRouter } from "next/navigation"
import { CalendarDays, ChevronDown, PlusCircle } from "lucide-react"
import { formatEther } from "viem"
import { liskSepolia } from "viem/chains"

import { DashboardShell } from "@/components/dashboard/dashboard-shell"
import { DashboardHeader } from "@/components/dashboard/investor-overview/dashboard-header"
Expand All @@ -24,6 +23,7 @@ import { formatNaira } from "@/lib/currency"
import { isMockStellar } from "@/lib/mock-stellar/mockConfig"
import { mockAccount } from "@/lib/mock-stellar/mockAccount"
import { mockActivity } from "@/lib/mock-stellar/mockActivity"
import { CURRENT_EMBEDDED_WALLET } from "@/lib/wallet/config"

type PoolPreview = {
id: string
Expand All @@ -48,14 +48,16 @@ function truncateAddress(address: string) {
}

function formatEthForUi(balanceEth: number | null) {
if (!Number.isFinite(balanceEth) || balanceEth === null) return "0 ETH"
if (balanceEth < 0.01) return "0 ETH"
if (balanceEth < 1) return `${balanceEth.toFixed(2)} ETH`
return `${balanceEth.toFixed(1)} ETH`
const asset = CURRENT_EMBEDDED_WALLET.network.nativeAsset
if (!Number.isFinite(balanceEth) || balanceEth === null) return `0 ${asset}`
if (balanceEth < 0.01) return `0 ${asset}`
if (balanceEth < 1) return `${balanceEth.toFixed(2)} ${asset}`
return `${balanceEth.toFixed(1)} ${asset}`
}

async function resolveOnchainBalanceEth(address: string) {
const rpcUrl = liskSepolia.rpcUrls.default.http[0]
const rpcUrl = CURRENT_EMBEDDED_WALLET.network.rpcUrl
if (!rpcUrl) return null
const response = await fetch(rpcUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
Expand Down
11 changes: 9 additions & 2 deletions components/dashboard/account-settings-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { formatNaira } from "@/lib/currency"
import { StellarLinkForm } from "@/components/dashboard/stellar-link-form"
import { getWalletDisplay } from "@/lib/wallet/config"

interface AccountSettingsFormProps {
roleLabel: "Driver" | "Investor"
Expand All @@ -38,6 +38,7 @@ interface AccountProfileResponse {
bio: string | null
role: string
walletAddress: string | null
stellarPublicKey: string | null
availableBalance: number
totalInvested: number
totalReturns: number
Expand Down Expand Up @@ -86,6 +87,7 @@ export function AccountSettingsForm({ roleLabel, kycHref }: AccountSettingsFormP
bio: authUser.bio || null,
role: authUser.role || roleLabel.toLowerCase(),
walletAddress: authUser.walletAddress || null,
stellarPublicKey: authUser.stellarPublicKey || null,
availableBalance: Number(authUser.availableBalance || 0),
totalInvested: Number(authUser.totalInvested || 0),
totalReturns: Number(authUser.totalReturns || 0),
Expand Down Expand Up @@ -126,6 +128,10 @@ export function AccountSettingsForm({ roleLabel, kycHref }: AccountSettingsFormP
}, [authUser?.id, loadProfile])

const isEmailManagedExternally = Boolean(profile?.privyUserId || authUser?.privyUserId)
const walletDisplay = getWalletDisplay({
embeddedWalletAddress: profile?.walletAddress || authUser?.walletAddress,
stellarPublicKey: profile?.stellarPublicKey || authUser?.stellarPublicKey,
})

const handleSave = async () => {
if (!form.fullName.trim()) {
Expand Down Expand Up @@ -272,7 +278,8 @@ export function AccountSettingsForm({ roleLabel, kycHref }: AccountSettingsFormP
<CardContent className="space-y-3 text-sm text-muted-foreground">
<div className="rounded-lg border border-border/70 p-3">
<p><span className="text-foreground">Role:</span> {profile?.role || authUser?.role || roleLabel.toLowerCase()}</p>
<p className="mt-1 break-all"><span className="text-foreground">Wallet:</span> {profile?.walletAddress || authUser?.walletAddress || "Not linked"}</p>
<p className="mt-1 break-all"><span className="text-foreground">{walletDisplay.addressLabel}:</span> {walletDisplay.address || "Not linked"}</p>
<p className="mt-1"><span className="text-foreground">Network:</span> {walletDisplay.networkLabel}</p>
</div>
<div className="rounded-lg border border-border/70 p-3">
<p><span className="text-foreground">Internal balance:</span> {formatNaira(Number(profile?.availableBalance || authUser?.availableBalance || 0))}</p>
Expand Down
37 changes: 19 additions & 18 deletions components/dashboard/investor-wallet-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import { useCallback, useEffect, useMemo, useState } from "react"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { formatEther } from "viem"
import { liskSepolia } from "viem/chains"
import {
ArrowRight,
CheckCircle2,
Expand Down Expand Up @@ -31,6 +30,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Skeleton } from "@/components/ui/skeleton"
import { CURRENT_EMBEDDED_WALLET, getWalletDisplay, shortenWalletAddress } from "@/lib/wallet/config"
import {
Table,
TableBody,
Expand Down Expand Up @@ -66,11 +66,6 @@ interface InvestorWalletPanelProps {
showTitle?: boolean
}

function truncateAddress(address: string) {
if (address.length < 10) return address
return `${address.slice(0, 6)}...${address.slice(-4)}`
}

function toPaystackAmount(value: string) {
const parsed = Number.parseFloat(value)
if (!Number.isFinite(parsed)) return null
Expand All @@ -87,7 +82,8 @@ function isValidEmail(value: string) {
}

async function resolveOnchainBalance(address: string) {
const rpcUrl = liskSepolia.rpcUrls.default.http[0]
const rpcUrl = CURRENT_EMBEDDED_WALLET.network.rpcUrl
if (!rpcUrl) return null
const response = await fetch(rpcUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
Expand All @@ -104,7 +100,7 @@ async function resolveOnchainBalance(address: string) {

const balance = Number.parseFloat(formatEther(BigInt(payload.result)))
if (!Number.isFinite(balance)) return null
return `${balance.toFixed(4)} ETH`
return `${balance.toFixed(4)} ${CURRENT_EMBEDDED_WALLET.network.nativeAsset}`
}

export function InvestorWalletPanel({ sectionId = "wallet", className, showTitle = true }: InvestorWalletPanelProps) {
Expand Down Expand Up @@ -133,6 +129,10 @@ export function InvestorWalletPanel({ sectionId = "wallet", className, showTitle
)

const walletAddress = isMockStellar ? mockAccount.publicKey : (walletSummary?.wallet.walletAddress || embeddedWallet?.address || authUser?.walletAddress || "")
const walletDisplay = getWalletDisplay({
embeddedWalletAddress: walletAddress,
stellarPublicKey: isMockStellar ? mockAccount.publicKey : authUser?.stellarPublicKey,
})
const internalBalance = walletSummary?.wallet.internalBalanceNgn || 0

const fundingTransactions = useMemo(() => {
Expand Down Expand Up @@ -224,12 +224,12 @@ export function InvestorWalletPanel({ sectionId = "wallet", className, showTitle
)

const openWalletExplorer = () => {
if (!walletAddress) return
window.open(`https://sepolia-blockscout.lisk.com/address/${walletAddress}`, "_blank", "noopener,noreferrer")
if (!walletDisplay.explorerUrl) return
window.open(walletDisplay.explorerUrl, "_blank", "noopener,noreferrer")
}

const handleOpenWalletView = () => {
if (!walletAddress) {
if (!walletDisplay.explorerUrl) {
toast({
title: "Wallet unavailable",
description: "Your embedded wallet address is not ready yet. Please sign out and sign in again.",
Expand Down Expand Up @@ -362,8 +362,8 @@ export function InvestorWalletPanel({ sectionId = "wallet", className, showTitle
}

const handleCopyAddress = async () => {
if (!walletAddress) return
await navigator.clipboard.writeText(walletAddress)
if (!walletDisplay.address) return
await navigator.clipboard.writeText(walletDisplay.address)
toast({
title: "Address copied",
description: "Wallet address copied to clipboard.",
Expand Down Expand Up @@ -510,14 +510,15 @@ export function InvestorWalletPanel({ sectionId = "wallet", className, showTitle
</div>

<div className="rounded-xl border bg-muted/20 p-4">
<p className="text-xs uppercase tracking-wide text-muted-foreground">Wallet address</p>
<p className="mt-2 break-all font-mono text-sm">{walletAddress ? truncateAddress(walletAddress) : "Not available"}</p>
<p className="text-xs uppercase tracking-wide text-muted-foreground">{walletDisplay.addressLabel}</p>
<p className="mt-2 break-all font-mono text-sm">{walletDisplay.address ? shortenWalletAddress(walletDisplay.address) : "Not available"}</p>
<p className="mt-1 text-xs text-muted-foreground">{walletDisplay.networkLabel}</p>
<div className="mt-2 flex flex-wrap gap-2">
<Button variant="outline" size="sm" className="flex-1 sm:flex-none" onClick={handleCopyAddress} disabled={!walletAddress}>
<Button variant="outline" size="sm" className="flex-1 sm:flex-none" onClick={handleCopyAddress} disabled={!walletDisplay.address}>
<Copy className="mr-1.5 h-3.5 w-3.5" />
Copy
</Button>
<Button variant="outline" size="sm" className="flex-1 sm:flex-none" onClick={handleOpenWalletView} disabled={!walletAddress}>
<Button variant="outline" size="sm" className="flex-1 sm:flex-none" onClick={handleOpenWalletView} disabled={!walletDisplay.explorerUrl}>
<ExternalLink className="mr-1.5 h-3.5 w-3.5" />
Open wallet
</Button>
Expand All @@ -539,7 +540,7 @@ export function InvestorWalletPanel({ sectionId = "wallet", className, showTitle
<p className="mt-2 text-xl font-semibold">{onchainLoading ? "Loading..." : onchainBalance || "Unavailable"}</p>
)}
<p className="mt-1 text-xs text-muted-foreground">
{isMockStellar ? "Stellar Mock Assets" : "Lisk Sepolia embedded wallet"}
{isMockStellar ? "Stellar Mock Assets" : `${CURRENT_EMBEDDED_WALLET.network.label} embedded wallet`}
</p>
</div>
</div>
Expand Down
Loading
Loading