Skip to content

Commit

Permalink
Merge pull request #5 from atmoner/v1.0.2
Browse files Browse the repository at this point in the history
Update 1.0.2
  • Loading branch information
atmoner authored Nov 25, 2023
2 parents b93f01f + cdf784d commit 6268874
Show file tree
Hide file tree
Showing 6 changed files with 317 additions and 123 deletions.
42 changes: 22 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
<p align="center">
<a href="https://www.cosmostation.io" target="_blank" rel="noopener noreferrer"><img src="https://i.imgur.com/TBapjYI.png" alt="Cosmostation logo"></a>
<img src="https://i.imgur.com/TBapjYI.png" alt="Cosmostation logo">
</p>
<h1 align="center">
Cosmos-faucet
</h1>

Cosmos-faucet
</h1>

<p align="center">
⭐ Cosmos-faucet is a simple alternative to the tendermint/faucet script. This is an idea adapted for ➡️ <a href="https://www.bitcanna.io/">Bitcanna</a> and can be used for any project using cosmos.
⭐ Cosmos-faucet is a simple alternative to the tendermint/faucet script.
This is an idea adapted for ➡️ <a href="https://www.bitcanna.io/">Bitcanna</a> and can be used for any project using cosmos.

</p>


## Prerequisites

node version >=14.0.0
node version >=18.0.0

## Installation

Expand All @@ -30,24 +30,26 @@ nano config.json
Edit this part with your value:
```
{
"mnemonic":"one flight badge two kiwi adapt snap arrest make blast three wet...",
"chainId":"bitcanna-testnet-1",
"lcdUrl":"https://cosmos-testnet.bitcanna.io",
"denom":"ubcna",
"prefix":"bcna",
"feeAmount":5000,
"AmountSend":1000000,
"memo":"Sent using Cosmostation-CosmoJS ;-)",
"lport":8000,
"gasLimit":200000
}
"name": "Bitcanna Testnet",
"mnemonic": "",
"chainId": "bitcanna-dev-1",
"lcdUrl": "https://lcd-testnet.bitcanna.io",
"rpcUrl": "https://rpc-testnet.bitcanna.io",
"denom": "ubcna",
"prefix": "bcna",
"gasPrice": 0.075,
"faucetAmount": 1000000,
"memo": "Faucet from cosmos-faucet",
"enableUi": false,
"enableSwagger": true,
"dappPort": "8000"
}
```
## Run it (server side)
```
node --experimental-modules --es-module-specifier-resolution=node app.js
node app.js
```
## Client request
```
curl -s "http://testnet-faucet.bitcanna.io:8000/?address=bcna1xvuxv4znmmeu96ulxhldvyt32whp57vhyzg5vh" | jq
curl -s "http://testnet-faucet.bitcanna.io:8000/faucet/claim/bcna1xvuxv4znmmeu96ulxhldvyt32whp57vhyzg5vh" | jq
```
237 changes: 151 additions & 86 deletions app.js
Original file line number Diff line number Diff line change
@@ -1,96 +1,161 @@
/************************************/
/* Dev by atmon3r for Bitcanna Team */
/* Run: node --experimental-modules --es-module-specifier-resolution=node app.js */
/************************************/

import fs from 'fs';
import axios from 'axios';
import express from 'express';
import bodyParser from 'body-parser';
import { Cosmos } from "@cosmostation/cosmosjs";
import message from "@cosmostation/cosmosjs/src/messages/proto.js";
import path from 'path';
import { fileURLToPath } from 'url';
import swaggerJsdoc from "swagger-jsdoc";
import { serve, setup } from "swagger-ui-express";
import { DirectSecp256k1HdWallet, coin, coins } from "@cosmjs/proto-signing";
import bech32 from "bech32";
import pkg from '@cosmjs/stargate';
const { assertIsDeliverTxSuccess, SigningStargateClient, defaultRegistryTypes, GasPrice } = pkg;

