├── .gitignore ├── README.md ├── billing └── index.html ├── components └── Layout.tsx ├── next-env.d.ts ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── api │ ├── auth │ │ └── [...nextauth].ts │ └── claim │ │ ├── new.ts │ │ └── status.ts └── index.tsx ├── public ├── favicon.ico ├── logo.png └── meta.png ├── styles ├── Home.module.scss ├── Layout.module.scss └── global.scss ├── tsconfig.json └── utils ├── addresses.ts └── dates.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .next/ 3 | *.log 4 | .env* 5 | cache/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terobox/ChatGPT-API-Faucet/1a2c4f6051dca65c3d65cc11193a014038950f36/README.md -------------------------------------------------------------------------------- /billing/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | OpenKey 令牌余额查询 7 | 8 | 9 | 24 | 149 | 150 | 151 |
152 |
153 |
154 |

OpenKey 令牌余额查询

155 |
156 | 157 | 159 | 162 | 163 |
164 |
165 |
166 |

OpenKey.Cloud - 提供企业级专业稳定的 ChatGPT 接口集成分发服务.

167 |
168 |
169 |
170 |
171 | 299 | 300 | 301 | -------------------------------------------------------------------------------- /components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import styles from "styles/Layout.module.scss"; // Styles 2 | import { default as HTMLHead } from "next/head"; // Meta 3 | 4 | // Page layout 5 | export default function Layout({ 6 | children, 7 | }: { 8 | children: (JSX.Element | null)[]; 9 | }) { 10 | return ( 11 |
12 | {/* Meta + Head */} 13 | 14 | 15 | {/* Layout sizer */} 16 |
{children}
17 | 18 | {/* Footer */} 19 |
21 | ); 22 | } 23 | 24 | // Head + Meta 25 | function Head() { 26 | return ( 27 | 28 | {/* Google Fonts */} 29 | 30 | 35 | 39 | 40 | {/* Favicon */} 41 | 42 | 43 | {/* Primary Meta Tags */} 44 | 水龙头 45 | 49 | 53 | 54 | {/* OG + Facebook */} 55 | 56 | 57 | 61 | 65 | 69 | 70 | {/* Twitter */} 71 | 72 | 73 | 77 | 81 | 85 | 86 | 90 | 91 | 92 | ); 93 | } 94 | 95 | // Footer 96 | function Footer() { 97 | return ( 98 |
99 | {/* Disclaimer */} 100 |

101 | 本平台提供的免费 ChatGPT API 令牌, 有 OpenKey 账号池服务提供支持. 我们不会收集用户信息, 并对使用此服务造成的任何损失不承担责任. 用户应该理解与此服务相关的风险, 并独立评估其价值. 我们保留随时修改或终止此服务的权利, 恕不另行通知. 请谨慎使用. 102 |

