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