import express from 'express'; import Keyv from 'keyv'; import fs from 'fs'; import path from 'path'; import toml from 'toml'; import cors from 'cors'; import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing'; import { GasPrice, SigningStargateClient } from '@cosmjs/stargate'; import { fromBech32 } from '@cosmjs/encoding'; import KeyvSqlite from '@keyv/sqlite'; const CONFIG_PATH = 'environments/local.toml'; const FAUCET_DATA_FILE = 'faucet_data.sqlite'; const FAUCET_DATA_TTL = 86400000; // 24 hrs interface Config { upstream: { rpcEndpoint: string chainId: string denom: string prefix: string gasPrice: string faucetKey: string }, server: { port: number transferAmount: string periodTransferLimit: string dbDir: string } } async function main (): Promise { // Read and parse the configuration const configFile = fs.readFileSync(CONFIG_PATH, 'utf-8'); const config: Config = toml.parse(configFile); console.log('Config: ', JSON.stringify(config, null, 2)); const faucetDataStore = await initKVStore(config.server.dbDir); const app = express(); app.use(express.json()); app.use(cors()); app.post('/faucet', async (req, res) => { const { address: accountAddress } = req.body; if (!accountAddress) { return res.status(400).json({ error: 'address is required' }); } if (!isValidAddress(accountAddress, config.upstream.prefix)) { return res.status(400).json({ error: 'invalid address' }); } // Check rate limit const now = Date.now(); const today = new Date(now).toISOString().split('T')[0]; const faucetStoreKey = `${accountAddress}:${today}`; const amountSentToAddress = await faucetDataStore.get(faucetStoreKey) || '0'; if (BigInt(amountSentToAddress) + BigInt(config.server.transferAmount) > BigInt(config.server.periodTransferLimit)) { return res.status(429).json({ error: 'Limit exceeded' }); } try { const txHash = await sendTokens(config, accountAddress, String(config.server.transferAmount)); console.log(`Sent tokens to address: ${accountAddress}, txHash: ${txHash}`); // Update rate limit await faucetDataStore.set(faucetStoreKey, (BigInt(amountSentToAddress) + BigInt(config.server.transferAmount)).toString(), FAUCET_DATA_TTL); res.json({ success: true, txHash }); } catch (error) { console.error('Error sending tokens:', error); res.status(500).json({ error: 'Failed to send tokens' }); } }); const port = config.server.port; app.listen(port, () => { console.log(`Faucet server running on port ${port}`); }); } async function initKVStore (dbDir: string): Promise { const dbDirPath = path.resolve(dbDir); // Create the database dir if it doesn't exist fs.mkdir(dbDirPath, { recursive: true }, (err) => { if (err) { if (err.code !== 'EEXIST') { console.error(`Error creating db directory: ${err.message}`); } } console.log(`Using DB directory '${dbDirPath}'`); }); // Initialize KV store with SQLite backend return new Keyv({ store: new KeyvSqlite({ uri: `sqlite://${path.resolve(dbDirPath, FAUCET_DATA_FILE)}`, table: 'keyv' }) }); } async function sendTokens (config: Config, recipientAddress: string, amount: string): Promise { let faucetKey = config.upstream.faucetKey; if (faucetKey.startsWith('0x')) { faucetKey = faucetKey.slice(2); } const wallet = await DirectSecp256k1Wallet.fromKey( Buffer.from(faucetKey, 'hex'), config.upstream.prefix ); const [faucetAccount] = await wallet.getAccounts(); const client = await SigningStargateClient.connectWithSigner( config.upstream.rpcEndpoint, wallet, { gasPrice: GasPrice.fromString(`${config.upstream.gasPrice}${config.upstream.denom}`) } ); const result = await client.sendTokens( faucetAccount.address, recipientAddress, [{ denom: config.upstream.denom, amount: amount }], 'auto', 'Faucet transfer' ); return result.transactionHash; } export function isValidAddress (address: string, requiredPrefix: string): boolean { try { const { prefix, data } = fromBech32(address); if (prefix !== requiredPrefix) { return false; } return data.length === 20; } catch { return false; } } main().catch(err => { console.log(err); });