const app = express();

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

let rawdata = fs.readFileSync('config.json');
let config = JSON.parse(rawdata);

// Cosmos config
const mnemonic = config.mnemonic;
const chainId = config.chainId;
const lcdUrl = config.lcdUrl;
const denom = config.denom;
const cosmos = new Cosmos(lcdUrl, chainId);
cosmos.setBech32MainPrefix(config.prefix);
cosmos.setPath("m/44'/118'/0'/0/0");
const address = cosmos.getAddress(mnemonic);
const privKey = cosmos.getECPairPriv(mnemonic);
const pubKeyAny = cosmos.getPubKeyAny(privKey);

// Express config
const app = express()
app.use(bodyParser.urlencoded({ extended: false }));

function sendTx(addressTo,res) {
cosmos.getAccounts(address).then(data => {
// signDoc = (1)txBody + (2)authInfo
// ---------------------------------- (1)txBody ----------------------------------
const msgSend = new message.cosmos.bank.v1beta1.MsgSend({
from_address: address,
to_address: addressTo,
amount: [{ denom: denom, amount: String(config.AmountSend) }] // 7 decimal places (1000000 ubcna = 1 BCNA)
});

const msgSendAny = new message.google.protobuf.Any({
type_url: "/cosmos.bank.v1beta1.MsgSend",
value: message.cosmos.bank.v1beta1.MsgSend.encode(msgSend).finish()
});

//console.log("msgSendAny: ", msgSendAny);

const txBody = new message.cosmos.tx.v1beta1.TxBody({ messages: [msgSendAny], memo: config.memo });

//console.log("txBody: ", txBody);
//return;

// --------------------------------- (2)authInfo ---------------------------------
const signerInfo = new message.cosmos.tx.v1beta1.SignerInfo({
public_key: pubKeyAny,
mode_info: { single: { mode: message.cosmos.tx.signing.v1beta1.SignMode.SIGN_MODE_DIRECT } },
sequence: data.account.sequence
});

const feeValue = new message.cosmos.tx.v1beta1.Fee({
amount: [{ denom: denom, amount: String(config.feeAmount) }],
gas_limit: config.gasLimit
});

const authInfo = new message.cosmos.tx.v1beta1.AuthInfo({ signer_infos: [signerInfo], fee: feeValue });

// -------------------------------- sign --------------------------------
const signedTxBytes = cosmos.sign(txBody, authInfo, data.account.account_number, privKey);

return cosmos.broadcast(signedTxBytes).then(
function(response) {res.send({response}); console.log(response) }
);

});


function checkBech32Address(address) {
try {
bech32.decode(address);
return true;
} catch (error) {
return false;
}
}
function checkBech32Prefix(address) {
try {
const { prefix } = bech32.decode(address);
if (prefix === config.prefix) {
return true;
}
} catch (error) {
return false;
}
}

// Routing
app.get('/', function (req, res) {
res.setHeader('Content-Type', 'application/json');
if (req.query.address === '') {
console.log(false)
res.send({error:'Already funded'});
} else {
var rtnTx = sendTx(req.query.address,res)

console.log(req.query.address)
}
})
app.get('/faucet/ui', async function(req, res) {
if (config.enableUi) {
res.sendFile(path.join(__dirname, '/claim.html'));
} else {
res.status(403).send('Forbidden');
}

})

app.get('/faucet/available', async function(req, res) {
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(config.mnemonic, { prefix: config.prefix });
const [firstAccount] = await wallet.getAccounts();

const account = await axios.get(config.lcdUrl + '/cosmos/bank/v1beta1/spendable_balances/' + firstAccount.address)
let available = 0;
account.data.balances.forEach(function (balance) {
if (balance.denom == config.denom) {
available = balance.amount;
}
})
res.json({
available: available
})
})

app.get('/faucet/last-claim', async function(req, res) {
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(config.mnemonic, { prefix: config.prefix });
const [firstAccount] = await wallet.getAccounts();

const resultSender = await axios(
config.lcdUrl +
"/cosmos/tx/v1beta1/txs?events=message.sender=%27" +
firstAccount.address +
"%27&limit=10&order_by=2"
);
res.json({
lastclaim: resultSender.data
})
})

app.listen(config.lport, function () {
console.log('***********************************************')
console.log('* Welcome on Cosmos-faucet')
console.log('* Cosmos-faucet app listening on port '+config.lport)
console.log('**********************************************')

app.get('/faucet/claim/:address', async function(req, res) {

let addressTo = req.params.address;
if (!checkBech32Address(addressTo)) {
res.status(403).json({
result: "Invalid address"
})
return;
}

if (!checkBech32Prefix(addressTo)) {
res.status(403).json({
result: "Invalid address prefix"
})
return;
}

const account = await axios.get(config.lcdUrl + '/cosmos/bank/v1beta1/spendable_balances/' + addressTo)
const found = account.data.balances.find((element) => element.denom === config.denom)

if (found.amount > 0) {
res.status(403).json({
result: "You already have funds"
})
return;
}

const wallet = await DirectSecp256k1HdWallet.fromMnemonic(config.mnemonic, { prefix: config.prefix });
const [firstAccount] = await wallet.getAccounts();
const client = await SigningStargateClient.connectWithSigner(config.rpcUrl, wallet, {
gasPrice: GasPrice.fromString(
config.gasPrice + config.denom
)
});

const foundMsgType = defaultRegistryTypes.find(
(element) => element[0] === "/cosmos.bank.v1beta1.MsgSend"
)

const finalMsg = {
typeUrl: foundMsgType[0],
value: foundMsgType[1].fromPartial({
"fromAddress": firstAccount.address,
"toAddress": addressTo,
"amount": coins(config.faucetAmount, config.denom)
}),
}
const result = await client.signAndBroadcast(firstAccount.address, [finalMsg], "auto", "")
assertIsDeliverTxSuccess(result);

res.json({
result: result
})
})

if (config.enableSwagger) {
// Swagger
const options = {
definition: {
openapi: "3.1.0",
info: {
title: config.name + " faucet",
version: "0.1.0",
},
},
apis: ["./routes/*.js"],
};

const specs = swaggerJsdoc(options);
app.use(
"/",
serve,
setup(specs, { explorer: false })
);
}

app.listen(config.dappPort, () => {
console.log(config.name + ` faucet app listening on port ${config.dappPort}`);
})

81 changes: 81 additions & 0 deletions claim.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<style>
body {font-family: Arial, Helvetica, sans-serif;}
* {box-sizing: border-box;}

input[type=text], select, textarea {
width: 100%;
padding: 12px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
margin-top: 6px;
margin-bottom: 16px;
resize: vertical;
}

input[type=submit] {
background-color: #04AA6D;
color: white;
padding: 12px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}

input[type=submit]:hover {
background-color: #45a049;
}

.container {
margin-left:auto;
margin-right:auto;
width: 500px;
border-radius: 5px;
background-color: #f2f2f2;
padding: 20px;
}

</style>
<body>

<div class="container" id="app">
<label for="fname">Your address</label>
<input v-model="address" type="text" placeholder="You address">
<input type="submit" value="Claim your faucet" @click="submitForm()">
<div >{{ message }}</div>
</div>
<script>
const { createApp, ref } = Vue

createApp({
setup() {
const message = ref('')
const address = ref('')
return {
message,
address
}
},
methods: {
submitForm() {
fetch('/faucet/claim/' + this.address)
.then(response => response.json())
.then(data => {
console.log(data)
this.message = data.result
})
.catch(error => {
console.error(error)
})
}
}
}).mount('#app')
</script>
</body>
</html>

Loading

0 comments on commit 6268874

Please sign in to comment.