@@ -8,263 +8,4 @@ export const metadata: Metadata = {
88
99export default function CreateStreamPage ( ) {
1010 return < CreateStreamContent /> ;
11- import React , { useState } from "react" ;
12- import {
13- createStream ,
14- toBaseUnits ,
15- toDurationSeconds ,
16- getTokenAddress ,
17- toSorobanErrorMessage ,
18- TOKEN_ADDRESSES
19- } from "@/lib/soroban" ;
20- import { hasValidPrecision , validateAmountInput } from "@/utils/amount" ;
21- import { isValidStellarPublicKey } from "@/lib/stellar" ;
22- import { toast } from "react-hot-toast" ;
23- import { useRouter } from "next/navigation" ;
24- import Link from "next/link" ;
25- import { ArrowLeft } from "lucide-react" ;
26- import { useWallet } from "@/context/wallet-context" ;
27-
28- const TOKEN_DECIMALS = 7 ;
29-
30- export default function CreateStreamPage ( ) {
31- const { status, session } = useWallet ( ) ;
32- const router = useRouter ( ) ;
33- const [ nowTimestamp ] = useState ( ( ) => Date . now ( ) ) ;
34- const [ loading , setLoading ] = useState ( false ) ;
35- const [ txState , setTxState ] = useState < "idle" | "signing" | "submitted" | "confirming" > ( "idle" ) ;
36- const [ formData , setFormData ] = useState ( {
37- recipient : "" ,
38- token : "XLM" ,
39- amount : "" ,
40- duration : "30" , // days
41- } ) ;
42-
43- const handleSubmit = async ( e : React . FormEvent ) => {
44- e . preventDefault ( ) ;
45- if ( status !== "connected" || ! session ) {
46- toast . error ( "Please connect your wallet first." ) ;
47- return ;
48- }
49-
50- // Validate recipient
51- if ( ! formData . recipient . trim ( ) ) {
52- toast . error ( "Recipient address is required" ) ;
53- return ;
54- }
55- if ( ! isValidStellarPublicKey ( formData . recipient ) ) {
56- toast . error ( "Invalid Stellar public key format" ) ;
57- return ;
58- }
59-
60- // Validate amount
61- const validationError = validateAmountInput ( formData . amount , TOKEN_DECIMALS ) ;
62- if ( validationError ) {
63- toast . error ( validationError ) ;
64- return ;
65- }
66-
67- // Validate duration
68- const durationNum = parseFloat ( formData . duration ) ;
69- if ( isNaN ( durationNum ) || durationNum <= 0 ) {
70- toast . error ( "Duration must be a positive number" ) ;
71- return ;
72- }
73-
74- setLoading ( true ) ;
75- setTxState ( "signing" ) ;
76-
77- try {
78- const amountBigInt = toBaseUnits ( formData . amount ) ;
79- const durationBigInt = toDurationSeconds ( formData . duration , "days" ) ;
80- const tokenAddress = getTokenAddress ( formData . token ) ;
81-
82- const result = await createStream ( session , {
83- recipient : formData . recipient ,
84- tokenAddress,
85- amount : amountBigInt ,
86- durationSeconds : durationBigInt ,
87- } ) ;
88-
89- if ( result . success ) {
90- setTxState ( "confirming" ) ;
91- toast . success ( "Stream created successfully!" ) ;
92- // Small delay to allow indexer to catch up
93- setTimeout ( ( ) => {
94- router . push ( "/dashboard" ) ;
95- } , 2000 ) ;
96- }
97- } catch ( error ) {
98- console . error ( "Stream creation failed:" , error ) ;
99- toast . error ( toSorobanErrorMessage ( error ) ) ;
100- } finally {
101- setLoading ( false ) ;
102- setTxState ( "idle" ) ;
103- }
104- } ;
105-
106- const getButtonText = ( ) => {
107- if ( ! loading ) return "Start Streaming" ;
108- switch ( txState ) {
109- case "signing" : return "Confirm in Wallet..." ;
110- case "submitted" : return "Submitting to Network..." ;
111- case "confirming" : return "Finalizing Stream..." ;
112- default : return "Processing..." ;
113- }
114- } ;
115-
116- // Inline validation feedback for the amount field. validateAmountInput
117- // returns an error message when invalid and null when valid. Only show it
118- // once the user has typed something — the empty case is handled on submit.
119- const amountError = formData . amount
120- ? validateAmountInput ( formData . amount , TOKEN_DECIMALS )
121- : null ;
122-
123- const recipientError = formData . recipient
124- ? ( ! isValidStellarPublicKey ( formData . recipient ) ? "Invalid Stellar public key format" : null )
125- : null ;
126-
127- const durationError = formData . duration
128- ? ( isNaN ( Number ( formData . duration ) ) || Number ( formData . duration ) <= 0
129- ? "Duration must be a positive number"
130- : null )
131- : null ;
132-
133- return (
134- < div className = "container mx-auto max-w-2xl px-4 py-12" >
135- < Link
136- href = "/dashboard"
137- className = "mb-8 inline-flex items-center text-sm font-medium text-slate-400 hover:text-white transition-colors"
138- >
139- < ArrowLeft className = "mr-2 h-4 w-4" />
140- Back to Dashboard
141- </ Link >
142-
143- < div className = "glass-card rounded-3xl border-slate-800 p-8" >
144- < h1 className = "mb-2 text-3xl font-bold" > Create New Stream</ h1 >
145- < p className = "mb-8 text-slate-400" >
146- Set up a real-time payment stream to any Stellar address.
147- </ p >
148-
149- < form onSubmit = { handleSubmit } className = "space-y-6" >
150- < div className = "space-y-2" >
151- < label className = "text-sm font-medium text-slate-300" >
152- Recipient Address
153- </ label >
154- < input
155- type = "text"
156- placeholder = "G..."
157- className = { `w-full rounded-xl border ${
158- recipientError ? "border-red-500 focus:border-red-500" : "border-slate-800 focus:border-accent"
159- } bg-slate-900/50 p-4 outline-none transition-colors`}
160- value = { formData . recipient }
161- onChange = { ( e ) => setFormData ( { ...formData , recipient : e . target . value } ) }
162- required
163- />
164- { recipientError && (
165- < p className = "text-xs text-red-400 mt-1" > { recipientError } </ p >
166- ) }
167- </ div >
168-
169- < div className = "grid grid-cols-2 gap-4" >
170- < div className = "space-y-2" >
171- < label className = "text-sm font-medium text-slate-300" >
172- Token
173- </ label >
174- < select
175- className = "w-full rounded-xl border border-slate-800 bg-slate-900/50 p-4 outline-none focus:border-accent transition-colors appearance-none"
176- value = { formData . token }
177- onChange = { ( e ) => setFormData ( { ...formData , token : e . target . value } ) }
178- >
179- { Object . keys ( TOKEN_ADDRESSES ) . map ( ( symbol ) => (
180- < option key = { symbol } value = { symbol } >
181- { symbol }
182- </ option >
183- ) ) }
184- </ select >
185- </ div >
186- < div className = "space-y-2" >
187- < label className = "text-sm font-medium text-slate-300" >
188- Total Amount
189- </ label >
190- < input
191- type = "text"
192- inputMode = "decimal"
193- placeholder = "0.00"
194- className = { `w-full rounded-xl border ${
195- amountError ? "border-red-500 focus:border-red-500" : "border-slate-800 focus:border-accent"
196- } bg-slate-900/50 p-4 outline-none transition-colors`}
197- value = { formData . amount }
198- onChange = { ( e ) => {
199- const newValue = e . target . value ;
200- // Only allow valid number characters and check precision
201- if ( newValue === '' || / ^ \d * \. ? \d * $ / . test ( newValue ) ) {
202- if ( hasValidPrecision ( newValue , TOKEN_DECIMALS ) ) {
203- setFormData ( { ...formData , amount : newValue } ) ;
204- }
205- }
206- } }
207- required
208- />
209- { amountError && (
210- < p className = "text-xs text-red-400 mt-1" > { amountError } </ p >
211- ) }
212- </ div >
213- </ div >
214-
215- < div className = "space-y-2" >
216- < label className = "text-sm font-medium text-slate-300" >
217- Duration (Days)
218- </ label >
219- < input
220- type = "number"
221- placeholder = "30"
222- className = { `w-full rounded-xl border ${
223- durationError ? "border-red-500 focus:border-red-500" : "border-slate-800 focus:border-accent"
224- } bg-slate-900/50 p-4 outline-none transition-colors`}
225- value = { formData . duration }
226- onChange = { ( e ) => setFormData ( { ...formData , duration : e . target . value } ) }
227- required
228- />
229- { durationError && (
230- < p className = "text-xs text-red-400 mt-1" > { durationError } </ p >
231- ) }
232- </ div >
233-
234- < div className = "rounded-2xl bg-accent/5 p-6 space-y-4" >
235- < div className = "flex justify-between items-center text-sm" >
236- < span className = "text-slate-400" > Streaming Rate</ span >
237- < span className = "font-mono font-medium text-accent" >
238- { formData . amount && formData . duration && Number ( formData . duration ) > 0
239- ? ( Number ( formData . amount ) / ( Number ( formData . duration ) * 86400 ) ) . toFixed ( 8 )
240- : "0.00000000" } { formData . token } /sec
241- </ span >
242- </ div >
243- < div className = "flex justify-between items-center text-sm" >
244- < span className = "text-slate-400" > Estimated End Date</ span >
245- < span className = "font-medium" >
246- { formData . duration && Number ( formData . duration ) > 0
247- ? new Date ( nowTimestamp + Number ( formData . duration ) * 86400000 ) . toLocaleDateString ( )
248- : "—" }
249- </ span >
250- </ div >
251- </ div >
252-
253- < button
254- type = "submit"
255- disabled = { loading || status !== "connected" || ! ! amountError || ! ! recipientError || ! ! durationError }
256- className = "w-full rounded-xl bg-accent py-4 text-lg font-bold text-background transition-all hover:opacity-90 disabled:opacity-50 active:scale-[0.98]"
257- >
258- { getButtonText ( ) }
259- </ button >
260-
261- { status !== "connected" && (
262- < p className = "text-center text-sm text-red-400" >
263- Please connect your wallet to create a stream.
264- </ p >
265- ) }
266- </ form >
267- </ div >
268- </ div >
269- ) ;
27011}
0 commit comments