Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,6 @@ export default function App() {
thresholdStat?.value.split(" ")[0] ?? "0",
10,
);
const totalWeight = Number.parseInt(
thresholdStat?.value.split(" of ")[1] ?? "0",
10,
);

function shortenAddr(addr: string) {
return `${addr.slice(0, 6)}…${addr.slice(-4)}`;
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/CreateRecurringPaymentModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ describe("CreateRecurringPaymentModal", () => {
null,
null,
null,
"Payroll"
"Payroll",
"FixedAmountPerPeriod"
);
expect(defaultProps.onSubmitted).toHaveBeenCalledTimes(1);
expect(defaultProps.onClose).toHaveBeenCalledTimes(1);
Expand Down
450 changes: 209 additions & 241 deletions frontend/src/components/CreateRecurringPaymentModal.tsx

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion frontend/src/components/GovernanceHealthWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import React from "react";
import { WhaleWarningBadge } from "./WhaleWarningBadge";
import { weightToPercent, formatWeightPercent, shortenAddr } from "../lib/soroban";

Expand Down
2 changes: 0 additions & 2 deletions frontend/src/components/GovernanceImpactBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import React from "react";

export type ImpactLevel = "low" | "medium" | "high" | "critical";

export interface GovernanceImpactBadgeProps {
Expand Down
4 changes: 1 addition & 3 deletions frontend/src/components/HistoricalWeightChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export function HistoricalWeightChart({
dataKey="timestamp"
stroke="#71717a"
style={{ fontSize: "0.75rem" }}
tickFormatter={(value, index) => formatXAxisLabel(index)}
tickFormatter={(_value: string, index: number) => formatXAxisLabel(index)}
/>
<YAxis stroke="#71717a" style={{ fontSize: "0.75rem" }} />
<Tooltip
Expand All @@ -167,8 +167,6 @@ export function HistoricalWeightChart({
borderRadius: "0.5rem",
}}
labelStyle={{ color: "#e4e4e7" }}
formatter={(value: number) => [value, "Total Weight"]}
labelFormatter={(label: string) => `${label}`}
/>
<Line
type="monotone"
Expand Down
5 changes: 1 addition & 4 deletions frontend/src/components/ProposalCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,8 @@ function KindSummary({ proposal, ownerWeights = {} }: KindSummaryProps) {
Recurring payment to {proposal.to}
</p>
);
default: {
// exhaustive check
const _: never = proposal.kind;
default:
return null;
}
}
}

Expand Down
5 changes: 0 additions & 5 deletions frontend/src/components/RecurringPaymentActionModals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@ import {
createModifyRecurringProposal,
} from "../lib/submit";

// Default governance deadline: 7 days from now (seconds).
function defaultDeadlineTs(): bigint {
return BigInt(Math.floor(Date.now() / 1000) + 7 * 86_400);
}

// ─── Shared modal shell ───────────────────────────────────────────────────────

type ModalShellProps = {
Expand Down
1 change: 0 additions & 1 deletion frontend/src/components/RecurringPaymentCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,6 @@ function VestingProgressBar({
const claimed = parse(disbursed);
const claimableNum = parse(claimable);
const total = parse(cap);
const unvested = Math.max(0, total - claimed - claimableNum);

const claimedPct = total > 0 ? Math.min(100, (claimed / total) * 100) : 0;
const claimablePct = total > 0 ? Math.min(100 - claimedPct, (claimableNum / total) * 100) : 0;
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/components/WhaleWarningBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import React from "react";

export interface WhaleWarningBadgeProps {
triggered?: boolean;
sharePct?: number;
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/hooks/useContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
hasApproved,
getApprovers,
getApproverWeight,
getOwnerWeight,
getRecurringPayments,
computeMonthlyOutflow,
} from "../lib/contract";
Expand Down Expand Up @@ -96,7 +97,7 @@ export function useContract(walletAddress: string | null): ContractState {
setProposals(proposalsWithApproval);
setOwnerAddresses(ownerAddrs);
const ownerWeights = await Promise.all(
ownerAddrs.map(async (addr) => Number(await getOwnerWeight(addr)))
ownerAddrs.map(async (addr) => { const w = await getOwnerWeight(addr); return Number(w); })
);
setOwners(
ownerAddrs.map((addr, i) => ({
Expand Down
87 changes: 13 additions & 74 deletions frontend/src/lib/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@ import type {
ProposalCategory,
ProposalEvent,
ProposalEventType,
ProposalKind,
ProposalStatus,
RecurringSchedule,
RecurringScheduleStatus,
RecurringKind,
RecurringPayment,
RecurringStatus,
Expand All @@ -26,7 +24,6 @@ import {
stroopsToDisplay,
formatDeadline,
shortenAddr,
formatInterval,
} from "./soroban";

const RPC_URL = import.meta.env.VITE_SOROBAN_RPC_URL as string;
Expand Down Expand Up @@ -78,19 +75,19 @@ function mapCategory(raw: unknown): ProposalCategory {
} else if (raw && typeof raw === "object") {
key = Object.keys(raw as object)[0] ?? "Other";
} else {
return "other";
return "Other";
}
switch (key.toLowerCase()) {
case "transfer":
return "transfer";
return "Transfer";
case "payroll":
return "payroll";
return "Payroll";
case "grant":
return "grant";
return "Grant";
case "ops":
return "ops";
return "Ops";
default:
return "other";
return "Other";
}
}

Expand Down Expand Up @@ -170,13 +167,6 @@ function mapKindDetails(
amount: String(values[1] ?? "Unknown"),
token: "Owner weight",
};
case "changeownerweight":
return {
kind: "change_owner_weight",
to: shortenAddr(String(values[0] ?? "Unknown")),
amount: String(values[1] ?? "0"),
token: "Weight",
};
default:
return {
kind: "transfer",
Expand Down Expand Up @@ -277,14 +267,15 @@ export async function getOwners(): Promise<string[]> {
return scValToNative(val) as string[];
}

export async function getOwnerWeight(owner: string): Promise<number> {
export async function getOwnerWeight(owner: string): Promise<bigint> {
try {
const val = await simulateView("get_owner_weight", [
nativeToScVal(owner, { type: "address" }),
]);
return Number(scValToNative(val));
const raw = scValToNative(val);
return safeBigInt(raw);
} catch {
return 1;
return 0n;
}
}

Expand Down Expand Up @@ -316,36 +307,6 @@ export async function getTotalWeight(): Promise<number> {
}
}

export async function getProposalApprovalProgress(
proposalId: number,
): Promise<{
approvalWeight: number;
quorumWeight: number;
totalWeight: number;
}> {
try {
const val = await simulateView("get_proposal_approval_progress", [
nativeToScVal(BigInt(proposalId), { type: "u64" }),
]);
const raw = scValToNative(val) as {
approval_weight?: number;
quorum_weight?: number;
total_weight?: number;
};
return {
approvalWeight: Number(raw.approval_weight ?? 0),
quorumWeight: Number(raw.quorum_weight ?? 0),
totalWeight: Number(raw.total_weight ?? 0),
};
} catch (error) {
console.error(
`Failed to get approval progress for proposal ${proposalId}:`,
error,
);
throw error;
}
}

export async function getRequiredQuorumWeight(): Promise<number> {
try {
const val = await simulateView("get_required_quorum_weight");
Expand All @@ -369,28 +330,6 @@ export async function getThreshold(): Promise<number> {
return Number(scValToNative(val));
}

export async function getRequiredQuorumWeight(): Promise<number> {
const val = await simulateView("get_required_quorum_weight");
return Number(scValToNative(val));
}

export async function getTotalWeight(): Promise<number> {
const val = await simulateView("get_total_weight");
return Number(scValToNative(val));
}

export async function getOwnerWeight(owner: string): Promise<bigint> {
try {
const val = await simulateView("get_owner_weight", [
nativeToScVal(owner, { type: "address" }),
]);
const raw = scValToNative(val);
return safeBigInt(raw);
} catch {
return 0n;
}
}

export async function getSpendingLimit(
owner: string,
token: string,
Expand Down Expand Up @@ -530,13 +469,13 @@ export async function getProposal(id: number): Promise<Proposal> {

export async function getProposalApprovalProgress(
proposalId: number,
): Promise<{ approvals: number; quorumWeight: number; totalWeight: number }> {
): Promise<{ approvalWeight: number; quorumWeight: number; totalWeight: number }> {
const val = await simulateView("get_proposal_approval_progress", [
nativeToScVal(BigInt(proposalId), { type: "u64" }),
]);
const raw = scValToNative(val) as [unknown, unknown, unknown];
return {
approvals: Number(raw[0] ?? 0),
approvalWeight: Number(raw[0] ?? 0),
quorumWeight: Number(raw[1] ?? 0),
totalWeight: Number(raw[2] ?? 0),
};
Expand Down Expand Up @@ -1394,7 +1333,7 @@ export function reconstructTotalWeightHistory(
}

const history: WeightHistoryPoint[] = [];
let currentTotal = 0;
let currentTotal = currentTotalWeight;

// Process events from oldest to newest
for (const event of events) {
Expand Down
60 changes: 0 additions & 60 deletions frontend/src/lib/submit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -625,66 +625,6 @@ export async function createResumeRecurringProposal(
]);
}

export async function createModifyRecurringProposal(
callerAddress: string,
scheduleId: number,
newAmount: bigint,
newIntervalSecs: bigint,
description: string,
deadlineTs: bigint
): Promise<void> {
await buildAndSubmit(callerAddress, "create_modify_recurring_proposal", [
nativeToScVal(callerAddress, { type: "address" }),
nativeToScVal(BigInt(scheduleId), { type: "u64" }),
nativeToScVal(newAmount, { type: "i128" }),
nativeToScVal(newIntervalSecs, { type: "u64" }),
xdr.ScVal.scvString(description),
nativeToScVal(deadlineTs, { type: "u64" }),
]);
}

export async function createPauseRecurringProposal(
callerAddress: string,
scheduleId: number,
description: string,
deadlineTs: bigint
): Promise<void> {
await buildAndSubmit(callerAddress, "create_pause_recurring_proposal", [
nativeToScVal(callerAddress, { type: "address" }),
nativeToScVal(BigInt(scheduleId), { type: "u64" }),
xdr.ScVal.scvString(description),
nativeToScVal(deadlineTs, { type: "u64" }),
]);
}

export async function createResumeRecurringProposal(
callerAddress: string,
scheduleId: number,
description: string,
deadlineTs: bigint
): Promise<void> {
await buildAndSubmit(callerAddress, "create_resume_recurring_proposal", [
nativeToScVal(callerAddress, { type: "address" }),
nativeToScVal(BigInt(scheduleId), { type: "u64" }),
xdr.ScVal.scvString(description),
nativeToScVal(deadlineTs, { type: "u64" }),
]);
}

export async function createCancelRecurringProposal(
callerAddress: string,
scheduleId: number,
description: string,
deadlineTs: bigint
): Promise<void> {
await buildAndSubmit(callerAddress, "create_cancel_recurring_proposal", [
nativeToScVal(callerAddress, { type: "address" }),
nativeToScVal(BigInt(scheduleId), { type: "u64" }),
xdr.ScVal.scvString(description),
nativeToScVal(deadlineTs, { type: "u64" }),
]);
}

export async function createModifyRecurringProposal(
callerAddress: string,
scheduleId: number,
Expand Down
37 changes: 34 additions & 3 deletions frontend/src/pages/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,40 @@ export function DashboardPage({
<ProposalCardSkeleton />
</>
) : activeProposals.length === 0 ? (
<div className="text-center py-16 text-zinc-500 text-sm">
<p className="font-semibold mb-2">No active proposals</p>
<p>Create a new proposal to start the approval flow.</p>
<div className="text-center py-20">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-zinc-800">
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
className="text-zinc-500"
>
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
<polyline points="14 2 14 8 20 8" />
<line x1="12" y1="18" x2="12" y2="12" />
<line x1="9" y1="15" x2="15" y2="15" />
</svg>
</div>
<h3 className="text-sm font-semibold text-zinc-300 mb-1">
No active proposals
</h3>
<p className="text-sm text-zinc-500 mb-5">
Proposals let signers vote on transactions before they execute.
</p>
<button
type="button"
onClick={onCreateProposal}
className="inline-flex items-center gap-1.5 text-sm bg-emerald-600 hover:bg-emerald-500 text-white px-4 py-2 rounded-lg transition-colors font-medium"
>
<Plus size={14} />
Create proposal
</button>
</div>
) : (
displayedProposals.map((proposal) => {
Expand Down
Loading