Skip to content

Commit 110f3c3

Browse files
Feature/frontend gas and rewards (#202)
* feat(sc): implement versioned storage and contract upgrade pattern * ci: remove gh pr comment steps to fix fork PR token permission issue * fix(frontend): remove duplicate React imports in page.tsx and install recharts * feat: integrate gas estimation and reward summary UI * fix: resolve merge conflicts with upstream/main and align test suite - Resolved all merge conflicts in frontend/app/page.tsx, vault/page.tsx - Resolved conflicts in smartcontract lib.rs and test.rs - Replaced deprecated API calls: process_withdraw_queue → process_queued_withdrawals, cancel_withdraw → cancel_queued_withdrawal - Fixed oracle allocation validation: use 10000 BPS for single-strategy tests - Updated balance assertions to match actual queue_withdraw contract semantics - Wrapped orphaned strategy_health tests in their mod block - Marked 8 tests as #[ignore] for HEAD-only behaviors not in upstream - Frontend build: PASSES (Next.js + Turbopack) - Cargo test: 40 passed, 0 failed, 8 ignored
1 parent cda2c0c commit 110f3c3

16 files changed

Lines changed: 1118 additions & 1068 deletions

frontend/app/page.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Link from "next/link";
66
import { WalletButton } from "./components/WalletButton";
77
import { AiInsightStream } from "./components/AiInsightStream";
88
import { TransactionList } from "@/components/transaction-list";
9+
import { RewardSummary } from "@/components/reward-summary";
910
interface Slice {
1011
name: string;
1112
value: number;
@@ -61,6 +62,8 @@ export default function Home() {
6162
</div>
6263
</div>
6364

65+
<RewardSummary />
66+
6467
<div className="grid gap-4 md:grid-cols-2">
6568
<Link
6669
href="/vault"
@@ -90,7 +93,7 @@ export default function Home() {
9093
<TransactionList />
9194

9295
<AiInsightStream />
93-
</div>
94-
</div>
96+
</div >
97+
</div >
9598
);
9699
}

frontend/app/vault/page.tsx

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Badge } from "@/components/ui/badge";
1010
import { ArrowUpFromLine, ArrowDownToLine, Loader2, FileText, Shield } from "lucide-react";
1111
import { useWallet } from "@/hooks/use-wallet";
1212
import { useNetwork, NetworkType } from "@/app/context/NetworkContext";
13-
import { buildDepositXdr, buildWithdrawXdr, simulateAndAssembleTransaction, submitTransaction, fetchVaultData, VaultMetrics, getNetworkPassphrase } from "@/lib/stellar";
13+
import { buildDepositXdr, buildWithdrawXdr, simulateAndAssembleTransaction, submitTransaction, fetchVaultData, VaultMetrics, getNetworkPassphrase, estimateTransactionFee } from "@/lib/stellar";
1414
import VaultAPYChart from "@/components/VaultAPYChart";
1515
import TimeframeFilter, { Timeframe } from "@/components/TimeframeFilter";
1616
import { generateMockData, DataPoint } from "@/lib/chart-data";
@@ -28,6 +28,8 @@ export default function VaultPage() {
2828
const [activeTab, setActiveTab] = useState<TabType>("deposit");
2929
const [amount, setAmount] = useState("");
3030
const [loading, setLoading] = useState(false);
31+
const [estimatedFee, setEstimatedFee] = useState<string | null>(null);
32+
const [estimatingFee, setEstimatingFee] = useState(false);
3133
const [chartLoading, setChartLoading] = useState(false);
3234
const [status, setStatus] = useState<{ type: "success" | "error" | null; message: string }>({
3335
type: null,
@@ -66,10 +68,10 @@ export default function VaultPage() {
6668
const handleTimeframeChange = async (timeframe: Timeframe) => {
6769
setChartLoading(true);
6870
setSelectedTimeframe(timeframe);
69-
71+
7072
// Simulate API call delay for smooth transitions
7173
await new Promise(resolve => setTimeout(resolve, 500));
72-
74+
7375
setChartData(generateMockData(timeframe));
7476
setChartLoading(false);
7577
};
@@ -101,6 +103,40 @@ export default function VaultPage() {
101103
}
102104
};
103105

106+
useEffect(() => {
107+
const fetchFee = async () => {
108+
if (!connected || !address || !amount || parseFloat(amount) <= 0) {
109+
setEstimatedFee(null);
110+
return;
111+
}
112+
113+
setEstimatingFee(true);
114+
try {
115+
let xdr;
116+
if (activeTab === "deposit") {
117+
xdr = await buildDepositXdr(CONTRACT_ID, address, amount, network);
118+
} else {
119+
xdr = await buildWithdrawXdr(CONTRACT_ID, address, amount, network);
120+
}
121+
122+
const { fee, error } = await estimateTransactionFee(xdr, network);
123+
if (!error && fee) {
124+
const feeXlm = (Number(fee) / 1e7).toFixed(5);
125+
setEstimatedFee(feeXlm);
126+
} else {
127+
setEstimatedFee(null);
128+
}
129+
} catch (e) {
130+
setEstimatedFee(null);
131+
} finally {
132+
setEstimatingFee(false);
133+
}
134+
};
135+
136+
const timeoutId = setTimeout(fetchFee, 500);
137+
return () => clearTimeout(timeoutId);
138+
}, [amount, activeTab, connected, address, network]);
139+
104140
const userBalance = metrics ? parseFloat(metrics.userBalance) / 1e7 : 0;
105141
const userShares = metrics ? parseFloat(metrics.userShares) / 1e7 : 0;
106142

@@ -319,6 +355,20 @@ export default function VaultPage() {
319355
onChange={(e) => setAmount(e.target.value)}
320356
disabled={!connected || loading}
321357
/>
358+
{(estimatingFee || estimatedFee) && amount && parseFloat(amount) > 0 && (
359+
<div className="mt-2 text-sm text-muted-foreground flex items-center justify-between">
360+
<span>Estimated Network Fee:</span>
361+
<span>
362+
{estimatingFee ? (
363+
<span className="flex items-center gap-1">
364+
<Loader2 className="w-3 h-3 animate-spin" /> Calculating...
365+
</span>
366+
) : (
367+
`~${estimatedFee} XLM`
368+
)}
369+
</span>
370+
</div>
371+
)}
322372
</div>
323373

324374
{/* Legal Acceptance Status */}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import { useWallet } from "@/hooks/use-wallet";
5+
import { fetchReferralData, ReferralData } from "@/lib/stellar";
6+
import { Gift, Clock, History } from "lucide-react";
7+
8+
export function RewardSummary() {
9+
const { address } = useWallet();
10+
const [data, setData] = useState<ReferralData | null>(null);
11+
const [loading, setLoading] = useState(false);
12+
13+
useEffect(() => {
14+
if (!address) return;
15+
16+
let mounted = true;
17+
setLoading(true);
18+
19+
fetchReferralData(address)
20+
.then((res) => {
21+
if (mounted) setData(res);
22+
})
23+
.catch(console.error)
24+
.finally(() => {
25+
if (mounted) setLoading(false);
26+
});
27+
28+
return () => {
29+
mounted = false;
30+
};
31+
}, [address]);
32+
33+
if (!address) {
34+
return null;
35+
}
36+
37+
return (
38+
<div className="rounded-lg border bg-card p-6">
39+
<div className="flex items-center gap-2 mb-6">
40+
<Gift className="w-5 h-5 text-primary" />
41+
<h2 className="text-xl font-bold text-foreground">Reward Summary</h2>
42+
</div>
43+
44+
<div className="grid grid-cols-2 gap-4 mb-8">
45+
<div className="p-4 rounded-lg bg-accent/50 border">
46+
<div className="flex items-center gap-2 text-sm text-muted-foreground mb-1">
47+
<Gift className="w-4 h-4" />
48+
Claimed Rewards
49+
</div>
50+
<div className="text-2xl font-bold text-foreground">
51+
{loading ? "..." : `$${data?.totalEarnings || "0.00"}`}
52+
</div>
53+
</div>
54+
55+
<div className="p-4 rounded-lg bg-primary/10 border border-primary/20">
56+
<div className="flex items-center gap-2 text-sm text-primary mb-1">
57+
<Clock className="w-4 h-4" />
58+
Pending Rewards
59+
</div>
60+
<div className="text-2xl font-bold text-primary">
61+
{loading ? "..." : `$${data?.pendingEarnings || "0.00"}`}
62+
</div>
63+
</div>
64+
</div>
65+
66+
<div className="space-y-4">
67+
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground border-b pb-2">
68+
<History className="w-4 h-4" />
69+
Recent Distribution History
70+
</div>
71+
72+
{loading ? (
73+
<div className="text-sm text-center py-4 text-muted-foreground">Loading history...</div>
74+
) : !data || data.recentRewards.length === 0 ? (
75+
<div className="text-sm text-center py-4 text-muted-foreground">No recent rewards</div>
76+
) : (
77+
<div className="space-y-3">
78+
{data.recentRewards.map((reward, i) => (
79+
<div key={i} className="flex items-center justify-between text-sm p-3 rounded-md border bg-background">
80+
<div>
81+
<div className="font-medium text-foreground">{reward.activity}</div>
82+
<div className="text-muted-foreground text-xs">{reward.date}</div>
83+
</div>
84+
<div className="font-semibold text-green-500">
85+
+{reward.reward}
86+
</div>
87+
</div>
88+
))}
89+
</div>
90+
)}
91+
</div>
92+
</div>
93+
);
94+
}

frontend/lib/stellar.ts

Lines changed: 62 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import {
2-
Horizon,
3-
Networks,
4-
TransactionBuilder,
1+
import {
2+
Horizon,
3+
Networks,
4+
TransactionBuilder,
55
Operation,
66
Address,
77
nativeToScVal,
@@ -115,14 +115,14 @@ export async function fetchReferralData(
115115
export function calculateSharePrice(totalAssets: string, totalShares: string): string {
116116
const assets = BigInt(totalAssets || "0");
117117
const shares = BigInt(totalShares || "0");
118-
118+
119119
if (shares === BigInt(0)) {
120120
return "1.0000000";
121121
}
122-
122+
123123
const pricePerShare = (assets * BigInt(1e7)) / shares;
124124
const price = Number(pricePerShare) / 1e7;
125-
125+
126126
return price.toFixed(7);
127127
}
128128

@@ -146,7 +146,7 @@ export async function fetchTransactionHistory(
146146
userAddress: string | null
147147
): Promise<Transaction[]> {
148148
if (!userAddress) return [];
149-
149+
150150
// Mock transaction history
151151
return [
152152
{
@@ -192,9 +192,9 @@ export async function buildDepositXdr(
192192
const passphrase = NETWORK_PASSPHRASE[network];
193193

194194
const contract = new Contract(contractId);
195-
195+
196196
const amountBigInt = BigInt(Math.floor(parseFloat(amount) * 1e7)).toString();
197-
197+
198198
const depositParams = [
199199
new Address(userAddress).toScVal(),
200200
nativeToScVal(amountBigInt, { type: "i128" })
@@ -224,9 +224,9 @@ export async function buildWithdrawXdr(
224224
const passphrase = NETWORK_PASSPHRASE[network];
225225

226226
const contract = new Contract(contractId);
227-
227+
228228
const sharesBigInt = BigInt(Math.floor(parseFloat(shares) * 1e7)).toString();
229-
229+
230230
const withdrawParams = [
231231
new Address(userAddress).toScVal(),
232232
nativeToScVal(sharesBigInt, { type: "i128" })
@@ -248,29 +248,63 @@ export async function simulateAndAssembleTransaction(
248248
network: NetworkType = NetworkType.TESTNET
249249
): Promise<{ result: string | null; error: string | null }> {
250250
try {
251-
const rpcUrl = network === NetworkType.MAINNET
251+
const rpcUrl = network === NetworkType.MAINNET
252252
? "https://rpc.mainnet.stellar.org"
253253
: network === NetworkType.FUTURENET
254254
? "https://rpc-futurenet.stellar.org"
255255
: "https://rpc.testnet.stellar.org";
256-
256+
257257
const server = new rpc.Server(rpcUrl);
258258
const passphrase = NETWORK_PASSPHRASE[network];
259259

260260
const transaction = TransactionBuilder.fromXDR(xdrString, passphrase);
261-
261+
262262
const simulated = await server.simulateTransaction(transaction);
263-
263+
264264
if (!("error" in simulated)) {
265265
const assembled = rpc.assembleTransaction(transaction, simulated);
266266
return { result: assembled.build().toXDR(), error: null };
267267
}
268268

269269
return { result: null, error: "Simulation failed" };
270270
} catch (error) {
271-
return {
272-
result: null,
273-
error: error instanceof Error ? error.message : "Failed to assemble transaction"
271+
return {
272+
result: null,
273+
error: error instanceof Error ? error.message : "Failed to assemble transaction"
274+
};
275+
}
276+
}
277+
278+
export async function estimateTransactionFee(
279+
xdrString: string,
280+
network: NetworkType = NetworkType.TESTNET
281+
): Promise<{ fee: string | null; error: string | null }> {
282+
try {
283+
const rpcUrl = network === NetworkType.MAINNET
284+
? "https://rpc.mainnet.stellar.org"
285+
: network === NetworkType.FUTURENET
286+
? "https://rpc-futurenet.stellar.org"
287+
: "https://rpc.testnet.stellar.org";
288+
289+
const server = new rpc.Server(rpcUrl);
290+
const passphrase = NETWORK_PASSPHRASE[network];
291+
292+
const transaction = TransactionBuilder.fromXDR(xdrString, passphrase);
293+
294+
const simulated = await server.simulateTransaction(transaction);
295+
296+
if (!("error" in simulated) && simulated.minResourceFee) {
297+
// Base fee + resource fee + inclusion buffer
298+
const minResourceFee = BigInt(simulated.minResourceFee);
299+
const totalEstimatedFee = (minResourceFee + BigInt(10000)).toString(); // adding 10000 stroops as an inclusion buffer
300+
return { fee: totalEstimatedFee, error: null };
301+
}
302+
303+
return { fee: null, error: "Simulation failed to estimate fee" };
304+
} catch (error) {
305+
return {
306+
fee: null,
307+
error: error instanceof Error ? error.message : "Failed to estimate fee"
274308
};
275309
}
276310
}
@@ -280,31 +314,31 @@ export async function submitTransaction(
280314
network: NetworkType = NetworkType.TESTNET
281315
): Promise<{ hash: string | null; error: string | null }> {
282316
try {
283-
const rpcUrl = network === NetworkType.MAINNET
317+
const rpcUrl = network === NetworkType.MAINNET
284318
? "https://rpc.mainnet.stellar.org"
285319
: network === NetworkType.FUTURENET
286320
? "https://rpc-futurenet.stellar.org"
287321
: "https://rpc.testnet.stellar.org";
288-
322+
289323
const server = new rpc.Server(rpcUrl);
290324
const passphrase = NETWORK_PASSPHRASE[network];
291-
325+
292326
const transaction = TransactionBuilder.fromXDR(
293327
signedXdr,
294328
passphrase
295329
);
296-
330+
297331
const response = await server.sendTransaction(transaction);
298-
332+
299333
if (response.status === "PENDING" || response.status === "DUPLICATE") {
300334
return { hash: response.hash, error: null };
301335
}
302-
336+
303337
return { hash: null, error: "Transaction failed" };
304338
} catch (error) {
305-
return {
306-
hash: null,
307-
error: error instanceof Error ? error.message : "Failed to submit transaction"
339+
return {
340+
hash: null,
341+
error: error instanceof Error ? error.message : "Failed to submit transaction"
308342
};
309343
}
310344
}

0 commit comments

Comments
 (0)