Implement a cosmos-sdk chain faucet API #1
@ -11,4 +11,4 @@ RUN yarn && yarn build
|
||||
EXPOSE 3000
|
||||
|
||||
# Run the app
|
||||
CMD ["yarn", "start-faucet"]
|
||||
CMD ["node", "dist/index.js"]
|
||||
|
13
README.md
13
README.md
@ -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"}
|
||||
```
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
48
src/index.ts
48
src/index.ts
@ -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);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user