Implement a cosmos-sdk chain faucet API #1

Merged
nabarun merged 7 commits from pm-add-faucet into main 2024-07-18 05:50:32 +00:00
5 changed files with 121 additions and 64 deletions
Showing only changes of commit 6436ec1bb9 - Show all commits

View File

@ -24,4 +24,4 @@
}
]
}
}
}

11
environments/local.toml Normal file
View File

@ -0,0 +1,11 @@
[upstream]
rpcEndpoint = "http://localhost:26657"
chainId = "laconic_9000-1"
denom = "photon"
prefix = "laconic"
faucetKey = ""
[server]
transferAmount = 1000000
dailyLimit = 3000000
dbDir = "db"

View File

@ -2,8 +2,6 @@
"name": "laconic-testnet-faucet",
"version": "0.1.0",
"main": "dist/index.js",
"repository": "git@github.com:deep-stack/laconic-testnet-faucet.git",
"author": "Prathamesh Musale <prathamesh.musale0@gmail.com>",
"license": "UNLICENSED",
"private": true,
"devDependencies": {
@ -24,7 +22,8 @@
"@cosmjs/proto-signing": "^0.32.4",
"@cosmjs/stargate": "^0.32.4",
"@keyv/sqlite": "^3.6.7",
"express": "^4.19.2"
"express": "^4.19.2",
"toml": "^3.0.0"
},
"scripts": {
"lint": "eslint .",

View File

@ -1,45 +1,120 @@
import express from 'express';
import Keyv from 'keyv';
import fs from 'fs';
import path from 'path';
import toml from 'toml';
import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing';
import { GasPrice, SigningStargateClient } from '@cosmjs/stargate';
import KeyvSqlite from '@keyv/sqlite';
// TODO: Take from a config file
const config = {
rpcEndpoint: 'https://rpc.example.com',
faucetKey: 'your faucet key here',
chainId: 'laconic_9000-1',
denom: 'photon',
amount: '1000000',
prefix: 'laconic',
dailyLimit: '3000000',
dbPath: './db/faucet_data.sqlite'
};
const CONFIG_PATH = 'environments/local.toml';
const FAUCET_DATA_FILE = 'faucet_data.sqlite';
// Initialize KV store with SQLite backend
const keyv = new Keyv({
store: new KeyvSqlite({
uri: `sqlite://${config.dbPath}`,
table: 'keyv'
})
});
interface Config {
upstream: {
rpcEndpoint: string
chainId: string
denom: string
prefix: string
faucetKey: string
},
server: {
transferAmount: string
dailyLimit: string
dbDir: string
}
}
const app = express();
app.use(express.json());
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 keyv = await initKVStore(config.server.dbDir);
const app = express();
app.use(express.json());
app.post('/faucet', async (req, res) => {
const { address } = req.body;
if (!address) {
return res.status(400).json({ error: 'Address is required' });
}
// 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';
if (BigInt(currentAmount) + BigInt(config.server.transferAmount) > BigInt(config.server.dailyLimit)) {
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}`);
// Update rate limit
await keyv.set(key, (BigInt(currentAmount) + BigInt(config.server.transferAmount)).toString(), 86400000); // 24 hours 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 = process.env.PORT || 3000;
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);
}
async function sendTokens (recipientAddress: string, amount: string): Promise<string> {
const wallet = await DirectSecp256k1Wallet.fromKey(
Buffer.from(config.faucetKey, 'hex'),
config.prefix
Buffer.from(faucetKey, 'hex'),
config.upstream.prefix
);
const [faucetAccount] = await wallet.getAccounts();
const client = await SigningStargateClient.connectWithSigner(config.rpcEndpoint, wallet, { gasPrice: GasPrice.fromString(`0.01${config.denom}`) });
const client = await SigningStargateClient.connectWithSigner(config.upstream.rpcEndpoint, wallet, { gasPrice: GasPrice.fromString(`0.01${config.upstream.denom}`) });
const result = await client.sendTokens(
faucetAccount.address,
recipientAddress,
[{ denom: config.denom, amount: amount }],
[{ denom: config.upstream.denom, amount: amount }],
'auto',
'Faucet transfer'
);
@ -47,39 +122,6 @@ async function sendTokens (recipientAddress: string, amount: string): Promise<st
return result.transactionHash;
}
app.post('/faucet', async (req, res) => {
const { address } = req.body;
if (!address) {
return res.status(400).json({ error: 'Address is required' });
}
// 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';
if (BigInt(currentAmount) + BigInt(config.amount) > BigInt(config.dailyLimit)) {
return res.status(429).json({ error: 'Limit exceeded' });
}
try {
const txHash = await sendTokens(address, config.amount);
console.log(`Sent tokens to address ${address}`);
// Update rate limit
await keyv.set(key, (BigInt(currentAmount) + BigInt(config.amount)).toString(), 86400000); // 24 hours 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 = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Faucet server running on port ${PORT}`);
main().catch(err => {
console.log(err);
});

View File

@ -3076,6 +3076,11 @@ toidentifier@1.0.1:
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
toml@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/toml/-/toml-3.0.0.tgz#342160f1af1904ec9d204d03a5d61222d762c5ee"
integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==
ts-node@^10.9.2:
version "10.9.2"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f"