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 54 additions and 17 deletions
Showing only changes of commit e271982345 - Show all commits

View File

@ -11,4 +11,4 @@ RUN yarn && yarn build
EXPOSE 3000
# Run the app
CMD ["yarn", "start-faucet"]
CMD ["node", "dist/index.js"]

View File

@ -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 <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"}
```

View File

@ -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"

View File

@ -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"
}
}

View File

@ -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<void> {
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);
});