Implement a cosmos-sdk chain faucet API #1
@ -11,4 +11,4 @@ RUN yarn && yarn build
|
|||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
# Run the app
|
# 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:
|
* Run the faucet:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn start-faucet
|
yarn start
|
||||||
|
|
||||||
# Expected output:
|
# Expected output:
|
||||||
# Config:
|
# Config:
|
||||||
@ -19,3 +19,14 @@
|
|||||||
# Faucet server running on port <port>
|
# Faucet server running on port <port>
|
||||||
# Using DB directory '/path/to/faucet/data/db'
|
# 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"
|
chainId = "laconic_9000-1"
|
||||||
denom = "photon"
|
denom = "photon"
|
||||||
prefix = "laconic"
|
prefix = "laconic"
|
||||||
|
gasPrice = "0.01"
|
||||||
faucetKey = ""
|
faucetKey = ""
|
||||||
|
|
||||||
[server]
|
[server]
|
||||||
port = 3000
|
port = 3000
|
||||||
transferAmount = 1000000
|
transferAmount = 1000000
|
||||||
dailyLimit = 3000000
|
periodTransferLimit = 3000000
|
||||||
dbDir = "db"
|
dbDir = "db"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "laconic-testnet-faucet",
|
"name": "laconic-faucet",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
@ -20,6 +20,7 @@
|
|||||||
"typescript": "^5.5.3"
|
"typescript": "^5.5.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@cosmjs/encoding": "^0.32.4",
|
||||||
"@cosmjs/proto-signing": "^0.32.4",
|
"@cosmjs/proto-signing": "^0.32.4",
|
||||||
"@cosmjs/stargate": "^0.32.4",
|
"@cosmjs/stargate": "^0.32.4",
|
||||||
"@keyv/sqlite": "^3.6.7",
|
"@keyv/sqlite": "^3.6.7",
|
||||||
@ -30,6 +31,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"build": "yarn tsc",
|
"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 { DirectSecp256k1Wallet } from '@cosmjs/proto-signing';
|
||||||
import { GasPrice, SigningStargateClient } from '@cosmjs/stargate';
|
import { GasPrice, SigningStargateClient } from '@cosmjs/stargate';
|
||||||
|
import { fromBech32 } from '@cosmjs/encoding';
|
||||||
import KeyvSqlite from '@keyv/sqlite';
|
import KeyvSqlite from '@keyv/sqlite';
|
||||||
|
|
||||||
const CONFIG_PATH = 'environments/local.toml';
|
const CONFIG_PATH = 'environments/local.toml';
|
||||||
const FAUCET_DATA_FILE = 'faucet_data.sqlite';
|
const FAUCET_DATA_FILE = 'faucet_data.sqlite';
|
||||||
|
const FAUCET_DATA_TTL = 86400000; // 24 hrs
|
||||||
|
|
||||||
interface Config {
|
interface Config {
|
||||||
upstream: {
|
upstream: {
|
||||||
@ -18,12 +20,13 @@ interface Config {
|
|||||||
chainId: string
|
chainId: string
|
||||||
denom: string
|
denom: string
|
||||||
prefix: string
|
prefix: string
|
||||||
|
gasPrice: string
|
||||||
faucetKey: string
|
faucetKey: string
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: number
|
port: number
|
||||||
transferAmount: string
|
transferAmount: string
|
||||||
dailyLimit: string
|
periodTransferLimit: string
|
||||||
dbDir: string
|
dbDir: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -34,35 +37,39 @@ async function main (): Promise<void> {
|
|||||||
const config: Config = toml.parse(configFile);
|
const config: Config = toml.parse(configFile);
|
||||||
console.log('Config: ', JSON.stringify(config, null, 2));
|
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();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
|
|
||||||
app.post('/faucet', async (req, res) => {
|
app.post('/faucet', async (req, res) => {
|
||||||
const { address } = req.body;
|
const { address: accountAddress } = req.body;
|
||||||
|
|
||||||
if (!address) {
|
if (!accountAddress) {
|
||||||
return res.status(400).json({ error: 'Address is required' });
|
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
|
// Check rate limit
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const today = new Date(now).toISOString().split('T')[0];
|
const today = new Date(now).toISOString().split('T')[0];
|
||||||
const key = `${address}:${today}`;
|
const faucetStoreKey = `${accountAddress}:${today}`;
|
||||||
const currentAmount = await keyv.get(key) || '0';
|
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' });
|
return res.status(429).json({ error: 'Limit exceeded' });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const txHash = await sendTokens(config, address, String(config.server.transferAmount));
|
const txHash = await sendTokens(config, accountAddress, String(config.server.transferAmount));
|
||||||
console.log(`Sent tokens to address: ${address}, txHash: ${txHash}`);
|
console.log(`Sent tokens to address: ${accountAddress}, txHash: ${txHash}`);
|
||||||
|
|
||||||
// Update rate limit
|
// 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 });
|
res.json({ success: true, txHash });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -112,7 +119,11 @@ async function sendTokens (config: Config, recipientAddress: string, amount: str
|
|||||||
);
|
);
|
||||||
const [faucetAccount] = await wallet.getAccounts();
|
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(
|
const result = await client.sendTokens(
|
||||||
faucetAccount.address,
|
faucetAccount.address,
|
||||||
@ -125,6 +136,19 @@ async function sendTokens (config: Config, recipientAddress: string, amount: str
|
|||||||
return result.transactionHash;
|
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 => {
|
main().catch(err => {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user