Skip to content

Commit

Permalink
Merge pull request #5 from allinbits/faucet
Browse files Browse the repository at this point in the history
chore: setup faucet as a netlify functions
  • Loading branch information
albttx authored Dec 13, 2024
2 parents b45cf7c + 349c6d9 commit bb92023
Show file tree
Hide file tree
Showing 16 changed files with 7,053 additions and 266 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ node_modules/
**/.vscode
yarn-error.log
dist
.idea
.idea
# Local Netlify folder
.netlify
22 changes: 21 additions & 1 deletion chains/mainnet/atomone.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,25 @@
"coingecko_id": "atone",
"logo": "/logos/A1.svg"
}
]
],
"faucet": {
"amount": [
{
"amount": "10000000",
"denom": "uatone"
}
],
"fees": {
"gas": "10000000000000",
"amount": [
{
"amount": "10000000",
"denom": "uatone"
}
]
},
"ip_limit": "10",
"address_limit": "5",
"whitelist": ["atone19vf5mfr40awvkefw69nl6p3mmlsnacmmy86rk0"]
}
}
49 changes: 49 additions & 0 deletions chains/testnet/atomone-devnet-1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"chain_name": "atomone-devnet-1",
"api": [
{
"provider": "allinbits",
"address": "https://atomone-devnet-1-api.allinbits.services"
}
],
"rpc": [
{
"provider": "allinbits",
"address": "https://atomone-devnet-1-rpc.allinbits.services"
}
],
"sdk_version": "0.45.1",
"coin_type": "118",
"min_tx_fee": "800",
"addr_prefix": "atone",
"logo": "/logos/A1.svg",
"assets": [
{
"base": "uatone",
"symbol": "ATONE",
"exponent": "6",
"coingecko_id": "",
"logo": "/logos/A1.svg"
}
],
"faucet": {
"amount": [
{
"amount": "10000000",
"denom": "uatone"
}
],
"fees": {
"gas": "100000000",
"amount": [
{
"amount": "250000",
"denom": "uatone"
}
]
},
"ip_limit": "10",
"address_limit": "5",
"whitelist": []
}
}
11 changes: 0 additions & 11 deletions chains/testnet/crossfi.json

This file was deleted.

53 changes: 53 additions & 0 deletions functions/balance.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { CosmosFaucet } from './pkg/faucet';
import { Store } from './pkg/store';

import type { Config, Context } from '@netlify/functions';
import type { Blockchain } from './types/chain';

export const config: Config = {
path: '/api/balance/:network/:chain_name',
};

export default async (req: Request, context: Context) => {
const { network, chain_name } = context.params;

const mnemonic = Netlify.env.get('MNEMONIC');
if (!mnemonic) {
return new Response(`env: MNEMONIC is missing`, {
status: 500,
});
}

var chain: Blockchain;

try {
chain = await import(`../chains/${network}/${chain_name}.json`);
} catch (err) {
return new Response(`${network}/${chain_name} not found`, {
status: 400,
});
}

const faucet = new CosmosFaucet(chain, mnemonic);

try {
const wallet = await faucet.get_wallet();
const accounts = await wallet.getAccounts();

const acc = accounts.find((x) => x.address.startsWith(chain.addr_prefix));
if (!acc) {
throw 'account not found';
}

const balance = await faucet.balanceOf(acc.address, chain.assets[0].base);

return Response.json({
...balance,
address: acc.address,
});
} catch (err) {
return new Response(`error: ${err}`, {
status: 500,
});
}
};
58 changes: 58 additions & 0 deletions functions/pkg/faucet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Blockchain } from '../types/chain';