103 |
104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multifaucet", 3 | "author": "Anish Agnihotri", 4 | "version": "0.1.0", 5 | "private": true, 6 | "license": "MIT", 7 | "scripts": { 8 | "dev": "next dev", 9 | "build": "next build", 10 | "start": "next start", 11 | "lint": "next lint" 12 | }, 13 | "dependencies": { 14 | "@slack/web-api": "^6.9.0", 15 | "axios": "^0.24.0", 16 | "ethers": "^5.7.2", 17 | "ioredis": "^4.28.5", 18 | "next": "^12.3.4", 19 | "next-auth": "^3.29.10", 20 | "react": "17.0.2", 21 | "react-dom": "17.0.2", 22 | "react-toastify": "^8.2.0", 23 | "sass": "^1.67.0", 24 | "swr": "^2.2.2" 25 | }, 26 | "devDependencies": { 27 | "@types/ioredis": "^4.28.10", 28 | "@types/react": "17.0.26", 29 | "eslint": "7.32.0", 30 | "eslint-config-next": "11.1.2", 31 | "typescript": "4.4.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "styles/global.scss"; // Global styles 2 | import type { AppProps } from "next/app"; // Types 3 | import "react-toastify/dist/ReactToastify.css"; // Toast styles 4 | import { ToastContainer } from "react-toastify"; // Toast notifications 5 | 6 | export default function MultiFaucet({ Component, pageProps }: AppProps) { 7 | return ( 8 | <> 9 | {/* Toast container */} 10 | 11 | 12 | {/* Site */} 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; // Next auth 2 | import Providers from "next-auth/providers"; // Twitter provider 3 | 4 | export default NextAuth({ 5 | providers: [ 6 | // Twitter OAuth provider 7 | Providers.Twitter({ 8 | clientId: process.env.TWITTER_CLIENT_ID, 9 | clientSecret: process.env.TWITTER_CLIENT_SECRET, 10 | }), 11 | ], 12 | // Custom page: 13 | pages: { 14 | // On error, throw to home 15 | error: "/", 16 | }, 17 | // Use JWT 18 | session: { 19 | jwt: true, 20 | // 30 day expiry 21 | maxAge: 30 * 24 * 60 * 60, 22 | // Refresh JWT on each login 23 | updateAge: 0, 24 | }, 25 | jwt: { 26 | // JWT secret 27 | secret: process.env.NEXTAUTH_JWT_SECRET, 28 | }, 29 | callbacks: { 30 | // On signin + signout 31 | jwt: async (token, user, account, profile) => { 32 | // Check if user is signing in (versus logging out) 33 | const isSignIn = user ? true : false; 34 | 35 | // If signing in 36 | if (isSignIn) { 37 | // Attach additional parameters (twitter id + handle + anti-bot measures) 38 | token.twitter_id = account?.id; 39 | token.twitter_handle = profile?.screen_name; 40 | token.twitter_num_tweets = profile?.statuses_count; 41 | token.twitter_num_followers = profile?.followers_count; 42 | token.twitter_created_at = profile?.created_at; 43 | } 44 | 45 | // Resolve JWT 46 | return Promise.resolve(token); 47 | }, 48 | // On session retrieval 49 | session: async (session, user) => { 50 | // Attach additional params from JWT to session 51 | session.twitter_id = user.twitter_id; 52 | session.twitter_handle = user.twitter_handle; 53 | session.twitter_num_tweets = user.twitter_num_tweets; 54 | session.twitter_num_followers = user.twitter_num_followers; 55 | session.twitter_created_at = user.twitter_created_at; 56 | 57 | // Resolve session 58 | return Promise.resolve(session); 59 | }, 60 | }, 61 | }); 62 | -------------------------------------------------------------------------------- /pages/api/claim/new.ts: -------------------------------------------------------------------------------- 1 | import Redis from "ioredis"; // Redis 2 | import { ethers } from "ethers"; // Ethers 3 | import { WebClient } from "@slack/web-api"; // Slack 4 | import { isValidInput } from "pages/index"; // Address check 5 | import parseTwitterDate from "utils/dates"; // Parse Twitter dates 6 | import { getSession } from "next-auth/client"; // Session management 7 | import { hasClaimed } from "pages/api/claim/status"; // Claim status 8 | import type { NextApiRequest, NextApiResponse } from "next"; // Types 9 | 10 | // Setup whitelist (Anish) 11 | const whitelist: string[] = ["1078014622525988864"]; 12 | 13 | // Setup redis client 14 | const client = new Redis(process.env.REDIS_URL); 15 | 16 | // Setup slack client 17 | const slack = new WebClient(process.env.SLACK_ACCESS_TOKEN); 18 | const slackChannel: string = process.env.SLACK_CHANNEL ?? ""; 19 | /** 20 | * Post message to slack channel 21 | * @param {string} message to post 22 | */ 23 | async function postSlackMessage(message: string): Promise { 24 | await slack.chat.postMessage({ 25 | channel: slackChannel, 26 | text: message, 27 | // Ping user on error 28 | link_names: true, 29 | }); 30 | } 31 | 32 | /** 33 | * Generate Alchemy RPC endpoint url from partials 34 | * @param {string} partial of network 35 | * @returns {string} full rpc url 36 | */ 37 | function generateAlchemy(partial: string): string { 38 | // Combine partial + API key 39 | return `https://${partial}/v2/${process.env.ALCHEMY_API_KEY}`; 40 | } 41 | 42 | // Setup networks 43 | const ARBITRUM: number = 421611; 44 | const mainRpcNetworks: Record = { 45 | //3: generateAlchemy("eth-ropsten.alchemyapi.io"), 46 | 4: generateAlchemy("eth-rinkeby.alchemyapi.io"), 47 | 5: generateAlchemy("eth-goerli.alchemyapi.io"), 48 | 42: generateAlchemy("eth-kovan.alchemyapi.io"), 49 | }; 50 | const secondaryRpcNetworks: Record = { 51 | 69: generateAlchemy("opt-kovan.g.alchemy.com"), 52 | //1287: "https://rpc.api.moonbase.moonbeam.network", 53 | 80001: generateAlchemy("polygon-mumbai.g.alchemy.com"), 54 | 421611: generateAlchemy("arb-rinkeby.g.alchemy.com"), 55 | //43113: "https://api.avax-test.network/ext/bc/C/rpc", 56 | }; 57 | 58 | // Setup faucet interface 59 | const iface = new ethers.utils.Interface([ 60 | "function drip(address _recipient) external", 61 | ]); 62 | 63 | /** 64 | * Generates tx input data for drip claim 65 | * @param {string} recipient address 66 | * @returns {string} encoded input data 67 | */ 68 | function generateTxData(recipient: string): string { 69 | // Encode address for drip function 70 | return iface.encodeFunctionData("drip", [recipient]); 71 | } 72 | 73 | /** 74 | * Collects StaticJsonRpcProvider by network 75 | * @param {number} network id 76 | * @returns {ethers.providers.StaticJsonRpcProvider} provider 77 | */ 78 | function getProviderByNetwork( 79 | network: number 80 | ): ethers.providers.StaticJsonRpcProvider { 81 | // Collect all RPC URLs 82 | const rpcNetworks = { ...mainRpcNetworks, ...secondaryRpcNetworks }; 83 | // Collect alchemy RPC URL 84 | const rpcUrl = rpcNetworks[network]; 85 | // Return static provider 86 | return new ethers.providers.StaticJsonRpcProvider(rpcUrl); 87 | } 88 | 89 | /** 90 | * Collects nonce by network (cache first) 91 | * @param {number} network id 92 | * @returns {Promise} network account nonce 93 | */ 94 | async function getNonceByNetwork(network: number): Promise { 95 | // Collect nonce from redis 96 | const redisNonce: string | null = await client.get(`nonce-${network}`); 97 | 98 | // If no redis nonce 99 | if (redisNonce == null) { 100 | // Update to last network nonce 101 | const provider = getProviderByNetwork(network); 102 | return await provider.getTransactionCount( 103 | // Collect nonce for operator 104 | process.env.NEXT_PUBLIC_OPERATOR_ADDRESS ?? "" 105 | ); 106 | } else { 107 | // Else, return cached nonce 108 | return Number(redisNonce); 109 | } 110 | } 111 | 112 | /** 113 | * Returns populated drip transaction for a network 114 | * @param {ethers.Wallet} wallet without RPC network connected 115 | * @param {number} network id 116 | * @param {string} data input for tx 117 | */ 118 | async function processDrip( 119 | wallet: ethers.Wallet, 120 | network: number, 121 | data: string 122 | ): Promise { 123 | // Collect provider 124 | const provider = getProviderByNetwork(network); 125 | 126 | // Connect wallet to network 127 | const rpcWallet = wallet.connect(provider); 128 | // Collect nonce for network 129 | const nonce = await getNonceByNetwork(network); 130 | // Collect gas price * 2 for network 131 | const gasPrice = (await provider.getGasPrice()).mul(2); 132 | 133 | // Update nonce for network in redis w/ 5m ttl 134 | await client.set(`nonce-${network}`, nonce + 1, "EX", 300); 135 | 136 | // Return populated transaction 137 | try { 138 | await rpcWallet.sendTransaction({ 139 | to: process.env.FAUCET_ADDRESS ?? "", 140 | from: wallet.address, 141 | gasPrice, 142 | // Custom gas override for Arbitrum w/ min gas limit 143 | gasLimit: network === ARBITRUM ? 5_000_000 : 500_000, 144 | data, 145 | nonce, 146 | type: 0, 147 | }); 148 | } catch (e) { 149 | await postSlackMessage( 150 | `@anish Error dripping for ${provider.network.chainId}, ${String( 151 | (e as any).reason 152 | )}` 153 | ); 154 | 155 | // Delete nonce key to attempt at self-heal 156 | const delStatus: number = await client.del( 157 | `nonce-${provider.network.chainId}` 158 | ); 159 | await postSlackMessage(`Attempting self heal: ${delStatus}`); 160 | 161 | // Throw error 162 | throw new Error(`Error when processing drip for network ${network}`); 163 | } 164 | } 165 | 166 | export default async (req: NextApiRequest, res: NextApiResponse) => { 167 | // Collect session (force any for extra twitter params) 168 | const session: any = await getSession({ req }); 169 | // Collect address 170 | const { address, others }: { address: string; others: boolean } = req.body; 171 | 172 | if (!session) { 173 | // Return unauthed status 174 | return res.status(401).send({ error: "Not authenticated." }); 175 | } 176 | 177 | // Basic anti-bot measures 178 | const ONE_MONTH_SECONDS = 2629746; 179 | if ( 180 | // Less than 1 tweet 181 | session.twitter_num_tweets == 0 || 182 | // Less than 15 followers 183 | session.twitter_num_followers < 15 || 184 | // Less than 1 month old 185 | new Date().getTime() - 186 | parseTwitterDate(session.twitter_created_at).getTime() < 187 | ONE_MONTH_SECONDS 188 | ) { 189 | // Return invalid Twitter account status 190 | return res 191 | .status(400) 192 | .send({ error: "Twitter account does not pass anti-bot checks." }); 193 | } 194 | 195 | if (!address || !isValidInput(address)) { 196 | // Return invalid address status 197 | return res.status(400).send({ error: "Invalid address." }); 198 | } 199 | 200 | // Collect address 201 | let addr: string = address; 202 | // If address is ENS name 203 | if (~address.toLowerCase().indexOf(".eth")) { 204 | // Setup custom mainnet provider 205 | const provider = new ethers.providers.StaticJsonRpcProvider( 206 | `https://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_API_KEY}` 207 | ); 208 | 209 | // Collect 0x address from ENS 210 | const resolvedAddress = await provider.resolveName(address); 211 | 212 | // If no resolver set 213 | if (!resolvedAddress) { 214 | // Return invalid ENS status 215 | return res 216 | .status(400) 217 | .send({ error: "Invalid ENS name. No reverse record." }); 218 | } 219 | 220 | // Else, set address 221 | addr = resolvedAddress; 222 | } 223 | 224 | const claimed: boolean = await hasClaimed(session.twitter_id); 225 | if (claimed) { 226 | // Return already claimed status 227 | return res.status(400).send({ error: "Already claimed in 24h window" }); 228 | } 229 | 230 | // Setup wallet w/o RPC provider 231 | const wallet = new ethers.Wallet(process.env.OPERATOR_PRIVATE_KEY ?? ""); 232 | 233 | // Generate transaction data 234 | const data: string = generateTxData(addr); 235 | 236 | // Networks to claim on (based on others toggle) 237 | const otherNetworks: Record = others 238 | ? secondaryRpcNetworks 239 | : {}; 240 | const claimNetworks: Record = { 241 | ...mainRpcNetworks, 242 | ...otherNetworks, 243 | }; 244 | 245 | // For each main network 246 | for (const networkId of Object.keys(claimNetworks)) { 247 | try { 248 | // Process faucet claims 249 | await processDrip(wallet, Number(networkId), data); 250 | } catch (e) { 251 | // If not whitelisted, force user to wait 15 minutes 252 | if (!whitelist.includes(session.twitter_id)) { 253 | // Update 24h claim status 254 | await client.set(session.twitter_id, "true", "EX", 900); 255 | } 256 | 257 | // If error in process, revert 258 | return res 259 | .status(500) 260 | .send({ error: "Error fully claiming, try again in 15 minutes." }); 261 | } 262 | } 263 | 264 | // If not whitelisted 265 | if (!whitelist.includes(session.twitter_id)) { 266 | // Update 24h claim status 267 | await client.set(session.twitter_id, "true", "EX", 86400); 268 | } 269 | 270 | return res.status(200).send({ claimed: address }); 271 | }; 272 | -------------------------------------------------------------------------------- /pages/api/claim/status.ts: -------------------------------------------------------------------------------- 1 | import Redis from "ioredis"; // Redis 2 | import { getSession } from "next-auth/client"; // Session management 3 | import type { NextApiRequest, NextApiResponse } from "next"; // Types 4 | 5 | // Setup redis client 6 | const client = new Redis(process.env.REDIS_URL); 7 | 8 | /** 9 | * Checks if a twitter id has claimed from faucet in last 24h 10 | * @param {string} twitter_id to check 11 | * @returns {Promise} claim status 12 | */ 13 | export async function hasClaimed(twitter_id: string): Promise { 14 | // Check if key exists 15 | const resp: string | null = await client.get(twitter_id); 16 | // If exists, return true, else return false 17 | return resp ? true : false; 18 | } 19 | 20 | export default async (req: NextApiRequest, res: NextApiResponse) => { 21 | // Collect session (force any for extra twitter params) 22 | const session: any = await getSession({ req }); 23 | 24 | if (session) { 25 | try { 26 | // Collect claim status 27 | const claimed: boolean = await hasClaimed(session.twitter_id); 28 | res.status(200).send({ claimed }); 29 | } catch { 30 | // If failure, return error checking status 31 | res.status(500).send({ error: "Error checking claim status." }); 32 | } 33 | } else { 34 | // Return unauthed status 35 | res.status(401).send({ error: "Not authenticated." }); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import axios from "axios"; // Requests 2 | import Image from "next/image"; // Image 3 | import { toast } from "react-toastify"; // Toast notifications 4 | import Layout from "components/Layout"; // Layout wrapper 5 | import { useRouter } from "next/router"; // Router 6 | import styles from "styles/Home.module.scss"; // Styles 7 | import { ReactElement, useState } from "react"; // Local state + types 8 | import { getAddressDetails } from "utils/addresses"; // Faucet addresses 9 | import { hasClaimed } from "pages/api/claim/status"; // Claim status 10 | import { signIn, getSession, signOut } from "next-auth/client"; // Auth 11 | import useSWR from 'swr' 12 | 13 | /** 14 | * Checks if a provider address or ENS name is valid 15 | * @param {string} address to check 16 | * @returns {boolean} validity 17 | */ 18 | export function isValidInput(address: string): boolean { 19 | // Check if valid email address 20 | const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; 21 | return emailPattern.test(address); 22 | } 23 | 24 | // 25 | const fetcher = (url: string) => axios.get(url).then(res => res.data) 26 | 27 | interface UserType { 28 | email: string; 29 | token: string; 30 | success_time: number; 31 | } 32 | 33 | export default function Home({ 34 | session, 35 | claimed: initialClaimed, 36 | }: { 37 | session: any; 38 | claimed: boolean; 39 | }) { 40 | // Collect prefilled address 41 | const { 42 | query: { addr }, 43 | } = useRouter(); 44 | // Fill prefilled address 45 | 46 | const prefilledAddress: string = addr && typeof addr === "string" ? addr : ""; 47 | const notify = (title:string)=>toast(title) 48 | // Claim address 49 | const [address, setAddress] = useState(prefilledAddress); 50 | // Claimed status 51 | const [claimed, setClaimed] = useState(initialClaimed); 52 | // First claim 53 | const [firstClaim, setFirstClaim] = useState(false); 54 | // Loading status 55 | const [loading, setLoading] = useState(false); 56 | // Claim other 57 | const [claimOther, setClaimOther] = useState(false); 58 | 59 | // Collect details about addresses 60 | const { networkCount, sortedAddresses } = getAddressDetails(); 61 | 62 | // 增加新的状态 63 | const [verificationCode, setVerificationCode] = useState(""); 64 | const [isVerificationSent, setVerificationSent] = useState(false); 65 | const [statusMessage, setStatusMessage] = useState(""); 66 | const [token, setToken] = useState(null); 67 | 68 | // 普通刷新 69 | // const { data: userList, error } = useSWR('/api/items', fetcher) 70 | // 轮询方案 71 | // 在useSWR中添加refreshInterval选项来实现轮询 72 | const { data: userList, error } = useSWR('/api/items', fetcher, { 73 | refreshInterval: 1000, // 这里的值是轮询的间隔时间,单位是毫秒,5000毫秒即5秒 74 | }); 75 | 76 | // 定义 handleCopy 函数,复制存储在状态中的 token 77 | const handleCopy = () => { 78 | if (token) { 79 | navigator.clipboard.writeText(token).then(() => { 80 | // 这里可以添加一些复制成功后的处理,例如显示一个通知 81 | notify(`成功复制: ${token}`); 82 | }); 83 | } 84 | }; 85 | 86 | /** 87 | * Processes a claim to the faucet 88 | */ 89 | const processClaim = async () => { 90 | setLoading(true); 91 | 92 | if (!isVerificationSent) { 93 | // 发送邮件验证码 94 | try { 95 | const response = await axios.post("/api/send_verification_code", { email: address }, { withCredentials: true }); 96 | console.log("Server Response:", response.data); 97 | if (response.data.status === 1) { 98 | notify(response.data.message) 99 | setVerificationSent(true); 100 | } else { 101 | notify(response.data.message) 102 | } 103 | } catch (error) { 104 | console.error(error); 105 | notify("未知错误, 请联系管理员处理") 106 | } 107 | } else { 108 | // 验证用户输入的验证码 109 | try { 110 | const response = await axios.post("/api/verify_code", { 111 | email: address, 112 | code: verificationCode 113 | }); 114 | console.log("Server Response:", response.data); 115 | if (response.data.status === 1) { 116 | notify(response.data.message) 117 | setToken(response.data.token); // 假设 token 存储在 response.data.token 中 118 | setClaimed(true); // 标记为已领取 119 | } else { 120 | notify(response.data.message) 121 | } 122 | } catch (error) { 123 | console.error(error); 124 | notify("未知错误, 请联系管理员处理") 125 | } 126 | } 127 | 128 | setLoading(false); 129 | }; 130 | 131 | // 检查复选框 132 | const [isChecked, setIsChecked] = useState(true); // 默认值设置为 true 133 | 134 | return ( 135 | 136 | {/* 1. description */} 137 |
138 |
139 | 144 | 145 | 146 |
147 |

