-
Notifications
You must be signed in to change notification settings - Fork 24
allow adding an end date to an existing stream without one #1844
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |
| id | ||
| name | ||
| isPaused | ||
| createdAt | ||
| config { | ||
| dripId | ||
| startDate | ||
|
|
@@ -49,6 +50,10 @@ | |
| import SafeAppDisclaimer from '$lib/components/safe-app-disclaimer/safe-app-disclaimer.svelte'; | ||
| import type { EditStreamFlowState } from './edit-stream-flow-state'; | ||
| import Wallet from '$lib/components/icons/Wallet.svelte'; | ||
| import Toggleable from '$lib/components/toggleable/toggleable.svelte'; | ||
| import parseDate from '$lib/flows/create-stream-flow/methods/parse-date'; | ||
| import parseTime from '$lib/flows/create-stream-flow/methods/parse-time'; | ||
| import combineDateAndTime from '$lib/flows/create-stream-flow/methods/combine-date-and-time'; | ||
| import type { EditStreamFlowStreamFragment } from './__generated__/gql.generated'; | ||
| import { buildEditStreamBatch } from '$lib/utils/streams/streams'; | ||
| import assert from '$lib/utils/assert'; | ||
|
|
@@ -72,6 +77,8 @@ | |
|
|
||
| let amountLocked = $derived(stream.isPaused === true); | ||
|
|
||
| let actualStartDate = $derived(new Date(stream.config.startDate ?? stream.createdAt)); | ||
|
|
||
| let newAmountValueParsed = $derived( | ||
| $context.newAmountValue | ||
| ? parseTokenAmount( | ||
|
|
@@ -89,15 +96,49 @@ | |
|
|
||
| let amountValidationState = $derived(validateAmtPerSecInput(newAmountPerSecond)); | ||
|
|
||
| let nameUpdated = $derived($context.newName !== stream.name); | ||
| let nameUpdated = $derived(($context.newName ?? null) !== (stream.name ?? null)); | ||
| let amountUpdated = $derived( | ||
| newAmountPerSecond?.toString() !== stream.config.amountPerSecond.amount.toString(), | ||
| ); | ||
|
|
||
| // End date handling | ||
| let canAddEndDate = $derived( | ||
| !stream.isPaused && (!stream.config.durationSeconds || stream.config.durationSeconds === 0), | ||
| ); | ||
|
|
||
| let endDateParsed = $derived(parseDate($context.endDateValue)); | ||
| let endTimeParsed = $derived(parseTime($context.endTimeValue)); | ||
|
|
||
| let combinedEndDate = $derived.by(() => { | ||
| if (!endDateParsed.date || !endTimeParsed.time) return undefined; | ||
| return combineDateAndTime(endDateParsed.date, endTimeParsed.time); | ||
| }); | ||
|
|
||
| let endDateInFuture = $derived.by(() => { | ||
| if (!combinedEndDate) return undefined; | ||
| return combinedEndDate.getTime() > Date.now(); | ||
| }); | ||
|
|
||
| let endDateValidationState = $derived.by(() => { | ||
| if (!$context.addEndDate) return { type: 'unvalidated' as const }; | ||
| if (endDateParsed.validationState.type !== 'valid') return endDateParsed.validationState; | ||
| if (endTimeParsed.validationState.type !== 'valid') return endTimeParsed.validationState; | ||
| if (endDateInFuture === false) { | ||
| return { type: 'invalid' as const, message: 'End date must be in the future' }; | ||
| } | ||
| if (combinedEndDate && combinedEndDate.getTime() <= actualStartDate.getTime()) { | ||
| return { type: 'invalid' as const, message: 'End date must be after stream start' }; | ||
| } | ||
| return { type: 'valid' as const }; | ||
| }); | ||
|
|
||
| let endDateUpdated = $derived($context.addEndDate && combinedEndDate !== undefined); | ||
|
|
||
| let canUpdate = $derived( | ||
| newAmountValueParsed && | ||
| $context.newName && | ||
| (nameUpdated || amountUpdated) && | ||
| amountValidationState?.type === 'valid', | ||
| (nameUpdated || amountUpdated || endDateUpdated) && | ||
| amountValidationState?.type === 'valid' && | ||
| (!$context.addEndDate || endDateValidationState.type === 'valid'), | ||
| ); | ||
|
|
||
| function updateStream() { | ||
|
|
@@ -109,12 +150,14 @@ | |
| const { newHash, batch } = await buildEditStreamBatch(stream.id, { | ||
| name: nameUpdated ? $context.newName : undefined, | ||
| amountPerSecond: amountUpdated ? newAmountPerSecond : undefined, | ||
| newEndDate: endDateUpdated ? combinedEndDate : undefined, | ||
| actualStartDate: endDateUpdated ? actualStartDate : undefined, | ||
efstajas marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
150
to
+154
|
||
| }); | ||
|
|
||
| return { | ||
| batch, | ||
| newHash, | ||
| needGasBuffer: amountUpdated, | ||
| needGasBuffer: amountUpdated || endDateUpdated, | ||
| }; | ||
| }, | ||
|
|
||
|
|
@@ -149,7 +192,7 @@ | |
|
|
||
| <StepLayout> | ||
| <StepHeader headline="Edit stream" description="Set a new name or edit the stream rate." /> | ||
efstajas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| <FormField title="New name*"> | ||
| <FormField title="New name"> | ||
| <TextInput bind:value={$context.newName} /> | ||
| </FormField> | ||
| <div class="form-row"> | ||
|
|
@@ -203,6 +246,29 @@ | |
| {#if amountLocked} | ||
| <p class="typo-text">Currently, the stream rate can not be edited for paused streams.</p> | ||
| {/if} | ||
| {#if canAddEndDate} | ||
| <Toggleable bind:toggled={$context.addEndDate} label="Add end date"> | ||
| <p class="typo-text"> | ||
| Set an end date for this stream. The stream will stop at the specified time. | ||
| </p> | ||
efstajas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| <div class="form-row" style="margin-top: 1rem"> | ||
| <FormField title="End date*"> | ||
| <TextInput | ||
| placeholder="YYYY-MM-DD" | ||
| bind:value={$context.endDateValue} | ||
| validationState={$context.addEndDate ? endDateParsed.validationState : undefined} | ||
| /> | ||
| </FormField> | ||
| <FormField title="End time (UTC, 24-hour)*"> | ||
| <TextInput | ||
| placeholder="HH:MM:SS" | ||
| bind:value={$context.endTimeValue} | ||
| validationState={$context.addEndDate ? endTimeParsed.validationState : undefined} | ||
| /> | ||
| </FormField> | ||
| </div> | ||
| </Toggleable> | ||
| {/if} | ||
| <SafeAppDisclaimer disclaimerType="drips" /> | ||
| {#snippet actions()} | ||
| <Button onclick={modal.hide} variant="ghost">Cancel</Button> | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,7 +8,6 @@ import type { | |||||||||||||||||||||||||||
| } from './__generated__/gql.generated'; | ||||||||||||||||||||||||||||
| import { pin } from '../ipfs'; | ||||||||||||||||||||||||||||
| import { toBigInt, type ContractTransaction, type Signer } from 'ethers'; | ||||||||||||||||||||||||||||
| import unreachable from '../unreachable'; | ||||||||||||||||||||||||||||
| import assert from '$lib/utils/assert'; | ||||||||||||||||||||||||||||
| import makeStreamId, { decodeStreamId } from './make-stream-id'; | ||||||||||||||||||||||||||||
| import extractAddressFromAccountId from '../sdk/utils/extract-address-from-accountId'; | ||||||||||||||||||||||||||||
|
|
@@ -412,6 +411,8 @@ export async function buildEditStreamBatch( | |||||||||||||||||||||||||||
| newData: { | ||||||||||||||||||||||||||||
| name?: string; | ||||||||||||||||||||||||||||
| amountPerSecond?: bigint; | ||||||||||||||||||||||||||||
| newEndDate?: Date; | ||||||||||||||||||||||||||||
| actualStartDate?: Date; | ||||||||||||||||||||||||||||
efstajas marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||
| const ownAccountId = await getOwnAccountId(); | ||||||||||||||||||||||||||||
|
|
@@ -430,7 +431,17 @@ export async function buildEditStreamBatch( | |||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| let hash: string | undefined; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if (newData.name) { | ||||||||||||||||||||||||||||
| // Calculate duration from the stream's actual start date | ||||||||||||||||||||||||||||
| let durationSeconds: number | undefined; | ||||||||||||||||||||||||||||
| let startSeconds: bigint | undefined; | ||||||||||||||||||||||||||||
| if (newData.newEndDate && newData.actualStartDate) { | ||||||||||||||||||||||||||||
| startSeconds = BigInt(Math.floor(newData.actualStartDate.getTime() / 1000)); | ||||||||||||||||||||||||||||
| const endDateSeconds = Math.floor(newData.newEndDate.getTime() / 1000); | ||||||||||||||||||||||||||||
| durationSeconds = endDateSeconds - Number(startSeconds); | ||||||||||||||||||||||||||||
efstajas marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||
| assert(durationSeconds > 0, 'Duration must be positive'); | ||||||||||||||||||||||||||||
|
Comment on lines
+438
to
+441
|
||||||||||||||||||||||||||||
| startSeconds = BigInt(Math.floor(newData.actualStartDate.getTime() / 1000)); | |
| const endDateSeconds = Math.floor(newData.newEndDate.getTime() / 1000); | |
| durationSeconds = endDateSeconds - Number(startSeconds); | |
| assert(durationSeconds > 0, 'Duration must be positive'); | |
| const UINT32_MAX = 0xFFFF_FFFFn; | |
| const startSecondsBigInt = BigInt(Math.floor(newData.actualStartDate.getTime() / 1000)); | |
| const endSecondsBigInt = BigInt(Math.floor(newData.newEndDate.getTime() / 1000)); | |
| const durationBigInt = endSecondsBigInt - startSecondsBigInt; | |
| assert(durationBigInt > 0n, 'Duration must be positive'); | |
| assert(startSecondsBigInt <= UINT32_MAX, 'Start time is too far in the future'); | |
| assert(durationBigInt <= UINT32_MAX, 'Duration exceeds maximum allowed value'); | |
| startSeconds = startSecondsBigInt; | |
| durationSeconds = Number(durationBigInt); |
Copilot
AI
Feb 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These conditionals treat name as truthy/falsy (if (newData.name ...)). If the UI passes an empty string (common when clearing an optional text input), this block is skipped and buildEditStreamBatch can return an empty batch, which will later result in attempting to submit a no-op/invalid transaction. Use explicit undefined checks (newData.name !== undefined) and decide how to handle empty-string names (normalize to undefined or allow clearing by deleting the name field in metadata).
Uh oh!
There was an error while loading. Please reload this page.