Implement a cosmos-sdk chain faucet API #1
2
.gitignore
vendored
2
.gitignore
vendored
@ -128,3 +128,5 @@ dist
|
|||||||
.yarn/build-state.yml
|
.yarn/build-state.yml
|
||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.pnp.*
|
||||||
|
|
||||||
|
db
|
||||||
|
10
package.json
10
package.json
@ -8,7 +8,8 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"typescript": "^5.5.3",
|
"@typescript-eslint/eslint-plugin": "^5.47.1",
|
||||||
|
"@typescript-eslint/parser": "^5.47.1",
|
||||||
"eslint": "^8.35.0",
|
"eslint": "^8.35.0",
|
||||||
"eslint-config-semistandard": "^15.0.1",
|
"eslint-config-semistandard": "^15.0.1",
|
||||||
"eslint-config-standard": "^16.0.3",
|
"eslint-config-standard": "^16.0.3",
|
||||||
@ -16,10 +17,13 @@
|
|||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-promise": "^5.1.0",
|
"eslint-plugin-promise": "^5.1.0",
|
||||||
"eslint-plugin-standard": "^5.0.0",
|
"eslint-plugin-standard": "^5.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.47.1",
|
"ts-node": "^10.9.2",
|
||||||
"@typescript-eslint/parser": "^5.47.1"
|
"typescript": "^5.5.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@cosmjs/proto-signing": "^0.32.4",
|
||||||
|
"@cosmjs/stargate": "^0.32.4",
|
||||||
|
"@keyv/sqlite": "^3.6.7",
|
||||||
"express": "^4.19.2"
|
"express": "^4.19.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
84
src/index.ts
84
src/index.ts
@ -1,11 +1,85 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
import Keyv from 'keyv';
|
||||||
|
|
||||||
|
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'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize KV store with SQLite backend
|
||||||
|
const keyv = new Keyv({
|
||||||
|
store: new KeyvSqlite({
|
||||||
|
uri: `sqlite://${config.dbPath}`,
|
||||||
|
table: 'keyv'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = 3000;
|
app.use(express.json());
|
||||||
|
|
||||||
app.get('/', (req, res) => {
|
async function sendTokens (recipientAddress: string, amount: string): Promise<string> {
|
||||||
res.send('Hello World!');
|
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) => {
|
||||||
|
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' });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.listen(port, () => {
|
const PORT = process.env.PORT || 3000;
|
||||||
return console.log(`Express is listening at http://localhost:${port}`);
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Faucet server running on port ${PORT}`);
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user