@@ -25,6 +25,9 @@ use serde::Deserialize;
2525use tower_http:: cors:: CorsLayer ;
2626use tracing:: info;
2727
28+ /// A rough estimate of the maximum fee for a block.
29+ const MAX_FEE : Amount = Amount :: from_millis ( 100 ) ;
30+
2831/// Returns an HTML response constructing the GraphiQL web page for the given URI.
2932pub ( crate ) async fn graphiql ( uri : axum:: http:: Uri ) -> impl axum:: response:: IntoResponse {
3033 axum:: response:: Html (
@@ -47,9 +50,11 @@ pub struct QueryRoot<C> {
4750
4851/// The root GraphQL mutation type.
4952pub struct MutationRoot < C > {
50- chain_id : ChainId ,
53+ main_chain_id : ChainId ,
54+ tmp_chain_id : Arc < Mutex < Option < ChainId > > > ,
5155 context : Arc < Mutex < C > > ,
5256 amount : Amount ,
57+ max_claims_per_chain : u32 ,
5358 end_timestamp : Timestamp ,
5459 start_timestamp : Timestamp ,
5560 start_balance : Amount ,
@@ -118,17 +123,55 @@ where
118123 C : ClientContext ,
119124{
120125 async fn do_claim ( & self , owner : AccountOwner ) -> Result < ClaimOutcome , Error > {
121- let client = self . context . lock ( ) . await . make_chain_client ( self . chain_id ) ?;
126+ let main_client = self
127+ . context
128+ . lock ( )
129+ . await
130+ . make_chain_client ( self . main_chain_id ) ?;
131+ let maybe_tmp_chain_id = * self . tmp_chain_id . lock ( ) . await ;
132+ let tmp_chain_id = match maybe_tmp_chain_id {
133+ Some ( tmp_chain_id) => tmp_chain_id,
134+ None => {
135+ let key_pair = main_client. key_pair ( ) . await ?;
136+ let balance = self
137+ . amount
138+ . try_add ( MAX_FEE ) ?
139+ . try_mul ( u128:: from ( self . max_claims_per_chain ) ) ?
140+ . try_add ( MAX_FEE ) ?; // One more block fee for closing the chain.
141+ let ownership = main_client. chain_state_view ( ) . await ?. ownership ( ) . clone ( ) ;
142+ let ( message_id, certificate) = main_client
143+ . open_chain ( ownership, ApplicationPermissions :: default ( ) , balance)
144+ . await ?
145+ . try_unwrap ( ) ?;
146+ let chain_id = ChainId :: child ( message_id) ;
147+ info ! ( "Switching to a new faucet chain {chain_id:8}" ) ;
148+ self . context
149+ . lock ( )
150+ . await
151+ . update_wallet_for_new_chain (
152+ chain_id,
153+ Some ( key_pair) ,
154+ certificate. block ( ) . header . timestamp ,
155+ )
156+ . await ?;
157+ * self . tmp_chain_id . lock ( ) . await = Some ( chain_id) ;
158+ chain_id
159+ }
160+ } ;
161+ let tmp_client = self . context . lock ( ) . await . make_chain_client ( tmp_chain_id) ?;
122162
123163 if self . start_timestamp < self . end_timestamp {
124- let local_time = client . storage_client ( ) . clock ( ) . current_time ( ) ;
164+ let local_time = tmp_client . storage_client ( ) . clock ( ) . current_time ( ) ;
125165 if local_time < self . end_timestamp {
126166 let full_duration = self
127167 . end_timestamp
128168 . delta_since ( self . start_timestamp )
129169 . as_micros ( ) ;
130170 let remaining_duration = self . end_timestamp . delta_since ( local_time) . as_micros ( ) ;
131- let balance = client. local_balance ( ) . await ?;
171+ let balance = tmp_client
172+ . local_balance ( )
173+ . await ?
174+ . try_add ( main_client. local_balance ( ) . await ?) ?;
132175 let Ok ( remaining_balance) = balance. try_sub ( self . amount ) else {
133176 return Err ( Error :: new ( "The faucet is empty." ) ) ;
134177 } ;
@@ -145,20 +188,29 @@ where
145188 }
146189
147190 let ownership = ChainOwnership :: single ( owner) ;
148- let result = client
191+ let result = tmp_client
149192 . open_chain ( ownership, ApplicationPermissions :: default ( ) , self . amount )
150193 . await ;
151- self . context . lock ( ) . await . update_wallet ( & client) . await ?;
152- let ( message_id, certificate) = match result? {
153- ClientOutcome :: Committed ( result) => result,
154- ClientOutcome :: WaitForTimeout ( timeout) => {
155- return Err ( Error :: new ( format ! (
156- "This faucet is using a multi-owner chain and is not the leader right now. \
157- Try again at {}",
158- timeout. timestamp,
159- ) ) ) ;
194+ self . context . lock ( ) . await . update_wallet ( & tmp_client) . await ?;
195+ let ( message_id, certificate) = result?. try_unwrap ( ) ?;
196+
197+ // Only keep using this chain if there will still be enough balance to close it.
198+ if tmp_client. local_balance ( ) . await ? < self . amount . try_add ( MAX_FEE . try_mul ( 2 ) ?) ? {
199+ // TODO(#1795): Move the remaining tokens back to the main chain.
200+ match tmp_client. close_chain ( ) . await {
201+ Ok ( outcome) => {
202+ outcome. try_unwrap ( ) ?;
203+ }
204+ Err ( err) => tracing:: warn!( "Failed to close the chain: {err:?}" ) ,
160205 }
161- } ;
206+ self . context
207+ . lock ( )
208+ . await
209+ . forget_chain ( & tmp_chain_id)
210+ . await ?;
211+ self . tmp_chain_id . lock ( ) . await . take ( ) ;
212+ }
213+
162214 let chain_id = ChainId :: child ( message_id) ;
163215 Ok ( ClaimOutcome {
164216 message_id,
@@ -185,14 +237,16 @@ pub struct FaucetService<C>
185237where
186238 C : ClientContext ,
187239{
188- chain_id : ChainId ,
240+ main_chain_id : ChainId ,
241+ tmp_chain_id : Arc < Mutex < Option < ChainId > > > ,
189242 context : Arc < Mutex < C > > ,
190243 genesis_config : Arc < GenesisConfig > ,
191244 config : ChainListenerConfig ,
192245 storage : C :: Storage ,
193246 port : NonZeroU16 ,
194247 amount : Amount ,
195248 end_timestamp : Timestamp ,
249+ max_claims_per_chain : u32 ,
196250 start_timestamp : Timestamp ,
197251 start_balance : Amount ,
198252}
@@ -203,13 +257,15 @@ where
203257{
204258 fn clone ( & self ) -> Self {
205259 Self {
206- chain_id : self . chain_id ,
260+ main_chain_id : self . main_chain_id ,
261+ tmp_chain_id : Arc :: clone ( & self . tmp_chain_id ) ,
207262 context : Arc :: clone ( & self . context ) ,
208263 genesis_config : Arc :: clone ( & self . genesis_config ) ,
209264 config : self . config . clone ( ) ,
210265 storage : self . storage . clone ( ) ,
211266 port : self . port ,
212267 amount : self . amount ,
268+ max_claims_per_chain : self . max_claims_per_chain ,
213269 end_timestamp : self . end_timestamp ,
214270 start_timestamp : self . start_timestamp ,
215271 start_balance : self . start_balance ,
@@ -228,6 +284,7 @@ where
228284 chain_id : ChainId ,
229285 context : C ,
230286 amount : Amount ,
287+ max_claims_per_chain : u32 ,
231288 end_timestamp : Timestamp ,
232289 genesis_config : Arc < GenesisConfig > ,
233290 config : ChainListenerConfig ,
@@ -239,13 +296,15 @@ where
239296 client. process_inbox ( ) . await ?;
240297 let start_balance = client. local_balance ( ) . await ?;
241298 Ok ( Self {
242- chain_id,
299+ main_chain_id : chain_id,
300+ tmp_chain_id : Arc :: new ( Mutex :: new ( None ) ) ,
243301 context,
244302 genesis_config,
245303 config,
246304 storage,
247305 port,
248306 amount,
307+ max_claims_per_chain,
249308 end_timestamp,
250309 start_timestamp,
251310 start_balance,
@@ -254,23 +313,28 @@ where
254313
255314 pub fn schema ( & self ) -> Schema < QueryRoot < C > , MutationRoot < C > , EmptySubscription > {
256315 let mutation_root = MutationRoot {
257- chain_id : self . chain_id ,
316+ main_chain_id : self . main_chain_id ,
317+ tmp_chain_id : Arc :: clone ( & self . tmp_chain_id ) ,
258318 context : Arc :: clone ( & self . context ) ,
259319 amount : self . amount ,
320+ max_claims_per_chain : self . max_claims_per_chain ,
260321 end_timestamp : self . end_timestamp ,
261322 start_timestamp : self . start_timestamp ,
262323 start_balance : self . start_balance ,
263324 } ;
264325 let query_root = QueryRoot {
265326 genesis_config : Arc :: clone ( & self . genesis_config ) ,
266327 context : Arc :: clone ( & self . context ) ,
267- chain_id : self . chain_id ,
328+ chain_id : self . main_chain_id ,
268329 } ;
269330 Schema :: build ( query_root, mutation_root, EmptySubscription ) . finish ( )
270331 }
271332
272333 /// Runs the faucet.
273- #[ tracing:: instrument( name = "FaucetService::run" , skip_all, fields( port = self . port, chain_id = ?self . chain_id) ) ]
334+ #[ tracing:: instrument(
335+ name = "FaucetService::run" ,
336+ skip_all, fields( port = self . port, chain_id = ?self . main_chain_id) )
337+ ]
274338 pub async fn run ( self ) -> anyhow:: Result < ( ) > {
275339 let port = self . port . get ( ) ;
276340 let index_handler = axum:: routing:: get ( graphiql) . post ( Self :: index_handler) ;
@@ -303,3 +367,27 @@ where
303367 schema. execute ( request. into_inner ( ) ) . await . into ( )
304368 }
305369}
370+
371+ trait ClientOutcomeExt {
372+ type Output ;
373+
374+ /// Returns the committed result or an error if we are not the leader.
375+ ///
376+ /// It is recommended to use single-owner chains for the faucet to avoid this error.
377+ fn try_unwrap ( self ) -> Result < Self :: Output , Error > ;
378+ }
379+
380+ impl < T > ClientOutcomeExt for ClientOutcome < T > {
381+ type Output = T ;
382+
383+ fn try_unwrap ( self ) -> Result < Self :: Output , Error > {
384+ match self {
385+ ClientOutcome :: Committed ( result) => Ok ( result) ,
386+ ClientOutcome :: WaitForTimeout ( timeout) => Err ( Error :: new ( format ! (
387+ "This faucet is using a multi-owner chain and is not the leader right now. \
388+ Try again at {}",
389+ timeout. timestamp,
390+ ) ) ) ,
391+ }
392+ }
393+ }
0 commit comments