diff --git a/Dockerfile b/Dockerfile index 098d910..24a3590 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,4 +11,4 @@ RUN yarn && yarn build EXPOSE 3000 # Run the app -CMD ["yarn", "start-faucet"] +CMD ["node", "dist/index.js"] diff --git a/README.md b/README.md index 3571164..daea16f 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ * Run the faucet: ```bash - yarn start-faucet + yarn start # Expected output: # Config: @@ -19,3 +19,14 @@ # Faucet server running on port # Using DB directory '/path/to/faucet/data/db' ``` + +* Example request: + + ```bash + curl -X POST http://localhost:3000/faucet \ + -H "Content-Type: application/json" \ + -d '{"address": "laconic1cndafgkspae7es7g2j52hmxxukwsy84v6h07w5"}' + + # Expected output: + # {"success":true,"txHash":"40405D3CCA8122482C36083762561908E2595A4B765B7457C3995525991D18CE"} + ``` diff --git a/environments/local.toml b/environments/local.toml index 2161630..87ea2d2 100644 --- a/environments/local.toml +++ b/environments/local.toml @@ -3,10 +3,11 @@ chainId = "laconic_9000-1" denom = "photon" prefix = "laconic" + gasPrice = "0.01" faucetKey = "" [server] port = 3000 transferAmount = 1000000 - dailyLimit = 3000000 + periodTransferLimit = 3000000 dbDir = "db" diff --git a/package.json b/package.json index 146b806..b91b254 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "laconic-testnet-faucet", + "name": "laconic-faucet", "version": "0.1.0", "main": "dist/index.js", "license": "UNLICENSED", @@ -20,6 +20,7 @@ "typescript": "^5.5.3" }, "dependencies": { + "@cosmjs/encoding": "^0.32.4", "@cosmjs/proto-signing": "^0.32.4", "@cosmjs/stargate": "^0.32.4", "@keyv/sqlite": "^3.6.7", @@ -30,6 +31,6 @@ "scripts": { "lint": "eslint .", "build": "yarn tsc", - "start-faucet": "node dist/index.js" + "start": "node dist/index.js" } } diff --git a/src/index.ts b/src/index.ts index 61271ce..47c2daa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,10 +7,12 @@ 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: { @@ -18,12 +20,13 @@ interface Config { chainId: string denom: string prefix: string + gasPrice: string faucetKey: string }, server: { port: number transferAmount: string - dailyLimit: string + periodTransferLimit: string dbDir: string } } @@ -34,35 +37,39 @@ async function main (): Promise { const config: Config = toml.parse(configFile); console.log('Config: ', JSON.stringify(config, null, 2)); - const keyv = await initKVStore(config.server.dbDir); + 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 } = req.body; + const { address: accountAddress } = req.body; - if (!address) { - return res.status(400).json({ error: 'Address is required' }); + 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 key = `${address}:${today}`; - const currentAmount = await keyv.get(key) || '0'; + const faucetStoreKey = `${accountAddress}:${today}`; + const amountSentToAddress = await faucetDataStore.get(faucetStoreKey) || '0'; - if (BigInt(currentAmount) + BigInt(config.server.transferAmount) > BigInt(config.server.dailyLimit)) { + 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, address, String(config.server.transferAmount)); - console.log(`Sent tokens to address: ${address}, txHash: ${txHash}`); + const txHash = await sendTokens(config, accountAddress, String(config.server.transferAmount)); + console.log(`Sent tokens to address: ${accountAddress}, txHash: ${txHash}`); // Update rate limit - await keyv.set(key, (BigInt(currentAmount) + BigInt(config.server.transferAmount)).toString(), 86400000); // 24 hours TTL + await faucetDataStore.set(faucetStoreKey, (BigInt(amountSentToAddress) + BigInt(config.server.transferAmount)).toString(), FAUCET_DATA_TTL); res.json({ success: true, txHash }); } catch (error) { @@ -112,7 +119,11 @@ async function sendTokens (config: Config, recipientAddress: string, amount: str ); const [faucetAccount] = await wallet.getAccounts(); - const client = await SigningStargateClient.connectWithSigner(config.upstream.rpcEndpoint, wallet, { gasPrice: GasPrice.fromString(`0.01${config.upstream.denom}`) }); + 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, @@ -125,6 +136,19 @@ async function sendTokens (config: Config, recipientAddress: string, amount: str 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); });