Skip to content

Commit 3e24b9b

Browse files
authored
Merge pull request #32 from Mosas2000/refactor-wallet-abstraction-stellar
Refactor wallet abstraction stellar
2 parents a8670f6 + a88dfeb commit 3e24b9b

13 files changed

Lines changed: 13372 additions & 13154 deletions

.vscode/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{
2+
}

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,26 @@ The current codebase already supports a meaningful offchain and wallet-enabled p
4444

4545
This README reflects the target Stellar direction of the project.
4646

47-
The full Stellar integration is not yet wired end-to-end in this repository. The app still contains temporary EVM-oriented wallet plumbing through `Privy`, `viem`, and Lisk Sepolia configuration, and `package.json` still includes non-Stellar chain dependencies. Those pieces should be treated as transitional until the Stellar wallet, asset, and smart contract layer lands.
47+
### Wallet Abstraction Layer
48+
49+
The codebase now includes a wallet abstraction layer that separates authentication, application identity, and blockchain account management. This abstraction:
50+
51+
- Provides a clean API for wallet and account operations
52+
- Supports multiple blockchain accounts per user (Stellar, EVM, embedded wallets)
53+
- Allows seamless network switching between Stellar Testnet, Mainnet, and transitional chains
54+
- Isolates EVM/Lisk-specific code for easier future removal
55+
- Prepares the foundation for full Stellar wallet and transaction support
56+
57+
See `docs/WALLET_ABSTRACTION.md` for architecture details and `docs/MIGRATION_GUIDE.md` for usage examples.
58+
59+
### Current State
60+
61+
The full Stellar integration is not yet wired end-to-end in this repository. The app still contains temporary EVM-oriented wallet plumbing through `Privy`, `viem`, and Lisk Sepolia configuration, and `package.json` still includes non-Stellar chain dependencies. Those pieces are now isolated through the wallet abstraction and will be removed when the Stellar wallet, asset, and smart contract layer lands.
4862

4963
In practical terms:
5064

5165
- the product and backend foundations are here
66+
- wallet abstraction layer provides structure for Stellar integration
5267
- Stellar asset issuance, account flows, payout tracking, and Soroban contracts are the next chain layer
5368
- README language, roadmap, and contribution guidance below are written for the Stellar path
5469

@@ -60,6 +75,7 @@ In practical terms:
6075
- Tailwind CSS + Radix UI
6176
- MongoDB + Mongoose
6277
- Privy for current auth and embedded wallet onboarding
78+
- Wallet abstraction layer for multi-chain account management
6379
- Paystack for fiat payment flows
6480
- Resend for email delivery
6581
- Stellar SDK, Horizon, Stellar RPC, and Soroban planned for chain integration

app/Providers.tsx

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
1-
"use client"
2-
1+
"use client"
2+
33
import type { FC, ReactNode } from "react"
44
import { liskSepolia } from "viem/chains"
55

66
import { PrivyProvider } from "@/lib/privy/react-auth"
7-
7+
import { WalletProvider } from "@/contexts/wallet-context"
8+
import { getStellarConfig } from "@/lib/stellar/config"
9+
import type { WalletNetwork } from "@/types/wallet"
10+
811
const privyAppId = process.env.NEXT_PUBLIC_PRIVY_APP_ID
912

13+
function getDefaultNetwork(): WalletNetwork {
14+
const stellarConfig = getStellarConfig()
15+
return stellarConfig.network.toLowerCase() === "mainnet" ? "stellar-mainnet" : "stellar-testnet"
16+
}
17+
1018
export const Providers: FC<{ children: ReactNode }> = ({ children }) => {
19+
const defaultNetwork = getDefaultNetwork()
20+
1121
return (
1222
<PrivyProvider
1323
appId={privyAppId || ""}
@@ -23,12 +33,14 @@ export const Providers: FC<{ children: ReactNode }> = ({ children }) => {
2333
},
2434
appearance: {
2535
theme: "light",
26-
accentColor: "#F2780E",
27-
logo: "/images/chainmovelogo.png",
28-
},
29-
}}
30-
>
31-
{children}
32-
</PrivyProvider>
33-
)
34-
}
36+
accentColor: "#F2780E",
37+
logo: "/images/chainmovelogo.png",
38+
},
39+
}}
40+
>
41+
<WalletProvider defaultNetwork={defaultNetwork}>
42+
{children}
43+
</WalletProvider>
44+
</PrivyProvider>
45+
)
46+
}

