This commit is contained in:
Adam Sobotka 2022-10-31 15:12:34 +01:00
revize 96226dd4f7
21 změnil soubory, kde provedl 24391 přidání a 0 odebrání

1
.env.example Normal file
Zobrazit soubor

@ -0,0 +1 @@
VITE_ALCHEMY_API_KEY=

5
.gitignore vendorováno Normal file
Zobrazit soubor

@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local

3
README.md Normal file
Zobrazit soubor

@ -0,0 +1,3 @@
# Vite React Example
This examples uses [Vite](https://vitejs.dev) and React.

28
index.html Normal file
Zobrazit soubor

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Ubuntu+Mono:wght@400;700&display=swap" rel="stylesheet">
<title>Soulbound Faucet</title>
<script>window.global = window;</script>
<script type="module">
import process from "process";
import { Buffer } from "buffer";
import EventEmitter from "events";
window.process = process;
window.Buffer = Buffer;
window.EventEmitter = EventEmitter;
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

23842
package-lock.json vygenerováno Normal file

Rozdílový obsah nebyl zobrazen, protože je příliš veliký Načíst rozdílové porovnání

30
package.json Normal file
Zobrazit soubor

@ -0,0 +1,30 @@
{
"name": "soulbound-faucet",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"buffer": "^6.0.3",
"ethers": "^5.7.0",
"events": "^3.3.0",
"process": "^0.11.10",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"util": "^0.12.4",
"wagmi": "^0.7.8"
},
"devDependencies": {
"@types/react": "^18.0.9",
"@types/react-dom": "^18.0.3",
"@vitejs/plugin-react": "^1.3.2",
"autoprefixer": "^10.4.13",
"postcss": "^8.4.18",
"tailwindcss": "^3.2.1",
"typescript": "^4.7.4",
"vite": "^2.9.8"
}
}

6
postcss.config.js Normal file
Zobrazit soubor

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

binární
public/eth-glyph-colored.png Normal file

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 48 KiB

28
public/eth.svg Normal file
Zobrazit soubor

@ -0,0 +1,28 @@
<svg alt="ETH diamond (color filled, SVG)" width="100%" height="100%" viewBox="0 0 142 215" version="1.1" xml:space="preserve" xmlnsSerif="http://www.serif.com/" style="fill-rule: evenodd; clip-rule: evenodd; stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 7;">
<g transform="matrix(1,0,0,1,-1259.52,-529.088)">
<g id="Ethereum">
<g id="Bottom">
<g id="Purple2" transform="matrix(1.08181,0,0,-1.03277,-108.829,1366.48)">
<path d="M1394.74,693.463L1330.36,604.024L1330.36,654.238L1394.74,693.463Z" style="fill: rgb(200, 178, 245); stroke: rgb(52, 65, 192); stroke-width: 2.27px;"></path>
</g>
<g id="Yellow2" transform="matrix(-1.08181,0,0,-1.03277,2769.57,1366.48)">
<path d="M1394.74,693.463L1330.36,604.024L1330.36,654.238L1394.74,693.463Z" style="fill: rgb(238, 203, 192); stroke: rgb(52, 65, 192); stroke-width: 2.27px;"></path>
</g>
</g>
<g id="Top">
<g id="Blue1" transform="matrix(-1,0,0,1,2659.32,-0.0263692)">
<path d="M1398.61,639.614L1328.95,608.641L1328.95,679.249L1398.61,639.614Z" style="fill: rgb(135, 169, 240); stroke: rgb(52, 65, 192); stroke-width: 2.4px;"></path>
</g>
<g id="Purple1" transform="matrix(1,0,0,1,1.41643,-0.0263692)">
<path d="M1398.61,639.614L1328.95,608.641L1328.95,679.249L1398.61,639.614Z" style="fill: rgb(202, 179, 245); stroke: rgb(52, 65, 192); stroke-width: 2.4px;"></path>
</g>
<g id="Yellow1" transform="matrix(-1.08181,0,0,1.03277,2769.57,-93.5314)">
<path d="M1394.74,709.855L1330.36,604.024L1330.36,679.865L1394.74,709.855Z" style="fill: rgb(238, 203, 192); stroke: rgb(52, 65, 192); stroke-width: 2.27px;"></path>
</g>
<g id="Green1" transform="matrix(1.08181,0,0,1.03277,-108.829,-93.5314)">
<path d="M1394.74,709.855L1330.36,604.024L1330.36,679.865L1394.74,709.855Z" style="fill: rgb(184, 251, 246); stroke: rgb(52, 65, 192); stroke-width: 2.27px;"></path>
</g>
</g>
</g>
</g>
</svg>

