Prathamesh Musale
5f71d4cc2c
Part of [laconicd testnet validator enrollment](https://www.notion.so/laconicd-testnet-validator-enrollment-6fc1d3cafcc64fef8c5ed3affa27c675) Co-authored-by: IshaVenikar <ishavenikar7@gmail.com> Reviewed-on: #1 Co-authored-by: Prathamesh Musale <prathamesh.musale0@gmail.com> Co-committed-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
155 lines
4.3 KiB
TypeScript
155 lines
4.3 KiB
TypeScript
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<void> {
|
|
// 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<Keyv> {
|
|
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<string> {
|
|
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);
|
|
});
|