laconic-faucet/src/index.ts
Prathamesh Musale 5f71d4cc2c Implement a cosmos-sdk chain faucet API (#1)
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>
2024-07-18 05:50:31 +00:00

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);
});