Implement a cosmos-sdk chain faucet API #1
11
environments/local.toml
Normal file
11
environments/local.toml
Normal 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"
|
@ -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 .",
|
||||
|
162
src/index.ts
162
src/index.ts
@ -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);
|
||||
});
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user