faucet-server-app 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +3 -0
  3. package/dist/esm/api/captcha.d.ts +38 -0
  4. package/dist/esm/api/captcha.d.ts.map +1 -0
  5. package/dist/esm/api/captcha.js +44 -0
  6. package/dist/esm/api/captcha.js.map +1 -0
  7. package/dist/esm/api/claim.d.ts +2 -0
  8. package/dist/esm/api/claim.d.ts.map +1 -0
  9. package/dist/esm/api/claim.js +2 -0
  10. package/dist/esm/api/claim.js.map +1 -0
  11. package/dist/esm/api/dummy.d.ts +28 -0
  12. package/dist/esm/api/dummy.d.ts.map +1 -0
  13. package/dist/esm/api/dummy.js +17 -0
  14. package/dist/esm/api/dummy.js.map +1 -0
  15. package/dist/esm/api/index.d.ts +41 -0
  16. package/dist/esm/api/index.d.ts.map +1 -0
  17. package/dist/esm/api/index.js +122 -0
  18. package/dist/esm/api/index.js.map +1 -0
  19. package/dist/esm/env.d.ts +8 -0
  20. package/dist/esm/env.d.ts.map +1 -0
  21. package/dist/esm/env.js +2 -0
  22. package/dist/esm/env.js.map +1 -0
  23. package/dist/esm/index.d.ts +82 -0
  24. package/dist/esm/index.d.ts.map +1 -0
  25. package/dist/esm/index.js +53 -0
  26. package/dist/esm/index.js.map +1 -0
  27. package/dist/esm/schema/ts/db.sql.d.ts +3 -0
  28. package/dist/esm/schema/ts/db.sql.d.ts.map +1 -0
  29. package/dist/esm/schema/ts/db.sql.js +7 -0
  30. package/dist/esm/schema/ts/db.sql.js.map +1 -0
  31. package/dist/esm/setup.d.ts +16 -0
  32. package/dist/esm/setup.d.ts.map +1 -0
  33. package/dist/esm/setup.js +19 -0
  34. package/dist/esm/setup.js.map +1 -0
  35. package/dist/esm/types.d.ts +12 -0
  36. package/dist/esm/types.d.ts.map +1 -0
  37. package/dist/esm/types.js +2 -0
  38. package/dist/esm/types.js.map +1 -0
  39. package/package.json +44 -0
  40. package/src/api/index.ts +179 -0
  41. package/src/env.ts +9 -0
  42. package/src/index.ts +72 -0
  43. package/src/schema/sql/db.sql +5 -0
  44. package/src/schema/ts/db.sql.ts +6 -0
  45. package/src/setup.ts +45 -0
  46. package/src/types.ts +8 -0