Za

Šířka:  |  Výška:  |  Velikost: 2.2 KiB

188
src/App.tsx Normal file
Zobrazit soubor

@ -0,0 +1,188 @@
import { useAccount, useSignMessage } from "wagmi";
import { useEffect, useState } from "react";
import { Account, Connect, NetworkSwitcher } from "./components";
import { verifyMessage } from "ethers/lib/utils";
import { SignMessageArgs } from "@wagmi/core";
export function App() {
const { isConnected, address } = useAccount();
const [isConnecting, setIsConnecting] = useState(isConnected);
const [isEligible, setIsEligible] = useState(false);
const [isFetching, setIsFetching] = useState(false);
const [network, setNetwork] = useState("goerli");
const baseUrl = "https://faucet-api.ethbrno.cz";
const getEligibilityData = async () => {
if (!address) return;
setIsFetching(true);
try {
const apiResponse = await fetch(`${baseUrl}/lookup?addr=${address}`);
const res = await apiResponse.json();
if (res?.tokenId) setIsEligible(true);
else setIsEligible(false);
} catch (error) {
console.error(error);
}
setIsFetching(false);
};
const { data: signdata, error, isLoading, signMessage } = useSignMessage({
onSuccess(data, variables) {
getTokenData(data, variables);
},
});
const getTokenData = async (data: string, variables: SignMessageArgs) => {
const valaddress = verifyMessage(variables.message, data);
const tokenmessage = {
"addr": valaddress,
"network": network,
"signature": {
"msg": variables.message,
"sig": data,
},
"wait": true,
};
//console.log(tokenmessage);
fetch(`${baseUrl}/request`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(tokenmessage),
})
.then((response) => response.json())
.then((data) => {
console.log("Success:", data);
})
.catch((error) => {
console.error("Error:", error);
});
};
function initiateTokenFall(): any {
setIsFetching(true);
const message = (`{
"id": "ethbrno-sbt-faucet",
"network": "${network}",
"timestamp": ${new Date().toISOString()},
}`);
signMessage({ message });
}
useEffect(() => {
getEligibilityData();
}, [address]);
function revalidate() {
}
//console.log(isEligible);
return (
<section>
<div className="container mx-auto flex px-5 py-24 md:flex-row flex-col items-center">
<div className="lg:flex-grow md:w-1/2 lg:pr-24 md:pr-16 flex flex-col md:items-start md:text-left mb-16 md:mb-0 items-center text-center">
<h1 className="title-font sm:text-4xl text-3xl mb-4 font-medium">
Soulbound
<br />
<p className=" text-teal-400">ETH Faucet</p>
</h1>
{isConnected && (
<div className="flex items-baseline my-6">
<div className="space-x-6 flex text-sm font-medium">
<label>
<input
className="sr-only peer"
name="size"
type="radio"
value="goerli"
checked={network == "goerli"}
onChange={() => setNetwork("goerli")}
/>
<div className="relative w-16 h-10 flex items-center justify-center text-black peer-checked:bg-black peer-checked:text-white before:absolute before:z-[-1] before:top-0.5 before:left-0.5 before:w-full before:h-full peer-checked:before:bg-teal-400 cursor-pointer">
GOERLI
</div>
</label>
<label>
<input
className="sr-only peer"
name="size"
type="radio"
value="sepolia"
checked={network == "sepolia"}
onChange={() => setNetwork("sepolia")}
/>
<div className="relative w-16 h-10 flex items-center justify-center text-black peer-checked:bg-black peer-checked:text-white before:absolute before:z-[-1] before:top-0.5 before:left-0.5 before:w-full before:h-full peer-checked:before:bg-teal-400 cursor-pointer">
SEPOLIA
</div>
</label>
</div>
</div>
)}
{}
<p className="mb-8 leading-relaxed">
If you own a Soulbound token you can ask for up to 200 ETH for
either Goerli and Sepolia testnets. This should be enough not only
for a development purposes, but also for prepaid gas transactions
for your testing users. These tokens have no real monetary value.
Every request grants you 50 ETH and there is a request cooldown for
5 hours to avoid misuse.
</p>
{!isConnecting || isConnected
? (
<div className="mb-4 text-sm font-medium">
<div className="flex space-x-4">
<button
className={isConnected ? `whitebtn` : `greenbtn`}
onClick={() => setIsConnecting(true)}
>
Connect
</button>
<button
className={isEligible ? `whitebtn` : `redbtn`}
onClick={() => {
revalidate;
}}
disabled={isFetching}
>
Validate
</button>
<button
className={isEligible ? `greenbtn` : `whitebtn`}
onClick={() => initiateTokenFall()}
disabled={isFetching}
>
Get Tokens
</button>
</div>
</div>
)
: (
<div className="mb-4 text-sm font-medium">
<Connect />
</div>
)}
{!isEligible && (
<div className="my-4 text-xs">
We cannot find a Soubound token in your wallet.{" "}
<a
className="font-bold underline"
href="https://mint.ethbrno.cz/"
>
Mint yours here.
</a>
</div>
)}
</div>
<div className="lg:max-w-lg lg:w-full md:w-1/3 w-5/6 flex justify-center">
<img
className="object-cover object-center w-36"
alt="ETH Logo"
src="eth-glyph-colored.png"
/>
</div>
</div>
</section>
);
}

