-
- Contribution
-
- {formatAmount(group.contributionAmount)} tokens
-
-
-
-
Members
-
- {group.members.length} / {group.maxMembers}
+
+
+
+
{group.name}
+
+ {getStatusLabel(group.status)}
-
-
Round
-
- {group.currentRound} / {group.totalRounds || group.maxMembers}
-
+
+
+ Contribution
+ {formatAmount(group.contributionAmount)} tokens
+
+
+ Members
+ {group.members.length} / {group.maxMembers}
+
+
+ Round
+ {group.currentRound} / {group.totalRounds || group.maxMembers}
+
-
-
+
+
);
}
diff --git a/src/components/GroupCompare.tsx b/src/components/GroupCompare.tsx
new file mode 100644
index 0000000..87ba6fa
--- /dev/null
+++ b/src/components/GroupCompare.tsx
@@ -0,0 +1,93 @@
+"use client";
+
+import { SavingsGroup, formatAmount } from "@/lib/sdk";
+
+interface GroupCompareProps {
+ groups: SavingsGroup[];
+ onClose: () => void;
+}
+
+const ROWS: { key: keyof SavingsGroup | "spotsLeft" | "cycleLabel"; label: string }[] = [
+ { key: "name", label: "Name" },
+ { key: "status", label: "Status" },
+ { key: "contributionAmount", label: "Contribution" },
+ { key: "cycleLabel", label: "Cycle" },
+ { key: "maxMembers", label: "Max Members" },
+ { key: "spotsLeft", label: "Spots Left" },
+];
+
+function cycleLabel(seconds: number) {
+ if (seconds >= 2592000) return "Monthly";
+ if (seconds >= 604800) return "Weekly";
+ if (seconds >= 86400) return "Daily";
+ return `${seconds}s`;
+}
+
+function getValue(group: SavingsGroup, key: string): string {
+ switch (key) {
+ case "contributionAmount": return `${formatAmount(group.contributionAmount)} tokens`;
+ case "cycleLabel": return cycleLabel(group.cycleLength);
+ case "spotsLeft": return String(group.maxMembers - group.members.length);
+ default: return String((group as any)[key] ?? "—");
+ }
+}
+
+// Find keys where values differ across groups
+function isDifferent(groups: SavingsGroup[], key: string): boolean {
+ const values = groups.map(g => getValue(g, key));
+ return new Set(values).size > 1;
+}
+
+export function GroupCompare({ groups, onClose }: GroupCompareProps) {
+ if (groups.length < 2) return null;
+
+ return (
+
+
+ {/* Header */}
+
+
Compare Groups
+
+
+
+ {/* Table */}
+
+
+
+
+ | Feature |
+ {groups.map(g => (
+ {g.name} |
+ ))}
+
+
+
+ {ROWS.map(row => {
+ const diff = isDifferent(groups, row.key);
+ return (
+
+ |
+ {row.label}
+ {diff && ≠}
+ |
+ {groups.map(g => (
+
+ {getValue(g, row.key)}
+ |
+ ))}
+
+ );
+ })}
+
+
+
+
+ {/* Legend */}
+
+
+ Highlighted rows indicate differences between groups
+
+
+
+ );
+}
diff --git a/src/components/MemberList.tsx b/src/components/MemberList.tsx
index bd469e1..8316322 100644
--- a/src/components/MemberList.tsx
+++ b/src/components/MemberList.tsx
@@ -1,6 +1,6 @@
"use client";
-import { shortenAddress } from "@sorosave/sdk";
+import { shortenAddress } from "@/lib/sdk";
interface MemberListProps {
members: string[];
diff --git a/src/lib/sdk.ts b/src/lib/sdk.ts
new file mode 100644
index 0000000..30a5678
--- /dev/null
+++ b/src/lib/sdk.ts
@@ -0,0 +1,62 @@
+// Local stub replacing @/lib/sdk until the package is available
+
+export enum GroupStatus {
+ Forming = "Forming",
+ Active = "Active",
+ Completed = "Completed",
+ Disputed = "Disputed",
+ Paused = "Paused",
+}
+
+export interface SavingsGroup {
+ id: number;
+ name: string;
+ admin: string;
+ token: string;
+ contributionAmount: bigint;
+ cycleLength: number;
+ maxMembers: number;
+ members: string[];
+ payoutOrder: string[];
+ currentRound: number;
+ totalRounds: number;
+ status: GroupStatus;
+ createdAt: number;
+}
+
+export function formatAmount(amount: bigint): string {
+ return (Number(amount) / 1e7).toFixed(2);
+}
+
+export function parseAmount(value: string): bigint {
+ return BigInt(Math.round(parseFloat(value) * 1e7));
+}
+
+export function shortenAddress(address: string, chars = 4): string {
+ if (!address) return "";
+ return `${address.slice(0, chars)}...${address.slice(-chars)}`;
+}
+
+export function getStatusLabel(status: GroupStatus | string): string {
+ return status.toString();
+}
+
+export class SoroSaveClient {
+ contractId: string;
+ rpcUrl: string;
+ networkPassphrase: string;
+
+ constructor(config: { contractId: string; rpcUrl: string; networkPassphrase: string }) {
+ this.contractId = config.contractId;
+ this.rpcUrl = config.rpcUrl;
+ this.networkPassphrase = config.networkPassphrase;
+ }
+
+ async contribute(_member: string, _groupId: number, _caller: string): Promise<{ toXDR: () => string }> {
+ throw new Error("SoroSaveClient not yet connected to a live contract.");
+ }
+
+ async createGroup(_params: Partial
, _caller: string): Promise<{ toXDR: () => string }> {
+ throw new Error("SoroSaveClient not yet connected to a live contract.");
+ }
+}
diff --git a/src/lib/sorosave.ts b/src/lib/sorosave.ts
index ca84abb..ba0bff4 100644
--- a/src/lib/sorosave.ts
+++ b/src/lib/sorosave.ts
@@ -1,4 +1,4 @@
-import { SoroSaveClient } from "@sorosave/sdk";
+import { SoroSaveClient } from "@/lib/sdk";
const TESTNET_RPC_URL =
process.env.NEXT_PUBLIC_RPC_URL || "https://soroban-testnet.stellar.org";
diff --git a/src/lib/wallet.ts b/src/lib/wallet.ts
index 825d2e2..e6f14ba 100644
--- a/src/lib/wallet.ts
+++ b/src/lib/wallet.ts
@@ -1,36 +1,31 @@
-import freighter from "@stellar/freighter-api";
+import { getActiveWalletAdapter } from "@/app/providers";
-export async function isFreighterInstalled(): Promise {
- try {
- return await freighter.isConnected();
- } catch {
- return false;
+export async function signTransaction(
+ xdr: string,
+ networkPassphrase: string
+): Promise {
+ // Route to active wallet adapter if available
+ const adapter = getActiveWalletAdapter?.();
+ if (!adapter) throw new Error("No wallet connected");
+
+ // Freighter: window.freighter.signTransaction
+ if (adapter.id === "freighter") {
+ const { signTransaction: freighterSign } = await import("@stellar/freighter-api");
+ const result = await freighterSign(xdr, { networkPassphrase });
+ return result;
}
-}
-export async function connectWallet(): Promise {
- try {
- const address = await freighter.requestAccess();
- return address;
- } catch (error) {
- console.error("Failed to connect wallet:", error);
- return null;
+ // xBull
+ if (adapter.id === "xbull" && typeof window !== "undefined" && (window as any).xBullSDK) {
+ return await (window as any).xBullSDK.sign({ xdr, network: networkPassphrase });
}
-}
-export async function getPublicKey(): Promise {
- try {
- return await freighter.getPublicKey();
- } catch {
- return null;
+ // Albedo
+ if (adapter.id === "albedo") {
+ const albedo = (await import("@albedo-link/intent")).default;
+ const res = await albedo.tx({ xdr, network: networkPassphrase === "Public Global Stellar Network ; September 2015" ? "public" : "testnet" });
+ return res.signed_envelope_xdr;
}
-}
-export async function signTransaction(
- xdr: string,
- networkPassphrase: string
-): Promise {
- return await freighter.signTransaction(xdr, {
- networkPassphrase,
- });
+ throw new Error(`signTransaction not implemented for wallet: ${adapter.id}`);
}
diff --git a/src/lib/wallets/albedo.ts b/src/lib/wallets/albedo.ts
new file mode 100644
index 0000000..e2d4ebc
--- /dev/null
+++ b/src/lib/wallets/albedo.ts
@@ -0,0 +1,31 @@
+import { WalletAdapter } from './index';
+import albedo from '@albedo-link/intent';
+
+export class AlbedoAdapter implements WalletAdapter {
+ id = 'albedo';
+ name = 'Albedo';
+ icon = 'A';
+ url = 'https://albedo.link/';
+
+ async isInstalled(): Promise {
+ return true; // Albedo is a web wallet, always "installed"
+ }
+
+ async connect(): Promise {
+ try {
+ const res = await albedo.publicKey({});
+ return res.pubkey;
+ } catch (e) {
+ return null;
+ }
+ }
+
+ async disconnect(): Promise {
+ // Albedo is stateless, no disconnect needed
+ }
+
+ async getPublicKey(): Promise {
+ // Requires a prompt for Albedo usually
+ return null;
+ }
+}
diff --git a/src/lib/wallets/freighter.ts b/src/lib/wallets/freighter.ts
new file mode 100644
index 0000000..2bb37e2
--- /dev/null
+++ b/src/lib/wallets/freighter.ts
@@ -0,0 +1,40 @@
+import { WalletAdapter } from './index';
+import {
+ isConnected,
+ isAllowed,
+ requestAccess,
+ getPublicKey,
+} from '@stellar/freighter-api';
+
+export class FreighterAdapter implements WalletAdapter {
+ id = 'freighter';
+ name = 'Freighter';
+ icon = '🚢';
+ url = 'https://www.freighter.app/';
+
+ async isInstalled(): Promise {
+ return await isConnected();
+ }
+
+ async connect(): Promise {
+ if (await isAllowed()) {
+ return await getPublicKey();
+ }
+ const access = await requestAccess();
+ if (access) {
+ return await getPublicKey();
+ }
+ return null;
+ }
+
+ async disconnect(): Promise {
+ // Freighter doesn't support explicit disconnect via API
+ }
+
+ async getPublicKey(): Promise {
+ if (await isAllowed()) {
+ return await getPublicKey();
+ }
+ return null;
+ }
+}
diff --git a/src/lib/wallets/index.ts b/src/lib/wallets/index.ts
new file mode 100644
index 0000000..ff9a87a
--- /dev/null
+++ b/src/lib/wallets/index.ts
@@ -0,0 +1,10 @@
+export interface WalletAdapter {
+ id: string;
+ name: string;
+ icon: string;
+ url: string;
+ isInstalled(): Promise;
+ connect(): Promise;
+ disconnect(): Promise;
+ getPublicKey(): Promise;
+}
diff --git a/src/lib/wallets/xbull.ts b/src/lib/wallets/xbull.ts
new file mode 100644
index 0000000..80a7c97
--- /dev/null
+++ b/src/lib/wallets/xbull.ts
@@ -0,0 +1,38 @@
+import { WalletAdapter } from './index';
+
+export class xBullAdapter implements WalletAdapter {
+ id = 'xbull';
+ name = 'xBull';
+ icon = '🐂';
+ url = 'https://xbull.app/';
+
+ async isInstalled(): Promise {
+ return typeof window !== 'undefined' && 'xBullSDK' in window;
+ }
+
+ async connect(): Promise {
+ if (!(await this.isInstalled())) return null;
+ try {
+ return await (window as any).xBullSDK.connect();
+ } catch (e) {
+ return null;
+ }
+ }
+
+ async disconnect(): Promise {
+ if (await this.isInstalled()) {
+ try {
+ await (window as any).xBullSDK.disconnect();
+ } catch (e) {}
+ }
+ }
+
+ async getPublicKey(): Promise {
+ if (!(await this.isInstalled())) return null;
+ try {
+ return await (window as any).xBullSDK.getPublicKey();
+ } catch (e) {
+ return null;
+ }
+ }
+}