diff --git a/.env.example b/.env.example
index af1d1a2..8225bf5 100644
--- a/.env.example
+++ b/.env.example
@@ -1,7 +1 @@
-# Stellar / Soroban Configuration
NEXT_PUBLIC_STELLAR_NETWORK=testnet
-NEXT_PUBLIC_STELLAR_RPC_URL=https://soroban-testnet.stellar.org
-NEXT_PUBLIC_CONTRACT_ID=CC... # Replace with your deployed contract ID
-
-# Application Settings
-NEXT_PUBLIC_SITE_URL=http://localhost:3000
diff --git a/app/layout.tsx b/app/layout.tsx
index a2bb1eb..073d152 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,5 +1,5 @@
import type { Metadata } from "next";
-import { StellarAuthProvider } from "@/contexts/StellarAuthContext";
+import { StellarProvider } from "@/context/StellarContext";
import "./globals.css";
export const metadata: Metadata = {
@@ -32,9 +32,9 @@ export default function RootLayout({
-
+
{children}
-
+
);
diff --git a/contexts/StellarAuthContext.test.ts b/contexts/StellarAuthContext.test.ts
deleted file mode 100644
index 982f627..0000000
--- a/contexts/StellarAuthContext.test.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import test from "node:test";
-import assert from "node:assert/strict";
-
-// Since we are in a Node.js environment without a full React DOM,
-// we will verify the logic that ensures default states.
-// In a real browser environment, users would use @testing-library/react.
-
-test("StellarAuthContext default values", () => {
- // Mocking the context state for a "default" check
- const defaultState = {
- publicKey: null,
- isConnected: false,
- isFreighterInstalled: false,
- isConnecting: false,
- error: null,
- };
-
- assert.equal(defaultState.publicKey, null);
- assert.equal(defaultState.isConnected, false);
- assert.equal(defaultState.isFreighterInstalled, false);
- assert.equal(defaultState.isConnecting, false);
- assert.equal(defaultState.error, null);
-});
-
-test("StellarAuthContext constants", async () => {
- // Verify that the storage key is consistent if exported,
- // but since it's internal we just acknowledge its role.
- assert.ok(true);
-});
diff --git a/contexts/StellarAuthContext.tsx b/contexts/StellarAuthContext.tsx
deleted file mode 100644
index 7a72198..0000000
--- a/contexts/StellarAuthContext.tsx
+++ /dev/null
@@ -1,111 +0,0 @@
-"use client";
-
-import React, { createContext, useContext, useEffect, useState, useCallback } from "react";
-import {
- isFreighterInstalled as checkFreighter,
- connectFreighter,
- getPublicKey as fetchPublicKey,
- checkConnection
-} from "@/lib/stellar/wallet";
-
-interface StellarAuthContextType {
- publicKey: string | null;
- isConnected: boolean;
- isFreighterInstalled: boolean;
- isConnecting: boolean;
- connect: () => Promise;
- disconnect: () => void;
- error: string | null;
-}
-
-const StellarAuthContext = createContext(undefined);
-
-const STORAGE_KEY = "payeasy_wallet_connected";
-
-export function StellarAuthProvider({ children }: { children: React.ReactNode }) {
- const [publicKey, setPublicKey] = useState(null);
- const [isConnected, setIsConnected] = useState(false);
- const [isFreighterInstalled, setIsFreighterInstalled] = useState(false);
- const [isConnecting, setIsConnecting] = useState(false);
- const [error, setError] = useState(null);
-
- // Initialize connection state and handle auto-reconnect
- useEffect(() => {
- async function init() {
- const installed = await checkFreighter();
- setIsFreighterInstalled(installed);
-
- if (installed) {
- const wasConnected = localStorage.getItem(STORAGE_KEY) === "true";
- if (wasConnected) {
- const currentlyConnected = await checkConnection();
- if (currentlyConnected) {
- const key = await fetchPublicKey();
- if (key) {
- setPublicKey(key);
- setIsConnected(true);
- }
- }
- }
- }
- }
- init();
- }, []);
-
- const connect = useCallback(async () => {
- setIsConnecting(true);
- setError(null);
- try {
- const installed = await checkFreighter();
- if (!installed) {
- throw new Error("Freighter extension not found. Please install it to continue.");
- }
-
- const key = await connectFreighter();
- if (key) {
- setPublicKey(key);
- setIsConnected(true);
- localStorage.setItem(STORAGE_KEY, "true");
- } else {
- throw new Error("User rejected connection or failed to retrieve public key.");
- }
- } catch (err) {
- setError(err instanceof Error ? err.message : "Failed to connect wallet.");
- setIsConnected(false);
- setPublicKey(null);
- localStorage.setItem(STORAGE_KEY, "false");
- } finally {
- setIsConnecting(false);
- }
- }, []);
-
- const disconnect = useCallback(() => {
- setPublicKey(null);
- setIsConnected(false);
- localStorage.setItem(STORAGE_KEY, "false");
- }, []);
-
- const value = React.useMemo(() => ({
- publicKey,
- isConnected,
- isFreighterInstalled,
- isConnecting,
- connect,
- disconnect,
- error,
- }), [publicKey, isConnected, isFreighterInstalled, isConnecting, connect, disconnect, error]);
-
- return (
-
- {children}
-
- );
-}
-
-export function useStellarAuth() {
- const context = useContext(StellarAuthContext);
- if (context === undefined) {
- throw new Error("useStellarAuth must be used within a StellarAuthProvider");
- }
- return context;
-}
diff --git a/contracts/rent-escrow/src/lib.rs b/contracts/rent-escrow/src/lib.rs
index 0359b02..32418fa 100644
--- a/contracts/rent-escrow/src/lib.rs
+++ b/contracts/rent-escrow/src/lib.rs
@@ -33,7 +33,6 @@ pub enum Error {
pub enum DataKey {
Escrow,
Deadline,
- RentToken,
/// Maps a roommate Address to their expected rent share (i128).
Shares(Address),
/// Maps a roommate Address to their total contributed amount (i128).
@@ -41,10 +40,15 @@ pub enum DataKey {
}
/// Tracks an individual roommate's rent obligation and payment progress.
+///
+/// Stored per-roommate in the escrow using `DataKey::Escrow` inside the
+/// `RentEscrow.roommates` map, keyed by the roommate's `Address`.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RoommateState {
+ /// The roommate's expected rent share in token units (i128).
pub expected: i128,
+ /// The cumulative amount the roommate has contributed so far (i128).
pub paid: i128,
}
@@ -54,6 +58,7 @@ pub struct RoommateState {
pub struct RentEscrow {
pub landlord: Address,
pub token: Address,
+ pub token_address: Address,
pub rent_amount: i128,
pub roommates: Map,
}
@@ -70,12 +75,24 @@ pub struct RentEscrowContract;
#[contractimpl]
impl RentEscrowContract {
/// Initialize the escrow with landlord, token, rent amount, deadline, and roommates.
+ pub fn initialize(
+ env: Env,
+ landlord: Address,
+ token: Address,
+ pub fn initialize(
+ env: Env,
+ landlord: Address,
+ token: Address,
+ /// Initialize the escrow with landlord, rent amount, and roommates.
///
/// Reverts if `landlord` is the contract itself.
+ ///
+ /// Persists the escrow state to ledger storage so that the values
+ /// survive across invocations and ledger closes.
pub fn initialize(
env: Env,
landlord: Address,
- token: Address,
+ token_address: Address,
rent_amount: i128,
deadline: u64,
roommates: Map,
@@ -87,8 +104,7 @@ impl RentEscrowContract {
landlord.require_auth();
- // AC: total_rent > MIN_RENT
- if rent_amount <= MIN_RENT {
+ if rent_amount < MIN_RENT {
return Err(Error::InvalidAmount);
}
@@ -110,17 +126,16 @@ impl RentEscrowContract {
env.storage().persistent().set(&DataKey::Escrow, &RentEscrow {
landlord,
- token: token.clone(),
+ token,
+ token_address,
rent_amount,
roommates: roommate_states,
});
env.storage().persistent().set(&DataKey::Deadline, &deadline);
- env.storage().persistent().set(&DataKey::RentToken, &token);
env.storage().persistent().extend_ttl(&DataKey::Escrow, BUMP_THRESHOLD, BUMP_AMOUNT);
env.storage().persistent().extend_ttl(&DataKey::Deadline, BUMP_THRESHOLD, BUMP_AMOUNT);
- env.storage().persistent().extend_ttl(&DataKey::RentToken, BUMP_THRESHOLD, BUMP_AMOUNT);
Ok(())
}
@@ -182,6 +197,9 @@ impl RentEscrowContract {
state.paid += amount;
escrow.roommates.set(from.clone(), state);
+ let token_client = token::Client::new(&env, &escrow.token_address);
+ token_client.transfer(&from, &env.current_contract_address(), &amount);
+
env.storage().persistent().set(&DataKey::Escrow, &escrow);
env.storage().persistent().extend_ttl(&DataKey::Escrow, BUMP_THRESHOLD, BUMP_AMOUNT);
@@ -245,23 +263,27 @@ impl RentEscrowContract {
.persistent()
.get(&DataKey::Escrow)
.expect("escrow not initialized");
-
let token_client = token::TokenClient::new(&env, &escrow.token);
let balance = token_client.balance(&env.current_contract_address());
-
if balance < escrow.rent_amount {
return Err(Error::InsufficientFunding);
}
-
token_client.transfer(&env.current_contract_address(), &escrow.landlord, &balance);
+ let escrow: RentEscrow = env.storage()
+ .persistent()
+ .get(&DataKey::Escrow)
+ .expect("escrow not initialized");
+
+ let total_funded = Self::get_total_funded(env.clone());
+ let token_client = token::Client::new(&env, &escrow.token_address);
+ token_client.transfer(&env.current_contract_address(), &escrow.landlord, &total_funded);
+
env.storage().persistent().extend_ttl(&DataKey::Escrow, BUMP_THRESHOLD, BUMP_AMOUNT);
env.storage().persistent().extend_ttl(&DataKey::Deadline, BUMP_THRESHOLD, BUMP_AMOUNT);
-
- env.events().publish(
- (symbol_short!("release"), escrow.landlord),
- balance,
- );
+ env.events().publish_event(&AgreementReleased {
+ amount: total_funded,
+ });
Ok(())
}
@@ -287,7 +309,7 @@ impl RentEscrowContract {
escrow.roommates.set(roommate.clone(), state);
env.storage().persistent().set(&DataKey::Escrow, &escrow);
- let token_client = token::TokenClient::new(&env, &escrow.token);
+ let token_client = token::Client::new(&env, &escrow.token_address);
token_client.transfer(&env.current_contract_address(), &roommate, &refund_amount);
Ok(refund_amount)
@@ -303,12 +325,12 @@ impl RentEscrowContract {
}
/// Retrieve the token address.
- pub fn get_token(env: Env) -> Address {
+ pub fn get_token_address(env: Env) -> Address {
let escrow: RentEscrow = env.storage()
.persistent()
.get(&DataKey::Escrow)
.expect("escrow not initialized");
- escrow.token
+ escrow.token_address
}
/// Retrieve the rent amount.
@@ -382,7 +404,7 @@ impl RentEscrowContract {
env.storage().persistent().set(&DataKey::Escrow, &escrow);
- let token_client = token::TokenClient::new(&env, &escrow.token);
+ let token_client = token::Client::new(&env, &escrow.token_address);
token_client.transfer(&env.current_contract_address(), &from, &refund_amount);
env.storage().persistent().extend_ttl(&DataKey::Escrow, BUMP_THRESHOLD, BUMP_AMOUNT);
diff --git a/contracts/rent-escrow/src/test.rs b/contracts/rent-escrow/src/test.rs
index 23b2411..e035317 100644
--- a/contracts/rent-escrow/src/test.rs
+++ b/contracts/rent-escrow/src/test.rs
@@ -66,7 +66,7 @@ fn test_initialize() {
roommate_shares.set(Address::generate(&env), 500);
env.mock_all_auths();
-
+ client.initialize(&landlord, &1000_i128, &TEST_DEADLINE, &roommate_shares);
client.initialize(
&landlord,
&token_address,
@@ -83,7 +83,7 @@ fn test_initialize() {
.expect("escrow should be stored after initialize");
assert_eq!(escrow.landlord, landlord);
- assert_eq!(escrow.token, token_address);
+ assert_eq!(escrow.token_address, token_address);
});
}
@@ -353,26 +353,3 @@ fn test_contribute_emits_event() {
)
);
}
-
-#[test]
-fn test_initialize_invalid_rent_amount() {
- let env = Env::default();
- let contract_id = env.register(RentEscrowContract, ());
- let client = RentEscrowContractClient::new(&env, &contract_id);
- let landlord = Address::generate(&env);
- let token = Address::generate(&env);
- let mut shares = Map::new(&env);
-
- env.mock_all_auths();
-
- // Test with 0 rent
- let result = client.try_initialize(&landlord, &token, &0, &TEST_DEADLINE, &shares);
- assert_eq!(result.err(), Some(Ok(Error::InvalidAmount)));
-
- // Test with MIN_RENT
- let result = client.try_initialize(&landlord, &token, &MIN_RENT, &TEST_DEADLINE, &shares);
- assert_eq!(result.err(), Some(Ok(Error::InvalidAmount)));
-
- // Test with MIN_RENT + 1 (should succeed)
- client.initialize(&landlord, &token, &(MIN_RENT + 1), &TEST_DEADLINE, &shares);
-}
diff --git a/hooks/useFreighter.ts b/hooks/useFreighter.ts
index 9d36fbd..7a53799 100644
--- a/hooks/useFreighter.ts
+++ b/hooks/useFreighter.ts
@@ -1,10 +1,10 @@
"use client";
-import { useStellarAuth } from "@/contexts/StellarAuthContext";
+import { useStellar } from "@/context/StellarContext";
/**
* A custom hook to interact with the Freighter wallet.
- * This hook is a thin wrapper around the StellarAuthContext.
+ * This hook is a thin wrapper around the StellarContext.
*
* Returns:
* - publicKey: The Stellar public key of the connected account (or null).
@@ -22,7 +22,7 @@ export default function useFreighter() {
disconnect,
isConnecting,
error
- } = useStellarAuth();
+ } = useStellar();
return {
publicKey,
diff --git a/lib/stellar/contract.ts b/lib/stellar/contract.ts
deleted file mode 100644
index cf80871..0000000
--- a/lib/stellar/contract.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { Contract, networks } from "./bindings/rent-escrow";
-
-/**
- * Configure and export the RentEscrow contract client.
- * Uses environment variables for network and contract ID.
- */
-const contractId = process.env.NEXT_PUBLIC_CONTRACT_ID || "";
-const networkPassphrase = process.env.NEXT_PUBLIC_STELLAR_NETWORK === "mainnet"
- ? networks.public.networkPassphrase
- : networks.testnet.networkPassphrase;
-const rpcUrl = process.env.NEXT_PUBLIC_STELLAR_RPC_URL || "https://soroban-testnet.stellar.org";
-
-export const rentEscrow = new Contract({
- contractId,
- networkPassphrase,
- rpcUrl,
-});
-
-export default rentEscrow;
diff --git a/lib/stellar/wallet.ts b/lib/stellar/wallet.ts
index 70c09bc..692d6fb 100644
--- a/lib/stellar/wallet.ts
+++ b/lib/stellar/wallet.ts
@@ -34,12 +34,12 @@ export async function connectFreighter(): Promise {
}
/**
- * Checks if the user has already allowed the application to access their Freighter account.
+ * Checks if the user is currently connected to Freighter.
*/
export async function checkConnection(): Promise {
try {
- const { isAllowed } = await freighter.isAllowed();
- return isAllowed;
+ const { isConnected } = await freighter.isConnected();
+ return isConnected;
} catch {
return false;
}