contexts/wallet-context.tsx

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
"use client"
2+
3+
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from "react"
4+
import { usePrivy, useWallets } from "@/lib/privy/react-auth"
5+
import type {
6+
WalletState,
7+
WalletIdentity,
8+
BlockchainAccount,
9+
WalletNetwork,
10+
} from "@/types/wallet"
11+
import { createEmptyWalletState, getNetworkMetadata } from "@/types/wallet"
12+
13+
interface WalletContextValue {
14+
walletState: WalletState
15+
updateIdentity: (identity: Partial<WalletIdentity>) => void
16+
addAccount: (account: BlockchainAccount) => void
17+
setPrimaryAccount: (publicKey: string) => void
18+
switchNetwork: (network: WalletNetwork) => void
19+
refreshWallet: () => Promise<void>
20+
}
21+
22+
const WalletContext = createContext<WalletContextValue | null>(null)
23+
24+
export function useWallet() {
25+
const context = useContext(WalletContext)
26+
if (!context) {
27+
throw new Error("useWallet must be used within a WalletProvider")
28+
}
29+
return context
30+
}
31+
32+
interface WalletProviderProps {
33+
children: ReactNode
34+
defaultNetwork?: WalletNetwork
35+
}
36+
37+
export function WalletProvider({ children, defaultNetwork = "stellar-testnet" }: WalletProviderProps) {
38+
const { ready: privyReady, authenticated, user: privyUser } = usePrivy()
39+
const { wallets } = useWallets()
40+
const [walletState, setWalletState] = useState<WalletState>(createEmptyWalletState(defaultNetwork))
41+
42+
const refreshWallet = useCallback(async () => {
43+
if (!privyReady) {
44+
return
45+
}
46+
47+
if (!authenticated || !privyUser) {
48+
setWalletState(createEmptyWalletState(defaultNetwork))
49+
return
50+
}
51+
52+
try {
53+
const response = await fetch("/api/auth/me", { cache: "no-store" })
54+
if (!response.ok) {
55+
setWalletState(createEmptyWalletState(defaultNetwork))
56+
return
57+
}
58+
59+
const userData = await response.json()
60+
61+
const identity: WalletIdentity = {
62+
userId: userData.id || "",
63+
privyUserId: userData.privyUserId || privyUser.id || null,
64+
email: userData.email || privyUser.email?.address || null,
65+
phoneNumber: userData.phoneNumber || privyUser.phone?.number || null,
66+
name: userData.name || null,
67+
fullName: userData.fullName || userData.name || null,
68+
}
69+
70+
const accounts: BlockchainAccount[] = []
71+
72+
if (userData.stellarPublicKey) {
73+
accounts.push({
74+
publicKey: userData.stellarPublicKey,
75+
accountId: userData.stellarPublicKey,
76+
network: defaultNetwork === "stellar-mainnet" ? "stellar-mainnet" : "stellar-testnet",
77+
type: "stellar",
78+
linked: true,
79+
})
80+
}
81+
82+
if (wallets && wallets.length > 0) {
83+
for (const wallet of wallets) {
84+
if (wallet.address && wallet.walletClientType === "privy") {
85+
accounts.push({
86+
publicKey: wallet.address.toLowerCase(),
87+
address: wallet.address.toLowerCase(),
88+
network: "lisk-sepolia",
89+
type: "embedded",
90+
linked: true,
91+
})
92+
}
93+
}
94+
}
95+
96+
if (userData.walletAddress) {
97+
const existingEvmAccount = accounts.find(
98+
(acc) => acc.address?.toLowerCase() === userData.walletAddress.toLowerCase()
99+
)
100+
if (!existingEvmAccount) {
101+
accounts.push({
102+
publicKey: userData.walletAddress.toLowerCase(),
103+
address: userData.walletAddress.toLowerCase(),
104+
network: "lisk-sepolia",
105+
type: "evm",
106+
linked: true,
107+
})
108+
}
109+
}
110+
111+
const primaryAccount = accounts.find((acc) => acc.type === "stellar") || accounts[0] || null
112+
113+
setWalletState({
114+
identity,
115+
accounts,
116+
primaryAccount,
117+
metadata: getNetworkMetadata(defaultNetwork),
118+
isReady: true,
119+
})
120+
} catch (error) {
121+
console.error("Error refreshing wallet:", error)
122+
setWalletState(createEmptyWalletState(defaultNetwork))
123+
}
124+
}, [privyReady, authenticated, privyUser, wallets, defaultNetwork])
125+
126+
useEffect(() => {
127+
let mounted = true
128+
129+
const loadWallet = async () => {
130+
if (mounted) {
131+
await refreshWallet()
132+
}
133+
}
134+
135+
loadWallet()
136+
137+
return () => {
138+
mounted = false
139+
}
140+
}, [refreshWallet])
141+
142+
const updateIdentity = useCallback((identity: Partial<WalletIdentity>) => {
143+
setWalletState((prev) => ({
144+
...prev,
145+
identity: prev.identity ? { ...prev.identity, ...identity } : null,
146+
}))
147+
}, [])
148+
149+
const addAccount = useCallback((account: BlockchainAccount) => {
150+
setWalletState((prev) => {
151+
const exists = prev.accounts.some((acc) => acc.publicKey === account.publicKey)
152+
if (exists) return prev
153+
154+
return {
155+
...prev,
156+
accounts: [...prev.accounts, account],
157+
}
158+
})
159+
}, [])
160+
161+
const setPrimaryAccount = useCallback((publicKey: string) => {
162+
setWalletState((prev) => {
163+
const account = prev.accounts.find((acc) => acc.publicKey === publicKey)
164+
if (!account) return prev
165+
166+
return {
167+
...prev,
168+
primaryAccount: account,
169+
}
170+
})
171+
}, [])
172+
173+
const switchNetwork = useCallback((network: WalletNetwork) => {
174+
setWalletState((prev) => ({
175+
...prev,
176+
metadata: getNetworkMetadata(network),
177+
}))
178+
}, [])
179+
180+
return (
181+
<WalletContext.Provider
182+
value={{
183+
walletState,
184+
updateIdentity,
185+
addAccount,
186+
setPrimaryAccount,
187+
switchNetwork,
188+
refreshWallet,
189+
}}
190+
>
191+
{children}
192+
</WalletContext.Provider>
193+
)
194+
}

0 commit comments

Comments
 (0)