@@ -4,9 +4,12 @@ import PQueue from 'p-queue';
44import { BigNumber } from 'bignumber.js' ;
55import {
66 AnchorMode ,
7+ estimateTransactionFeeWithFallback ,
8+ getAddressFromPrivateKey ,
79 makeSTXTokenTransfer ,
810 SignedTokenTransferOptions ,
911 StacksTransaction ,
12+ TransactionVersion ,
1013} from '@stacks/transactions' ;
1114import { StacksNetwork } from '@stacks/network' ;
1215import {
@@ -16,7 +19,7 @@ import {
1619 isValidBtcAddress ,
1720} from '../../btc-faucet' ;
1821import { DbFaucetRequestCurrency } from '../../datastore/common' ;
19- import { getChainIDNetwork , getStxFaucetNetworks , intMax , stxToMicroStx } from '../../helpers' ;
22+ import { getChainIDNetwork , getStxFaucetNetwork , stxToMicroStx } from '../../helpers' ;
2023import { testnetKeys } from './debug' ;
2124import { StacksCoreRpcClient } from '../../core-rpc/client' ;
2225import { logger } from '../../logger' ;
@@ -27,25 +30,6 @@ import { Server } from 'node:http';
2730import { OptionalNullable } from '../schemas/util' ;
2831import { RunFaucetResponseSchema } from '../schemas/responses/responses' ;
2932
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-
4933function clientFromNetwork ( network : StacksNetwork ) : StacksCoreRpcClient {
5034 const coreUrl = new URL ( network . coreApiUrl ) ;
5135 return new StacksCoreRpcClient ( { host : coreUrl . hostname , port : coreUrl . port } ) ;
@@ -230,9 +214,63 @@ export const FaucetRoutes: FastifyPluginAsync<
230214 const FAUCET_STACKING_WINDOW = 2 * 24 * 60 * 60 * 1000 ; // 2 days
231215 const FAUCET_STACKING_TRIGGER_COUNT = 1 ;
232216
233- const STX_FAUCET_NETWORKS = ( ) => getStxFaucetNetworks ( ) ;
217+ const STX_FAUCET_NETWORK = getStxFaucetNetwork ( ) ;
234218 const STX_FAUCET_KEYS = ( process . env . FAUCET_PRIVATE_KEY ?? testnetKeys [ 0 ] . secretKey ) . split ( ',' ) ;
235219
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 buildSTXFaucetTx (
243+ recipient : string ,
244+ amount : bigint ,
245+ network : StacksNetwork ,
246+ senderKey : string ,
247+ nonce : bigint ,
248+ fee ?: bigint
249+ ) : Promise < StacksTransaction > {
250+ try {
251+ const options : SignedTokenTransferOptions = {
252+ recipient,
253+ amount,
254+ senderKey,
255+ network,
256+ memo : 'faucet' ,
257+ anchorMode : AnchorMode . Any ,
258+ nonce,
259+ } ;
260+ if ( fee ) options . fee = fee ;
261+ return await makeSTXTokenTransfer ( options ) ;
262+ } catch ( error : any ) {
263+ if (
264+ fee === undefined &&
265+ ( error as Error ) . message &&
266+ / 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 )
267+ ) {
268+ return await buildSTXFaucetTx ( recipient , amount , network , senderKey , nonce , 200n ) ;
269+ }
270+ throw error ;
271+ }
272+ }
273+
236274 fastify . post (
237275 '/stx' ,
238276 {
@@ -302,193 +340,97 @@ export const FaucetRoutes: FastifyPluginAsync<
302340 } ) ;
303341 }
304342
305- const address = req . query . address ;
306- if ( ! address ) {
343+ const recipientAddress = req . query . address ;
344+ if ( ! recipientAddress ) {
307345 return await reply . status ( 400 ) . send ( {
308346 error : 'address required' ,
309347 success : false ,
310348 } ) ;
311349 }
312350
313351 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-
319352 // Guard condition: requests are limited to x times per y minutes.
320353 // Only based on address for now, but we're keeping the IP in case
321354 // we want to escalate and implement a per IP policy
355+ const ip = req . headers [ 'x-forwarded-for' ] || req . connection . remoteAddress ;
356+ const lastRequests = await fastify . db . getSTXFaucetRequests ( recipientAddress ) ;
322357 const now = Date . now ( ) ;
358+ const isStackingReq = req . query . stacking ?? false ;
323359 const [ window , triggerCount ] = isStackingReq
324360 ? [ FAUCET_STACKING_WINDOW , FAUCET_STACKING_TRIGGER_COUNT ]
325361 : [ FAUCET_DEFAULT_WINDOW , FAUCET_DEFAULT_TRIGGER_COUNT ] ;
326-
327362 const requestsInWindow = lastRequests . results
328363 . map ( r => now - r . occurred_at )
329364 . filter ( r => r <= window ) ;
330365 if ( requestsInWindow . length >= triggerCount ) {
331- logger . warn ( `STX faucet rate limit hit for address ${ address } ` ) ;
366+ logger . warn ( `STX faucet rate limit hit for address ${ recipientAddress } ` ) ;
332367 return await reply . status ( 429 ) . send ( {
333368 error : 'Too many requests' ,
334369 success : false ,
335370 } ) ;
336371 }
337372
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- }
373+ // Start with a random key index. We will try others in order if this one fails.
374+ let keyIndex = Math . round ( Math . random ( ) * ( STX_FAUCET_KEYS . length - 1 ) ) ;
375+ let attempts = 0 ;
376+ let sendSuccess : { txId : string ; txRaw : string } | undefined ;
377+ const stxAmount = await calculateSTXFaucetAmount ( STX_FAUCET_NETWORK , isStackingReq ) ;
378+ const rpcClient = clientFromNetwork ( STX_FAUCET_NETWORK ) ;
379+ do {
380+ attempts ++ ;
381+ const senderKey = STX_FAUCET_KEYS [ keyIndex ] ;
382+ const senderAddress = getAddressFromPrivateKey ( senderKey , TransactionVersion . Testnet ) ;
383+ logger . debug ( `StxFaucet attempting faucet transaction from sender: ${ senderAddress } ` ) ;
384+ const nonces = await fastify . db . getAddressNonces ( { stxAddress : senderAddress } ) ;
385+ const tx = await buildSTXFaucetTx (
386+ recipientAddress ,
387+ stxAmount ,
388+ STX_FAUCET_NETWORK ,
389+ senderKey ,
390+ BigInt ( nonces . possibleNextNonce )
391+ ) ;
392+ const rawTx = Buffer . from ( tx . serialize ( ) ) ;
379393 try {
380- return await makeSTXTokenTransfer ( txOpts ) ;
394+ const res = await rpcClient . sendTransaction ( rawTx ) ;
395+ sendSuccess = { txId : res . txId , txRaw : rawTx . toString ( 'hex' ) } ;
396+ logger . info (
397+ `StxFaucet success. Sent ${ stxAmount } uSTX from ${ senderAddress } to ${ recipientAddress } .`
398+ ) ;
381399 } catch ( error : any ) {
382400 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 )
401+ error . message ?. includes ( 'ConflictingNonceInMempool' ) ||
402+ error . message ?. includes ( 'TooMuchChaining' )
386403 ) {
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- } ) ;
404+ if ( attempts == STX_FAUCET_KEYS . length ) {
405+ logger . error (
406+ `StxFaucet attempts exhausted for all faucet keys. Last error: ${ error } `
407+ ) ;
408+ throw error ;
446409 }
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 ;
410+ // Try with the next key. Wrap around the keys array if necessary.
411+ keyIndex ++ ;
412+ if ( keyIndex > STX_FAUCET_KEYS . length ) keyIndex = 0 ;
413+ logger . warn (
414+ `StxFaucet transaction failed for sender ${ senderAddress } , trying with next key: ${ error } `
415+ ) ;
463416 } else {
464- retrySend = true ;
465- stxKeyIndex ++ ;
417+ logger . warn ( `StxFaucet unexpected error when sending transaction: ${ error } ` ) ;
418+ throw error ;
466419 }
467- } else {
468- retrySend = false ;
469420 }
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- }
421+ } while ( ! sendSuccess ) ;
485422
486423 await fastify . writeDb ?. insertFaucetRequest ( {
487424 ip : `${ ip } ` ,
488- address : address ,
425+ address : recipientAddress ,
489426 currency : DbFaucetRequestCurrency . STX ,
490427 occurred_at : now ,
491428 } ) ;
429+ await reply . send ( {
430+ success : true ,
431+ txId : sendSuccess . txId ,
432+ txRaw : sendSuccess . txRaw ,
433+ } ) ;
492434 } ) ;
493435 }
494436 ) ;
0 commit comments