Zobrazit soubor

@ -0,0 +1,13 @@
import { useAccount, useEnsName } from 'wagmi'
export function Account() {
const { address } = useAccount()
const { data: ensNameData } = useEnsName({ address })
return (
<div>
{ensNameData ?? address}
{ensNameData ? ` (${address})` : null}
</div>
)
}

Zobrazit soubor

@ -0,0 +1,38 @@
import { useAccount, useConnect, useDisconnect } from "wagmi";
export function Connect() {
const { connector, isConnected } = useAccount();
const { connect, connectors, error, isLoading, pendingConnector } =
useConnect();
const { disconnect } = useDisconnect();
return (
<div>
<div>
{isConnected && (
<button
className="greenbtn"
onClick={() => disconnect()}
>
Disconnect from {connector?.name}
</button>
)}
<div className="flex space-x-4">
{connectors
.filter((x) => x.ready && x.id !== connector?.id)
.map((x) => (
<button
className="whitebtn hover:border-teal-400"
key={x.id}
onClick={() => connect({ connector: x })}
>
{x.name}
{isLoading && x.id === pendingConnector?.id && " (connecting)" }
</button>
))}
</div>
</div>
{error && <div>{error.message}</div>}
</div>
);
}

Zobrazit soubor

@ -0,0 +1,33 @@
import { useNetwork, useSwitchNetwork } from 'wagmi'
export function NetworkSwitcher() {
const { chain } = useNetwork()
const { chains, error, isLoading, pendingChainId, switchNetwork } =
useSwitchNetwork()
if (!chain) return null
return (
<div>
<div>
Connected to {chain?.name ?? chain?.id}
{chain?.unsupported && ' (unsupported)'}
</div>
{switchNetwork && (
<div>
{chains.map((x) =>
x.id === chain?.id ? null : (
<button key={x.id} onClick={() => switchNetwork(x.id)}>
{x.name}
{isLoading && x.id === pendingChainId && ' (switching)'}
</button>
),
)}
</div>
)}
<div>{error && error.message}</div>
</div>
)
}

3
src/components/index.ts Normal file
Zobrazit soubor

@ -0,0 +1,3 @@
export { Account } from './Account'
export { Connect } from './Connect'
export { NetworkSwitcher } from './NetworkSwitcher'

Zobrazit soubor

