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.
- package/LICENSE +661 -0
- package/README.md +3 -0
- package/dist/esm/api/captcha.d.ts +38 -0
- package/dist/esm/api/captcha.d.ts.map +1 -0
- package/dist/esm/api/captcha.js +44 -0
- package/dist/esm/api/captcha.js.map +1 -0
- package/dist/esm/api/claim.d.ts +2 -0
- package/dist/esm/api/claim.d.ts.map +1 -0
- package/dist/esm/api/claim.js +2 -0
- package/dist/esm/api/claim.js.map +1 -0
- package/dist/esm/api/dummy.d.ts +28 -0
- package/dist/esm/api/dummy.d.ts.map +1 -0
- package/dist/esm/api/dummy.js +17 -0
- package/dist/esm/api/dummy.js.map +1 -0
- package/dist/esm/api/index.d.ts +41 -0
- package/dist/esm/api/index.d.ts.map +1 -0
- package/dist/esm/api/index.js +122 -0
- package/dist/esm/api/index.js.map +1 -0
- package/dist/esm/env.d.ts +8 -0
- package/dist/esm/env.d.ts.map +1 -0
- package/dist/esm/env.js +2 -0
- package/dist/esm/env.js.map +1 -0
- package/dist/esm/index.d.ts +82 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +53 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/schema/ts/db.sql.d.ts +3 -0
- package/dist/esm/schema/ts/db.sql.d.ts.map +1 -0
- package/dist/esm/schema/ts/db.sql.js +7 -0
- package/dist/esm/schema/ts/db.sql.js.map +1 -0
- package/dist/esm/setup.d.ts +16 -0
- package/dist/esm/setup.d.ts.map +1 -0
- package/dist/esm/setup.js +19 -0
- package/dist/esm/setup.js.map +1 -0
- package/dist/esm/types.d.ts +12 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +2 -0
- package/dist/esm/types.js.map +1 -0
- package/package.json +44 -0
- package/src/api/index.ts +179 -0
- package/src/env.ts +9 -0
- package/src/index.ts +72 -0
- package/src/schema/sql/db.sql +5 -0
- package/src/schema/ts/db.sql.ts +6 -0
- package/src/setup.ts +45 -0
- package/src/types.ts +8 -0
package/src/api/index.ts
ADDED
|
@@ -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);
|
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
|
+
};
|