diff --git a/src/lib/flows/edit-stream-flow/edit-stream-flow-state.ts b/src/lib/flows/edit-stream-flow/edit-stream-flow-state.ts index 915beffd6..a9e8d2435 100644 --- a/src/lib/flows/edit-stream-flow/edit-stream-flow-state.ts +++ b/src/lib/flows/edit-stream-flow/edit-stream-flow-state.ts @@ -9,6 +9,9 @@ export interface EditStreamFlowState { newAmountValue: string | undefined; newName: string | undefined; newSelectedMultiplier: string; + addEndDate: boolean; + endDateValue: string | undefined; + endTimeValue: string | undefined; } export default (stream: EditStreamFlowStreamFragment) => { @@ -23,5 +26,8 @@ export default (stream: EditStreamFlowStreamFragment) => { ), newName: stream.name ?? undefined, newSelectedMultiplier: '1', + addEndDate: false, + endDateValue: undefined, + endTimeValue: undefined, }); }; diff --git a/src/lib/flows/edit-stream-flow/enter-new-details.svelte b/src/lib/flows/edit-stream-flow/enter-new-details.svelte index c016d5d85..284bfd773 100644 --- a/src/lib/flows/edit-stream-flow/enter-new-details.svelte +++ b/src/lib/flows/edit-stream-flow/enter-new-details.svelte @@ -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, }); return { batch, newHash, - needGasBuffer: amountUpdated, + needGasBuffer: amountUpdated || endDateUpdated, }; }, @@ -149,7 +192,7 @@ - +
@@ -203,6 +246,29 @@ {#if amountLocked}

Currently, the stream rate can not be edited for paused streams.

{/if} + {#if canAddEndDate} + +

+ Set an end date for this stream. The stream will stop at the specified time. +

+
+ + + + + + +
+
+ {/if} {#snippet actions()} diff --git a/src/lib/utils/streams/streams.ts b/src/lib/utils/streams/streams.ts index 6e45fffc0..aeee5fe54 100644 --- a/src/lib/utils/streams/streams.ts +++ b/src/lib/utils/streams/streams.ts @@ -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; }, ) { 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); + assert(durationSeconds > 0, 'Duration must be positive'); + } + + if (newData.name || durationSeconds !== undefined) { const metadata = _buildMetadata(currentStreams, ownAccountId); const assetConfigIndex = metadata.assetConfigs.findIndex( @@ -446,7 +457,15 @@ export async function buildEditStreamBatch( `Stream ${streamId} not found in metadata`, ); - metadata.assetConfigs[assetConfigIndex].streams[streamIndex].name = newData.name; + if (newData.name) { + metadata.assetConfigs[assetConfigIndex].streams[streamIndex].name = newData.name; + } + + if (durationSeconds !== undefined) { + metadata.assetConfigs[assetConfigIndex].streams[ + streamIndex + ].initialDripsConfig.durationSeconds = durationSeconds; + } hash = await pin(metadata); @@ -458,7 +477,7 @@ export async function buildEditStreamBatch( ); } - if (newData.amountPerSecond) { + if (newData.amountPerSecond || durationSeconds !== undefined) { const newReceivers = currentReceivers.map((r) => { const streamConfig = streamConfigFromUint256(r.config); @@ -467,9 +486,10 @@ export async function buildEditStreamBatch( accountId: r.accountId, config: streamConfigToUint256({ dripId: streamConfig.dripId, - start: streamConfig.start, - duration: streamConfig.duration, - amountPerSec: newData.amountPerSecond ?? unreachable(), + start: startSeconds ?? streamConfig.start, + duration: + durationSeconds !== undefined ? BigInt(durationSeconds) : streamConfig.duration, + amountPerSec: newData.amountPerSecond ?? streamConfig.amountPerSec, }), }; } diff --git a/src/routes/(pages)/app/(app)/[accountId]/tokens/[token]/streams/[dripId]/+page.svelte b/src/routes/(pages)/app/(app)/[accountId]/tokens/[token]/streams/[dripId]/+page.svelte index 1756781d3..e26ea7c30 100644 --- a/src/routes/(pages)/app/(app)/[accountId]/tokens/[token]/streams/[dripId]/+page.svelte +++ b/src/routes/(pages)/app/(app)/[accountId]/tokens/[token]/streams/[dripId]/+page.svelte @@ -309,7 +309,7 @@