├── LICENSE
├── smartcontract
└── faucet.sol
├── frontend
├── style.css
├── json
│ ├── faucet.json
│ └── erc20.json
├── index.html
└── faucet.js
└── README.md
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Marvin Kruse
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/smartcontract/faucet.sol:
--------------------------------------------------------------------------------
1 | pragma solidity ^0.5.1;
2 |
3 | interface ERC20 {
4 | function transfer(address to, uint256 value) external returns (bool);
5 | event Transfer(address indexed from, address indexed to, uint256 value);
6 | }
7 |
8 | contract Faucet {
9 | uint256 constant public tokenAmount = 100000000000000000000;
10 | uint256 constant public waitTime = 30 minutes;
11 |
12 | ERC20 public tokenInstance;
13 |
14 | mapping(address => uint256) lastAccessTime;
15 |
16 | constructor(address _tokenInstance) public {
17 | require(_tokenInstance != address(0));
18 | tokenInstance = ERC20(_tokenInstance);
19 | }
20 |
21 | function requestTokens() public {
22 | require(allowedToWithdraw(msg.sender));
23 | tokenInstance.transfer(msg.sender, tokenAmount);
24 | lastAccessTime[msg.sender] = block.timestamp + waitTime;
25 | }
26 |
27 | function allowedToWithdraw(address _address) public view returns (bool) {
28 | if(lastAccessTime[_address] == 0) {
29 | return true;
30 | } else if(block.timestamp >= lastAccessTime[_address]) {
31 | return true;
32 | }
33 | return false;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/frontend/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | width: 550px;
3 | margin-top: 20px;
4 | margin-left: auto;
5 | margin-right: auto;
6 | font-family: 'Roboto';
7 | }
8 |
9 | h1 {
10 | text-align: center;
11 | font-weight: 600;
12 | }
13 |
14 | #intro {
15 | text-align: justify;
16 | }
17 |
18 | #wrong_network {
19 | margin-top: 20px;
20 | }
21 |
22 | .metamask_info {
23 | text-decoration: underline;
24 | font-weight: 600;
25 | }
26 |
27 | ul li {
28 | margin-left: -15px;
29 | }
30 |
31 | #correct_network {
32 | margin-top: 20px;
33 | text-align: center;
34 | font-weight: 500;
35 | display: none;
36 | }
37 |
38 | #faucet {
39 | margin-top: 30px;
40 | text-align: center;
41 | display: none;
42 | }
43 |
44 | #faucet h2 {
45 | font-style: inherit;
46 | font-size: 1.1em;
47 | font-weight: 700;
48 | margin-bottom: -3px;
49 | margin-top: 10px;
50 | }
51 |
52 | #requestButton {
53 | margin-top: 20px;
54 | margin-bottom: 10px;
55 | font-weight: 500;
56 | }
57 |
58 | #requestButton:disabled,
59 | #requestButton[disabled]{
60 | cursor: no-drop;
61 | }
62 |
63 | #warning {
64 | font-style: italic;
65 | }
66 |
67 | #footer {
68 | margin-top: 40px;
69 | text-align: center;
70 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Simple Faucet
2 | Simple Faucet is a pretty minimalistic faucet for Ethereum-based ERC20 tokens. It's based on a simple smart contract that will allow you to withdraw some tokens every couple of minutes. The amount of tokens and the waiting period can be defined upon deployment of the smart contract. It works with Metamask in your browser, so it's very easy to use.
3 |
4 | ## How to use it
5 | In order to use and deploy this simple faucet, you need to deploy the smart contract and the frontend. To do this follow the tutorial below.
6 |
7 | ### Smart Contract
8 | Simply deploy the smart contract onto your chain. You need to specify the erc20 tokens' address in the constructor when deploying. By default the contract allows 100 tokens to be withdrawn every 30 minutes - feel free to change this in the `faucet.sol` smart contract (*line 9+10*).
9 |
10 | ### Frontend
11 | You can simply put the contents of the `frontend` folder onto your preferred webserver (*it's just html and javascript*). In the `faucet.js` file you need to specify the RPC url, the network ID and the minimum gas price as well as the tokens' and the faucets address. Nothing else needs to be changed.
12 |
13 | ## Feedback, Problems, Suggestions?
14 | Simple open up an issue in this repositoriy and I'll get back to you as soon as possible. I'm always open to your feedback and ideas on how to improve this faucet.
--------------------------------------------------------------------------------
/frontend/json/faucet.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "constant": true,
4 | "inputs": [
5 | {
6 | "name": "_address",
7 | "type": "address"
8 | }
9 | ],
10 | "name": "allowedToWithdraw",
11 | "outputs": [
12 | {
13 | "name": "",
14 | "type": "bool"
15 | }
16 | ],
17 | "payable": false,
18 | "stateMutability": "view",
19 | "type": "function"
20 | },
21 | {
22 | "constant": false,
23 | "inputs": [],
24 | "name": "requestTokens",
25 | "outputs": [],
26 | "payable": false,
27 | "stateMutability": "nonpayable",
28 | "type": "function"
29 | },
30 | {
31 | "constant": true,
32 | "inputs": [],
33 | "name": "tokenInstance",
34 | "outputs": [
35 | {
36 | "name": "",
37 | "type": "address"
38 | }
39 | ],
40 | "payable": false,
41 | "stateMutability": "view",
42 | "type": "function"
43 | },
44 | {
45 | "constant": true,
46 | "inputs": [],
47 | "name": "waitTime",
48 | "outputs": [
49 | {
50 | "name": "",
51 | "type": "uint256"
52 | }
53 | ],
54 | "payable": false,
55 | "stateMutability": "view",
56 | "type": "function"
57 | },
58 | {
59 | "constant": true,
60 | "inputs": [],
61 | "name": "tokenAmount",
62 | "outputs": [
63 | {
64 | "name": "",
65 | "type": "uint256"
66 | }
67 | ],
68 | "payable": false,
69 | "stateMutability": "view",
70 | "type": "function"
71 | },
72 | {
73 | "inputs": [
74 | {
75 | "name": "_tokenInstance",
76 | "type": "address"
77 | }
78 | ],
79 | "payable": false,
80 | "stateMutability": "nonpayable",
81 | "type": "constructor"
82 | }
83 | ]
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Simple Faucet
13 |
14 |
15 |
16 | Simple Faucet
17 |
18 |
Welcome to the simple faucet, a minimalistic ERC20-token faucet for Ethereum. Developing dApps or smart contracts requires you to test what you have built.
19 | Since begging for tokens face-to-face is outdated, this faucet offers a very easy smart contract based solution.
20 | Just click the button below and you will receive some test tokens. The only thing that you need is Metamask.
21 |
It seems that you are
not connected to the testnet . Please switch to it via Metamask. If you haven't added the testnet already, follow these simple steps:
22 |
23 | Open and unlock your Metamask
24 | Click on the network selector at the top
25 | Select "Custom RPC"
26 | Scroll down to "New Network" and enter the following (enable the advanced options):
27 |
28 | New RPC URL:
29 | ChainID:
30 | Nickname: MyFancyTestnet
31 |
32 | Click on save and you're done!
33 |
34 |
35 |
You are connected to the right testnet - great! Go ahead and claim some tokens!
36 |
37 |
38 |
39 |
Your Address:
40 |
0x0000000000000000000000000000000000000000
41 |
42 |
Your Ether Balance:
43 |
0
44 |
45 |
Your Token Balance:
46 |
0
47 |
Request Test Tokens
48 |
49 |
50 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/frontend/faucet.js:
--------------------------------------------------------------------------------
1 | $(document).ready(function() {
2 |
3 | //////////////////////////////////////////////////////////////////////////////
4 | //// INSERT YOUR NODE RPC URL, NETWORK ID AND GAS PRICE HERE //////
5 | //////////////////////////////////////////////////////////////////////////////
6 | var rpcURL = "http://your-fancy-node-url:8545";
7 | var networkID = 12345;
8 | var minGasPrice = 0;
9 | //////////////////////////////////////////////////////////////////////////////
10 | //// INSERT THE TOKEN AND FAUCET ADDRESS HERE //////
11 | //////////////////////////////////////////////////////////////////////////////
12 | var token_address = '0x0000000000000000000000000000000000000000';
13 | var faucet_address = '0x0000000000000000000000000000000000000000';
14 | //////////////////////////////////////////////////////////////////////////////
15 |
16 | var account;
17 | var web3Provider;
18 |
19 | var contract_token;
20 | var contract_faucet;
21 |
22 | var balanceETH = 0;
23 | var balanceToken = 0;
24 |
25 | function initialize() {
26 | setAccount();
27 | setTokenBalance();
28 | checkFaucet();
29 | }
30 |
31 | function setAccount() {
32 | web3.version.getNetwork(function(err, netId) {
33 | if (!err && netId == networkID) {
34 | $("#wrong_network").fadeOut(1000);
35 | setTimeout(function(){ $("#correct_network").fadeIn(); $("#faucet").fadeIn(); }, 1000);
36 | account = web3.eth.accounts[0];
37 | $("#address").text(account);
38 | web3.eth.getBalance(account, function(err, res) {
39 | if(!err) {
40 | balanceETH = Number(web3.fromWei(res, 'ether'));
41 | $('#balanceETH').text(balanceETH + " ETH");
42 | $('#balanceETH').show();
43 | }
44 | });
45 | }
46 | });
47 | }
48 |
49 | function setTokenBalance() {
50 | contract_token.balanceOf(web3.eth.accounts[0], function(err, result) {
51 | if(!err) {
52 | $('#balanceToken').text(web3.fromWei(balanceToken, 'ether') + " Tokens");
53 | if(Number(result) != balanceToken) {
54 | balanceToken = Number(result);
55 | $('#balanceToken').text(web3.fromWei(balanceToken, 'ether') + " Tokens");
56 | }
57 | }
58 | });
59 | }
60 |
61 | function checkFaucet() {
62 | var tokenAmount = 0;
63 | contract_faucet.tokenAmount(function(err, result) {
64 | if(!err) {
65 | tokenAmount = result;
66 | $("#requestButton").text("Request " + web3.fromWei(result, 'ether') + " Test Tokens");
67 | }
68 | });
69 |
70 | contract_token.balanceOf(faucet_address, function(errCall, result) {
71 | if(!errCall) {
72 | if(result < tokenAmount) {
73 | $("#warning").html("Sorry - the faucet is out of tokens! But don't worry, we're on it!")
74 | } else {
75 | contract_faucet.allowedToWithdraw(web3.eth.accounts[0], function(err, result) {
76 | if(!err) {
77 | if(result && balanceToken < tokenAmount*1000) {
78 | $("#requestButton").removeAttr('disabled');
79 | } else {
80 | contract_faucet.waitTime(function(err, result) {
81 | if(!err) {
82 | $("#warning").html("Sorry - you can only request tokens every " + (result)/60 + " minutes. Please wait!")
83 | }
84 | });
85 | }
86 | }
87 | });
88 | }
89 | }
90 | });
91 | }
92 |
93 | function getTestTokens() {
94 | $("#requestButton").attr('disabled', true);
95 | web3.eth.getTransactionCount(account, function(errNonce, nonce) {
96 | if(!errNonce) {
97 | contract_faucet.requestTokens({value: 0, gas: 200000, gasPrice: minGasPrice, from: account, nonce: nonce}, function(errCall, result) {
98 | if(!errCall) {
99 | testTokensRequested = true;
100 | $('#getTokens').hide();
101 | } else {
102 | testTokensRequested = true;
103 | $('#getTokens').hide();
104 | }
105 | });
106 | }
107 | });
108 | }
109 |
110 | $("#rpc_url").text(rpcURL);
111 | $("#network_id").text(networkID);
112 |
113 | if (typeof web3 !== 'undefined') {
114 | web3Provider = web3.currentProvider;
115 | }
116 |
117 | web3 = new Web3(web3Provider);
118 |
119 | $.getJSON('json/erc20.json', function(data) {
120 | contract_token = web3.eth.contract(data).at(token_address);
121 | });
122 | $.getJSON('json/faucet.json', function(data) {
123 | contract_faucet = web3.eth.contract(data).at(faucet_address);
124 | });
125 |
126 | setTimeout(function(){ initialize(); }, 1000);
127 |
128 | let tokenButton = document.querySelector('#requestButton');
129 | tokenButton.addEventListener('click', function() {
130 | getTestTokens();
131 | });
132 | });
--------------------------------------------------------------------------------
/frontend/json/erc20.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "constant": true,
4 | "inputs": [],
5 | "name": "name",
6 | "outputs": [
7 | {
8 | "name": "",
9 | "type": "string"
10 | }
11 | ],
12 | "payable": false,
13 | "stateMutability": "view",
14 | "type": "function"
15 | },
16 | {
17 | "constant": false,
18 | "inputs": [
19 | {
20 | "name": "_spender",
21 | "type": "address"
22 | },
23 | {
24 | "name": "_value",
25 | "type": "uint256"
26 | }
27 | ],
28 | "name": "approve",
29 | "outputs": [
30 | {
31 | "name": "",
32 | "type": "bool"
33 | }
34 | ],
35 | "payable": false,
36 | "stateMutability": "nonpayable",
37 | "type": "function"
38 | },
39 | {
40 | "constant": true,
41 | "inputs": [],
42 | "name": "totalSupply",
43 | "outputs": [
44 | {
45 | "name": "",
46 | "type": "uint256"
47 | }
48 | ],
49 | "payable": false,
50 | "stateMutability": "view",
51 | "type": "function"
52 | },
53 | {
54 | "constant": false,
55 | "inputs": [
56 | {
57 | "name": "_from",
58 | "type": "address"
59 | },
60 | {
61 | "name": "_to",
62 | "type": "address"
63 | },
64 | {
65 | "name": "_value",
66 | "type": "uint256"
67 | }
68 | ],
69 | "name": "transferFrom",
70 | "outputs": [
71 | {
72 | "name": "",
73 | "type": "bool"
74 | }
75 | ],
76 | "payable": false,
77 | "stateMutability": "nonpayable",
78 | "type": "function"
79 | },
80 | {
81 | "constant": true,
82 | "inputs": [],
83 | "name": "decimals",
84 | "outputs": [
85 | {
86 | "name": "",
87 | "type": "uint8"
88 | }
89 | ],
90 | "payable": false,
91 | "stateMutability": "view",
92 | "type": "function"
93 | },
94 | {
95 | "constant": true,
96 | "inputs": [
97 | {
98 | "name": "_owner",
99 | "type": "address"
100 | }
101 | ],
102 | "name": "balanceOf",
103 | "outputs": [
104 | {
105 | "name": "balance",
106 | "type": "uint256"
107 | }
108 | ],
109 | "payable": false,
110 | "stateMutability": "view",
111 | "type": "function"
112 | },
113 | {
114 | "constant": true,
115 | "inputs": [],
116 | "name": "symbol",
117 | "outputs": [
118 | {
119 | "name": "",
120 | "type": "string"
121 | }
122 | ],
123 | "payable": false,
124 | "stateMutability": "view",
125 | "type": "function"
126 | },
127 | {
128 | "constant": false,
129 | "inputs": [
130 | {
131 | "name": "_to",
132 | "type": "address"
133 | },
134 | {
135 | "name": "_value",
136 | "type": "uint256"
137 | }
138 | ],
139 | "name": "transfer",
140 | "outputs": [
141 | {
142 | "name": "",
143 | "type": "bool"
144 | }
145 | ],
146 | "payable": false,
147 | "stateMutability": "nonpayable",
148 | "type": "function"
149 | },
150 | {
151 | "constant": true,
152 | "inputs": [
153 | {
154 | "name": "_owner",
155 | "type": "address"
156 | },
157 | {
158 | "name": "_spender",
159 | "type": "address"
160 | }
161 | ],
162 | "name": "allowance",
163 | "outputs": [
164 | {
165 | "name": "",
166 | "type": "uint256"
167 | }
168 | ],
169 | "payable": false,
170 | "stateMutability": "view",
171 | "type": "function"
172 | },
173 | {
174 | "payable": true,
175 | "stateMutability": "payable",
176 | "type": "fallback"
177 | },
178 | {
179 | "anonymous": false,
180 | "inputs": [
181 | {
182 | "indexed": true,
183 | "name": "owner",
184 | "type": "address"
185 | },
186 | {
187 | "indexed": true,
188 | "name": "spender",
189 | "type": "address"
190 | },
191 | {
192 | "indexed": false,
193 | "name": "value",
194 | "type": "uint256"
195 | }
196 | ],
197 | "name": "Approval",
198 | "type": "event"
199 | },
200 | {
201 | "anonymous": false,
202 | "inputs": [
203 | {
204 | "indexed": true,
205 | "name": "from",
206 | "type": "address"
207 | },
208 | {
209 | "indexed": true,
210 | "name": "to",
211 | "type": "address"
212 | },
213 | {
214 | "indexed": false,
215 | "name": "value",
216 | "type": "uint256"
217 | }
218 | ],
219 | "name": "Transfer",
220 | "type": "event"
221 | }
222 | ]
223 |
--------------------------------------------------------------------------------