ChatGPT API 水龙头

148 | 149 | 每24小时可领取一个 $1.00 令牌用于开发测试 AI 产品. 150 | 151 | {/* 添加GitHub链接 */} 152 | 163 | GitHub链接 164 | 165 |
166 | 167 | {/* 2. Claim from facuet card */} 168 |
169 | {/* Card title */} 170 |
171 |

申请令牌

172 |
173 | 174 | {/* Card content */} 175 |
176 | {!session ? ( 177 |
178 | {claimed ? ( 179 |
180 |

181 | {firstClaim 182 | ? "You have successfully claimed tokens. You can request again in 24 hours." 183 | : "恭喜! 您已成功领取一个令牌, 可在24小时后再次申请. "} 184 |

185 | {token && ( 186 |
{/* 添加 marginTop 和 marginBottom 样式 */} 187 |
188 |

获得令牌:

189 |
190 |

199 | {token}

200 | 201 |
202 |

接口地址:

203 |
204 |

213 | https://openkey.cloud

214 | 215 |
216 |

217 | 可用余额: $1.00 218 | 228 | 余额查询 229 | 230 |

231 |
232 | 233 | 234 |
235 | )} 236 | 245 |
246 | ) : ( 247 |
248 |

{isVerificationSent ? "请输入收到的邮箱验证码:" : "请输入您的电子邮件地址以获取ChatGPT API令牌:"}

249 | {/*

请输入您的电子邮件地址以获取ChatGPT API令牌:

*/} 250 | 251 | {/* 根据 isVerificationSent 状态切换输入框 */} 252 | isVerificationSent ? setVerificationCode(e.target.value) : setAddress(e.target.value)} 257 | /> 258 | 259 |
260 | setIsChecked((previous) => !previous)} 264 | /> 265 | 268 |
269 | 270 | {statusMessage &&

{statusMessage}

} 271 | 272 | {isValidInput(address) ? ( 273 | isChecked ? ( 274 | 281 | ) : ( 282 | 285 | ) 286 | ) : ( 287 | 290 | )} 291 |
292 | )} 293 |
294 | ) : ( 295 |
296 | {/* Reasoning for Twitter OAuth */} 297 | {/* Sign in with Twitter */} 298 |
299 | )} 300 |
301 |
302 | 303 | {/* 3. Faucet details card */} 304 |
305 |
306 | {error &&
} 307 | {userList ? ( 308 |
309 | {userList.map((user: UserType, index: number) => ( 310 |
320 | {new Date(user.success_time).toLocaleString()} 321 | {user.email} 322 | {user.token} 323 |
324 | ))} 325 |
326 | ) : ( 327 |
正在加载记录...
328 | )} 329 |
330 |
331 | 332 | {/* 4. Tips card */} 333 |
334 | {/* Card title */} 335 |
336 |