@@ -0,0 +1,179 @@
1
+ import {Hono} from 'hono';
2
+ import {ServerOptions} from '../types.js';
3
+ import {setup} from '../setup.js';
4
+ import {Env} from '../env.js';
5
+ import {
6
+ createWalletClient,
7
+ http,
8
+ isAddress,
9
+ parseEther,
10
+ type Hex,
11
+ type Address,
12
+ } from 'viem';
13
+ import {privateKeyToAccount} from 'viem/accounts';
14
+
15
+ const PROSOPO_VERIFY_ENDPOINT = 'https://api.prosopo.io/siteverify';
16
+
17
+ type VerifyResponse = {
18
+ verified: boolean;
19
+ [key: string]: unknown;
20
+ };
21
+
22
+ type ClaimRequest = {
23
+ token: string;
24
+ chainId: string;
25
+ address: string;
26
+ };
27
+
28
+ async function verifyProsopoCaptcha(
29
+ token: string,
30
+ secret: string,
31
+ ): Promise<boolean> {
32
+ const body = {
33
+ token,
34
+ secret,
35
+ };
36
+
37
+ const response = await fetch(PROSOPO_VERIFY_ENDPOINT, {
38
+ method: 'POST',
39
+ headers: {
40
+ 'Content-Type': 'application/json',
41
+ },
42
+ body: JSON.stringify(body),
43
+ });
44
+
45
+ const result = (await response.json()) as VerifyResponse;
46
+ return result.verified === true;
47
+ }
48
+
49
+ function parseChainConfig(
50
+ configValue: string,
51
+ ): {amount: string; rpcUrl: string} | null {
52
+ // Format: <amount>:<rpc_endpoint>
53
+ const colonIndex = configValue.indexOf(':');
54
+ if (colonIndex === -1) {
55
+ return null;
56
+ }
57
+ const amount = configValue.substring(0, colonIndex);
58
+ const rpcUrl = configValue.substring(colonIndex + 1);
59
+ if (!amount || !rpcUrl) {
60
+ return null;
61
+ }
62
+ return {amount, rpcUrl};
63
+ }
64
+
65
+ export function getAPI<CustomEnv extends Env>(
66
+ options: ServerOptions<CustomEnv>,
67
+ ) {
68
+ const app = new Hono<{Bindings: CustomEnv}>()
69
+ .use(setup({serverOptions: options}))
70
+ .post('/claim', async (c) => {
71
+ const config = c.get('config');
72
+ const env = config.env;
73
+
74
+ // Check if captcha is disabled (for localhost development)
75
+ const captchaDisabled = env.DISABLE_CAPTCHA === 'true';
76
+
77
+ // Validate PROSOPO secret (only required if captcha is enabled)
78
+ const secret = env.PROSOPO_SITE_PRIVATE_KEY;
79
+ if (!captchaDisabled && !secret) {
80
+ return c.json({error: 'PROSOPO_SITE_PRIVATE_KEY not configured'}, 500);
81
+ }
82
+
83
+ // Validate FAUCET_PRIVATE_KEY
84
+ const faucetPrivateKey = env.FAUCET_PRIVATE_KEY;
85
+ if (!faucetPrivateKey) {
86
+ return c.json({error: 'FAUCET_PRIVATE_KEY not configured'}, 500);
87
+ }
88
+
89
+ const body = await c.req.json<ClaimRequest>();
90
+ const {token, chainId, address} = body;
91
+
92
+ // Validate required fields
93
+ if (!token) {
94
+ return c.json({error: 'Missing token'}, 400);
95
+ }
96
+
97
+ if (!chainId) {
98
+ return c.json({error: 'Missing chainId'}, 400);
99
+ }
100
+
101
+ if (!address) {
102
+ return c.json({error: 'Missing address'}, 400);
103
+ }
104
+
105
+ // Validate address format
106
+ if (!isAddress(address)) {
107
+ return c.json({error: 'Invalid Ethereum address'}, 400);
108
+ }
109
+
110
+ // Parse chainId
111
+ const chainIdNum = parseInt(chainId, 10);
112
+ if (isNaN(chainIdNum) || chainIdNum <= 0) {
113
+ return c.json({error: 'Invalid chainId'}, 400);
114
+ }
115
+
116
+ // Check for chain-specific config (format: <amount>:<rpc_endpoint>)
117
+ const chainConfigKey = `CHAIN_${chainId}` as `CHAIN_${string}`;
118
+ const chainConfigValue = env[chainConfigKey];
119
+
120
+ if (!chainConfigValue) {
121
+ return c.json(
122
+ {error: `Faucet not configured for chain ${chainId}`},
123
+ 400,
124
+ );
125
+ }
126
+
127
+ const chainConfig = parseChainConfig(chainConfigValue);
128
+ if (!chainConfig) {
129
+ return c.json(
130
+ {
131
+ error: `Invalid chain config format for chain ${chainId}. Expected: <amount>:<rpc_endpoint>`,
132
+ },
133
+ 500,
134
+ );
135
+ }
136
+
137
+ // Verify captcha (skip if disabled for localhost development)
138
+ if (!captchaDisabled) {
139
+ const verified = await verifyProsopoCaptcha(token, secret!);
140
+
141
+ if (!verified) {
142
+ return c.json({error: 'Captcha verification failed'}, 401);
143
+ }
144
+ }
145
+
146
+ try {
147
+ // Create wallet client and send transaction
148
+ const account = privateKeyToAccount(faucetPrivateKey as Hex);
149
+ const {amount, rpcUrl} = chainConfig;
150
+
151
+ const walletClient = createWalletClient({
152
+ account,
153
+ chain: {
154
+ id: chainIdNum,
155
+ name: `Chain ${chainIdNum}`,
156
+ nativeCurrency: {name: 'ETH', symbol: 'ETH', decimals: 18},
157
+ rpcUrls: {
158
+ default: {http: [rpcUrl]},
159
+ },
160
+ },
161
+ transport: http(rpcUrl),
162
+ });
163
+
164
+ const txHash = await walletClient.sendTransaction({
165
+ to: address as Address,
166
+ value: BigInt(amount),
167
+ });
168
+
169
+ return c.json({success: true, txHash});
170
+ } catch (err) {
171
+ console.error('Transaction error:', err);
172
+ const errorMessage =
173
+ err instanceof Error ? err.message : 'Transaction failed';
174
+ return c.json({error: errorMessage}, 500);
175
+ }
176
+ });
177
+
178
+ return app;
179
+ }
package/src/env.ts ADDED
@@ -0,0 +1,9 @@
1
+ export type Env = {
2
+ DEV?: string;
3
+ PROSOPO_SITE_PRIVATE_KEY?: string;
4
+ FAUCET_PRIVATE_KEY?: string;
5
+ // Set to 'true' to disable captcha verification (useful for localhost development)
6
+ DISABLE_CAPTCHA?: string;
7
+ // Dynamic env variables: CHAIN_<CHAIN_ID>=<amount>:<rpc_endpoint> for chain config
8
+ [key: `CHAIN_${string}`]: string | undefined;
9
+ };
package/src/index.ts ADDED
@@ -0,0 +1,72 @@
1
+ import {Hono} from 'hono';
2
+ import {cors} from 'hono/cors';
3
+ import {ServerOptions} from './types.js';
4
+ import {hc} from 'hono/client';
5
+ import {HTTPException} from 'hono/http-exception';
6
+ import {Env} from './env.js';
7
+ import {getAPI} from './api/index.js';
8
+
9
+ export type {Env};
10
+
11
+ // export type {Storage} from './storage/index.js';
12
+
13
+ const corsSetup = cors({
14
+ origin: '*',
15
+ allowHeaders: [
16
+ 'X-Custom-Header',
17
+ 'Upgrade-Insecure-Requests',
18
+ 'Content-Type',
19
+ 'SIGNATURE',
20
+ ],
21
+ allowMethods: ['POST', 'GET', 'HEAD', 'OPTIONS'],
22
+ exposeHeaders: ['Content-Length', 'X-Kuma-Revision'],
23
+ maxAge: 600,
24
+ credentials: true,
25
+ });
26
+
27
+ export function createServer<CustomEnv extends Env>(
28
+ options: ServerOptions<CustomEnv>,
29
+ ) {
30
+ const app = new Hono<{Bindings: CustomEnv}>();
31
+
32
+ const api = getAPI(options);
33
+
34
+ return app
35
+ .use('/api/*', corsSetup)
36
+ .route('/api/', api)
37
+ .onError((err, c) => {
38
+ const config = c.get('config');
39
+ const env = config?.env || {};
40
+ console.error(err);
41
+ if (err instanceof HTTPException) {
42
+ if (err.res) {
43
+ return err.getResponse();
44
+ }
45
+ }
46
+
47
+ return c.json(
48
+ {
49
+ success: false,
50
+ errors: [
51
+ {
52
+ name: 'name' in err ? err.name : undefined,
53
+ code: 'code' in err ? err.code : 5000,
54
+ status: 'status' in err ? err.status : undefined,
55
+ message: err.message,
56
+ cause: env.DEV ? err.cause : undefined,
57
+ stack: env.DEV ? err.stack : undefined,
58
+ },
59
+ ],
60
+ },
61
+ 500,
62
+ );
63
+ });
64
+ }
65
+
66
+ export type App = ReturnType<typeof createServer>;
67
+
68
+ // this is a trick to calculate the type when compiling
69
+ const client = hc<App>('');
70
+ export type Client = typeof client;
71
+ export const createClient = (...args: Parameters<typeof hc>): Client =>
72
+ hc<App>(...args);
@@ -0,0 +1,5 @@
1
+ CREATE TABLE IF NOT EXISTS Users (
2
+ id TEXT PRIMARY KEY,
3
+ email TEXT NOT NULL UNIQUE,
4
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
5
+ );
@@ -0,0 +1,6 @@
1
+ export default `CREATE TABLE IF NOT EXISTS Users (
2
+ id TEXT PRIMARY KEY,
3
+ email TEXT NOT NULL UNIQUE,
4
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
5
+ );
6
+ `
package/src/setup.ts ADDED
@@ -0,0 +1,45 @@
1
+ import {MiddlewareHandler} from 'hono/types';
2
+ import {ServerOptions} from './types.js';
3
+ import {Env} from './env.js';
4
+ // import {RemoteSQLStorage} from './storage/RemoteSQLStorage.js';
5
+
6
+ export type SetupOptions<CustomEnv extends Env> = {
7
+ serverOptions: ServerOptions<CustomEnv>;
8
+ };
9
+
10
+ export type Config<CustomEnv extends Env> = {
11
+ // storage: RemoteSQLStorage;
12
+ env: CustomEnv;
13
+ };
14
+
15
+ declare module 'hono' {
16
+ interface ContextVariableMap {
17
+ config: Config<Env>; // We cannot use generics here, but that is fine as server code is expected to only use Env
18
+ }
19
+ }
20
+
21
+ export function setup<CustomEnv extends Env>(
22
+ options: SetupOptions<CustomEnv>,
23
+ ): MiddlewareHandler {
24
+ const {getDB, getEnv} = options.serverOptions;
25
+
26
+ return async (c, next) => {
27
+ const env = getEnv(c);
28
+
29
+ const db = getDB(c);
30
+ // use db
31
+ // const storage = new RemoteSQLStorage(db);
32
+
33
+ c.set('config', {
34
+ // storage,
35
+ env,
36
+ });
37
+
38
+ // // auto setup
39
+ // if (c.req.query('_initDB') == 'true') {
40
+ // await storage.setup();
41
+ // }
42
+
43
+ return next();
44
+ };
45
+ }
package/src/types.ts ADDED
@@ -0,0 +1,8 @@
1
+ import {Context} from 'hono';
2
+ import {Bindings} from 'hono/types';
3
+ import {RemoteSQL} from 'remote-sql';
4
+
5
+ export type ServerOptions<Env extends Bindings = Bindings> = {
6
+ getDB: (c: Context<{Bindings: Env}>) => RemoteSQL;
7
+ getEnv: (c: Context<{Bindings: Env}>) => Env;
8
+ };