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
7 changes: 2 additions & 5 deletions .github/workflows/contract.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,10 @@ jobs:
with:
targets: wasm32-unknown-unknown

- name: Install cargo-binstall
run: |
curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install.sh | sh

- name: Install cargo-audit and cargo-deny
run: |
cargo binstall -y cargo-audit cargo-deny
cargo install cargo-audit --locked
cargo install cargo-deny --locked

- name: Run cargo audit
run: |
Expand Down
62 changes: 56 additions & 6 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { useState } from "react";
import { Link, Route, Routes, useLocation, useNavigate } from "react-router-dom";
import {
Link,
Route,
Routes,
useLocation,
useNavigate,
} from "react-router-dom";
import { CreateProposalModal } from "./components/CreateProposalModal";
import { useContract } from "./hooks/useContract";
import { useEventPolling } from "./hooks/useEventPolling";
Expand All @@ -10,6 +16,7 @@ import { DashboardPage } from "./pages/DashboardPage";
import { HistoryPage } from "./pages/HistoryPage";
import { NotFoundPage } from "./pages/NotFoundPage";
import { OwnersPage } from "./pages/OwnersPage";
import { ProposalDetailPage } from "./pages/ProposalDetailPage";
import { SettingsPage } from "./pages/SettingsPage";
import type { Proposal } from "./types/accord";