关于水龙头

337 |
338 |
339 | {/*

平台说明

*/} 340 |

341 | - 您将收到一个API令牌和接口地址, 将 OpenAI 官方接口替换为此处提供的地址, 即可开始使用. 342 |

343 |

344 | - 默认情况下, 令牌有 $1.00 的使用限制, 有效期为1个月. 这是完全免费的, 请勿滥用. 345 |

346 |

347 | - 您可以每24小时从水龙头领取一次. 348 |

349 |

350 | - 余额查询地址. 351 |

352 |

361 | https://billing.openkey.cloud/

362 |

363 | - 如果您愿意支持这个项目, 我们将不胜感激. 364 |

365 |

374 | USDT TRC-20: TTtgEjbTWcv5hryt4pKTQK9Zov47ffA8s1

375 |

376 | - 欢迎评论交流, 今天你打卡了吗? 377 |

378 |
379 |
380 | 381 | {/* 5. 评论区 */} 382 |
383 | {/* Card title */} 384 |
385 |

讨论区

386 |
387 | 388 | {/* General information */} 389 |
390 | {/* Waline 评论插件 */} 391 |
392 |
396 | import { init } from 'https://unpkg.com/@waline/client@v2/dist/waline.mjs'; 397 | init({ 398 | el: '#waline', 399 | serverURL: 'https://waline.openkey.cloud/', 400 | }); 401 | 402 | `, 403 | }} 404 | >
405 |
406 |
407 |
408 | ); 409 | } 410 | 411 | export async function getServerSideProps(context: any) { 412 | // Collect session 413 | const session: any = await getSession(context); 414 | 415 | return { 416 | props: { 417 | session, 418 | // If session exists, collect claim status, else return false 419 | claimed: session ? await hasClaimed(session.twitter_id) : false, 420 | }, 421 | }; 422 | } 423 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terobox/ChatGPT-API-Faucet/1a2c4f6051dca65c3d65cc11193a014038950f36/public/favicon.ico -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terobox/ChatGPT-API-Faucet/1a2c4f6051dca65c3d65cc11193a014038950f36/public/logo.png -------------------------------------------------------------------------------- /public/meta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terobox/ChatGPT-API-Faucet/1a2c4f6051dca65c3d65cc11193a014038950f36/public/meta.png -------------------------------------------------------------------------------- /styles/Home.module.scss: -------------------------------------------------------------------------------- 1 | // Top CTA 2 | .home__cta { 3 | text-align: center; 4 | 5 | > div { 6 | margin-top: 30px; 7 | margin-bottom: 10px; 8 | 9 | > a:hover { 10 | opacity: 0.8; 11 | } 12 | } 13 | 14 | > h1 { 15 | font-size: 2.5rem; 16 | color: #19232d; 17 | margin: 10px 0px; 18 | } 19 | 20 | > span { 21 | display: block; 22 | font-size: 18px; 23 | line-height: 28px; 24 | max-width: 500px; 25 | margin: 0px auto; 26 | color: #19232d; 27 | } 28 | } 29 | 30 | // Add network button 31 | .addNetworkButton { 32 | padding: 0px; 33 | background-color: transparent; 34 | border: none; 35 | margin-left: 2px; 36 | font-weight: 500; 37 | font-size: 16px; 38 | text-decoration: underline; 39 | cursor: pointer; 40 | 41 | &:hover { 42 | opacity: 0.8; 43 | } 44 | } 45 | 46 | // Individual address containers 47 | .address { 48 | > a { 49 | padding: 0.2em 0.4em; 50 | font-size: 85%; 51 | text-decoration: none; 52 | border-radius: 6px; 53 | color: inherit; 54 | background-color: rgba(175, 184, 193, 0.2); 55 | 56 | &:hover { 57 | opacity: 0.8; 58 | } 59 | } 60 | 61 | > button { 62 | padding: 0.2em 0.4em; 63 | margin-left: 10px; 64 | background-color: rgba(175, 184, 193, 0.2); 65 | border: none; 66 | border-radius: 6px; 67 | color: inherit; 68 | cursor: pointer; 69 | 70 | &:hover { 71 | opacity: 0.8; 72 | } 73 | } 74 | } 75 | 76 | // Individual token containers 77 | .token { 78 | background-color: white; 79 | padding: 0px 5px; 80 | border: 1px solid rgb(236, 237, 239); 81 | border-radius: 4px; 82 | display: inline-flex; 83 | vertical-align: middle; 84 | align-items: center; 85 | 86 | img { 87 | height: 18px; 88 | padding-right: 5px; 89 | } 90 | 91 | span { 92 | font-size: 16px; 93 | font-weight: 500; 94 | } 95 | } 96 | 97 | // Claim card 98 | .home__card { 99 | margin-top: 40px; 100 | background-color: white; 101 | border: 1px solid rgb(236, 237, 239); 102 | border-radius: 5px; 103 | box-shadow: 0px 6px 10px 1px #eeeeee; 104 | 105 | .home__card_title { 106 | border-bottom: 2px solid rgb(237, 238, 240); 107 | padding: 15px 20px; 108 | 109 | > h3 { 110 | margin: 0px; 111 | font-weight: 500; 112 | font-size: 18px; 113 | transform: translateY(1px); 114 | } 115 | } 116 | 117 | .home__card_content { 118 | padding: 20px; 119 | 120 | > p { 121 | line-height: 24px; 122 | } 123 | } 124 | } 125 | 126 | .home__card_content_section { 127 | padding: 20px; 128 | border-bottom: 1px solid rgb(237, 238, 240); 129 | 130 | > h4 { 131 | font-weight: 500; 132 | font-size: 16px; 133 | margin: 0px; 134 | 135 | a { 136 | text-decoration: none; 137 | color: inherit; 138 | text-decoration: underline; 139 | 140 | &:hover { 141 | opacity: 0.8; 142 | } 143 | } 144 | } 145 | 146 | .home__card_content_section_lh { 147 | line-height: 24px; 148 | } 149 | 150 | > span { 151 | display: block; 152 | color: #6e7a85; 153 | font-size: 14px; 154 | margin: 2px 0px; 155 | } 156 | 157 | > p { 158 | color: #19232d; 159 | margin: 10px 0px; 160 | 161 | &:nth-last-of-type(1) { 162 | margin-bottom: 0px; 163 | } 164 | } 165 | } 166 | 167 | // Depleted section 168 | .home__card_depleted { 169 | color: #e74747; 170 | } 171 | 172 | // Twitter sign-out button 173 | .content__twitter { 174 | margin-top: 20px; 175 | width: 100%; 176 | display: flex; 177 | justify-content: center; 178 | 179 | > button { 180 | background-color: transparent; 181 | border: none; 182 | cursor: pointer; 183 | text-decoration: underline; 184 | color: rgb(114, 114, 114); 185 | } 186 | } 187 | 188 | // Unauthenticated claim status 189 | .content__unauthenticated { 190 | p { 191 | margin: 0px; 192 | 193 | a { 194 | color: inherit; 195 | } 196 | } 197 | } 198 | 199 | // Authenticated claim status 200 | .content__authenticated { 201 | > button { 202 | display: inline-block; 203 | margin: 0px auto; 204 | } 205 | 206 | input { 207 | display: block; 208 | width: calc(100% - 14px); 209 | border: 2px solid rgb(234, 234, 234); 210 | font-size: 16px; 211 | padding: 10px 5px; 212 | border-radius: 5px; 213 | margin: 20px 0px 10px 0px; 214 | 215 | &:disabled { 216 | cursor: not-allowed; 217 | } 218 | } 219 | } 220 | 221 | // Authenticated claim status — already claimed 222 | .content__claimed { 223 | p { 224 | margin: 0px; 225 | } 226 | 227 | > button { 228 | margin-top: 0px !important; 229 | } 230 | } 231 | 232 | // Authenticated claim status — unclaimed 233 | .content__unclaimed { 234 | p { 235 | line-height: 24px; 236 | 237 | &:nth-of-type(1) { 238 | margin-top: 0px; 239 | } 240 | 241 | &:nth-last-of-type(1) { 242 | margin-bottom: 0px; 243 | } 244 | } 245 | 246 | > button { 247 | margin-top: 0px !important; 248 | } 249 | } 250 | 251 | // Authenticated claim status — unclaimed — other networks 252 | .content__unclaimed_others { 253 | display: flex; 254 | padding: 10px 0px 20px 0px; 255 | align-items: center; 256 | 257 | > input { 258 | width: auto; 259 | margin: 0px; 260 | transform: scale(1.3); 261 | margin-left: 5px; 262 | cursor: pointer; 263 | } 264 | 265 | > label { 266 | padding-left: 10px; 267 | display: inline-block; 268 | font-size: 14px; 269 | color: rgb(114, 114, 114); 270 | } 271 | } 272 | 273 | // Contract balance status 274 | .home__balances { 275 | text-align: center; 276 | 277 | > p { 278 | font-size: 14px; 279 | color: rgb(114, 114, 114); 280 | } 281 | } 282 | 283 | // Main blue button 284 | .button__main { 285 | width: 100%; 286 | margin-top: 20px; 287 | background-color: rgb(25, 35, 45); 288 | color: white; 289 | padding: 10px 0px; 290 | border-radius: 5px; 291 | border: none; 292 | font-size: 15px; 293 | cursor: pointer; 294 | transition: 100ms ease-in-out; 295 | 296 | &:hover { 297 | opacity: 0.9; 298 | } 299 | 300 | &:disabled { 301 | background-color: rgb(220, 230, 240); 302 | color: #19232d; 303 | cursor: not-allowed; 304 | } 305 | 306 | &:hover:disabled { 307 | opacity: 1; 308 | cursor: not-allowed; 309 | } 310 | } 311 | 312 | // Responsive 313 | @media screen and (max-width: 750px) { 314 | // Top CTA 315 | .home__cta { 316 | > h1 { 317 | font-size: 2.3rem; 318 | } 319 | 320 | > span { 321 | font-size: 17px; 322 | } 323 | } 324 | 325 | // Card 326 | .home__card { 327 | p { 328 | line-height: 24px; 329 | } 330 | } 331 | 332 | // Individual address containers 333 | .address { 334 | display: block; 335 | margin-top: 5px; 336 | padding-bottom: 10px; 337 | 338 | > a { 339 | display: block; 340 | width: calc(100% - 0.8em); 341 | white-space: nowrap; 342 | overflow: hidden; 343 | text-overflow: ellipsis; 344 | text-align: center; 345 | } 346 | 347 | > button { 348 | display: block; 349 | padding: 0.4em; 350 | margin-left: 0px; 351 | margin-top: 5px; 352 | width: 100%; 353 | } 354 | } 355 | } 356 | 357 | // Scrollable Stock 358 | 359 | .home__card { 360 | border: 1px solid #ccc; 361 | border-radius: 8px; 362 | margin: 10px; 363 | padding: 20px; 364 | box-shadow: 0px 4px 6px #ccc; 365 | } 366 | 367 | .home__card_content_section { 368 | margin-top: 20px; 369 | } 370 | 371 | .error { 372 | color: red; 373 | font-weight: bold; 374 | } 375 | 376 | .userList { 377 | list-style-type: none; 378 | padding: 0; 379 | } 380 | 381 | .userItem { 382 | border-bottom: 1px solid #eee; 383 | margin-bottom: 10px; 384 | padding-bottom: 10px; 385 | } 386 | 387 | .userData { 388 | display: flex; 389 | flex-direction: column; 390 | } 391 | 392 | .userEmail, 393 | .userToken, 394 | .userTime { 395 | color: #333; 396 | } 397 | 398 | .loading { 399 | color: #888; 400 | } 401 | -------------------------------------------------------------------------------- /styles/Layout.module.scss: -------------------------------------------------------------------------------- 1 | // Main container 2 | .layout { 3 | max-width: 800px; 4 | padding: 20px; 5 | margin: 0px auto; 6 | min-height: calc(100vh - 40px); 7 | 8 | display: flex; 9 | flex-direction: column; 10 | 11 | > div { 12 | width: 100%; 13 | } 14 | } 15 | 16 | // Footer 17 | .layout__footer { 18 | margin-top: auto; 19 | text-align: center; 20 | padding: 30px 0px; 21 | 22 | > p { 23 | font-size: 12px; 24 | line-height: 18px; 25 | color: rgb(93, 93, 93); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /styles/global.scss: -------------------------------------------------------------------------------- 1 | // Background colors 2 | $bg-color: rgb(246, 246, 247); 3 | $bg-dot-color: rgb(203, 203, 205); 4 | 5 | // Background dimensions 6 | $dot-size: 2px; 7 | $dot-space: 30px; 8 | 9 | html { 10 | // Scroll background 11 | background-color: $bg-color; 12 | } 13 | 14 | body { 15 | // Fix spacing 16 | margin: 0px; 17 | padding: 0px; 18 | 19 | // Font 20 | font-feature-settings: "ss01"; 21 | text-rendering: optimizeLegibility; 22 | font-family: "Inter", sans-serif; 23 | 24 | // Background 25 | min-height: 100vh; 26 | min-width: 100vw; 27 | background-color: rgb(246, 246, 247); 28 | background: linear-gradient( 29 | 90deg, 30 | $bg-color ($dot-space - $dot-size), 31 | transparent 1% 32 | ) 33 | center, 34 | linear-gradient($bg-color ($dot-space - $dot-size), transparent 1%) center, 35 | $bg-dot-color; 36 | background-size: $dot-space $dot-space; 37 | } 38 | 39 | button, 40 | input { 41 | // Font override 42 | font-feature-settings: "ss01"; 43 | text-rendering: optimizeLegibility; 44 | font-family: "Inter", sans-serif; 45 | } 46 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "baseUrl": "./", 17 | "incremental": true 18 | }, 19 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /utils/addresses.ts: -------------------------------------------------------------------------------- 1 | // Export faucet addresses 2 | export const ADDRESSES = [ 3 | { 4 | network: "ropsten", 5 | depleted: true, 6 | disclaimer: "Faucet drips 1 ETH, 1 wETH, and 5 NFTs (ERC721).", 7 | etherscanPrefix: "ropsten.etherscan.io", 8 | formattedName: "Ropsten", 9 | addresses: { 10 | NFTs: "0xf5de760f2e916647fd766b4ad9e85ff943ce3a2b", 11 | wETH: "0xc778417e063141139fce010982780140aa0cd5ab", 12 | }, 13 | }, 14 | { 15 | network: "kovan", 16 | disclaimer: "Faucet drips 1 ETH, 1 wETH, 500 DAI, and 5 NFTs (ERC721).", 17 | etherscanPrefix: "kovan.etherscan.io", 18 | formattedName: "Kovan", 19 | addresses: { 20 | NFTs: "0xf5de760f2e916647fd766b4ad9e85ff943ce3a2b", 21 | wETH: "0xd0a1e359811322d97991e03f863a0c30c2cf029c", 22 | DAI: "0x4f96fe3b7a6cf9725f59d353f723c1bdb64ca6aa", 23 | }, 24 | }, 25 | { 26 | network: "rinkeby", 27 | disclaimer: "Faucet drips 0.5 ETH, 0.5 wETH, 50 DAI, and 5 NFTs (ERC721).", 28 | etherscanPrefix: "rinkeby.etherscan.io", 29 | formattedName: "Rinkeby", 30 | addresses: { 31 | NFTs: "0xf5de760f2e916647fd766b4ad9e85ff943ce3a2b", 32 | wETH: "0xc778417E063141139Fce010982780140Aa0cD5Ab", 33 | DAI: "0x6A9865aDE2B6207dAAC49f8bCba9705dEB0B0e6D", 34 | }, 35 | }, 36 | { 37 | network: "goerli", 38 | disclaimer: "Faucet drips 1 ETH, 1 wETH, and 5 NFTs (ERC721).", 39 | etherscanPrefix: "goerli.etherscan.io", 40 | formattedName: "Görli", 41 | addresses: { 42 | NFTs: "0xf5de760f2e916647fd766b4ad9e85ff943ce3a2b", 43 | wETH: "0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6", 44 | }, 45 | }, 46 | { 47 | network: "kovan-optimistic", 48 | disclaimer: "Faucet drips 1 ETH, 1 wETH, 500 DAI, and 5 NFTs (ERC721).", 49 | etherscanPrefix: "kovan-optimistic.etherscan.io", 50 | formattedName: "Optimistic Kovan", 51 | connectionDetails: 52 | "https://community.optimism.io/docs/useful-tools/networks/#optimism-kovan-testnet", 53 | autoconnect: { 54 | chainId: "0x45", 55 | chainName: "Optimistic Kovan", 56 | nativeCurrency: { 57 | name: "Ethereum", 58 | symbol: "ETH", 59 | decimals: 18, 60 | }, 61 | rpcUrls: ["https://kovan.optimism.io"], 62 | blockExplorerUrls: ["https://kovan-optimistic.etherscan.io/"], 63 | }, 64 | addresses: { 65 | NFTs: "0xf5de760f2e916647fd766b4ad9e85ff943ce3a2b", 66 | wETH: "0xbc6f6b680bc61e30db47721c6d1c5cde19c1300d", 67 | DAI: "0xda10009cbd5d07dd0cecc66161fc93d7c9000da1", 68 | }, 69 | }, 70 | { 71 | network: "mumbai", 72 | disclaimer: 73 | "Faucet drips 0.1 MATIC, 0.1 wMATIC, 500 DAI, and 5 NFTs (ERC721).", 74 | etherscanPrefix: "mumbai.polygonscan.com", 75 | formattedName: "Polygon Mumbai", 76 | connectionDetails: 77 | "https://blog.pods.finance/guide-connecting-mumbai-testnet-to-your-metamask-87978071aca8", 78 | autoconnect: { 79 | chainId: "0x13881", 80 | chainName: "Polygon Mumbai", 81 | nativeCurrency: { 82 | name: "MATIC", 83 | symbol: "MATIC", 84 | decimals: 18, 85 | }, 86 | rpcUrls: ["https://rpc-mumbai.maticvigil.com/"], 87 | blockExplorerUrls: ["https://mumbai.polygonscan.com/"], 88 | }, 89 | addresses: { 90 | NFTs: "0xf5de760f2e916647fd766b4ad9e85ff943ce3a2b", 91 | wETH: "0x9c3c9283d3e44854697cd22d3faa240cfb032889", 92 | DAI: "0x001b3b4d0f3714ca98ba10f6042daebf0b1b7b6f", 93 | }, 94 | }, 95 | { 96 | network: "arb-rinkeby", 97 | disclaimer: "Faucet drips 0.5 ETH, 0.5 wETH, 50 DAI, and 5 NFTs (ERC721).", 98 | etherscanPrefix: "testnet.arbiscan.io", 99 | formattedName: "Arbitrum Rinkeby", 100 | connectionDetails: "https://developer.offchainlabs.com/docs/public_testnet", 101 | autoconnect: { 102 | chainId: "0x66eeb", 103 | chainName: "Arbitrum Testnet", 104 | nativeCurrency: { 105 | name: "Ethereum", 106 | symbol: "ETH", 107 | decimals: 18, 108 | }, 109 | rpcUrls: ["https://rinkeby.arbitrum.io/rpc"], 110 | blockExplorerUrls: ["https://testnet.arbiscan.io/"], 111 | }, 112 | addresses: { 113 | NFTs: "0xf5de760f2e916647fd766b4ad9e85ff943ce3a2b", 114 | wETH: "0xebbc3452cc911591e4f18f3b36727df45d6bd1f9", 115 | DAI: "0x2f3c1b6a51a469051a22986aa0ddf98466cc8d3c", 116 | }, 117 | }, 118 | { 119 | network: "avalanche-fuji", 120 | depleted: true, 121 | disclaimer: 122 | "Faucet drips 0.1 AVAX, 0.1 wAVAX, 500 DAI, and 5 NFTs (ERC721).", 123 | etherscanPrefix: "testnet.snowtrace.io", 124 | formattedName: "Avalanche Fuji", 125 | connectionDetails: 126 | "https://docs.avax.network/build/tutorials/smart-contracts/deploy-a-smart-contract-on-avalanche-using-remix-and-metamask#step-1-setting-up-metamask", 127 | autoconnect: { 128 | chainId: "0xa869", 129 | chainName: "Avalanche FUJI C-Chain", 130 | nativeCurrency: { 131 | name: "Avalanche", 132 | symbol: "AVAX", 133 | decimals: 18, 134 | }, 135 | rpcUrls: ["https://api.avax-test.network/ext/bc/C/rpc"], 136 | blockExplorerUrls: ["https://testnet.snowtrace.io/"], 137 | }, 138 | addresses: { 139 | NFTs: "0xf5de760f2e916647fd766b4ad9e85ff943ce3a2b", 140 | wETH: "0xd00ae08403b9bbb9124bb305c09058e32c39a48c", 141 | DAI: "0xebbc3452cc911591e4f18f3b36727df45d6bd1f9", 142 | }, 143 | }, 144 | { 145 | network: "moonbase-alpha", 146 | depleted: true, 147 | disclaimer: "Faucet drips 1 DEV, 1 wDEV, 500 DAI, and 5 NFTs (ERC721).", 148 | etherscanPrefix: "moonbase.moonscan.io", 149 | formattedName: "Moonbase Alpha", 150 | connectionDetails: 151 | "https://docs.moonbeam.network/learn/platform/networks/moonbase/", 152 | autoconnect: { 153 | chainId: "0x507", 154 | chainName: "Moonbase Alpha", 155 | nativeCurrency: { 156 | name: "Dev", 157 | symbol: "DEV", 158 | decimals: 18, 159 | }, 160 | rpcUrls: ["https://rpc.api.moonbase.moonbeam.network"], 161 | blockExplorerUrls: ["https://moonbase.moonscan.io/"], 162 | }, 163 | addresses: { 164 | NFTs: "0xf5de760f2e916647fd766B4AD9E85ff943cE3A2b", 165 | wETH: "0xD909178CC99d318e4D46e7E66a972955859670E1", 166 | DAI: "0x4C153BFaD26628BdbaFECBCD160A0790b1b8F212", 167 | }, 168 | }, 169 | ]; 170 | 171 | /** 172 | * Export details about networks 173 | */ 174 | export function getAddressDetails() { 175 | // Get active networks 176 | const activeNetworks: string[] = ADDRESSES.filter( 177 | // Filter for non-depleted 178 | ({ depleted }) => !depleted 179 | // Collect just formatted name 180 | ).map(({ formattedName }) => formattedName); 181 | // Get number of active networks 182 | const networkCount: number = activeNetworks.length; 183 | 184 | // Sort addresses (depleted last) 185 | const sortedAddresses = ADDRESSES.sort((a, b) => { 186 | const first = a.depleted ?? false; 187 | const second = b.depleted ?? false; 188 | return Number(first) - Number(second); 189 | }); 190 | 191 | // Return details 192 | return { networkCount, sortedAddresses }; 193 | } 194 | -------------------------------------------------------------------------------- /utils/dates.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Parse Twitter date as JS Date 3 | * https://stackoverflow.com/questions/13132964/how-to-format-twitter-facebook-feed-date-with-javascript 4 | * @param {string} dateString from Twitter 5 | * @returns {Date} parsed 6 | */ 7 | export default function parseTwitterDate(dateString: string): Date { 8 | const b: string[] = dateString.split(/[: ]/g); 9 | const m: Record = { 10 | jan: 0, 11 | feb: 1, 12 | mar: 2, 13 | apr: 3, 14 | may: 4, 15 | jun: 5, 16 | jul: 6, 17 | aug: 7, 18 | sep: 8, 19 | oct: 9, 20 | nov: 10, 21 | dec: 11, 22 | }; 23 | 24 | return new Date( 25 | Date.UTC( 26 | Number(b[7]), 27 | m[b[1].toLowerCase()], 28 | Number(b[2]), 29 | Number(b[3]), 30 | Number(b[4]), 31 | Number(b[5]) 32 | ) 33 | ); 34 | } 35 | --------------------------------------------------------------------------------