@aztec/aztec-faucet 0.0.0-test.0
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/README.md +3 -0
- package/dest/bin/index.d.ts +3 -0
- package/dest/bin/index.d.ts.map +1 -0
- package/dest/bin/index.js +21 -0
- package/dest/config.d.ts +17 -0
- package/dest/config.d.ts.map +1 -0
- package/dest/config.js +48 -0
- package/dest/faucet.d.ts +28 -0
- package/dest/faucet.d.ts.map +1 -0
- package/dest/faucet.js +132 -0
- package/dest/http.d.ts +6 -0
- package/dest/http.d.ts.map +1 -0
- package/dest/http.js +73 -0
- package/dest/index.d.ts +4 -0
- package/dest/index.d.ts.map +1 -0
- package/dest/index.js +3 -0
- package/package.json +92 -0
- package/src/bin/index.ts +26 -0
- package/src/config.ts +58 -0
- package/src/faucet.ts +168 -0
- package/src/http.ts +85 -0
- package/src/index.ts +3 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/bin/index.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --no-warnings
|
|
2
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
3
|
+
import { getFaucetConfigFromEnv } from '../config.js';
|
|
4
|
+
import { Faucet } from '../faucet.js';
|
|
5
|
+
import { createFaucetHttpServer } from '../http.js';
|
|
6
|
+
const logger = createLogger('aztec:faucet:http');
|
|
7
|
+
/**
|
|
8
|
+
* Create and start a new Aztec Node HTTP Server
|
|
9
|
+
*/ async function main() {
|
|
10
|
+
const config = getFaucetConfigFromEnv();
|
|
11
|
+
const faucet = await Faucet.create(config);
|
|
12
|
+
const httpServer = createFaucetHttpServer(faucet, '/', logger);
|
|
13
|
+
const port = parseInt(process.env?.AZTEC_PORT ?? '', 10) || 8080;
|
|
14
|
+
httpServer.listen(port);
|
|
15
|
+
logger.info(`Aztec Faucet listening on port ${port}`);
|
|
16
|
+
await Promise.resolve();
|
|
17
|
+
}
|
|
18
|
+
main().catch((err)=>{
|
|
19
|
+
logger.error(err);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
});
|
package/dest/config.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type L1ReaderConfig } from '@aztec/ethereum';
|
|
2
|
+
import { type ConfigMappingsType } from '@aztec/foundation/config';
|
|
3
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
4
|
+
export type L1AssetConfig = {
|
|
5
|
+
address: EthAddress;
|
|
6
|
+
amount: bigint;
|
|
7
|
+
};
|
|
8
|
+
export type FaucetConfig = L1ReaderConfig & {
|
|
9
|
+
l1Mnemonic: string;
|
|
10
|
+
mnemonicAccountIndex: number;
|
|
11
|
+
interval: number;
|
|
12
|
+
ethAmount: string;
|
|
13
|
+
l1Assets: L1AssetConfig[];
|
|
14
|
+
};
|
|
15
|
+
export declare const faucetConfigMapping: ConfigMappingsType<FaucetConfig>;
|
|
16
|
+
export declare function getFaucetConfigFromEnv(): FaucetConfig;
|
|
17
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAA0B,MAAM,iBAAiB,CAAC;AAC9E,OAAO,EAAE,KAAK,kBAAkB,EAA6C,MAAM,0BAA0B,CAAC;AAC9G,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAE3D,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,UAAU,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,cAAc,GAAG;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B,CAAC;AAEF,eAAO,MAAM,mBAAmB,EAAE,kBAAkB,CAAC,YAAY,CAoChE,CAAC;AAEF,wBAAgB,sBAAsB,IAAI,YAAY,CAErD"}
|
package/dest/config.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { l1ReaderConfigMappings } from '@aztec/ethereum';
|
|
2
|
+
import { getConfigFromMappings, numberConfigHelper } from '@aztec/foundation/config';
|
|
3
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
4
|
+
export const faucetConfigMapping = {
|
|
5
|
+
...l1ReaderConfigMappings,
|
|
6
|
+
l1Mnemonic: {
|
|
7
|
+
env: 'MNEMONIC',
|
|
8
|
+
description: 'The mnemonic for the faucet account'
|
|
9
|
+
},
|
|
10
|
+
mnemonicAccountIndex: {
|
|
11
|
+
env: 'FAUCET_MNEMONIC_ACCOUNT_INDEX',
|
|
12
|
+
description: 'The account to use',
|
|
13
|
+
...numberConfigHelper(0)
|
|
14
|
+
},
|
|
15
|
+
interval: {
|
|
16
|
+
env: 'FAUCET_INTERVAL_MS',
|
|
17
|
+
description: 'How often the faucet can be dripped',
|
|
18
|
+
...numberConfigHelper(1 * 60 * 60 * 1000)
|
|
19
|
+
},
|
|
20
|
+
ethAmount: {
|
|
21
|
+
env: 'FAUCET_ETH_AMOUNT',
|
|
22
|
+
description: 'How much eth the faucet should drip per call',
|
|
23
|
+
defaultValue: '1.0'
|
|
24
|
+
},
|
|
25
|
+
l1Assets: {
|
|
26
|
+
env: 'FAUCET_L1_ASSETS',
|
|
27
|
+
description: 'Which other L1 assets the faucet is able to drip',
|
|
28
|
+
defaultValue: '',
|
|
29
|
+
parseEnv (val) {
|
|
30
|
+
const assetConfigs = val.split(',');
|
|
31
|
+
return assetConfigs.flatMap((assetConfig)=>{
|
|
32
|
+
if (!assetConfig) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
const [address, amount = '1e9'] = assetConfig.split(':');
|
|
36
|
+
return [
|
|
37
|
+
{
|
|
38
|
+
address: EthAddress.fromString(address),
|
|
39
|
+
amount: BigInt(amount)
|
|
40
|
+
}
|
|
41
|
+
];
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
export function getFaucetConfigFromEnv() {
|
|
47
|
+
return getConfigFromMappings(faucetConfigMapping);
|
|
48
|
+
}
|
package/dest/faucet.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
2
|
+
import { type LocalAccount } from 'viem';
|
|
3
|
+
import type { FaucetConfig, L1AssetConfig } from './config.js';
|
|
4
|
+
export declare class Faucet {
|
|
5
|
+
private config;
|
|
6
|
+
private account;
|
|
7
|
+
private timeFn;
|
|
8
|
+
private log;
|
|
9
|
+
private walletClient;
|
|
10
|
+
private publicClient;
|
|
11
|
+
private dripHistory;
|
|
12
|
+
private l1Assets;
|
|
13
|
+
constructor(config: FaucetConfig, account: LocalAccount, timeFn?: () => number, log?: import("@aztec/foundation/log").Logger);
|
|
14
|
+
static create(config: FaucetConfig): Promise<Faucet>;
|
|
15
|
+
send(to: EthAddress, assetName: string): Promise<void>;
|
|
16
|
+
sendEth(to: EthAddress): Promise<void>;
|
|
17
|
+
sendERC20(to: EthAddress, assetName: string): Promise<void>;
|
|
18
|
+
addL1Asset(l1AssetConfig: L1AssetConfig): Promise<void>;
|
|
19
|
+
private checkThrottle;
|
|
20
|
+
private updateThrottle;
|
|
21
|
+
}
|
|
22
|
+
export declare class ThrottleError extends Error {
|
|
23
|
+
constructor(address: string, asset: string);
|
|
24
|
+
}
|
|
25
|
+
export declare class UnknownAsset extends Error {
|
|
26
|
+
constructor(asset: string);
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=faucet.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"faucet.d.ts","sourceRoot":"","sources":["../src/faucet.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAIhE,OAAO,EAKL,KAAK,YAAY,EAQlB,MAAM,MAAM,CAAC;AAGd,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAO/D,qBAAa,MAAM;IAQf,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,GAAG;IAVb,OAAO,CAAC,YAAY,CAAmB;IACvC,OAAO,CAAC,YAAY,CAAmB;IAEvC,OAAO,CAAC,WAAW,CAA0C;IAC7D,OAAO,CAAC,QAAQ,CAA8B;gBAGpC,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,YAAY,EACrB,MAAM,GAAE,MAAM,MAAiB,EAC/B,GAAG,yCAA+B;WAgBxB,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IAe1D,IAAI,CAAC,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQhD,OAAO,CAAC,EAAE,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IActC,SAAS,CAAC,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB3D,UAAU,CAAC,aAAa,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAyBpE,OAAO,CAAC,aAAa;IAarB,OAAO,CAAC,cAAc;CASvB;AAED,qBAAa,aAAc,SAAQ,KAAK;gBAC1B,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;CAG3C;AAED,qBAAa,YAAa,SAAQ,KAAK;gBACzB,KAAK,EAAE,MAAM;CAG1B"}
|
package/dest/faucet.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { createEthereumChain } from '@aztec/ethereum';
|
|
2
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
3
|
+
import { TestERC20Abi } from '@aztec/l1-artifacts';
|
|
4
|
+
import { createPublicClient, createWalletClient, fallback, getContract, parseEther, http as viemHttp } from 'viem';
|
|
5
|
+
import { mnemonicToAccount } from 'viem/accounts';
|
|
6
|
+
export class Faucet {
|
|
7
|
+
config;
|
|
8
|
+
account;
|
|
9
|
+
timeFn;
|
|
10
|
+
log;
|
|
11
|
+
walletClient;
|
|
12
|
+
publicClient;
|
|
13
|
+
dripHistory;
|
|
14
|
+
l1Assets;
|
|
15
|
+
constructor(config, account, timeFn = Date.now, log = createLogger('aztec:faucet')){
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.account = account;
|
|
18
|
+
this.timeFn = timeFn;
|
|
19
|
+
this.log = log;
|
|
20
|
+
this.dripHistory = new Map();
|
|
21
|
+
this.l1Assets = new Map();
|
|
22
|
+
const chain = createEthereumChain(config.l1RpcUrls, config.l1ChainId);
|
|
23
|
+
this.walletClient = createWalletClient({
|
|
24
|
+
account: this.account,
|
|
25
|
+
chain: chain.chainInfo,
|
|
26
|
+
transport: fallback(chain.rpcUrls.map((url)=>viemHttp(url)))
|
|
27
|
+
});
|
|
28
|
+
this.publicClient = createPublicClient({
|
|
29
|
+
chain: chain.chainInfo,
|
|
30
|
+
transport: fallback(chain.rpcUrls.map((url)=>viemHttp(url)))
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
static async create(config) {
|
|
34
|
+
if (!config.l1Mnemonic) {
|
|
35
|
+
throw new Error('Missing faucet mnemonic');
|
|
36
|
+
}
|
|
37
|
+
const account = mnemonicToAccount(config.l1Mnemonic, {
|
|
38
|
+
addressIndex: config.mnemonicAccountIndex
|
|
39
|
+
});
|
|
40
|
+
const faucet = new Faucet(config, account);
|
|
41
|
+
for (const asset of config.l1Assets){
|
|
42
|
+
await faucet.addL1Asset(asset);
|
|
43
|
+
}
|
|
44
|
+
return faucet;
|
|
45
|
+
}
|
|
46
|
+
send(to, assetName) {
|
|
47
|
+
if (assetName.toUpperCase() === 'ETH') {
|
|
48
|
+
return this.sendEth(to);
|
|
49
|
+
} else {
|
|
50
|
+
return this.sendERC20(to, assetName);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async sendEth(to) {
|
|
54
|
+
this.checkThrottle(to, 'ETH');
|
|
55
|
+
const hash = await this.walletClient.sendTransaction({
|
|
56
|
+
account: this.account,
|
|
57
|
+
to: to.toString(),
|
|
58
|
+
value: parseEther(this.config.ethAmount)
|
|
59
|
+
});
|
|
60
|
+
await this.publicClient.waitForTransactionReceipt({
|
|
61
|
+
hash
|
|
62
|
+
});
|
|
63
|
+
this.updateThrottle(to, 'ETH');
|
|
64
|
+
this.log.info(`Sent ETH ${this.config.ethAmount} to ${to} in tx ${hash}`);
|
|
65
|
+
}
|
|
66
|
+
async sendERC20(to, assetName) {
|
|
67
|
+
const asset = this.l1Assets.get(assetName);
|
|
68
|
+
if (!asset) {
|
|
69
|
+
throw new UnknownAsset(assetName);
|
|
70
|
+
}
|
|
71
|
+
this.checkThrottle(to, assetName);
|
|
72
|
+
const hash = await asset.contract.write.mint([
|
|
73
|
+
to.toString(),
|
|
74
|
+
asset.amount
|
|
75
|
+
]);
|
|
76
|
+
await this.publicClient.waitForTransactionReceipt({
|
|
77
|
+
hash
|
|
78
|
+
});
|
|
79
|
+
this.updateThrottle(to, assetName);
|
|
80
|
+
this.log.info(`Sent ${assetName} ${asset.amount} to ${to} in tx ${hash}`);
|
|
81
|
+
}
|
|
82
|
+
async addL1Asset(l1AssetConfig) {
|
|
83
|
+
const contract = getContract({
|
|
84
|
+
abi: TestERC20Abi,
|
|
85
|
+
address: l1AssetConfig.address.toString(),
|
|
86
|
+
client: this.walletClient
|
|
87
|
+
});
|
|
88
|
+
const [name, owner] = await Promise.all([
|
|
89
|
+
contract.read.name(),
|
|
90
|
+
contract.read.owner()
|
|
91
|
+
]);
|
|
92
|
+
if (owner !== this.account.address) {
|
|
93
|
+
throw new Error(`Owner mismatch. Expected contract ${name} to be owned by ${this.account.address}, received ${owner}`);
|
|
94
|
+
}
|
|
95
|
+
if (this.l1Assets.has(name) && this.l1Assets.get(name).contract.address.toLowerCase() !== l1AssetConfig.address.toString().toLowerCase()) {
|
|
96
|
+
this.log.warn(`Updating asset ${name} to address=${contract.address}`);
|
|
97
|
+
}
|
|
98
|
+
this.l1Assets.set(name, {
|
|
99
|
+
contract,
|
|
100
|
+
amount: l1AssetConfig.amount
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
checkThrottle(address, asset = 'ETH') {
|
|
104
|
+
const addressHistory = this.dripHistory.get(address.toString());
|
|
105
|
+
if (!addressHistory) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const now = this.timeFn();
|
|
109
|
+
const last = addressHistory.get(asset);
|
|
110
|
+
if (typeof last === 'number' && last + this.config.interval > now) {
|
|
111
|
+
throw new ThrottleError(address.toString(), asset);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
updateThrottle(address, asset = 'ETH') {
|
|
115
|
+
const addressStr = address.toString();
|
|
116
|
+
if (!this.dripHistory.has(addressStr)) {
|
|
117
|
+
this.dripHistory.set(addressStr, new Map());
|
|
118
|
+
}
|
|
119
|
+
const addressHistory = this.dripHistory.get(addressStr);
|
|
120
|
+
addressHistory.set(asset, this.timeFn());
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export class ThrottleError extends Error {
|
|
124
|
+
constructor(address, asset){
|
|
125
|
+
super(`Not funding ${asset}: throttled ${address}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
export class UnknownAsset extends Error {
|
|
129
|
+
constructor(asset){
|
|
130
|
+
super(`Unknown asset: ${asset}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
package/dest/http.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
import { type ApiSchemaFor } from '@aztec/foundation/schemas';
|
|
3
|
+
import { type Faucet } from './faucet.js';
|
|
4
|
+
export declare function createFaucetHttpServer(faucet: Faucet, apiPrefix?: string, logger?: import("@aztec/foundation/log").Logger): import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>;
|
|
5
|
+
export declare const FaucetSchema: ApiSchemaFor<Faucet>;
|
|
6
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,KAAK,YAAY,EAAW,MAAM,2BAA2B,CAAC;AASvE,OAAO,EAAE,KAAK,MAAM,EAAiB,MAAM,aAAa,CAAC;AAEzD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,SAAK,EAAE,MAAM,yCAAoC,sGA6DhH;AAED,eAAO,MAAM,YAAY,EAAE,YAAY,CAAC,MAAM,CAQ7C,CAAC"}
|
package/dest/http.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
2
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
3
|
+
import { schemas } from '@aztec/foundation/schemas';
|
|
4
|
+
import cors from '@koa/cors';
|
|
5
|
+
import { createServer } from 'http';
|
|
6
|
+
import Koa from 'koa';
|
|
7
|
+
import bodyParser from 'koa-bodyparser';
|
|
8
|
+
import Router from 'koa-router';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { ThrottleError } from './faucet.js';
|
|
11
|
+
export function createFaucetHttpServer(faucet, apiPrefix = '', logger = createLogger('aztec:faucet:http')) {
|
|
12
|
+
const router = new Router({
|
|
13
|
+
prefix: `${apiPrefix}`
|
|
14
|
+
});
|
|
15
|
+
router.get('/drip/:address', async (ctx)=>{
|
|
16
|
+
const { address } = ctx.params;
|
|
17
|
+
const { asset } = ctx.query;
|
|
18
|
+
if (typeof asset !== 'string') {
|
|
19
|
+
throw new Error(`Bad asset: [${asset}]`);
|
|
20
|
+
}
|
|
21
|
+
await faucet.send(EthAddress.fromString(address), asset);
|
|
22
|
+
ctx.status = 200;
|
|
23
|
+
});
|
|
24
|
+
const L1AssetRequestSchema = z.object({
|
|
25
|
+
address: z.string().transform((str)=>EthAddress.fromString(str)),
|
|
26
|
+
amount: z.string().transform((str)=>BigInt(str))
|
|
27
|
+
});
|
|
28
|
+
router.post('/l1-asset', async (ctx)=>{
|
|
29
|
+
if (!ctx.request.body) {
|
|
30
|
+
throw new Error('Invalid request body');
|
|
31
|
+
}
|
|
32
|
+
const result = L1AssetRequestSchema.safeParse(ctx.request.body);
|
|
33
|
+
if (!result.success) {
|
|
34
|
+
throw new Error(`Invalid request: ${result.error.message}`);
|
|
35
|
+
}
|
|
36
|
+
const { address, amount } = result.data;
|
|
37
|
+
await faucet.addL1Asset({
|
|
38
|
+
address,
|
|
39
|
+
amount
|
|
40
|
+
});
|
|
41
|
+
ctx.status = 200;
|
|
42
|
+
});
|
|
43
|
+
const app = new Koa();
|
|
44
|
+
app.on('error', (error)=>{
|
|
45
|
+
logger.error(`Error on API handler: ${error}`);
|
|
46
|
+
});
|
|
47
|
+
app.use(async (ctx, next)=>{
|
|
48
|
+
try {
|
|
49
|
+
await next();
|
|
50
|
+
} catch (err) {
|
|
51
|
+
logger.error(err);
|
|
52
|
+
ctx.status = err instanceof ThrottleError ? 429 : 400;
|
|
53
|
+
ctx.body = {
|
|
54
|
+
error: err.message
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
app.use(cors());
|
|
59
|
+
app.use(bodyParser());
|
|
60
|
+
app.use(router.routes());
|
|
61
|
+
app.use(router.allowedMethods());
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
63
|
+
return createServer(app.callback());
|
|
64
|
+
}
|
|
65
|
+
export const FaucetSchema = {
|
|
66
|
+
send: z.function().args(schemas.EthAddress, z.string()).returns(z.void()),
|
|
67
|
+
sendERC20: z.function().args(schemas.EthAddress, z.string()).returns(z.void()),
|
|
68
|
+
sendEth: z.function().args(schemas.EthAddress).returns(z.void()),
|
|
69
|
+
addL1Asset: z.function().args(z.object({
|
|
70
|
+
address: schemas.EthAddress,
|
|
71
|
+
amount: z.bigint()
|
|
72
|
+
})).returns(z.void())
|
|
73
|
+
};
|
package/dest/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC"}
|
package/dest/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aztec/aztec-faucet",
|
|
3
|
+
"version": "0.0.0-test.0",
|
|
4
|
+
"main": "dest/bin/index.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": "./dest/bin/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dest/index.js",
|
|
9
|
+
"./config": "./dest/config.js"
|
|
10
|
+
},
|
|
11
|
+
"typedocOptions": {
|
|
12
|
+
"entryPoints": [
|
|
13
|
+
"./src/bin/index.ts"
|
|
14
|
+
],
|
|
15
|
+
"name": "Aztec Faucet",
|
|
16
|
+
"tsconfig": "./tsconfig.json"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"start": "node --no-warnings ./dest/bin",
|
|
20
|
+
"build": "yarn clean && tsc -b",
|
|
21
|
+
"build:dev": "tsc -b --watch",
|
|
22
|
+
"clean": "rm -rf ./dest .tsbuildinfo",
|
|
23
|
+
"formatting": "run -T prettier --check ./src && run -T eslint ./src",
|
|
24
|
+
"formatting:fix": "run -T eslint --fix ./src && run -T prettier -w ./src",
|
|
25
|
+
"test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests --maxWorkers=${JEST_MAX_WORKERS:-8}"
|
|
26
|
+
},
|
|
27
|
+
"inherits": [
|
|
28
|
+
"../package.common.json"
|
|
29
|
+
],
|
|
30
|
+
"jest": {
|
|
31
|
+
"moduleNameMapper": {
|
|
32
|
+
"^(\\.{1,2}/.*)\\.[cm]?js$": "$1"
|
|
33
|
+
},
|
|
34
|
+
"testRegex": "./src/.*\\.test\\.(js|mjs|ts)$",
|
|
35
|
+
"rootDir": "./src",
|
|
36
|
+
"transform": {
|
|
37
|
+
"^.+\\.tsx?$": [
|
|
38
|
+
"@swc/jest",
|
|
39
|
+
{
|
|
40
|
+
"jsc": {
|
|
41
|
+
"parser": {
|
|
42
|
+
"syntax": "typescript",
|
|
43
|
+
"decorators": true
|
|
44
|
+
},
|
|
45
|
+
"transform": {
|
|
46
|
+
"decoratorVersion": "2022-03"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
"extensionsToTreatAsEsm": [
|
|
53
|
+
".ts"
|
|
54
|
+
],
|
|
55
|
+
"reporters": [
|
|
56
|
+
"default"
|
|
57
|
+
],
|
|
58
|
+
"testTimeout": 120000,
|
|
59
|
+
"setupFiles": [
|
|
60
|
+
"../../foundation/src/jest/setup.mjs"
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"@aztec/ethereum": "0.0.0-test.0",
|
|
65
|
+
"@aztec/foundation": "0.0.0-test.0",
|
|
66
|
+
"@aztec/l1-artifacts": "0.0.0-test.0",
|
|
67
|
+
"@koa/cors": "^5.0.0",
|
|
68
|
+
"koa": "^2.14.2",
|
|
69
|
+
"koa-bodyparser": "^4.4.1",
|
|
70
|
+
"koa-router": "^12.0.0",
|
|
71
|
+
"viem": "2.22.8",
|
|
72
|
+
"zod": "^3.23.8"
|
|
73
|
+
},
|
|
74
|
+
"devDependencies": {
|
|
75
|
+
"@jest/globals": "^29.5.0",
|
|
76
|
+
"@types/jest": "^29.5.0",
|
|
77
|
+
"@types/koa-bodyparser": "^4.3.12",
|
|
78
|
+
"@types/node": "^18.7.23",
|
|
79
|
+
"jest": "^29.5.0",
|
|
80
|
+
"ts-node": "^10.9.1",
|
|
81
|
+
"typescript": "^5.0.4"
|
|
82
|
+
},
|
|
83
|
+
"files": [
|
|
84
|
+
"dest",
|
|
85
|
+
"src",
|
|
86
|
+
"!*.test.*"
|
|
87
|
+
],
|
|
88
|
+
"types": "./dest/index.d.ts",
|
|
89
|
+
"engines": {
|
|
90
|
+
"node": ">=18"
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/bin/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --no-warnings
|
|
2
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
3
|
+
|
|
4
|
+
import { getFaucetConfigFromEnv } from '../config.js';
|
|
5
|
+
import { Faucet } from '../faucet.js';
|
|
6
|
+
import { createFaucetHttpServer } from '../http.js';
|
|
7
|
+
|
|
8
|
+
const logger = createLogger('aztec:faucet:http');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create and start a new Aztec Node HTTP Server
|
|
12
|
+
*/
|
|
13
|
+
async function main() {
|
|
14
|
+
const config = getFaucetConfigFromEnv();
|
|
15
|
+
const faucet = await Faucet.create(config);
|
|
16
|
+
const httpServer = createFaucetHttpServer(faucet, '/', logger);
|
|
17
|
+
const port = parseInt(process.env?.AZTEC_PORT ?? '', 10) || 8080;
|
|
18
|
+
httpServer.listen(port);
|
|
19
|
+
logger.info(`Aztec Faucet listening on port ${port}`);
|
|
20
|
+
await Promise.resolve();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
main().catch(err => {
|
|
24
|
+
logger.error(err);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
});
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type L1ReaderConfig, l1ReaderConfigMappings } from '@aztec/ethereum';
|
|
2
|
+
import { type ConfigMappingsType, getConfigFromMappings, numberConfigHelper } from '@aztec/foundation/config';
|
|
3
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
4
|
+
|
|
5
|
+
export type L1AssetConfig = {
|
|
6
|
+
address: EthAddress;
|
|
7
|
+
amount: bigint;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type FaucetConfig = L1ReaderConfig & {
|
|
11
|
+
l1Mnemonic: string;
|
|
12
|
+
mnemonicAccountIndex: number;
|
|
13
|
+
interval: number;
|
|
14
|
+
ethAmount: string;
|
|
15
|
+
l1Assets: L1AssetConfig[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const faucetConfigMapping: ConfigMappingsType<FaucetConfig> = {
|
|
19
|
+
...l1ReaderConfigMappings,
|
|
20
|
+
l1Mnemonic: {
|
|
21
|
+
env: 'MNEMONIC',
|
|
22
|
+
description: 'The mnemonic for the faucet account',
|
|
23
|
+
},
|
|
24
|
+
mnemonicAccountIndex: {
|
|
25
|
+
env: 'FAUCET_MNEMONIC_ACCOUNT_INDEX',
|
|
26
|
+
description: 'The account to use',
|
|
27
|
+
...numberConfigHelper(0),
|
|
28
|
+
},
|
|
29
|
+
interval: {
|
|
30
|
+
env: 'FAUCET_INTERVAL_MS',
|
|
31
|
+
description: 'How often the faucet can be dripped',
|
|
32
|
+
...numberConfigHelper(1 * 60 * 60 * 1000), // 1 hour
|
|
33
|
+
},
|
|
34
|
+
ethAmount: {
|
|
35
|
+
env: 'FAUCET_ETH_AMOUNT',
|
|
36
|
+
description: 'How much eth the faucet should drip per call',
|
|
37
|
+
defaultValue: '1.0',
|
|
38
|
+
},
|
|
39
|
+
l1Assets: {
|
|
40
|
+
env: 'FAUCET_L1_ASSETS',
|
|
41
|
+
description: 'Which other L1 assets the faucet is able to drip',
|
|
42
|
+
defaultValue: '',
|
|
43
|
+
parseEnv(val): L1AssetConfig[] {
|
|
44
|
+
const assetConfigs = val.split(',');
|
|
45
|
+
return assetConfigs.flatMap(assetConfig => {
|
|
46
|
+
if (!assetConfig) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const [address, amount = '1e9'] = assetConfig.split(':');
|
|
50
|
+
return [{ address: EthAddress.fromString(address), amount: BigInt(amount) }];
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function getFaucetConfigFromEnv(): FaucetConfig {
|
|
57
|
+
return getConfigFromMappings(faucetConfigMapping);
|
|
58
|
+
}
|
package/src/faucet.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { type ViemPublicClient, type ViemWalletClient, createEthereumChain } from '@aztec/ethereum';
|
|
2
|
+
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
3
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
4
|
+
import { TestERC20Abi } from '@aztec/l1-artifacts';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
type Account,
|
|
8
|
+
type Chain,
|
|
9
|
+
type GetContractReturnType,
|
|
10
|
+
type HttpTransport,
|
|
11
|
+
type LocalAccount,
|
|
12
|
+
type WalletClient,
|
|
13
|
+
createPublicClient,
|
|
14
|
+
createWalletClient,
|
|
15
|
+
fallback,
|
|
16
|
+
getContract,
|
|
17
|
+
parseEther,
|
|
18
|
+
http as viemHttp,
|
|
19
|
+
} from 'viem';
|
|
20
|
+
import { mnemonicToAccount } from 'viem/accounts';
|
|
21
|
+
|
|
22
|
+
import type { FaucetConfig, L1AssetConfig } from './config.js';
|
|
23
|
+
|
|
24
|
+
type L1Asset = {
|
|
25
|
+
contract: GetContractReturnType<typeof TestERC20Abi, WalletClient<HttpTransport, Chain, Account>>;
|
|
26
|
+
amount: bigint;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export class Faucet {
|
|
30
|
+
private walletClient: ViemWalletClient;
|
|
31
|
+
private publicClient: ViemPublicClient;
|
|
32
|
+
|
|
33
|
+
private dripHistory = new Map<string, Map<string, number>>();
|
|
34
|
+
private l1Assets = new Map<string, L1Asset>();
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
private config: FaucetConfig,
|
|
38
|
+
private account: LocalAccount,
|
|
39
|
+
private timeFn: () => number = Date.now,
|
|
40
|
+
private log = createLogger('aztec:faucet'),
|
|
41
|
+
) {
|
|
42
|
+
const chain = createEthereumChain(config.l1RpcUrls, config.l1ChainId);
|
|
43
|
+
|
|
44
|
+
this.walletClient = createWalletClient({
|
|
45
|
+
account: this.account,
|
|
46
|
+
chain: chain.chainInfo,
|
|
47
|
+
transport: fallback(chain.rpcUrls.map(url => viemHttp(url))),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
this.publicClient = createPublicClient({
|
|
51
|
+
chain: chain.chainInfo,
|
|
52
|
+
transport: fallback(chain.rpcUrls.map(url => viemHttp(url))),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public static async create(config: FaucetConfig): Promise<Faucet> {
|
|
57
|
+
if (!config.l1Mnemonic) {
|
|
58
|
+
throw new Error('Missing faucet mnemonic');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const account = mnemonicToAccount(config.l1Mnemonic, { addressIndex: config.mnemonicAccountIndex });
|
|
62
|
+
const faucet = new Faucet(config, account);
|
|
63
|
+
|
|
64
|
+
for (const asset of config.l1Assets) {
|
|
65
|
+
await faucet.addL1Asset(asset);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return faucet;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public send(to: EthAddress, assetName: string): Promise<void> {
|
|
72
|
+
if (assetName.toUpperCase() === 'ETH') {
|
|
73
|
+
return this.sendEth(to);
|
|
74
|
+
} else {
|
|
75
|
+
return this.sendERC20(to, assetName);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public async sendEth(to: EthAddress): Promise<void> {
|
|
80
|
+
this.checkThrottle(to, 'ETH');
|
|
81
|
+
|
|
82
|
+
const hash = await this.walletClient.sendTransaction({
|
|
83
|
+
account: this.account,
|
|
84
|
+
to: to.toString(),
|
|
85
|
+
value: parseEther(this.config.ethAmount),
|
|
86
|
+
});
|
|
87
|
+
await this.publicClient.waitForTransactionReceipt({ hash });
|
|
88
|
+
|
|
89
|
+
this.updateThrottle(to, 'ETH');
|
|
90
|
+
this.log.info(`Sent ETH ${this.config.ethAmount} to ${to} in tx ${hash}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public async sendERC20(to: EthAddress, assetName: string): Promise<void> {
|
|
94
|
+
const asset = this.l1Assets.get(assetName);
|
|
95
|
+
if (!asset) {
|
|
96
|
+
throw new UnknownAsset(assetName);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
this.checkThrottle(to, assetName);
|
|
100
|
+
|
|
101
|
+
const hash = await asset.contract.write.mint([to.toString(), asset.amount]);
|
|
102
|
+
await this.publicClient.waitForTransactionReceipt({ hash });
|
|
103
|
+
|
|
104
|
+
this.updateThrottle(to, assetName);
|
|
105
|
+
|
|
106
|
+
this.log.info(`Sent ${assetName} ${asset.amount} to ${to} in tx ${hash}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public async addL1Asset(l1AssetConfig: L1AssetConfig): Promise<void> {
|
|
110
|
+
const contract = getContract({
|
|
111
|
+
abi: TestERC20Abi,
|
|
112
|
+
address: l1AssetConfig.address.toString(),
|
|
113
|
+
client: this.walletClient,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const [name, owner] = await Promise.all([contract.read.name(), contract.read.owner()]);
|
|
117
|
+
|
|
118
|
+
if (owner !== this.account.address) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Owner mismatch. Expected contract ${name} to be owned by ${this.account.address}, received ${owner}`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (
|
|
125
|
+
this.l1Assets.has(name) &&
|
|
126
|
+
this.l1Assets.get(name)!.contract.address.toLowerCase() !== l1AssetConfig.address.toString().toLowerCase()
|
|
127
|
+
) {
|
|
128
|
+
this.log.warn(`Updating asset ${name} to address=${contract.address}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.l1Assets.set(name, { contract, amount: l1AssetConfig.amount });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private checkThrottle(address: EthAddress, asset = 'ETH') {
|
|
135
|
+
const addressHistory = this.dripHistory.get(address.toString());
|
|
136
|
+
if (!addressHistory) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const now = this.timeFn();
|
|
141
|
+
const last = addressHistory.get(asset);
|
|
142
|
+
if (typeof last === 'number' && last + this.config.interval > now) {
|
|
143
|
+
throw new ThrottleError(address.toString(), asset);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private updateThrottle(address: EthAddress, asset = 'ETH') {
|
|
148
|
+
const addressStr = address.toString();
|
|
149
|
+
if (!this.dripHistory.has(addressStr)) {
|
|
150
|
+
this.dripHistory.set(addressStr, new Map());
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const addressHistory = this.dripHistory.get(addressStr)!;
|
|
154
|
+
addressHistory.set(asset, this.timeFn());
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export class ThrottleError extends Error {
|
|
159
|
+
constructor(address: string, asset: string) {
|
|
160
|
+
super(`Not funding ${asset}: throttled ${address}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export class UnknownAsset extends Error {
|
|
165
|
+
constructor(asset: string) {
|
|
166
|
+
super(`Unknown asset: ${asset}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
package/src/http.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
2
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
3
|
+
import { type ApiSchemaFor, schemas } from '@aztec/foundation/schemas';
|
|
4
|
+
|
|
5
|
+
import cors from '@koa/cors';
|
|
6
|
+
import { createServer } from 'http';
|
|
7
|
+
import Koa from 'koa';
|
|
8
|
+
import bodyParser from 'koa-bodyparser';
|
|
9
|
+
import Router from 'koa-router';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
|
|
12
|
+
import { type Faucet, ThrottleError } from './faucet.js';
|
|
13
|
+
|
|
14
|
+
export function createFaucetHttpServer(faucet: Faucet, apiPrefix = '', logger = createLogger('aztec:faucet:http')) {
|
|
15
|
+
const router = new Router({ prefix: `${apiPrefix}` });
|
|
16
|
+
router.get('/drip/:address', async ctx => {
|
|
17
|
+
const { address } = ctx.params;
|
|
18
|
+
const { asset } = ctx.query;
|
|
19
|
+
|
|
20
|
+
if (typeof asset !== 'string') {
|
|
21
|
+
throw new Error(`Bad asset: [${asset}]`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
await faucet.send(EthAddress.fromString(address), asset);
|
|
25
|
+
|
|
26
|
+
ctx.status = 200;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const L1AssetRequestSchema = z.object({
|
|
30
|
+
address: z.string().transform(str => EthAddress.fromString(str)),
|
|
31
|
+
amount: z.string().transform(str => BigInt(str)),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
router.post('/l1-asset', async ctx => {
|
|
35
|
+
if (!ctx.request.body) {
|
|
36
|
+
throw new Error('Invalid request body');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const result = L1AssetRequestSchema.safeParse(ctx.request.body);
|
|
40
|
+
|
|
41
|
+
if (!result.success) {
|
|
42
|
+
throw new Error(`Invalid request: ${result.error.message}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { address, amount } = result.data;
|
|
46
|
+
|
|
47
|
+
await faucet.addL1Asset({ address, amount });
|
|
48
|
+
|
|
49
|
+
ctx.status = 200;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const app = new Koa();
|
|
53
|
+
|
|
54
|
+
app.on('error', error => {
|
|
55
|
+
logger.error(`Error on API handler: ${error}`);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
app.use(async (ctx, next) => {
|
|
59
|
+
try {
|
|
60
|
+
await next();
|
|
61
|
+
} catch (err: any) {
|
|
62
|
+
logger.error(err);
|
|
63
|
+
ctx.status = err instanceof ThrottleError ? 429 : 400;
|
|
64
|
+
ctx.body = { error: err.message };
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
app.use(cors());
|
|
69
|
+
app.use(bodyParser());
|
|
70
|
+
app.use(router.routes());
|
|
71
|
+
app.use(router.allowedMethods());
|
|
72
|
+
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
74
|
+
return createServer(app.callback());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const FaucetSchema: ApiSchemaFor<Faucet> = {
|
|
78
|
+
send: z.function().args(schemas.EthAddress, z.string()).returns(z.void()),
|
|
79
|
+
sendERC20: z.function().args(schemas.EthAddress, z.string()).returns(z.void()),
|
|
80
|
+
sendEth: z.function().args(schemas.EthAddress).returns(z.void()),
|
|
81
|
+
addL1Asset: z
|
|
82
|
+
.function()
|
|
83
|
+
.args(z.object({ address: schemas.EthAddress, amount: z.bigint() }))
|
|
84
|
+
.returns(z.void()),
|
|
85
|
+
};
|
package/src/index.ts
ADDED