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",
|
"name": "laconic-testnet-faucet",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"repository": "git@github.com:deep-stack/laconic-testnet-faucet.git",
|
|
||||||
"author": "Prathamesh Musale <prathamesh.musale0@gmail.com>",
|
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"private": true,
|
"private": true,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -24,7 +22,8 @@
|
|||||||
"@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",
|
||||||
"express": "^4.19.2"
|
"express": "^4.19.2",
|
||||||
|
"toml": "^3.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
|
138
src/index.ts
138
src/index.ts
@ -1,53 +1,43 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import Keyv from 'keyv';
|
import Keyv from 'keyv';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import toml from 'toml';
|
||||||
|
|
||||||
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 KeyvSqlite from '@keyv/sqlite';
|
import KeyvSqlite from '@keyv/sqlite';
|
||||||
|
|
||||||
// TODO: Take from a config file
|
const CONFIG_PATH = 'environments/local.toml';
|
||||||
const config = {
|
const FAUCET_DATA_FILE = 'faucet_data.sqlite';
|
||||||
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'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize KV store with SQLite backend
|
interface Config {
|
||||||
const keyv = new Keyv({
|
upstream: {
|
||||||
store: new KeyvSqlite({
|
rpcEndpoint: string
|
||||||
uri: `sqlite://${config.dbPath}`,
|
chainId: string
|
||||||
table: 'keyv'
|
denom: string
|
||||||
})
|
prefix: string
|
||||||
});
|
faucetKey: string
|
||||||
|
},
|
||||||
const app = express();
|
server: {
|
||||||
app.use(express.json());
|
transferAmount: string
|
||||||
|
dailyLimit: string
|
||||||
async function sendTokens (recipientAddress: string, amount: string): Promise<string> {
|
dbDir: string
|
||||||
const wallet = await DirectSecp256k1Wallet.fromKey(
|
}
|
||||||
Buffer.from(config.faucetKey, 'hex'),
|
|
||||||
config.prefix
|
|
||||||
);
|
|
||||||
const [faucetAccount] = await wallet.getAccounts();
|
|
||||||
const client = await SigningStargateClient.connectWithSigner(config.rpcEndpoint, wallet, { gasPrice: GasPrice.fromString(`0.01${config.denom}`) });
|
|
||||||
|
|
||||||
const result = await client.sendTokens(
|
|
||||||
faucetAccount.address,
|
|
||||||
recipientAddress,
|
|
||||||
[{ denom: config.denom, amount: amount }],
|
|
||||||
'auto',
|
|
||||||
'Faucet transfer'
|
|
||||||
);
|
|
||||||
|
|
||||||
return result.transactionHash;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app.post('/faucet', async (req, res) => {
|
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;
|
const { address } = req.body;
|
||||||
|
|
||||||
if (!address) {
|
if (!address) {
|
||||||
@ -60,26 +50,78 @@ app.post('/faucet', async (req, res) => {
|
|||||||
const key = `${address}:${today}`;
|
const key = `${address}:${today}`;
|
||||||
const currentAmount = await keyv.get(key) || '0';
|
const currentAmount = await keyv.get(key) || '0';
|
||||||
|
|
||||||
if (BigInt(currentAmount) + BigInt(config.amount) > BigInt(config.dailyLimit)) {
|
if (BigInt(currentAmount) + BigInt(config.server.transferAmount) > BigInt(config.server.dailyLimit)) {
|
||||||
return res.status(429).json({ error: 'Limit exceeded' });
|
return res.status(429).json({ error: 'Limit exceeded' });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const txHash = await sendTokens(address, config.amount);
|
const txHash = await sendTokens(config, address, String(config.server.transferAmount));
|
||||||
|
console.log(`Sent tokens to address: ${address}, txHash: ${txHash}`);
|
||||||
console.log(`Sent tokens to address ${address}`);
|
|
||||||
|
|
||||||
// Update rate limit
|
// Update rate limit
|
||||||
await keyv.set(key, (BigInt(currentAmount) + BigInt(config.amount)).toString(), 86400000); // 24 hours TTL
|
await keyv.set(key, (BigInt(currentAmount) + BigInt(config.server.transferAmount)).toString(), 86400000); // 24 hours TTL
|
||||||
|
|
||||||
res.json({ success: true, txHash });
|
res.json({ success: true, txHash });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sending tokens:', error);
|
console.error('Error sending tokens:', error);
|
||||||
res.status(500).json({ error: 'Failed to send tokens' });
|
res.status(500).json({ error: 'Failed to send tokens' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Faucet server running on port ${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);
|
||||||
|
}
|
||||||
|
|
||||||
|
const wallet = await DirectSecp256k1Wallet.fromKey(
|
||||||
|
Buffer.from(faucetKey, 'hex'),
|
||||||
|
config.upstream.prefix
|
||||||
|
);
|
||||||
|
const [faucetAccount] = await wallet.getAccounts();
|
||||||
|
|
||||||
|
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.upstream.denom, amount: amount }],
|
||||||
|
'auto',
|
||||||
|
'Faucet transfer'
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.transactionHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
|
||||||
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
|
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:
|
ts-node@^10.9.2:
|
||||||
version "10.9.2"
|
version "10.9.2"
|
||||||
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f"
|
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f"
|
||||||
|
Loading…
Reference in New Issue
Block a user