import { stringToPath } from '@cosmjs/crypto';
import { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing';
import { SigningStargateClient, StargateClient } from '@cosmjs/stargate';

export class Faucet {
mnemonic: string;
chain: Blockchain;

constructor(chain: Blockchain, mnemonic: string) {
this.chain = chain;
this.mnemonic = mnemonic;
}
}

export class CosmosFaucet extends Faucet {
wallet: DirectSecp256k1HdWallet | undefined;

constructor(chain: Blockchain, mnemonic: string) {
super(chain, mnemonic);
}

async get_wallet(): Promise<DirectSecp256k1HdWallet> {
if (!this.wallet) {
this.wallet = await DirectSecp256k1HdWallet.fromMnemonic(this.mnemonic, {
prefix: this.chain.addr_prefix,
hdPaths: [stringToPath("m/44'/118'/0'/0/0")],
});
}
return this.wallet;
}

async balanceOf(addr: string, prefix: string) {
const client = await StargateClient.connect(this.chain.rpc[0].address);

return await client.getBalance(addr, prefix);
}

async send(addr: string) {
const wallet = await this.get_wallet();
const client = await SigningStargateClient.connectWithSigner(
this.chain.rpc[0].address,
wallet,
);

const [fromAccount] = await wallet.getAccounts();

const resp = await client.sendTokens(
fromAccount.address,
addr,
this.chain.faucet.amount,
this.chain.faucet.fees,
);

return resp;
}
}
30 changes: 30 additions & 0 deletions functions/pkg/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { getStore } from '@netlify/blobs';

export interface ClaimStore {
value: number;
latest_claim: number;
}

export class Store {
claim_key: string = 'faucet:claim';

constructor() {}

async getClaim(path: string): Promise<ClaimStore | null> {
const store = getStore(this.claim_key);
const res = await store.get(path);

return JSON.parse(res) as ClaimStore;
}

async setClaim(path: string, value: number) {
const store = getStore(this.claim_key);

const now = new Date();

await store.setJSON(path, {
value: value,
latest_claim: now.getTime(),
});
}
}
92 changes: 92 additions & 0 deletions functions/send.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import type { Blockchain } from './types/chain';
import { CosmosFaucet } from './pkg/faucet';
import { Store } from './pkg/store';

import type { Config, Context } from '@netlify/functions';

// ts-ignore
BigInt.prototype.toJSON = function () {
return this.toString();
};

export const config: Config = {
path: '/api/send/:network/:chain_name/:address',
};

export default async (_req: Request, context: Context) => {
const { network, chain_name, address } = context.params;

const mnemonic = Netlify.env.get('MNEMONIC');
if (!mnemonic) {
console.error('mnemonic is missing');
return new Response(JSON.stringify({ error: `env: MNEMONIC is missing` }), {
status: 500,
});
}

const path = `${network}/${chain_name}/${address}`;
const store = new Store();
const stored_claim = await store.getClaim(path);

if (stored_claim) {
const now = new Date().getTime();
const since = (now - stored_claim.latest_claim) / (1000 * 60 * 60); // in hours
if (since < 1) {
return new Response(JSON.stringify({ error: `already claimed` }), {
status: 401,
});
}
}

var chain: Blockchain;

try {
chain = await import(`../chains/${network}/${chain_name}.json`);
} catch (err) {
console.error(err);

return new Response(
JSON.stringify({ error: `${network}/${chain_name} not found` }),
{
status: 400,
},
);
}

// Ensure address is whitelisted
if (
chain.faucet.whitelist &&
chain.faucet.whitelist.length !== 0 &&
!chain.faucet.whitelist.find((x: string) => x === address)
) {
return new Response(
JSON.stringify({ error: `address is not whitelisted` }),
{
status: 400,
},
);
}

// Send tokens
try {
const faucet = new CosmosFaucet(chain, mnemonic);
const resp = await faucet.send(address);

await store.setClaim(
path,
stored_claim && stored_claim.value ? (stored_claim.value += 1) : 1,
);

return new Response(JSON.stringify(resp), {
status: 200,
headers: {
'Content-Type': 'application/json',
},
});
} catch (err) {
console.error(err);
return new Response(`failed to send token: ${err}`, {
status: 500,
});
}
};
41 changes: 41 additions & 0 deletions functions/types/chain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { Coin } from 'cosmjs-types/cosmos/base/v1beta1/coin';

export interface Blockchain {
chain_name: string;
registry_name: string;
api: Api[];
rpc: Rpc[];
sdk_version: string;
coin_type: string;
min_tx_fee: string;
addr_prefix: string;
logo: string;
assets: Asset[];
faucet: Faucet;
}

export interface Api {
provider: string;
address: string;
}

export interface Rpc {
provider: string;
address: string;
}

export interface Asset {
base: string;
symbol: string;
exponent: string;
coingecko_id: string;
logo: string;
}

export interface Faucet {
amount: Coin[];
ip_limit: string;
address_limit: string;
fees: string;
whitelist: string[];
}
3 changes: 3 additions & 0 deletions netlify.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[build]
functions = "./functions"

[[redirects]]
from = "/*"
to = "/index.html"
Expand Down
Loading

0 comments on commit bb92023

Please sign in to comment.