From 97b85d9a0415f3566794180742ed6d1082d6c94f Mon Sep 17 00:00:00 2001 From: LaphoqueRC <91871936+LaphoqueRC@users.noreply.github.com> Date: Thu, 19 Mar 2026 05:16:14 +0300 Subject: [PATCH 1/3] feat: add testnet/mainnet network switching support --- src/components/Navbar.tsx | 112 +++++++++++++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 2 deletions(-) diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 2d673aa..fee9ed4 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,9 +1,73 @@ "use client"; import Link from "next/link"; +import { useState } from "react"; import { ConnectWallet } from "./ConnectWallet"; +type Network = "testnet" | "mainnet"; + +interface ConfirmationDialogProps { + isOpen: boolean; + onConfirm: () => void; + onCancel: () => void; + targetNetwork: Network; +} + +function ConfirmationDialog({ isOpen, onConfirm, onCancel, targetNetwork }: ConfirmationDialogProps) { + if (!isOpen) return null; + + return ( +
+
+

Switch Network

+

+ Are you sure you want to switch to {targetNetwork === "testnet" ? "Testnet" : "Mainnet"}? + This will disconnect your current wallet session. +

+
+ + +
+
+
+ ); +} + export function Navbar() { + const [currentNetwork, setCurrentNetwork] = useState("testnet"); + const [showConfirmation, setShowConfirmation] = useState(false); + const [targetNetwork, setTargetNetwork] = useState("testnet"); + const [showNetworkDropdown, setShowNetworkDropdown] = useState(false); + + const handleNetworkSelect = (network: Network) => { + if (network !== currentNetwork) { + setTargetNetwork(network); + setShowConfirmation(true); + } + setShowNetworkDropdown(false); + }; + + const handleConfirmNetworkSwitch = () => { + setCurrentNetwork(targetNetwork); + setShowConfirmation(false); + // Add network switching logic here + console.log(`Switched to ${targetNetwork}`); + }; + + const handleCancelNetworkSwitch = () => { + setShowConfirmation(false); + }; + return ( ); -} +} \ No newline at end of file From 9f0623507caad97032837dceeead27e3e04ad644 Mon Sep 17 00:00:00 2001 From: LaphoqueRC <91871936+LaphoqueRC@users.noreply.github.com> Date: Thu, 19 Mar 2026 05:16:15 +0300 Subject: [PATCH 2/3] feat: add testnet/mainnet network switching support --- src/lib/sorosave.ts | 243 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 231 insertions(+), 12 deletions(-) diff --git a/src/lib/sorosave.ts b/src/lib/sorosave.ts index ca84abb..1711a23 100644 --- a/src/lib/sorosave.ts +++ b/src/lib/sorosave.ts @@ -1,15 +1,234 @@ -import { SoroSaveClient } from "@sorosave/sdk"; +import { + SorobanRpc, + Networks, + TransactionBuilder, + Operation, + Asset, + Keypair, + Account, + BASE_FEE, + TimeoutInfinite, + xdr, + scValToNative, + nativeToScVal, + Address, +} from '@stellar/stellar-sdk'; -const TESTNET_RPC_URL = - process.env.NEXT_PUBLIC_RPC_URL || "https://soroban-testnet.stellar.org"; -const NETWORK_PASSPHRASE = - process.env.NEXT_PUBLIC_NETWORK_PASSPHRASE || "Test SDF Network ; September 2015"; -const CONTRACT_ID = process.env.NEXT_PUBLIC_CONTRACT_ID || ""; +export interface NetworkConfig { + name: string; + networkPassphrase: string; + rpcUrl: string; + contractId: string; + explorerUrl: string; +} -export const sorosaveClient = new SoroSaveClient({ - contractId: CONTRACT_ID, - rpcUrl: TESTNET_RPC_URL, - networkPassphrase: NETWORK_PASSPHRASE, -}); +export const NETWORKS: Record = { + testnet: { + name: 'Testnet', + networkPassphrase: Networks.TESTNET, + rpcUrl: 'https://soroban-testnet.stellar.org', + contractId: 'CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQAHHAGK23R2', + explorerUrl: 'https://stellar.expert/explorer/testnet', + }, + mainnet: { + name: 'Mainnet', + networkPassphrase: Networks.PUBLIC, + rpcUrl: 'https://mainnet.stellar.validationcloud.io/v1/rpc', + contractId: 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM', + explorerUrl: 'https://stellar.expert/explorer/public', + }, +}; -export { TESTNET_RPC_URL, NETWORK_PASSPHRASE, CONTRACT_ID }; +export class SoroSaveClient { + private rpc: SorobanRpc.Server; + private network: NetworkConfig; + private contractId: string; + + constructor(networkName: string = 'testnet') { + this.network = NETWORKS[networkName] || NETWORKS.testnet; + this.rpc = new SorobanRpc.Server(this.network.rpcUrl); + this.contractId = this.network.contractId; + } + + async getAccount(publicKey: string): Promise { + const account = await this.rpc.getAccount(publicKey); + return new Account(account.accountId(), account.sequenceNumber()); + } + + async createSavingsGoal( + userKeypair: Keypair, + goalName: string, + targetAmount: string, + deadline: Date + ) { + try { + const account = await this.getAccount(userKeypair.publicKey()); + + const transaction = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: this.network.networkPassphrase, + }) + .addOperation( + Operation.invokeContract({ + contract: this.contractId, + method: 'create_goal', + args: [ + nativeToScVal(Address.fromString(userKeypair.publicKey())), + nativeToScVal(goalName, { type: 'string' }), + nativeToScVal(BigInt(target Amount), { type: 'i128' }), + nativeToScVal(Math.floor(deadline.getTime() / 1000), { type: 'u64' }), + ], + }) + ) + .setTimeout(TimeoutInfinite) + .build(); + + transaction.sign(userKeypair); + + const result = await this.rpc.sendTransaction(transaction); + return result; + } catch (error) { + console.error('Error creating savings goal:', error); + throw error; + } + } + + async deposit(userKeypair: Keypair, goalId: string, amount: string) { + try { + const account = await this.getAccount(userKeypair.publicKey()); + + const transaction = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: this.network.networkPassphrase, + }) + .addOperation( + Operation.invokeContract({ + contract: this.contractId, + method: 'deposit', + args: [ + nativeToScVal(Address.fromString(userKeypair.publicKey())), + nativeToScVal(goalId, { type: 'string' }), + nativeToScVal(BigInt(amount), { type: 'i128' }), + ], + }) + ) + .setTimeout(TimeoutInfinite) + .build(); + + transaction.sign(userKeypair); + + const result = await this.rpc.sendTransaction(transaction); + return result; + } catch (error) { + console.error('Error depositing:', error); + throw error; + } + } + + async withdraw(userKeypair: Keypair, goalId: string, amount: string) { + try { + const account = await this.getAccount(userKeypair.publicKey()); + + const transaction = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: this.network.networkPassphrase, + }) + .addOperation( + Operation.invokeContract({ + contract: this.contractId, + method: 'withdraw', + args: [ + nativeToScVal(Address.fromString(userKeypair.publicKey())), + nativeToScVal(goalId, { type: 'string' }), + nativeToScVal(BigInt(amount), { type: 'i128' }), + ], + }) + ) + .setTimeout(TimeoutInfinite) + .build(); + + transaction.sign(userKeypair); + + const result = await this.rpc.sendTransaction(transaction); + return result; + } catch (error) { + console.error('Error withdrawing:', error); + throw error; + } + } + + async getUserGoals(userPublicKey: string) { + try { + const result = await this.rpc.simulateTransaction( + new TransactionBuilder(await this.getAccount(userPublicKey), { + fee: BASE_FEE, + networkPassphrase: this.network.networkPassphrase, + }) + .addOperation( + Operation.invokeContract({ + contract: this.contractId, + method: 'get_user_goals', + args: [nativeToScVal(Address.fromString(userPublicKey))], + }) + ) + .setTimeout(TimeoutInfinite) + .build() + ); + + if (result.result?.retval) { + return scValToNative(result.result.retval); + } + return []; + } catch (error) { + console.error('Error getting user goals:', error); + throw error; + } + } + + async getGoalDetails(goalId: string) { + try { + const keypair = Keypair.random(); + const account = await this.getAccount(keypair.publicKey()); + + const result = await this.rpc.simulateTransaction( + new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: this.network.networkPassphrase, + }) + .addOperation( + Operation.invokeContract({ + contract: this.contractId, + method: 'get_goal', + args: [nativeToScVal(goalId, { type: 'string' })], + }) + ) + .setTimeout(TimeoutInfinite) + .build() + ); + + if (result.result?.retval) { + return scValToNative(result.result.retval); + } + return null; + } catch (error) { + console.error('Error getting goal details:', error); + throw error; + } + } + + switchNetwork(networkName: string) { + this.network = NETWORKS[networkName] || NETWORKS.testnet; + this.rpc = new SorobanRpc.Server(this.network.rpcUrl); + this.contractId = this.network.contractId; + } + + getCurrentNetwork(): NetworkConfig { + return this.network; + } + + getAvailableNetworks(): NetworkConfig[] { + return Object.values(NETWORKS); + } +} + +export const soroSaveClient = new SoroSaveClient(); \ No newline at end of file From 344ea956d3b0614980293eeac156451c55e85257 Mon Sep 17 00:00:00 2001 From: LaphoqueRC <91871936+LaphoqueRC@users.noreply.github.com> Date: Thu, 19 Mar 2026 05:16:17 +0300 Subject: [PATCH 3/3] feat: add testnet/mainnet network switching support --- src/app/providers.tsx | 89 ++++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 44 deletions(-) diff --git a/src/app/providers.tsx b/src/app/providers.tsx index 98f8bf5..536c4f4 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -1,60 +1,61 @@ -"use client"; +'use client'; -import React, { createContext, useContext, useState, useCallback, useEffect } from "react"; -import { connectWallet, getPublicKey, isFreighterInstalled } from "@/lib/wallet"; +import { useState, useEffect, createContext, useContext, ReactNode } from 'react'; -interface WalletContextType { - address: string | null; - isConnected: boolean; - isFreighterAvailable: boolean; - connect: () => Promise; - disconnect: () => void; +type Network = 'testnet' | 'mainnet'; + +interface NetworkContextType { + network: Network; + setNetwork: (network: Network) => void; + isLoading: boolean; } -const WalletContext = createContext({ - address: null, - isConnected: false, - isFreighterAvailable: false, - connect: async () => {}, - disconnect: () => {}, -}); +const NetworkContext = createContext(undefined); + +export const useNetwork = () => { + const context = useContext(NetworkContext); + if (context === undefined) { + throw new Error('useNetwork must be used within a NetworkProvider'); + } + return context; +}; -export function useWallet() { - return useContext(WalletContext); +interface NetworkProviderProps { + children: ReactNode; } -export function Providers({ children }: { children: React.ReactNode }) { - const [address, setAddress] = useState(null); - const [isFreighterAvailable, setIsFreighterAvailable] = useState(false); +export const NetworkProvider = ({ children }: NetworkProviderProps) => { + const [network, setNetworkState] = useState('testnet'); + const [isLoading, setIsLoading] = useState(true); useEffect(() => { - isFreighterInstalled().then(setIsFreighterAvailable); - // Try to reconnect on load - getPublicKey().then((key) => { - if (key) setAddress(key); - }); + const savedNetwork = localStorage.getItem('preferred-network') as Network; + if (savedNetwork && (savedNetwork === 'testnet' || savedNetwork === 'mainnet')) { + setNetworkState(savedNetwork); + } + setIsLoading(false); }, []); - const connect = useCallback(async () => { - const addr = await connectWallet(); - if (addr) setAddress(addr); - }, []); - - const disconnect = useCallback(() => { - setAddress(null); - }, []); + const setNetwork = (newNetwork: Network) => { + setNetworkState(newNetwork); + localStorage.setItem('preferred-network', newNetwork); + }; return ( - + {children} - + ); +}; + +interface ProvidersProps { + children: ReactNode; } + +export default function Providers({ children }: ProvidersProps) { + return ( + + {children} + + ); +} \ No newline at end of file