This commit is contained in:
revize
96226dd4f7
|
@ -0,0 +1 @@
|
|||
VITE_ALCHEMY_API_KEY=
|
|
@ -0,0 +1,5 @@
|
|||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
|
@ -0,0 +1,3 @@
|
|||
# Vite React Example
|
||||
|
||||
This examples uses [Vite](https://vitejs.dev) and React.
|
|
@ -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>
|
Rozdílový obsah nebyl zobrazen, protože je příliš veliký
Načíst rozdílové porovnání
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
Binární soubor nebyl zobrazen.
Za Šířka: | Výška: | Velikost: 48 KiB |
|
@ -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 |
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export { Account } from './Account'
|
||||
export { Connect } from './Connect'
|
||||
export { NetworkSwitcher } from './NetworkSwitcher'
|
|
@ -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 };
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>,
|
||||
);
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
|
@ -0,0 +1,11 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
|
@ -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"]
|
||||
}
|
|
@ -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()],
|
||||
})
|
Načítá se…
Odkázat v novém úkolu