@ -0,0 +1,45 @@
import { useState, useEffect } from 'react';
export type SoulApiResponse = {
status: Number;
statusText: String;
data: any;
error: any;
loading: Boolean;
isEligible: Boolean;
};
const baseUrl = "https://faucet-api.ethbrno.cz/lookup?addr="
export const useApiGet = (addr: string): SoulApiResponse => {
const [status, setStatus] = useState<Number>(0);
const [statusText, setStatusText] = useState<String>('');
const [data, setData] = useState<any>();
const [error, setError] = useState<any>();
const [loading, setLoading] = useState<boolean>(false);
const [isEligible, setIsEligible] = useState<boolean>(false);
const getAPIData = async () => {
setLoading(true);
try {
const apiResponse = await fetch(baseUrl + addr);
const json = await apiResponse.json();
setStatus(apiResponse.status);
setStatusText(apiResponse.statusText);
setData(json);
} catch (error) {
setError(error);
}
//console.log(data.tokenId)
if (data?.tokenId) setIsEligible(true);
setLoading(false);
};
useEffect(() => {
getAPIData();
}, []);
return { status, statusText, data, error, loading, isEligible };
};

24
src/index.css Normal file
Zobrazit soubor

@ -0,0 +1,24 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
body {
@apply text-gray-900 bg-white;
}
.title-font {
font-family: 'Ubuntu Mono', monospace;
}
.greenbtn {
@apply px-6 h-12 uppercase font-semibold tracking-wider border-2 border-black bg-teal-400 text-black hover:border-teal-400 disabled:opacity-10;
}
.whitebtn {
@apply px-6 h-12 uppercase font-semibold tracking-wider border border-slate-200 text-slate-900 hover:border-teal-400 disabled:opacity-10;
}
.redbtn {
@apply px-6 h-12 uppercase font-semibold tracking-wider border-2 border-black bg-red-400 text-black hover:border-red-400 disabled:opacity-10;
}
}

56
src/main.tsx Normal file
Zobrazit soubor

@ -0,0 +1,56 @@
import * as React from "react";
import * as ReactDOM from "react-dom/client";
import "./index.css";
import {
configureChains,
createClient,
defaultChains,
WagmiConfig,
} from "wagmi";
import { CoinbaseWalletConnector } from "wagmi/connectors/coinbaseWallet";
import { InjectedConnector } from "wagmi/connectors/injected";
import { MetaMaskConnector } from "wagmi/connectors/metaMask";
import { WalletConnectConnector } from "wagmi/connectors/walletConnect";
import { alchemyProvider } from "wagmi/providers/alchemy";
import { App } from "./App";
const { chains, provider, webSocketProvider } = configureChains(defaultChains, [
alchemyProvider({ apiKey: import.meta.env.VITE_ALCHEMY_API_KEY as string }),
]);
const client = createClient({
autoConnect: true,
connectors: [
new InjectedConnector({
chains,
options: {
name: "Browser",
shimDisconnect: true,
},
}),
new CoinbaseWalletConnector({
chains,
options: {
appName: "wagmi",
},
}),
new WalletConnectConnector({
chains,
options: {
qrcode: true,
},
}),
],
provider,
webSocketProvider,
});
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<WagmiConfig client={client}>
<App />
</WagmiConfig>
</React.StrictMode>,
);

1
src/vite-env.d.ts vendorováno Normal file
Zobrazit soubor

@ -0,0 +1 @@
/// <reference types="vite/client" />

11
tailwind.config.js Normal file
Zobrazit soubor

@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
};

20
tsconfig.json Normal file
Zobrazit soubor

@ -0,0 +1,20 @@
{
"compilerOptions": {
"allowJs": false,
"allowSyntheticDefaultImports": true,
"esModuleInterop": false,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"jsx": "react-jsx",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"module": "ESNext",
"moduleResolution": "Node",
"noEmit": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "ESNext",
"useDefineForClassFields": true
},
"include": ["./src"]
}

16
vite.config.ts Normal file
Zobrazit soubor

@ -0,0 +1,16 @@
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
// https://vitejs.dev/config/
export default defineConfig({
define: {
global: 'globalThis',
},
resolve: {
alias: {
process: 'process/browser',
util: 'util',
},
},
plugins: [react()],
})