Expand All @@ -34,6 +41,8 @@ export default function App() {
const navigate = useNavigate();
const location = useLocation();

const { proposals, owners, ownerAddresses, stats, loading, error, refresh } =
useContract(wallet.address);
const {
proposals,
owners,
Expand All @@ -49,13 +58,13 @@ export default function App() {
useNotifications(wallet.address, proposals);

const activeProposals = proposals.filter((proposal) =>
["pending", "ready"].includes(proposal.status)
["pending", "ready"].includes(proposal.status),
);
const isOwner = Boolean(
wallet.address && ownerAddresses.includes(wallet.address)
wallet.address && ownerAddresses.includes(wallet.address),
);
const showReadOnlyBanner = Boolean(
wallet.address && !loading && !error && !isOwner
wallet.address && !loading && !error && !isOwner,
);

async function withTx(
Expand Down Expand Up @@ -96,6 +105,8 @@ export default function App() {
return withTx(() => approveProposal(wallet.address!, id), {
id,
patch: {
approvals: newApprovals,
status: newStatus,
approvals,
status,
userHasApproved: true,
Expand Down Expand Up @@ -134,7 +145,7 @@ export default function App() {
const thresholdStat = stats.find((stat) => stat.label === "Threshold");
const threshold = Number.parseInt(
thresholdStat?.value.split(" ")[0] ?? "0",
10
10,
);

function shortenAddr(addr: string) {
Expand All @@ -147,6 +158,7 @@ export default function App() {
<div className="mx-auto flex max-w-4xl items-center justify-between">
<Link
to="/"
className="flex items-center gap-3 hover:opacity-80 transition-opacity"
className="flex items-center gap-3 transition-opacity hover:opacity-80"
>
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-emerald-500 text-xs font-bold text-black">
Expand All @@ -163,6 +175,7 @@ export default function App() {
<Link
key={label}
to={to}
className={`rounded-lg px-3 py-1.5 text-sm capitalize transition-colors focus:ring-2 focus:ring-zinc-400 focus:outline-none ${
className={`rounded-lg px-3 py-1.5 text-sm capitalize transition-colors focus:outline-none focus:ring-2 focus:ring-zinc-400 ${
location.pathname === to ||
(to === "/app" && location.pathname === "/app/")
Expand Down Expand Up @@ -223,6 +236,10 @@ export default function App() {
)}

{wallet.networkMismatch && (
<div className="bg-amber-500/10 border border-amber-500/20 rounded-xl px-4 py-3 mb-6 text-sm text-amber-400">
Your wallet network does not match this app. Expected network:{" "}
{import.meta.env.VITE_NETWORK_PASSPHRASE}. Switch Freighter network
to continue.
<div className="mb-6 rounded-xl border border-amber-500/20 bg-amber-500/10 px-4 py-3 text-sm text-amber-400">
Your wallet network does not match this app. Expected network:{" "}
{import.meta.env.VITE_NETWORK_PASSPHRASE}. Switch Freighter network to
Expand Down Expand Up @@ -261,7 +278,9 @@ export default function App() {
/>
</svg>
</div>
<h1 className="text-2xl font-semibold">Freighter wallet required</h1>
<h1 className="text-2xl font-semibold">
Freighter wallet required
</h1>
<p className="mt-3 max-w-md text-sm leading-6 text-zinc-400">
Freighter is the supported browser extension for signing Stellar
transactions in Accord. Install it to connect a wallet and use the
Expand All @@ -276,6 +295,36 @@ export default function App() {
Install Freighter
</a>
</div>
) : loading ? (
<div className="py-16 text-center text-sm text-zinc-500">
Loading contract data…
</div>
) : page === "dashboard" ? (
<DashboardPage
activeProposals={activeProposals}
owners={owners}
dashboardStats={stats}
walletAddress={wallet.address}
onApprove={handleApprove}
onExecute={handleExecute}
onRevoke={handleRevoke}
onCreateProposal={() => setShowCreate(true)}
error={null}
loading={loading}
/>
) : page === "history" ? (
<HistoryPage proposals={proposals} onApprove={handleApprove} />
) : page === "owners" ? (
<OwnersPage
owners={owners}
threshold={parseInt(
stats.find((s) => s.label === "Threshold")?.value.split(" ")[0] ||
"0",
)}
totalOwners={owners.length}
/>
) : page === "settings" ? (
<SettingsPage stats={stats} />
) : (
<Routes>
<Route
Expand Down Expand Up @@ -312,6 +361,7 @@ export default function App() {
}
/>
<Route path="settings" element={<SettingsPage stats={stats} />} />
<Route path="proposals/:id" element={<ProposalDetailPage />} />
<Route
path="*"
element={<NotFoundPage onGoHome={() => navigate("/app")} />}
Expand Down
105 changes: 105 additions & 0 deletions frontend/src/pages/ProposalDetailPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,109 @@
import { Link, useParams } from "react-router-dom";
import { useProposal } from "../hooks/useProposal";

export function ProposalDetailPage() {
const { id: idParam } = useParams();
const proposalId = idParam ? Number(idParam) : NaN;
const invalidId = !idParam || Number.isNaN(proposalId);
const { proposal, loading, error } = useProposal(invalidId ? -1 : proposalId);

return (
<div className="space-y-8">
<div className="mb-6 flex flex-wrap items-center gap-2 text-sm text-zinc-400">
<Link
to="/app"
className="font-medium text-zinc-400 hover:text-zinc-200 transition-colors"
>
Dashboard
</Link>
<span className="text-zinc-600">›</span>
<span className="font-semibold text-white whitespace-nowrap">
Proposal #{idParam ?? ""}
</span>
</div>

{invalidId ? (
<div className="rounded-xl border border-red-500/20 bg-red-500/10 px-4 py-5 text-sm text-red-200">
Invalid proposal id.
</div>
) : loading ? (
<div className="py-16 text-center text-zinc-500">
Loading proposal details…
</div>
) : error ? (
<div className="rounded-xl border border-red-500/20 bg-red-500/10 px-4 py-5 text-sm text-red-200">
{error}
</div>
) : !proposal ? (
<div className="rounded-xl border border-zinc-800 bg-zinc-900 px-4 py-5 text-sm text-zinc-300">
Proposal not found.
</div>
) : (
<section className="space-y-6">
<div className="rounded-3xl border border-zinc-800 bg-zinc-900 p-6">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="text-xs text-zinc-500 font-mono mb-1">
Proposal #{proposal.id}
</p>
<h1 className="text-2xl font-semibold text-white">
Send {proposal.amount} {proposal.token}
</h1>
</div>
<div className="rounded-full bg-zinc-800 px-3 py-1 text-xs uppercase tracking-wide text-zinc-300">
{proposal.status}
</div>
</div>

<div className="mt-6 grid gap-4 sm:grid-cols-2">
<div className="rounded-2xl border border-zinc-800 bg-zinc-950/40 p-4">
<p className="text-xs uppercase tracking-wide text-zinc-500">
Recipient
</p>
<p className="mt-2 text-sm text-zinc-200">{proposal.to}</p>
</div>
<div className="rounded-2xl border border-zinc-800 bg-zinc-950/40 p-4">
<p className="text-xs uppercase tracking-wide text-zinc-500">
Proposed by
</p>
<p className="mt-2 text-sm text-zinc-200">
{proposal.proposer}
</p>
</div>
</div>

<div className="mt-6 grid gap-4 sm:grid-cols-2">
<div className="rounded-2xl border border-zinc-800 bg-zinc-950/40 p-4">
<p className="text-xs uppercase tracking-wide text-zinc-500">
Created
</p>
<p className="mt-2 text-sm text-zinc-200">
{proposal.createdAt}
</p>
</div>
<div className="rounded-2xl border border-zinc-800 bg-zinc-950/40 p-4">
<p className="text-xs uppercase tracking-wide text-zinc-500">
Approvals
</p>
<p className="mt-2 text-sm text-zinc-200">
{proposal.approvals}/{proposal.threshold}
</p>
</div>
</div>

{proposal.description && (
<div className="mt-6 rounded-2xl border border-zinc-800 bg-zinc-950/40 p-4">
<p className="text-xs uppercase tracking-wide text-zinc-500 mb-2">
Description
</p>
<p className="text-sm leading-6 text-zinc-300">
{proposal.description}
</p>
</div>
)}
</div>
</section>
)}
import { StatusBadge } from "../components/StatusBadge";
import { useProposal } from "../hooks/useProposal";

Expand Down
Loading