@@ -4,9 +4,12 @@ import PQueue from 'p-queue';
4
4
import { BigNumber } from 'bignumber.js' ;
5
5
import {
6
6
AnchorMode ,
7
+ estimateTransactionFeeWithFallback ,
8
+ getAddressFromPrivateKey ,
7
9
makeSTXTokenTransfer ,
8
10
SignedTokenTransferOptions ,
9
11
StacksTransaction ,
12
+ TransactionVersion ,
10
13
} from '@stacks/transactions' ;
11
14
import { StacksNetwork } from '@stacks/network' ;
12
15
import {
@@ -16,7 +19,7 @@ import {
16
19
isValidBtcAddress ,
17
20
} from '../../btc-faucet' ;
18
21
import { DbFaucetRequestCurrency } from '../../datastore/common' ;
19
- import { getChainIDNetwork , getStxFaucetNetworks , intMax , stxToMicroStx } from '../../helpers' ;
22
+ import { getChainIDNetwork , getStxFaucetNetwork , stxToMicroStx } from '../../helpers' ;
20
23
import { testnetKeys } from './debug' ;
21
24
import { StacksCoreRpcClient } from '../../core-rpc/client' ;
22
25
import { logger } from '../../logger' ;
@@ -27,25 +30,6 @@ import { Server } from 'node:http';
27
30
import { OptionalNullable } from '../schemas/util' ;
28
31
import { RunFaucetResponseSchema } from '../schemas/responses/responses' ;
29
32
30
- enum TxSendResultStatus {
31
- Success ,
32
- ConflictingNonce ,
33
- TooMuchChaining ,
34
- Error ,
35
- }
36
-
37
- interface TxSendResultSuccess {
38
- status : TxSendResultStatus . Success ;
39
- txId : string ;
40
- }
41
-
42
- interface TxSendResultError {
43
- status : TxSendResultStatus ;
44
- error : Error ;
45
- }
46
-
47
- type TxSendResult = TxSendResultSuccess | TxSendResultError ;
48
-
49
33
function clientFromNetwork ( network : StacksNetwork ) : StacksCoreRpcClient {
50
34
const coreUrl = new URL ( network . coreApiUrl ) ;
51
35
return new StacksCoreRpcClient ( { host : coreUrl . hostname , port : coreUrl . port } ) ;
@@ -230,9 +214,73 @@ export const FaucetRoutes: FastifyPluginAsync<
230
214
const FAUCET_STACKING_WINDOW = 2 * 24 * 60 * 60 * 1000 ; // 2 days
231
215
const FAUCET_STACKING_TRIGGER_COUNT = 1 ;
232
216
233
- const STX_FAUCET_NETWORKS = ( ) => getStxFaucetNetworks ( ) ;
217
+ const STX_FAUCET_NETWORK = ( ) => getStxFaucetNetwork ( ) ;
234
218
const STX_FAUCET_KEYS = ( process . env . FAUCET_PRIVATE_KEY ?? testnetKeys [ 0 ] . secretKey ) . split ( ',' ) ;
235
219
220
+ async function calculateSTXFaucetAmount (
221
+ network : StacksNetwork ,
222
+ stacking : boolean
223
+ ) : Promise < bigint > {
224
+ if ( stacking ) {
225
+ try {
226
+ const poxInfo = await clientFromNetwork ( network ) . getPox ( ) ;
227
+ let stxAmount = BigInt ( poxInfo . min_amount_ustx ) ;
228
+ const padPercent = new BigNumber ( 0.2 ) ;
229
+ const padAmount = new BigNumber ( stxAmount . toString ( ) )
230
+ . times ( padPercent )
231
+ . integerValue ( )
232
+ . toString ( ) ;
233
+ stxAmount = stxAmount + BigInt ( padAmount ) ;
234
+ return stxAmount ;
235
+ } catch ( error ) {
236
+ // ignore
237
+ }
238
+ }
239
+ return FAUCET_DEFAULT_STX_AMOUNT ;
240
+ }
241
+
242
+ async function fetchNetworkChainID ( network : StacksNetwork ) : Promise < number > {
243
+ const rpcClient = clientFromNetwork ( network ) ;
244
+ const info = await rpcClient . getInfo ( ) ;
245
+ return info . network_id ;
246
+ }
247
+
248
+ async function buildSTXFaucetTx (
249
+ recipient : string ,
250
+ amount : bigint ,
251
+ network : StacksNetwork ,
252
+ senderKey : string ,
253
+ nonce : bigint ,
254
+ fee ?: bigint
255
+ ) : Promise < StacksTransaction > {
256
+ try {
257
+ const options : SignedTokenTransferOptions = {
258
+ recipient,
259
+ amount,
260
+ senderKey,
261
+ network,
262
+ memo : 'faucet' ,
263
+ anchorMode : AnchorMode . Any ,
264
+ nonce,
265
+ } ;
266
+ if ( fee ) options . fee = fee ;
267
+
268
+ // Detect possible custom network chain ID
269
+ network . chainId = await fetchNetworkChainID ( network ) ;
270
+
271
+ return await makeSTXTokenTransfer ( options ) ;
272
+ } catch ( error : any ) {
273
+ if (
274
+ fee === undefined &&
275
+ ( error as Error ) . message &&
276
+ / e s t i m a t i n g t r a n s a c t i o n f e e | N o E s t i m a t e A v a i l a b l e / . test ( error . message )
277
+ ) {
278
+ return await buildSTXFaucetTx ( recipient , amount , network , senderKey , nonce , 200n ) ;
279
+ }
280
+ throw error ;
281
+ }
282
+ }
283
+
236
284
fastify . post (
237
285
'/stx' ,
238
286
{
@@ -302,193 +350,97 @@ export const FaucetRoutes: FastifyPluginAsync<
302
350
} ) ;
303
351
}
304
352
305
- const address = req . query . address ;
306
- if ( ! address ) {
353
+ const recipientAddress = req . query . address ;
354
+ if ( ! recipientAddress ) {
307
355
return await reply . status ( 400 ) . send ( {
308
356
error : 'address required' ,
309
357
success : false ,
310
358
} ) ;
311
359
}
312
360
313
361
await stxFaucetRequestQueue . add ( async ( ) => {
314
- const ip = req . headers [ 'x-forwarded-for' ] || req . connection . remoteAddress ;
315
- const lastRequests = await fastify . db . getSTXFaucetRequests ( address ) ;
316
-
317
- const isStackingReq = req . query . stacking ?? false ;
318
-
319
362
// Guard condition: requests are limited to x times per y minutes.
320
363
// Only based on address for now, but we're keeping the IP in case
321
364
// we want to escalate and implement a per IP policy
365
+ const ip = req . headers [ 'x-forwarded-for' ] || req . connection . remoteAddress ;
366
+ const lastRequests = await fastify . db . getSTXFaucetRequests ( recipientAddress ) ;
322
367
const now = Date . now ( ) ;
368
+ const isStackingReq = req . query . stacking ?? false ;
323
369
const [ window , triggerCount ] = isStackingReq
324
370
? [ FAUCET_STACKING_WINDOW , FAUCET_STACKING_TRIGGER_COUNT ]
325
371
: [ FAUCET_DEFAULT_WINDOW , FAUCET_DEFAULT_TRIGGER_COUNT ] ;
326
-
327
372
const requestsInWindow = lastRequests . results
328
373
. map ( r => now - r . occurred_at )
329
374
. filter ( r => r <= window ) ;
330
375
if ( requestsInWindow . length >= triggerCount ) {
331
- logger . warn ( `STX faucet rate limit hit for address ${ address } ` ) ;
376
+ logger . warn ( `StxFaucet rate limit hit for address ${ recipientAddress } ` ) ;
332
377
return await reply . status ( 429 ) . send ( {
333
378
error : 'Too many requests' ,
334
379
success : false ,
335
380
} ) ;
336
381
}
337
382
338
- const stxAmounts : bigint [ ] = [ ] ;
339
- for ( const network of STX_FAUCET_NETWORKS ( ) ) {
340
- try {
341
- let stxAmount = FAUCET_DEFAULT_STX_AMOUNT ;
342
- if ( isStackingReq ) {
343
- const poxInfo = await clientFromNetwork ( network ) . getPox ( ) ;
344
- stxAmount = BigInt ( poxInfo . min_amount_ustx ) ;
345
- const padPercent = new BigNumber ( 0.2 ) ;
346
- const padAmount = new BigNumber ( stxAmount . toString ( ) )
347
- . times ( padPercent )
348
- . integerValue ( )
349
- . toString ( ) ;
350
- stxAmount = stxAmount + BigInt ( padAmount ) ;
351
- }
352
- stxAmounts . push ( stxAmount ) ;
353
- } catch ( error ) {
354
- // ignore
355
- }
356
- }
357
- const stxAmount = intMax ( stxAmounts ) ;
358
-
359
- const generateTx = async (
360
- network : StacksNetwork ,
361
- keyIndex : number ,
362
- nonce ?: bigint ,
363
- fee ?: bigint
364
- ) : Promise < StacksTransaction > => {
365
- const txOpts : SignedTokenTransferOptions = {
366
- recipient : address ,
367
- amount : stxAmount ,
368
- senderKey : STX_FAUCET_KEYS [ keyIndex ] ,
369
- network : network ,
370
- memo : 'Faucet' ,
371
- anchorMode : AnchorMode . Any ,
372
- } ;
373
- if ( fee !== undefined ) {
374
- txOpts . fee = fee ;
375
- }
376
- if ( nonce !== undefined ) {
377
- txOpts . nonce = nonce ;
378
- }
383
+ // Start with a random key index. We will try others in order if this one fails.
384
+ let keyIndex = Math . round ( Math . random ( ) * ( STX_FAUCET_KEYS . length - 1 ) ) ;
385
+ let keysAttempted = 0 ;
386
+ let sendSuccess : { txId : string ; txRaw : string } | undefined ;
387
+ const stxAmount = await calculateSTXFaucetAmount ( STX_FAUCET_NETWORK ( ) , isStackingReq ) ;
388
+ const rpcClient = clientFromNetwork ( STX_FAUCET_NETWORK ( ) ) ;
389
+ do {
390
+ keysAttempted ++ ;
391
+ const senderKey = STX_FAUCET_KEYS [ keyIndex ] ;
392
+ const senderAddress = getAddressFromPrivateKey ( senderKey , TransactionVersion . Testnet ) ;
393
+ logger . debug ( `StxFaucet attempting faucet transaction from sender: ${ senderAddress } ` ) ;
394
+ const nonces = await fastify . db . getAddressNonces ( { stxAddress : senderAddress } ) ;
395
+ const tx = await buildSTXFaucetTx (
396
+ recipientAddress ,
397
+ stxAmount ,
398
+ STX_FAUCET_NETWORK ( ) ,
399
+ senderKey ,
400
+ BigInt ( nonces . possibleNextNonce )
401
+ ) ;
402
+ const rawTx = Buffer . from ( tx . serialize ( ) ) ;
379
403
try {
380
- return await makeSTXTokenTransfer ( txOpts ) ;
404
+ const res = await rpcClient . sendTransaction ( rawTx ) ;
405
+ sendSuccess = { txId : res . txId , txRaw : rawTx . toString ( 'hex' ) } ;
406
+ logger . info (
407
+ `StxFaucet success. Sent ${ stxAmount } uSTX from ${ senderAddress } to ${ recipientAddress } .`
408
+ ) ;
381
409
} catch ( error : any ) {
382
410
if (
383
- fee === undefined &&
384
- ( error as Error ) . message &&
385
- / e s t i m a t i n g t r a n s a c t i o n f e e | N o E s t i m a t e A v a i l a b l e / . test ( error . message )
411
+ error . message ?. includes ( 'ConflictingNonceInMempool' ) ||
412
+ error . message ?. includes ( 'TooMuchChaining' )
386
413
) {
387
- const defaultFee = 200n ;
388
- return await generateTx ( network , keyIndex , nonce , defaultFee ) ;
389
- }
390
- throw error ;
391
- }
392
- } ;
393
-
394
- const nonces : bigint [ ] = [ ] ;
395
- const fees : bigint [ ] = [ ] ;
396
- let txGenFetchError : Error | undefined ;
397
- for ( const network of STX_FAUCET_NETWORKS ( ) ) {
398
- try {
399
- const tx = await generateTx ( network , 0 ) ;
400
- nonces . push ( tx . auth . spendingCondition ?. nonce ?? BigInt ( 0 ) ) ;
401
- fees . push ( tx . auth . spendingCondition . fee ) ;
402
- } catch ( error : any ) {
403
- txGenFetchError = error ;
404
- }
405
- }
406
- if ( nonces . length === 0 ) {
407
- throw txGenFetchError ;
408
- }
409
- let nextNonce = intMax ( nonces ) ;
410
- const fee = intMax ( fees ) ;
411
-
412
- const sendTxResults : TxSendResult [ ] = [ ] ;
413
- let retrySend = false ;
414
- let sendSuccess : { txId : string ; txRaw : string } | undefined ;
415
- let lastSendError : Error | undefined ;
416
- let stxKeyIndex = 0 ;
417
- do {
418
- const tx = await generateTx ( STX_FAUCET_NETWORKS ( ) [ 0 ] , stxKeyIndex , nextNonce , fee ) ;
419
- const rawTx = Buffer . from ( tx . serialize ( ) ) ;
420
- for ( const network of STX_FAUCET_NETWORKS ( ) ) {
421
- const rpcClient = clientFromNetwork ( network ) ;
422
- try {
423
- const res = await rpcClient . sendTransaction ( rawTx ) ;
424
- sendSuccess = { txId : res . txId , txRaw : rawTx . toString ( 'hex' ) } ;
425
- sendTxResults . push ( {
426
- status : TxSendResultStatus . Success ,
427
- txId : res . txId ,
428
- } ) ;
429
- } catch ( error : any ) {
430
- lastSendError = error ;
431
- if ( error . message ?. includes ( 'ConflictingNonceInMempool' ) ) {
432
- sendTxResults . push ( {
433
- status : TxSendResultStatus . ConflictingNonce ,
434
- error,
435
- } ) ;
436
- } else if ( error . message ?. includes ( 'TooMuchChaining' ) ) {
437
- sendTxResults . push ( {
438
- status : TxSendResultStatus . TooMuchChaining ,
439
- error,
440
- } ) ;
441
- } else {
442
- sendTxResults . push ( {
443
- status : TxSendResultStatus . Error ,
444
- error,
445
- } ) ;
414
+ if ( keysAttempted == STX_FAUCET_KEYS . length ) {
415
+ logger . warn (
416
+ `StxFaucet attempts exhausted for all faucet keys. Last error: ${ error } `
417
+ ) ;
418
+ throw error ;
446
419
}
447
- }
448
- }
449
- if ( sendTxResults . every ( res => res . status === TxSendResultStatus . Success ) ) {
450
- retrySend = false ;
451
- } else if (
452
- sendTxResults . every ( res => res . status === TxSendResultStatus . ConflictingNonce )
453
- ) {
454
- retrySend = true ;
455
- sendTxResults . length = 0 ;
456
- nextNonce = nextNonce + 1n ;
457
- } else if (
458
- sendTxResults . every ( res => res . status === TxSendResultStatus . TooMuchChaining )
459
- ) {
460
- // Try with the next key in case we have one.
461
- if ( stxKeyIndex + 1 === STX_FAUCET_KEYS . length ) {
462
- retrySend = false ;
420
+ // Try with the next key. Wrap around the keys array if necessary.
421
+ keyIndex ++ ;
422
+ if ( keyIndex >= STX_FAUCET_KEYS . length ) keyIndex = 0 ;
423
+ logger . warn (
424
+ `StxFaucet transaction failed for sender ${ senderAddress } , trying with next key: ${ error } `
425
+ ) ;
463
426
} else {
464
- retrySend = true ;
465
- stxKeyIndex ++ ;
427
+ logger . warn ( `StxFaucet unexpected error when sending transaction: ${ error } ` ) ;
428
+ throw error ;
466
429
}
467
- } else {
468
- retrySend = false ;
469
430
}
470
- } while ( retrySend ) ;
471
-
472
- if ( ! sendSuccess ) {
473
- if ( lastSendError ) {
474
- throw lastSendError ;
475
- } else {
476
- throw new Error ( `Unexpected failure to send or capture error` ) ;
477
- }
478
- } else {
479
- await reply . send ( {
480
- success : true ,
481
- txId : sendSuccess . txId ,
482
- txRaw : sendSuccess . txRaw ,
483
- } ) ;
484
- }
431
+ } while ( ! sendSuccess ) ;
485
432
486
433
await fastify . writeDb ?. insertFaucetRequest ( {
487
434
ip : `${ ip } ` ,
488
- address : address ,
435
+ address : recipientAddress ,
489
436
currency : DbFaucetRequestCurrency . STX ,
490
437
occurred_at : now ,
491
438
} ) ;
439
+ await reply . send ( {
440
+ success : true ,
441
+ txId : sendSuccess . txId ,
442
+ txRaw : sendSuccess . txRaw ,
443
+ } ) ;
492
444
} ) ;
493
445
}
494
446
) ;
0 commit comments