├── .eslintrc.json ├── src ├── locales │ ├── en.json │ ├── zh-CN.json │ ├── zh-CN │ │ ├── home.json │ │ └── common.json │ ├── en │ │ ├── home.json │ │ └── common.json │ └── initI18n.ts ├── app │ ├── globals.css │ ├── favicon.ico │ ├── api │ │ ├── brc20 │ │ │ ├── tick │ │ │ │ ├── list │ │ │ │ │ └── route.ts │ │ │ │ └── [tick] │ │ │ │ │ └── route.ts │ │ │ ├── orders │ │ │ │ └── route.ts │ │ │ ├── mint │ │ │ │ ├── paid │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── tasks │ │ │ │ ├── route.ts │ │ │ │ └── create │ │ │ │ │ └── route.ts │ │ │ └── inscribe │ │ │ │ └── route.ts │ │ └── telegram │ │ │ └── validate │ │ │ └── route.ts │ └── [lang] │ │ ├── wallet │ │ └── page.tsx │ │ ├── inscribe │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── orders │ │ └── page.tsx │ │ └── page.tsx ├── hooks │ ├── useLatest.ts │ ├── useNetwork.ts │ ├── useToast.ts │ ├── useTgInitData.ts │ ├── useLoading.ts │ ├── useCopy.ts │ ├── useThrottleFn.ts │ ├── useLocalstorage.ts │ └── useWallet.ts ├── api │ ├── brc20.ts │ ├── mint.ts │ └── chain.ts ├── utils │ ├── formater.ts │ ├── etc.ts │ ├── unibabel.ts │ ├── address.ts │ ├── mint.ts │ ├── transaction.ts │ └── browser-passworder.ts ├── components │ ├── InitApp │ │ └── index.tsx │ ├── TranslationsProvider │ │ └── index.tsx │ ├── Brc20Minter │ │ ├── SpeedItem.tsx │ │ ├── useMint.ts │ │ ├── SendModal.tsx │ │ └── index.tsx │ ├── OrderList │ │ ├── useOrders.ts │ │ ├── index.tsx │ │ ├── WalletSelectModal.tsx │ │ └── TaskDisplay.tsx │ ├── WalletManager │ │ ├── index.tsx │ │ ├── SelectSource.tsx │ │ ├── RestoreMnemonic.tsx │ │ ├── ReceiveModal.tsx │ │ ├── Mnemonic.tsx │ │ ├── SetPassword.tsx │ │ ├── ViewMnemonicModal.tsx │ │ ├── CreateOrRestoreWallet.tsx │ │ ├── ConfirmMnemonic.tsx │ │ ├── WalletOperator.tsx │ │ └── SendModal.tsx │ ├── HomeView │ │ ├── MintButton.tsx │ │ └── index.tsx │ ├── Login │ │ └── index.tsx │ ├── LanguageChanger │ │ └── index.tsx │ ├── Navigator │ │ └── index.tsx │ └── TransactionConfirm │ │ └── index.tsx ├── types │ └── wallet.ts ├── server │ └── btc.ts ├── i18n-config.ts ├── ui │ ├── Button │ │ └── index.tsx │ ├── Modal │ │ └── index.tsx │ └── LoadingModal │ │ └── index.tsx └── middleware.ts ├── .vscode └── settings.json ├── next.config.js ├── postcss.config.js ├── public └── assets │ ├── icon │ └── outline │ │ ├── back.svg │ │ ├── check.svg │ │ ├── close.svg │ │ ├── orders.svg │ │ ├── home.svg │ │ ├── wallet.svg │ │ ├── inscribe.svg │ │ ├── setting.svg │ │ ├── copy.svg │ │ └── qrcode.svg │ ├── vercel.svg │ ├── telegram.svg │ ├── next.svg │ ├── loading2.svg │ ├── loading.svg │ ├── okx.svg │ └── telegram-widget.js ├── .gitignore ├── prisma └── schema.prisma ├── tsconfig.json ├── tailwind.config.ts ├── README.md └── package.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "products": { 3 | "cart": "Add to Cart" 4 | } 5 | } -------------------------------------------------------------------------------- /src/locales/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "products": { 3 | "cart": "Add to Cart" 4 | } 5 | } -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobotLivermore/brc20-inscribe-bot/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.inlineSuggest.showToolbar": "onHover", 3 | "svg.preview.background": "white" 4 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/hooks/useLatest.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | function useLatest(value: T) { 4 | const ref = useRef(value); 5 | ref.current = value; 6 | 7 | return ref; 8 | } 9 | 10 | export default useLatest; -------------------------------------------------------------------------------- /public/assets/icon/outline/back.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/icon/outline/check.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/icon/outline/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/locales/zh-CN/home.json: -------------------------------------------------------------------------------- 1 | { 2 | "home": { 3 | "mint": "铸造", 4 | "hotBRC20Mints": "热门BRC-20铸造", 5 | "hotBRC100Mints": "热门BRC-100铸造", 6 | "tick": "币种(tick)", 7 | "holders": "持有人数", 8 | "process": "铸造进度", 9 | "pleaseSetUpWallet": "请先创建或导入钱包" 10 | } 11 | } -------------------------------------------------------------------------------- /src/api/brc20.ts: -------------------------------------------------------------------------------- 1 | export const fetchTickList = async () => { 2 | const resp = await fetch(`/api/brc20/tick/list`, { 3 | method: "GET", 4 | headers: { 5 | "Content-Type": "application/json", 6 | }, 7 | }); 8 | const data = await resp.json(); 9 | return data?.data; 10 | } -------------------------------------------------------------------------------- /src/locales/en/home.json: -------------------------------------------------------------------------------- 1 | { 2 | "home": { 3 | "mint": "Mint", 4 | "hotBRC20Mints": "Hot BRC-20 Mints", 5 | "hotBRC100Mints": "Hot BRC-100 Mints", 6 | "tick": "Ttick", 7 | "holders": "Holders", 8 | "process": "Process", 9 | "pleaseSetUpWallet": "Please create or restore your wallet first" 10 | } 11 | } -------------------------------------------------------------------------------- /src/hooks/useNetwork.ts: -------------------------------------------------------------------------------- 1 | import useLocalStorage from "./useLocalstorage"; 2 | 3 | const useNetwork = () => { 4 | const [network, setNetwork] = useLocalStorage<'main' | 'testnet'>("wallet::network", "main"); 5 | 6 | return [network, setNetwork] as ['main' | 'testnet', (network: 'main' | 'testnet') => void]; 7 | } 8 | 9 | export default useNetwork; -------------------------------------------------------------------------------- /src/utils/formater.ts: -------------------------------------------------------------------------------- 1 | export function abbreviateText(text: string, front = 6, back = 6) { 2 | if (text.length <= front + back) { 3 | return text; // 如果字符串长度小于等于12,直接返回整个字符串 4 | } 5 | 6 | // 否则,显示前6位和后6位字符 7 | const prefix = text.slice(0, front); 8 | const suffix = text.slice(0 - back); 9 | return `${prefix}...${suffix}`; 10 | } -------------------------------------------------------------------------------- /src/hooks/useToast.ts: -------------------------------------------------------------------------------- 1 | import { toast } from "react-hot-toast"; 2 | import useThrottleFn from "./useThrottleFn"; 3 | 4 | function useToast(type: "error" | "success" | "loading" | "custom" = 'success') { 5 | const fn = toast[type] 6 | const showToast = useThrottleFn(fn, 3000); 7 | 8 | return showToast; 9 | } 10 | 11 | export default useToast; 12 | -------------------------------------------------------------------------------- /public/assets/icon/outline/orders.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/icon/outline/home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/InitApp/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React, { useEffect } from 'react' 4 | 5 | const InitApp: React.FC = () => { 6 | useEffect(() => { 7 | if (typeof window !== 'undefined') { 8 | if ((window as any)?.Telegram?.WebApp?.expand) { 9 | (window as any)?.Telegram?.WebApp?.expand() 10 | } 11 | } 12 | }, []) 13 | return null 14 | } 15 | 16 | export default InitApp -------------------------------------------------------------------------------- /src/types/wallet.ts: -------------------------------------------------------------------------------- 1 | export interface WalletCore { 2 | encryptedSeed: string; 3 | taprootAddress: string; 4 | publicKey: string; 5 | network?: "main" | "testnet"; 6 | } 7 | 8 | export interface UtxoInfo { 9 | status: { 10 | confirmed: boolean; 11 | block_height: number; 12 | block_hash: string; 13 | block_time: number; 14 | }; 15 | txid: string; 16 | vout: number; 17 | value: number; 18 | } 19 | -------------------------------------------------------------------------------- /public/assets/icon/outline/wallet.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/icon/outline/inscribe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/server/btc.ts: -------------------------------------------------------------------------------- 1 | 2 | export const broardTx = async (txHex: string, network="testnet") => { 3 | let net = "" 4 | if (network == "testnet") { 5 | net = "testnet/" 6 | } 7 | const base_url = "https://mempool.space/" + net + "api/tx" 8 | const resp = await fetch(base_url, { 9 | method: 'POST', 10 | body: txHex, 11 | headers: { 12 | 'Content-Type': 'application/json' 13 | } 14 | }); 15 | const data = await resp.text(); 16 | return data; 17 | } -------------------------------------------------------------------------------- /src/i18n-config.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from 'next/server'; 2 | 3 | export interface Config { 4 | locales: string[]; 5 | defaultLocale: string; 6 | localeCookie?: string; 7 | localeDetector?: ((request: NextRequest, config: Config) => string) | false; 8 | prefixDefault?: boolean; 9 | basePath?: string; 10 | } 11 | 12 | 13 | export const i18n: Config = { 14 | defaultLocale: "en", 15 | locales: ["zh-CN", "en"], 16 | }; 17 | 18 | export type Locale = (typeof i18n)["locales"][number]; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /src/app/api/brc20/tick/list/route.ts: -------------------------------------------------------------------------------- 1 | 2 | export const dynamic = "force-dynamic"; 3 | 4 | export async function GET(): Promise { 5 | const baseUrl = process.env.ALPHA_BOT_URL 6 | 7 | if (!baseUrl) { 8 | throw new Error('ALPHA_BOT_URL not found') 9 | } 10 | const resp = await fetch(`${baseUrl}/api/brc20/ticks/list`, { 11 | method: 'GET', 12 | headers: { 13 | 'Content-Type': 'application/json', 14 | } 15 | }) 16 | const data = await resp.json(); 17 | return new Response(JSON.stringify(data), { 18 | status: 200, 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/hooks/useTgInitData.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | const useTgInitData = () => { 4 | const [initDataUnsafe, setInitDataUnsafe] = useState(null); 5 | useEffect(() => { 6 | if ((window as any)?.Telegram?.WebApp?.initDataUnsafe) { 7 | console.log((window as any).Telegram?.WebApp?.initDataUnsafe); 8 | setInitDataUnsafe((window as any)?.Telegram?.WebApp?.initDataUnsafe); 9 | (window as any)?.Telegram.WebApp.MainButton.show(); 10 | } 11 | }, []); 12 | 13 | return initDataUnsafe 14 | }; 15 | 16 | export default useTgInitData; 17 | -------------------------------------------------------------------------------- /public/assets/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/hooks/useLoading.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef, useState } from 'react'; 2 | 3 | const useLoading = () => { 4 | const [loading, setLoading] = useState(false); 5 | const loadingRef = useRef(loading); 6 | loadingRef.current = loading; 7 | 8 | const getIsLoading = useCallback(() => { 9 | return loadingRef.current; 10 | }, []) 11 | 12 | const setIsLoading = useCallback((value: boolean) => { 13 | loadingRef.current = value; 14 | setLoading(value); 15 | },[] ); 16 | 17 | return [loading, getIsLoading, setIsLoading ] as [boolean, () => boolean, (value: boolean) => void]; 18 | } 19 | 20 | export default useLoading; -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "postgresql" 7 | url = env("POSTGRES_PRISMA_URL") 8 | } 9 | 10 | model inscribe_text_tasks { 11 | id String @id @db.VarChar(255) 12 | user_id String? @db.VarChar(255) 13 | secret String? @db.VarChar(255) 14 | text String? 15 | receive_address String? @db.VarChar(255) 16 | inscribe_address String? @db.VarChar(255) 17 | created_at DateTime? @db.Timestamp(6) 18 | updated_at DateTime? @db.Timestamp(6) 19 | status String? @db.VarChar(50) 20 | } 21 | -------------------------------------------------------------------------------- /src/app/api/brc20/tick/[tick]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | 3 | export async function GET(req: NextRequest, { params }: { params: { tick: string } }): Promise { 4 | console.log(params) 5 | const baseUrl = process.env.ALPHA_BOT_URL 6 | 7 | if (!baseUrl) { 8 | throw new Error('ALPHA_BOT_URL not found') 9 | } 10 | const resp = await fetch(`${baseUrl}/api/brc20/tick/info/${params.tick}`, { 11 | method: 'GET', 12 | headers: { 13 | 'Content-Type': 'application/json', 14 | } 15 | }) 16 | const data = await resp.json(); 17 | return new Response(JSON.stringify(data), { 18 | status: 200, 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/hooks/useCopy.ts: -------------------------------------------------------------------------------- 1 | import useToast from "@/hooks/useToast"; 2 | import copy from "copy-to-clipboard"; 3 | import { useTranslation } from "react-i18next"; 4 | import { useCallback } from "react"; 5 | 6 | const useCopy = () => { 7 | const { t } = useTranslation(); 8 | const showToast = useToast(); 9 | 10 | const copyText = useCallback( 11 | (text: string) => { 12 | const success = copy(text); 13 | if (success) { 14 | showToast(t("common.copySuccess")); 15 | } else { 16 | showToast(t("common.copyFail")); 17 | } 18 | }, 19 | [showToast, t] 20 | ); 21 | 22 | return copyText; 23 | }; 24 | 25 | export default useCopy; 26 | -------------------------------------------------------------------------------- /src/components/TranslationsProvider/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { I18nextProvider } from 'react-i18next'; 4 | import initTranslations from '@/locales/initI18n'; 5 | import { createInstance } from 'i18next'; 6 | import React from 'react' 7 | 8 | export default function TranslationsProvider({ 9 | children, 10 | locale, 11 | namespaces, 12 | resources 13 | }: { 14 | children: React.ReactNode, 15 | locale: string, 16 | namespaces: string[], 17 | resources: any 18 | }) { 19 | const i18n = createInstance(); 20 | 21 | initTranslations(locale, namespaces, i18n, resources); 22 | 23 | return {children}; 24 | } -------------------------------------------------------------------------------- /src/hooks/useThrottleFn.ts: -------------------------------------------------------------------------------- 1 | // useThrottleFn 2 | 3 | import { useCallback, useRef } from "react"; 4 | import useLatest from './useLatest' 5 | 6 | 7 | type noop = (...args: any[]) => any; 8 | 9 | 10 | 11 | function useThrottleFn(fn: T, wait: number) { 12 | const fnRef = useLatest(fn); 13 | 14 | 15 | const lastCallTimeRef = useRef(0); 16 | 17 | const run = useCallback((...args: Parameters) => { 18 | const now = Date.now(); 19 | if (now - lastCallTimeRef.current > wait) { 20 | lastCallTimeRef.current = now; 21 | fnRef.current(...args); 22 | } 23 | }, [fnRef, wait]) 24 | 25 | return run 26 | } 27 | 28 | export default useThrottleFn; 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /src/components/Brc20Minter/SpeedItem.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | const SpeedItem: React.FC<{ 5 | level: string; 6 | fee: number; 7 | active: boolean; 8 | onClick: () => void; 9 | }> = ({ level, fee, active, onClick }) => { 10 | const cls = twMerge( 11 | "flex flex-col items-center justify-between mt-2 rounded py-2 text-sm cursor-pointer", 12 | !active 13 | ? "border border-black text-black" 14 | : "border border-black bg-black text-white" 15 | ); 16 | return ( 17 |
18 | {level} 19 | {fee} sat/vB 20 |
21 | ); 22 | }; 23 | 24 | export default SpeedItem; 25 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | import daisyui from 'daisyui' 3 | 4 | const config: Config = { 5 | content: [ 6 | // './src/pages/**/*.{js,ts,jsx,tsx,mdx}', 7 | './src/ui/**/*.{js,ts,jsx,tsx,mdx}', 8 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 9 | './src/app/**/*.{js,ts,jsx,tsx,mdx}', 10 | ], 11 | theme: { 12 | extend: { 13 | backgroundImage: { 14 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 15 | 'gradient-conic': 16 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 17 | }, 18 | }, 19 | }, 20 | daisyui: { 21 | themes: ["light", "dark", "lofi"], 22 | }, 23 | plugins: [daisyui], 24 | } 25 | export default config 26 | -------------------------------------------------------------------------------- /src/app/api/brc20/orders/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | 3 | 4 | export async function POST(req: NextRequest): Promise { 5 | const requestData = await req.json(); 6 | 7 | // TODO: validate requestData 8 | 9 | const baseUrl = process.env.ALPHA_BOT_URL 10 | 11 | if (!baseUrl) { 12 | throw new Error('ALPHA_BOT_URL not found') 13 | } 14 | 15 | const resp = await fetch(`${baseUrl}/api/brc20/mint/tasks/status`, { 16 | method: 'POST', 17 | body: JSON.stringify({ 18 | ids: requestData.ids, 19 | }), 20 | headers: { 21 | 'Content-Type': 'application/json', 22 | } 23 | }) 24 | const data = await resp.json(); 25 | return new Response(JSON.stringify(data), { 26 | status: 200, 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /src/components/OrderList/useOrders.ts: -------------------------------------------------------------------------------- 1 | import { useState, useCallback, useEffect } from 'react' 2 | 3 | export default function useOrders(ids: string[]) { 4 | const [orders, setOrders] = useState([]); 5 | 6 | const fetchOrders = useCallback(async () => { 7 | const resp = await fetch("/api/brc20/orders", { 8 | method: "POST", 9 | body: JSON.stringify({ 10 | ids, 11 | }), 12 | headers: { 13 | "Content-Type": "application/json", 14 | }, 15 | }); 16 | const data = await resp.json(); 17 | setOrders(data?.data || []); 18 | }, [ids]); 19 | 20 | useEffect(() => { 21 | if (ids.length > 0) { 22 | fetchOrders(); 23 | } 24 | }, [ids, fetchOrders]); 25 | 26 | return { 27 | orders, 28 | updateOrders: fetchOrders, 29 | } 30 | } -------------------------------------------------------------------------------- /public/assets/icon/outline/setting.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/telegram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/app/api/brc20/mint/paid/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | 3 | 4 | export async function POST(req: NextRequest): Promise { 5 | const requestData = await req.json(); 6 | 7 | // TODO: validate requestData 8 | 9 | const baseUrl = process.env.ALPHA_BOT_URL 10 | 11 | if (!baseUrl) { 12 | throw new Error('ALPHA_BOT_URL not found') 13 | } 14 | 15 | const resp = await fetch(`${baseUrl}/api/brc20/mint/paid`, { 16 | method: 'POST', 17 | body: JSON.stringify({ 18 | taskId: requestData.taskId, 19 | txid: requestData.txid, 20 | vout: requestData.vout, 21 | amount: requestData.amount, 22 | }), 23 | headers: { 24 | 'Content-Type': 'application/json', 25 | } 26 | }) 27 | const data = await resp.json(); 28 | return new Response(JSON.stringify(data), { 29 | status: 200, 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /src/app/api/brc20/mint/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | 3 | 4 | export async function POST(req: NextRequest): Promise { 5 | const requestData = await req.json(); 6 | 7 | // TODO: validate requestData 8 | 9 | const baseUrl = process.env.ALPHA_BOT_URL 10 | 11 | if (!baseUrl) { 12 | throw new Error('ALPHA_BOT_URL not found') 13 | } 14 | 15 | const resp = await fetch(`${baseUrl}/api/brc20/mint`, { 16 | method: 'POST', 17 | body: JSON.stringify({ 18 | priv: requestData.priv, 19 | tick: requestData.tick, 20 | amt: requestData.amt, 21 | receive_address: requestData.receiveAddress, 22 | }), 23 | headers: { 24 | 'Content-Type': 'application/json', 25 | } 26 | }) 27 | const data = await resp.json(); 28 | return new Response(JSON.stringify(data), { 29 | status: 200, 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /src/app/api/brc20/tasks/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | 3 | import { PrismaClient } from "@prisma/client"; 4 | 5 | const prisma = new PrismaClient(); 6 | 7 | export async function POST(req: NextRequest): Promise { 8 | const requestData = await req.json(); 9 | 10 | const task = await prisma.inscribe_text_tasks.findMany({ 11 | where: { 12 | user_id: requestData.userId, 13 | }, 14 | select: { 15 | id: true, 16 | user_id: true, 17 | secret: false, 18 | text: true, 19 | receive_address: true, 20 | inscribe_address: true, 21 | status: true, 22 | created_at: true, 23 | updated_at: true, 24 | }, 25 | }); 26 | 27 | return new Response( 28 | JSON.stringify({ 29 | code: 0, 30 | data: task, 31 | }), 32 | { 33 | status: 200, 34 | } 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/app/api/brc20/tasks/create/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | import { PrismaClient } from '@prisma/client' 3 | 4 | const prisma = new PrismaClient() 5 | 6 | export async function POST(req: NextRequest): Promise { 7 | const requestData = await req.json(); 8 | 9 | const task = await prisma.inscribe_text_tasks.create({ 10 | data: { 11 | id: requestData.id, 12 | user_id: requestData.userId, 13 | secret: requestData.secret, 14 | text: requestData.text, 15 | receive_address: requestData.receiveAddress, 16 | inscribe_address: requestData.inscribeAddress, 17 | status: requestData.status, 18 | created_at: requestData.createdAt, 19 | updated_at: requestData.updatedAt, 20 | } 21 | }) 22 | 23 | return new Response(JSON.stringify({ 24 | code: 0, 25 | data: task, 26 | }), { 27 | status: 200, 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/components/WalletManager/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { FC } from "react"; 4 | import { WalletCore } from "@/types/wallet"; 5 | import CreateOrRestoreWallet from "./CreateOrRestoreWallet"; 6 | import WalletOperator from "./WalletOperator"; 7 | import useWallet from "@/hooks/useWallet"; 8 | 9 | const WalletManager: FC = () => { 10 | const { wallet, saveWallet, clearWallet } = useWallet(); 11 | 12 | const hasWallet = Boolean(wallet); 13 | return ( 14 | <> 15 | {hasWallet && ( 16 | 20 | )} 21 | {!hasWallet && ( 22 | { 24 | saveWallet(w); 25 | }} 26 | /> 27 | )} 28 | 29 | ); 30 | }; 31 | 32 | export default WalletManager; 33 | -------------------------------------------------------------------------------- /src/app/[lang]/wallet/page.tsx: -------------------------------------------------------------------------------- 1 | import Login from "@/components/Login"; 2 | import Navigator from "@/components/Navigator"; 3 | import initTranslations from "@/locales/initI18n"; 4 | import TranslationsProvider from "@/components/TranslationsProvider"; 5 | 6 | export default async function Home({ params: { lang } }: any) { 7 | const i18nNamespaces = ["common"]; 8 | const { resources } = await initTranslations(lang, i18nNamespaces); 9 | 10 | return ( 11 | 16 |
17 |
18 | 19 |
20 | 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/components/Brc20Minter/useMint.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import useLoading from "@/hooks/useLoading"; 3 | import { fetchBrc20MintInscriptionAddress } from '@/api/mint' 4 | 5 | const useMint = () => { 6 | const [isMinting, getIsMinting, setIsMinting] = useLoading(); 7 | 8 | const handle = useCallback( 9 | async (tick: string, amount: number, receiveAddress: string) => { 10 | if (getIsMinting()) { 11 | return; 12 | } 13 | try { 14 | setIsMinting(true); 15 | const result = await fetchBrc20MintInscriptionAddress(tick, amount, receiveAddress); 16 | console.log(result); 17 | return result 18 | } catch (e) { 19 | console.error(e); 20 | } finally { 21 | setIsMinting(false); 22 | } 23 | }, 24 | [getIsMinting, setIsMinting] 25 | ); 26 | 27 | return { isMinting, onMint: handle }; 28 | }; 29 | 30 | export default useMint; 31 | -------------------------------------------------------------------------------- /src/app/[lang]/inscribe/page.tsx: -------------------------------------------------------------------------------- 1 | import Brc20Minter from "@/components/Brc20Minter"; 2 | import Navigator from "@/components/Navigator"; 3 | import initTranslations from "@/locales/initI18n"; 4 | import TranslationsProvider from "@/components/TranslationsProvider"; 5 | 6 | export default async function Inscribe({ params: { lang } }: any) { 7 | const i18nNamespaces = ["common"]; 8 | const { resources } = await initTranslations(lang, i18nNamespaces); 9 | return ( 10 | 15 |
16 |
17 | 18 |
19 | 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/components/OrderList/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import useLocalStorage from "@/hooks/useLocalstorage"; 4 | import React, { useState } from "react"; 5 | import TaskDisplay from "./TaskDisplay"; 6 | // import useOrders from "./useOrders"; 7 | // import WalletSelectModal from "./WalletSelectModal"; 8 | 9 | const OrderList: React.FC = () => { 10 | const [orderList] = useLocalStorage("orderList", []); 11 | 12 | return ( 13 |
14 | {orderList 15 | .filter((item) => Boolean(item.taskId)) 16 | .map((item, index) => ( 17 | 26 | ))} 27 |
28 | ); 29 | }; 30 | 31 | export default OrderList; 32 | -------------------------------------------------------------------------------- /public/assets/icon/outline/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/app/[lang]/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import Script from "next/script"; 4 | import "../globals.css"; 5 | import InitApp from "@/components/InitApp"; 6 | import { Toaster } from "react-hot-toast"; 7 | 8 | const inter = Inter({ subsets: ["latin"] }); 9 | 10 | export const metadata: Metadata = { 11 | title: "BRC20 Minter Tool", 12 | description: "Generated by create next app", 13 | }; 14 | 15 | export default function RootLayout({ 16 | children, 17 | params, 18 | }: { 19 | children: React.ReactNode; 20 | params: any; 21 | }) { 22 | return ( 23 | 24 | 25 | */} 73 | 74 | 75 | 76 | 77 | 78 | 79 | ); 80 | }; 81 | 82 | export default WalletSelectModal; 83 | -------------------------------------------------------------------------------- /src/components/WalletManager/CreateOrRestoreWallet.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { FC, useCallback, useEffect, useState } from "react"; 4 | import { WalletCore } from "@/types/wallet"; 5 | import SelectSource from "./SelectSource"; 6 | import SetPassword from "./SetPassword"; 7 | import ConfirmMnemonic from "./ConfirmMnemonic"; 8 | import Mnemonic from "./Mnemonic"; 9 | import RestoreMnemonic from "./RestoreMnemonic"; 10 | import { generateWalletCore } from '@/utils/address' 11 | 12 | interface Props { 13 | onFinishCreateWallet: (w: WalletCore) => void; 14 | } 15 | 16 | const CreateOrRestoreWallet: FC = ({ onFinishCreateWallet }) => { 17 | /** 18 | * page value init, setPassword, mnemonic, confirmMnemonic, inputMnemonic, restoreWallet 19 | */ 20 | const [page, setPage] = useState("init"); 21 | const [source, setSource] = useState("create"); // create || restore 22 | 23 | const [tempMnemonic, setTempMnemonic] = useState(""); 24 | const [password, setPassword] = useState(""); 25 | 26 | const saveTempMnemonic = useCallback((tm: string) => { 27 | localStorage.setItem( 28 | "tempMnemonic", 29 | JSON.stringify({ tempMnemonic: tm, createdAt: Date.now() }) 30 | ); 31 | }, []); 32 | 33 | const clearTempMnemonic = useCallback(() => { 34 | localStorage.removeItem("tempMnemonic"); 35 | }, []); 36 | 37 | const handleChangeTempMnemonic = useCallback( 38 | (tm: string) => { 39 | setTempMnemonic(tm); 40 | saveTempMnemonic(tm); 41 | }, 42 | [saveTempMnemonic] 43 | ); 44 | 45 | const onCreateNewWallet = useCallback(() => { 46 | setSource("create"); 47 | setPage("setPassword"); 48 | }, []); 49 | 50 | const onRestoreWallet = useCallback(() => { 51 | setSource("restore"); 52 | setPage("setPassword"); 53 | }, []); 54 | 55 | const onCreatPasswordBack = useCallback(() => { 56 | setPage("init"); 57 | }, []); 58 | 59 | const onCreatePasswordNext = useCallback( 60 | (psw: string) => { 61 | setPassword(psw); 62 | if (source === "create") { 63 | setPage("mnemonic"); 64 | } else { 65 | setPage("inputMnemonic"); 66 | } 67 | }, 68 | [source] 69 | ); 70 | 71 | const onConfirmMnemonic = async () => { 72 | clearTempMnemonic(); 73 | const newWallet = await generateWalletCore(tempMnemonic, password) 74 | onFinishCreateWallet(newWallet); 75 | }; 76 | 77 | const onConfirmRestoreMnemonic = async (mnemonic: string) => { 78 | const newWallet = await generateWalletCore(mnemonic, password) 79 | onFinishCreateWallet(newWallet); 80 | }; 81 | 82 | if (page === "init") { 83 | return ( 84 | 88 | ); 89 | } else if (page === "setPassword") { 90 | return ( 91 | 92 | ); 93 | } else if (page === "mnemonic") { 94 | return ( 95 | { 97 | handleChangeTempMnemonic(m); 98 | setPage("confirmMnemonic"); 99 | }} 100 | /> 101 | ); 102 | } else if (page === "confirmMnemonic") { 103 | return ( 104 | { 108 | setPage("mnemonic"); 109 | }} 110 | /> 111 | ); 112 | } else if (page === "inputMnemonic") { 113 | return ( 114 | { 116 | onConfirmRestoreMnemonic(m); 117 | }} 118 | /> 119 | ); 120 | } 121 | return null; 122 | }; 123 | 124 | export default CreateOrRestoreWallet; 125 | -------------------------------------------------------------------------------- /src/components/OrderList/TaskDisplay.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import { abbreviateText } from "@/utils/formater"; 5 | import useCopy from "@/hooks/useCopy"; 6 | 7 | const formatStatus = (status: string) => { 8 | if (status === "waiting_pay") { 9 | return "待支付"; 10 | } else if (status === "waiting_mint") { 11 | return "待支付"; 12 | } else if (status === "minted") { 13 | return "铭刻成功"; 14 | } else if (status === "failed") { 15 | return "超时失败"; 16 | } else if (status === "waiting_refund") { 17 | return "等待退款"; 18 | } else if (status === "refunded") { 19 | return "已退款"; 20 | } else { 21 | return status; 22 | } 23 | }; 24 | 25 | interface Props { 26 | taskId: string; 27 | inscriptionAddress: string; 28 | fee: number; 29 | status: string; 30 | createdAt: number; 31 | secret?: string; 32 | } 33 | 34 | const TaskDisplay: React.FC = ({ 35 | taskId, 36 | inscriptionAddress, 37 | fee, 38 | status, 39 | secret, 40 | createdAt, 41 | }) => { 42 | const copy = useCopy(); 43 | return ( 44 |
45 |
46 | Order ID: 47 | 48 | {abbreviateText(taskId)} 49 | copy { 56 | copy(taskId); 57 | }} 58 | /> 59 | 60 |
61 |
62 |
63 |
64 | 铭刻地址: 65 | { 68 | copy(inscriptionAddress); 69 | }} 70 | > 71 | 72 | {abbreviateText(inscriptionAddress, 8, 8)} 73 | copy { 80 | copy(taskId); 81 | }} 82 | /> 83 | 84 | 85 |
86 |
87 | 转入金额: 88 | {Math.ceil(fee) / 100000000} BTC 89 |
90 | 91 |
92 | 订单状态: 93 | {formatStatus(status)} 94 |
95 |
96 | {secret && ( 97 |
98 | 交易私钥 99 | 100 | {abbreviateText(secret, 8, 8)}{" "} 101 | copy { 108 | copy(secret); 109 | }} 110 | /> 111 | 112 |
113 | )} 114 |
115 | {createdAt && {new Date(createdAt).toLocaleString()}} 116 |
117 |
118 |
119 |
120 | ); 121 | }; 122 | 123 | export default TaskDisplay; 124 | -------------------------------------------------------------------------------- /src/api/chain.ts: -------------------------------------------------------------------------------- 1 | import { UtxoInfo } from '@/types/wallet' 2 | 3 | export const fetchChainFeeRate = async (network: "main" | "testnet") => { 4 | const url = 5 | network === "main" 6 | ? "https://mempool.space/api/v1/fees/recommended" 7 | : "https://mempool.space/testnet/api/v1/fees/recommended"; 8 | const resp = await fetch(url); 9 | const data = await resp.json(); 10 | return data; 11 | }; 12 | 13 | interface AddressStatInfo { 14 | address: string; 15 | chain_stats: { 16 | funded_txo_count: number; 17 | funded_txo_sum: number; 18 | spent_txo_count: number; 19 | spent_txo_sum: number; 20 | tx_count: number; 21 | }; 22 | mempool_stats: { 23 | funded_txo_count: number; 24 | funded_txo_sum: number; 25 | spent_txo_count: number; 26 | spent_txo_sum: number; 27 | tx_count: number; 28 | }; 29 | } 30 | 31 | export const fetchChainBalance = async ( 32 | address: string, 33 | network: "main" | "testnet" 34 | ) => { 35 | const url = 36 | network === "main" 37 | ? `https://mempool.space/api/address/${address}` 38 | : `https://mempool.space/testnet/api/address/${address}`; 39 | const resp = await fetch(url); 40 | const data = (await resp.json()) as AddressStatInfo; 41 | return data; 42 | }; 43 | 44 | export const fetchChainTx = async ( 45 | txid: string, 46 | network: "main" | "testnet" 47 | ) => { 48 | const url = 49 | network === "main" 50 | ? `https://mempool.space/api/tx/${txid}` 51 | : `https://mempool.space/testnet/api/tx/${txid}`; 52 | const resp = await fetch(url); 53 | const data = await resp.json(); 54 | return data; 55 | }; 56 | 57 | export const fetchChainTxHex = async ( 58 | txid: string, 59 | network: "main" | "testnet" 60 | ) => { 61 | const url = 62 | network === "main" 63 | ? `https://mempool.space/api/tx/${txid}/hex` 64 | : `https://mempool.space/testnet/api/tx/${txid}/hex`; 65 | const resp = await fetch(url); 66 | const data = await resp.text(); 67 | return data; 68 | }; 69 | 70 | export const fetchChainTxList = async ( 71 | address: string, 72 | network: "main" | "testnet" 73 | ) => { 74 | const url = 75 | network === "main" 76 | ? `https://mempool.space/api/address/${address}/txs` 77 | : `https://mempool.space/testnet/api/address/${address}/txs`; 78 | const resp = await fetch(url); 79 | const data = await resp.json(); 80 | return data; 81 | }; 82 | 83 | export const fetchChainTxListByBlock = async ( 84 | block: number, 85 | network: "main" | "testnet" 86 | ) => { 87 | const url = 88 | network === "main" 89 | ? `https://mempool.space/api/block/${block}/txids` 90 | : `https://mempool.space/testnet/api/block/${block}/txids`; 91 | const resp = await fetch(url); 92 | const data = await resp.json(); 93 | return data; 94 | }; 95 | 96 | export const fetchChainBlock = async ( 97 | block: number, 98 | network: "main" | "testnet" 99 | ) => { 100 | const url = 101 | network === "main" 102 | ? `https://mempool.space/api/block/${block}` 103 | : `https://mempool.space/testnet/api/block/${block}`; 104 | const resp = await fetch(url); 105 | const data = await resp.json(); 106 | return data; 107 | }; 108 | 109 | 110 | export const fetchAddressUtxo = async ( 111 | address: string, 112 | network: "main" | "testnet" 113 | ) => { 114 | const url = 115 | network === "main" 116 | ? `https://mempool.space/api/address/${address}/utxo` 117 | : `https://mempool.space/testnet/api/address/${address}/utxo`; 118 | const resp = await fetch(url); 119 | const data = await resp.json() as UtxoInfo[]; 120 | return data; 121 | }; 122 | 123 | export const broadcastTx = async ( 124 | txHex: string, 125 | network: "main" | "testnet" 126 | ) => { 127 | const url = 128 | network === "main" 129 | ? `https://mempool.space/api/tx` 130 | : `https://mempool.space/testnet/api/tx`; 131 | const resp = await fetch(url, { 132 | method: "POST", 133 | body: txHex, 134 | }); 135 | const data = await resp.text(); 136 | return data; 137 | } -------------------------------------------------------------------------------- /src/app/api/brc20/inscribe/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | import { Address, Signer, Tap, Tx } from "@cmdcode/tapscript"; 3 | 4 | import { broardTx } from "@/server/btc"; 5 | 6 | import { keys } from "@cmdcode/crypto-utils"; 7 | 8 | /* 9 | 铭刻过程 10 | */ 11 | export async function POST(req: NextRequest): Promise { 12 | const requestData = await req.json(); 13 | // 读取数据 14 | const secret = requestData.secret; 15 | const text = requestData.text; 16 | const receiveAddress = requestData.receiveAddress; 17 | const txid = requestData.txid; 18 | const vout = requestData.vout; 19 | const amount = requestData.amount; 20 | const outputAmount = requestData.outputAmount; 21 | const network = requestData?.network || 'testnet' 22 | 23 | const seckey = keys.get_seckey(secret); 24 | const pubkey = keys.get_pubkey(seckey, true); 25 | // Basic format of an 'inscription' script. 26 | const ec = new TextEncoder(); 27 | const content = ec.encode(text); 28 | const mimetype = ec.encode("text/plain;charset=utf-8"); 29 | 30 | const script = [ 31 | pubkey, 32 | "OP_CHECKSIG", 33 | "OP_0", 34 | "OP_IF", 35 | ec.encode("ord"), 36 | "01", 37 | mimetype, 38 | "OP_0", 39 | content, 40 | "OP_ENDIF", 41 | ]; 42 | 43 | // For tapscript spends, we need to convert this script into a 'tapleaf'. 44 | const tapleaf = Tap.encodeScript(script); 45 | // Generate a tapkey that includes our leaf script. Also, create a merlke proof 46 | // (cblock) that targets our leaf and proves its inclusion in the tapkey. 47 | const [tpubkey, cblock] = Tap.getPubKey(pubkey, { target: tapleaf }); 48 | console.log("tpubkey", tpubkey); 49 | // A taproot address is simply the tweaked public key, encoded in bech32 format. 50 | const address = Address.p2tr.fromPubKey(tpubkey, network); 51 | console.log("Your address:", address, Address.toScriptPubKey(receiveAddress)); 52 | 53 | const txdata = Tx.create({ 54 | vin: [ 55 | { 56 | // Use the txid of the funding transaction used to send the sats. 57 | txid: txid, 58 | // Specify the index value of the output that you are going to spend from. 59 | vout: vout, 60 | // Also include the value and script of that ouput. 61 | prevout: { 62 | // Feel free to change this if you sent a different amount. 63 | value: amount, 64 | // This is what our address looks like in script form. 65 | scriptPubKey: ["OP_1", tpubkey], 66 | }, 67 | }, 68 | ], 69 | vout: [ 70 | { 71 | // We are leaving behind 1000 sats as a fee to the miners. 72 | value: outputAmount || 546, 73 | // This is the new script that we are locking our funds to. 74 | scriptPubKey: Address.toScriptPubKey(receiveAddress), 75 | }, 76 | ], 77 | }); 78 | 79 | // For this example, we are signing for input 0 of our transaction, 80 | // using the untweaked secret key. We are also extending the signature 81 | // to include a commitment to the tapleaf script that we wish to use. 82 | const sig = Signer.taproot.sign(seckey, txdata, 0, { extension: tapleaf }); 83 | 84 | // Add the signature to our witness data for input 0, along with the script 85 | // and merkle proof (cblock) for the script. 86 | txdata.vin[0].witness = [sig, script, cblock]; 87 | 88 | // Check if the signature is valid for the provided public key, and that the 89 | // transaction is also valid (the merkle proof will be validated as well). 90 | const isValid = Signer.taproot.verify(txdata, 0, { pubkey, throws: true }); 91 | console.log("isValid", isValid); 92 | 93 | console.log("Your txhex:", Tx.encode(txdata).hex); 94 | const result = await broardTx(Tx.encode(txdata).hex, network); 95 | // await broadcast(Tx.encode(txdata).hex); 96 | return new Response( 97 | JSON.stringify({ 98 | message: "ok", 99 | code: 0, 100 | data: { 101 | txHex: Tx.encode(txdata).hex, 102 | result, 103 | }, 104 | }), 105 | { 106 | status: 200, 107 | } 108 | ); 109 | } 110 | -------------------------------------------------------------------------------- /public/assets/okx.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 25 | 36 | 47 | 58 | 59 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /src/utils/transaction.ts: -------------------------------------------------------------------------------- 1 | import { Address, Signer, Tx, Tap } from "@cmdcode/tapscript"; 2 | import { UtxoInfo } from "@/types/wallet"; 3 | import { broadcastTx } from "@/api/chain"; 4 | import { fetchAddressUtxo } from '@/api/chain' 5 | 6 | 7 | export const estimateTxSize = (inputs: number, outputs: number) => { 8 | return inputs * 148 + outputs * 34 + 10 + inputs; 9 | }; 10 | 11 | export const estimateTxFee = ( 12 | inputs: number, 13 | outputs: number, 14 | feeRate: number 15 | ) => { 16 | return estimateTxSize(inputs, outputs) * feeRate; 17 | }; 18 | 19 | export const selectUtxos = ( 20 | utxos: UtxoInfo[], 21 | amount: number, 22 | feeRate: number 23 | ) => { 24 | let sum = 0; 25 | 26 | const selected: UtxoInfo[] = []; 27 | for (const utxo of utxos) { 28 | const spendAmount = 29 | amount + estimateTxSize(selected.length + 1, 2) * feeRate; 30 | selected.push(utxo); 31 | sum += utxo.value; 32 | if (sum >= spendAmount) { 33 | break; 34 | } 35 | } 36 | if (sum < amount + estimateTxSize(selected.length + 1, 2) * feeRate) { 37 | throw new Error("Insufficient balance"); 38 | } 39 | 40 | return selected; 41 | }; 42 | 43 | export const estimateTxFeeByUtxos = ( 44 | utxos: UtxoInfo[], 45 | amount: number, 46 | feeRate: number 47 | ) => { 48 | const selected = selectUtxos(utxos, amount, feeRate); 49 | return estimateTxSize(selected.length, 2) * feeRate; 50 | } 51 | 52 | export const sendBTC = async ( 53 | priv: string, 54 | utxos: UtxoInfo[], 55 | amount: number, 56 | feeRate: number, 57 | toAddress: string, 58 | changeAddress: string, 59 | network: "main" | "testnet" 60 | ) => { 61 | const safeUtxos = utxos.filter((utxo) => utxo.value > 1000); 62 | const selected = selectUtxos(safeUtxos, amount, feeRate); 63 | console.log(selected); 64 | const inputs = selected.map((utxo) => ({ 65 | txid: utxo.txid, 66 | vout: utxo.vout, 67 | value: utxo.value, 68 | address: changeAddress, 69 | })); 70 | 71 | console.log(Address.toScriptPubKey(changeAddress)) 72 | 73 | const remainedValue = Math.floor(selected.reduce((sum, utxo) => sum + utxo.value, 0) - amount - estimateTxSize(selected.length , 2) * feeRate) 74 | 75 | console.log(remainedValue) 76 | const [tseckey] = Tap.getSecKey(priv); 77 | 78 | const txdata = Tx.create({ 79 | vin: inputs.map((input) => ({ 80 | txid: input.txid, 81 | vout: input.vout, 82 | prevout: { 83 | value: input.value, 84 | scriptPubKey: Address.toScriptPubKey(changeAddress), 85 | }})), 86 | vout: [ 87 | { 88 | // We are locking up 99_000 sats (minus 1000 sats for fees.) 89 | value: amount, 90 | // We are locking up funds to this address. 91 | scriptPubKey: Address.toScriptPubKey( 92 | toAddress 93 | ), 94 | }, 95 | { 96 | value: selected.reduce((sum, utxo) => sum + utxo.value, 0) - amount - estimateTxSize(selected.length , 2) * feeRate, 97 | scriptPubKey: Address.toScriptPubKey( 98 | changeAddress 99 | ), 100 | } 101 | ], 102 | }); 103 | 104 | for (let i = 0; i < inputs.length; i++) { 105 | console.log("signing input", i) 106 | const sig = Signer.taproot.sign(tseckey, txdata, i); 107 | txdata.vin[i].witness = [sig]; 108 | } 109 | 110 | console.log(inputs.length, txdata) 111 | 112 | // For verification, provided your 113 | // await Signer.taproot.verify(txdata, 0, { throws: true }); 114 | 115 | const txhex = Tx.encode(txdata).hex; 116 | 117 | const result = await broadcastTx(txhex, network); 118 | 119 | if (result.includes('error')) { 120 | throw new Error(JSON.parse(result.slice(result.indexOf('{')))?.message || result); 121 | } 122 | 123 | return result; 124 | } 125 | 126 | export const sendBTCByPriv = async ( 127 | priv: string, 128 | amount: number, 129 | feeRate: number, 130 | toAddress: string, 131 | changeAddress: string, 132 | network: "main" | "testnet" 133 | ) => { 134 | console.log(priv) 135 | const utxos = await fetchAddressUtxo(changeAddress, network); 136 | console.log(changeAddress, utxos); 137 | const result = await sendBTC(priv, utxos, amount, feeRate, toAddress, changeAddress, network); 138 | return result; 139 | } -------------------------------------------------------------------------------- /src/components/WalletManager/ConfirmMnemonic.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { FC, useState } from "react"; 4 | import Image from "next/image"; 5 | import { useTranslation } from "react-i18next"; 6 | import { ReactSVG } from "react-svg"; 7 | 8 | const Item: FC<{ 9 | no: number; 10 | word: string; 11 | isCorrect: boolean; 12 | onRemove: () => void; 13 | }> = ({ no, word, isCorrect, onRemove }) => { 14 | return ( 15 |
16 | 17 | {no} 18 | 19 | 24 | {word} 25 | {word && isCorrect && ( 26 | 29 | 33 | 34 | )} 35 | {word && !isCorrect && ( 36 | close 44 | )} 45 | 46 |
47 | ); 48 | }; 49 | 50 | interface Props { 51 | mnemonic: string; 52 | onConfirm: () => void; 53 | onBack: () => void 54 | } 55 | 56 | const ConfirmMnemonic: FC = ({ mnemonic, onConfirm, onBack }) => { 57 | const { t } = useTranslation(); 58 | const wordList = mnemonic.split(" "); 59 | const [selectedWords, setSelectedWords] = useState( 60 | new Array(4).fill("") as string[] 61 | ); 62 | 63 | const handleSelectWord = (word: string) => { 64 | let idx = -1; 65 | for (let i = 0; i < selectedWords.length; i++) { 66 | if (!selectedWords[i]) { 67 | idx = i; 68 | break; 69 | } 70 | } 71 | if (idx === -1) { 72 | return; 73 | } 74 | const newSelectedWords = [...selectedWords]; 75 | newSelectedWords[idx] = word; 76 | setSelectedWords(newSelectedWords); 77 | }; 78 | 79 | const handleRemoveSelectedWord = (index: number) => { 80 | const newSelectedWords = [...selectedWords]; 81 | newSelectedWords[index] = ""; 82 | setSelectedWords(newSelectedWords); 83 | }; 84 | 85 | const isPass = selectedWords.every((word, index) => { 86 | return word === wordList[index * 3 + 2]; 87 | }) 88 | 89 | return ( 90 |
91 |

Confirm back up

92 |

93 | Select words 3, 6, 9 and 12 of your mnemonic. 94 |

95 |
96 | { 101 | handleRemoveSelectedWord(0); 102 | }} 103 | /> 104 | { 109 | handleRemoveSelectedWord(1); 110 | }} 111 | /> 112 | { 117 | handleRemoveSelectedWord(2); 118 | }} 119 | /> 120 | { 125 | handleRemoveSelectedWord(3); 126 | }} 127 | /> 128 |
129 | 130 |
131 | {wordList 132 | .filter((word) => !selectedWords.includes(word)) 133 | .map((word, index) => ( 134 | { 138 | handleSelectWord(word); 139 | }} 140 | > 141 | {word} 142 | 143 | ))} 144 |
145 | 152 | 158 |
159 | ); 160 | }; 161 | 162 | export default ConfirmMnemonic; 163 | -------------------------------------------------------------------------------- /src/components/WalletManager/WalletOperator.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { WalletCore } from "@/types/wallet"; 4 | import { FC, useState, useEffect, useCallback, use } from "react"; 5 | import { generateAddressFromPubKey } from "@/utils/address"; 6 | import { abbreviateText } from "@/utils/formater"; 7 | import Button from "@/ui/Button"; 8 | import { useTranslation } from "react-i18next"; 9 | import ReceiveModal from "./ReceiveModal"; 10 | import SendModal from "./SendModal"; 11 | import { fetchChainBalance, fetchAddressUtxo } from "@/api/chain"; 12 | import { ReactSVG } from "react-svg"; 13 | import TransactionConfirm from "../TransactionConfirm"; 14 | import useNetwork from "@/hooks/useNetwork"; 15 | import useCopy from "@/hooks/useCopy"; 16 | import ViewMnemonicModal from "./ViewMnemonicModal"; 17 | 18 | interface Props { 19 | wallet: WalletCore; 20 | onDeleteWallet: () => void; 21 | } 22 | 23 | const WalletOperator: FC = ({ wallet, onDeleteWallet }) => { 24 | const { t } = useTranslation(); 25 | const copy = useCopy(); 26 | 27 | const [address, setAddress] = useState(""); 28 | const [network, setNetwork] = useNetwork(); 29 | 30 | const [page, setPage] = useState<"home" | "setting">("home"); 31 | 32 | const [balance, setBalance] = useState(0); 33 | const [utxos, setUtxos] = useState([]); 34 | 35 | useEffect(() => { 36 | if (wallet?.publicKey) { 37 | const _addr = generateAddressFromPubKey(wallet.publicKey, network); 38 | setAddress(_addr); 39 | } 40 | }, [network, wallet.publicKey]); 41 | 42 | const updateBalance = useCallback(async () => { 43 | const balanceInfo = await fetchChainBalance(address, network); 44 | const b = 45 | balanceInfo.chain_stats.funded_txo_sum - 46 | balanceInfo.chain_stats.spent_txo_sum + 47 | balanceInfo.mempool_stats.funded_txo_sum - 48 | balanceInfo.mempool_stats.spent_txo_sum; 49 | setBalance(b); 50 | }, [address, network]); 51 | 52 | useEffect(() => { 53 | if (address) { 54 | updateBalance(); 55 | } 56 | }, [address, updateBalance]); 57 | 58 | const updateUtxos = useCallback(async () => { 59 | const utxos = await fetchAddressUtxo(address, network); 60 | setUtxos(utxos); 61 | }, [address, network]); 62 | 63 | useEffect(() => { 64 | if (address) { 65 | updateUtxos(); 66 | } 67 | }, [address, updateUtxos]); 68 | 69 | useEffect(() => { 70 | if (address) { 71 | updateUtxos(); 72 | } 73 | }, [address, updateUtxos]); 74 | 75 | const [isOpenReceiveModal, setIsOpenReceiveModal] = useState(false); 76 | 77 | const handleClose = useCallback(() => { 78 | setIsOpenReceiveModal(false); 79 | }, []); 80 | 81 | const [isOpenSendModal, setIsOpenSendModal] = useState(false); 82 | const handleCloseSendModal = useCallback(() => { 83 | setIsOpenSendModal(false); 84 | }, []); 85 | 86 | const [isConfirmDelete, setIsConfirmDelete] = useState(false); 87 | 88 | const [isViewMnemonic, setIsViewMnemonic] = useState(false); 89 | 90 | return ( 91 |
92 |
93 | { 96 | copy(address); 97 | }} 98 | > 99 | {abbreviateText(address, 4)} 100 | 101 |
{ 104 | if (page === "home") { 105 | setPage("setting"); 106 | } else { 107 | setPage("home"); 108 | } 109 | }} 110 | > 111 | {page === "home" && ( 112 | 116 | )} 117 | {page === "setting" && ( 118 | 122 | )} 123 |
124 |
125 | {page === "home" && ( 126 |
127 | {t("wallet.homeTitle")} 128 | {`${balance / 100000000} ${ 129 | network === "main" ? "BTC" : "tBTC" 130 | }`} 131 |
132 |
149 |
150 | )} 151 | {page === "setting" && ( 152 |
153 | 154 | {t("wallet.settingTitle")} 155 | 156 |
157 | 158 | 168 |
169 | 170 |
188 | )} 189 | 194 | 203 | { 206 | setIsConfirmDelete(false); 207 | }} 208 | onConfirm={() => { 209 | onDeleteWallet(); 210 | setIsConfirmDelete(false); 211 | }} 212 | /> 213 | { 216 | setIsViewMnemonic(false); 217 | }} 218 | /> 219 |
220 | ); 221 | }; 222 | 223 | export default WalletOperator; 224 | -------------------------------------------------------------------------------- /src/components/WalletManager/SendModal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Modal from "@/ui/Modal"; 4 | import React, { useCallback, useEffect, useRef, useState } from "react"; 5 | import { useTranslation } from "react-i18next"; 6 | import { fetchChainFeeRate } from "@/api/chain"; 7 | import { UtxoInfo, WalletCore } from "@/types/wallet"; 8 | import { twMerge } from "tailwind-merge"; 9 | import Button from "@/ui/Button"; 10 | import Image from "next/image"; 11 | import { estimateTxFeeByUtxos } from "@/utils/transaction"; 12 | import { decrypt } from "@/utils/browser-passworder"; 13 | import { getPrivFromMnemonic } from "@/utils/address"; 14 | import { sendBTC } from "@/utils/transaction"; 15 | import useToast from "@/hooks/useToast"; 16 | import { abbreviateText } from "@/utils/formater"; 17 | 18 | const SpeedItem: React.FC<{ 19 | level: string; 20 | fee: number; 21 | active: boolean; 22 | onClick: () => void; 23 | }> = ({ level, fee, active, onClick }) => { 24 | const cls = twMerge( 25 | "flex flex-col items-center justify-between mt-2 rounded py-2 text-sm cursor-pointer", 26 | !active 27 | ? "border border-black text-black" 28 | : "border border-black bg-black text-white" 29 | ); 30 | return ( 31 |
32 | {level} 33 | {fee} sat/vB 34 |
35 | ); 36 | }; 37 | 38 | interface Props { 39 | visible: boolean; 40 | balance: number; 41 | utxos: UtxoInfo[]; 42 | wallet: WalletCore; 43 | network: "main" | "testnet"; 44 | onUpdate: () => void 45 | onClose: () => void; 46 | } 47 | 48 | const SendModal: React.FC = ({ 49 | visible, 50 | utxos = [], 51 | wallet, 52 | network, 53 | onClose, 54 | onUpdate, 55 | }) => { 56 | const { t } = useTranslation(); 57 | const [stage, setStage] = useState< 58 | "input" | "confirm" | "password" | "success" 59 | >("input"); 60 | const [receipient, setReceipient] = useState(""); 61 | const [amount, setAmount] = useState(); 62 | const [feeRate, setFeeRate] = useState<{ 63 | slow: number; 64 | average: number; 65 | fast: number; 66 | }>({ slow: 1, average: 1, fast: 1 }); 67 | const [isSending, setIsSending] = useState(false); 68 | const isSendingRef = useRef(false); 69 | isSendingRef.current = isSending; 70 | const toastSuccess = useToast("success"); 71 | const toasstError = useToast("error"); 72 | 73 | const [fee, setFee] = useState(0); 74 | 75 | const [speed, setSpeed] = useState<"slow" | "average" | "fast">("average"); 76 | 77 | const [password, setPassword] = useState(""); 78 | 79 | const updateFeeRate = useCallback(async () => { 80 | const feeInfo = await fetchChainFeeRate("testnet"); 81 | setFeeRate({ 82 | slow: feeInfo.hourFee, 83 | average: feeInfo.halfHourFee, 84 | fast: feeInfo.fastestFee, 85 | }); 86 | }, []); 87 | 88 | const handleCalculateFee = useCallback(async () => { 89 | if (amount === undefined) { 90 | return 0; 91 | } 92 | const selectableUtxos = utxos.filter((utxo) => { 93 | return utxo.value > 800; 94 | }); 95 | const fee = await estimateTxFeeByUtxos( 96 | selectableUtxos, 97 | amount, 98 | feeRate[speed] 99 | ); 100 | setFee(fee); 101 | }, [amount, feeRate, speed, utxos]); 102 | 103 | useEffect(() => { 104 | if (visible) { 105 | updateFeeRate(); 106 | } 107 | }, [visible, updateFeeRate]); 108 | 109 | const availableBalance = utxos.reduce((acc, cur) => { 110 | if (cur.value > 800) { 111 | return acc + cur.value; 112 | } 113 | return acc; 114 | }, 0); 115 | 116 | const handleClose = () => { 117 | onClose(); 118 | setStage("input"); 119 | setReceipient(""); 120 | setAmount(undefined); 121 | setSpeed("average"); 122 | setFee(0); 123 | setPassword(""); 124 | }; 125 | 126 | const confirmSend = async () => { 127 | try { 128 | if (isSendingRef.current) { 129 | return; 130 | } 131 | isSendingRef.current = true; 132 | setIsSending(true); 133 | const decryptedWallet = await decrypt(password, wallet?.encryptedSeed); 134 | const priv = getPrivFromMnemonic(decryptedWallet as string); 135 | const availableUtxos = utxos.filter((utxo) => { 136 | return utxo.value > 800; 137 | }); 138 | await sendBTC( 139 | priv, 140 | availableUtxos, 141 | Math.floor((amount as number) * 100000000), 142 | feeRate[speed], 143 | receipient, 144 | wallet?.taprootAddress, 145 | network 146 | ); 147 | toastSuccess(t("wallet.sendSuccess")); 148 | handleClose(); 149 | onUpdate(); 150 | } catch (error) { 151 | console.log(error); 152 | toasstError(t("wallet.sendFailed")); 153 | } finally { 154 | setIsSending(false); 155 | isSendingRef.current = false; 156 | } 157 | }; 158 | 159 | return ( 160 | 164 |
165 | {stage === "input" && ( 166 | <> 167 | { 173 | setReceipient(e.target.value); 174 | }} 175 | /> 176 |
177 | {t("wallet.available")} 178 | {availableBalance / 100000000} 179 |
180 | { 186 | if (e.target.value === "") { 187 | setAmount(undefined); 188 | return; 189 | } 190 | setAmount(Number(e.target.value)); 191 | }} 192 | /> 193 |
194 | {t("wallet.fee")} 195 |
196 | { 201 | setSpeed("slow"); 202 | }} 203 | /> 204 | { 209 | setSpeed("average"); 210 | }} 211 | /> 212 | { 217 | setSpeed("fast"); 218 | }} 219 | /> 220 |
221 |
222 |
319 |
320 | ); 321 | }; 322 | 323 | export default SendModal; 324 | -------------------------------------------------------------------------------- /src/components/Brc20Minter/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState, useEffect, useCallback } from "react"; 4 | import useLocalStorage from "@/hooks/useLocalstorage"; 5 | import { useRouter, useSearchParams } from "next/navigation"; 6 | import { useTranslation } from "react-i18next"; 7 | import { fetchChainFeeRate } from "@/api/chain"; 8 | import Button from "@/ui/Button"; 9 | import TransactionConfirm from "../TransactionConfirm"; 10 | import { sendBTCByPriv } from "@/utils/transaction"; 11 | import { generateAddressFromPubKey } from "@/utils/address"; 12 | import useToast from "@/hooks/useToast"; 13 | import { inscribeBrc20Mint, fetchTickInfo } from "@/api/mint"; 14 | import { 15 | generateInscribe, 16 | generatePrivateKey, 17 | generateBrc20MintContent, 18 | } from "@/utils/mint"; 19 | import useNetwork from "@/hooks/useNetwork"; 20 | import { v4 as uuidV4 } from "uuid"; 21 | import SpeedItem from "./SpeedItem"; 22 | import useLoading from "@/hooks/useLoading"; 23 | import { ReactSVG } from "react-svg"; 24 | import LoadingModal from "@/ui/LoadingModal"; 25 | import useWallet from "@/hooks/useWallet"; 26 | 27 | const Brc20Minter = () => { 28 | const router = useRouter(); 29 | const { t } = useTranslation(); 30 | const searchParams = useSearchParams(); 31 | 32 | const [tick, setTick] = React.useState(""); 33 | const [amt, setAmt] = React.useState(0); 34 | const [to, setTo] = React.useState(""); 35 | const toastError = useToast("error"); 36 | 37 | const [network] = useNetwork(); 38 | const [protocol, setProtocol] = useState<"brc-20" | "brc-100">("brc-20"); 39 | 40 | const [isConfirmPay, setIsConfirmPay] = useState(false); 41 | const [isInscribing, setIsInscribing] = useState(false); 42 | 43 | const [feeRate, setFeeRate] = useState<{ 44 | slow: number; 45 | average: number; 46 | fast: number; 47 | }>({ slow: 1, average: 1, fast: 1 }); 48 | const [speed, setSpeed] = useState<"slow" | "average" | "fast">("average"); 49 | const updateFeeRate = useCallback(async () => { 50 | const feeInfo = await fetchChainFeeRate(network); 51 | setFeeRate({ 52 | slow: feeInfo.hourFee, 53 | average: feeInfo.halfHourFee, 54 | fast: feeInfo.fastestFee, 55 | }); 56 | }, [network]); 57 | 58 | useEffect(() => { 59 | updateFeeRate(); 60 | }, [updateFeeRate]); 61 | 62 | const { wallet } = useWallet(); 63 | 64 | const [isQueryTick, getIsQueryTick, setIsQueryTick] = useLoading(); 65 | const updateAmount = useCallback( 66 | async (tick: string) => { 67 | if (!getIsQueryTick() && protocol === 'brc-20') { 68 | try { 69 | setIsQueryTick(true); 70 | const res = await fetchTickInfo(tick); 71 | setAmt(Number(res.limit)); 72 | } catch (error) { 73 | console.log(error); 74 | } finally { 75 | setIsQueryTick(false); 76 | } 77 | } 78 | }, 79 | [getIsQueryTick, setIsQueryTick, protocol] 80 | ); 81 | 82 | useEffect(() => { 83 | if (searchParams.get("tick")) { 84 | setTick(searchParams.get("tick") as string); 85 | console.log(searchParams.get("amt")); 86 | if (searchParams.get("amt")) { 87 | setAmt(Number(searchParams.get("amt"))); 88 | } else { 89 | updateAmount(searchParams.get("tick") as string); 90 | } 91 | } 92 | if (searchParams.get('protocol') && ['brc-20', 'brc-100'].includes(searchParams.get('protocol') as string)) { 93 | setProtocol(searchParams.get('protocol') as 'brc-20' | 'brc-100') 94 | } 95 | }, [amt, searchParams, updateAmount]); 96 | 97 | const [orderList, setOrderList] = useLocalStorage("orderList", []); 98 | 99 | const addOrderAndJumpToOrderList = ( 100 | _taskId: string, 101 | _secret: string, 102 | _content: string, 103 | _addr: string, 104 | _receipt: string, 105 | _fee: number, 106 | _protocol: string, 107 | ) => { 108 | setOrderList([ 109 | { 110 | taskId: _taskId, 111 | content: _content, 112 | secret: _secret, 113 | inscriptionAddress: _addr, 114 | receiveAddress: _receipt, 115 | fee: _fee, 116 | protocol: _protocol, 117 | status: "waiting_pay", 118 | createdAt: new Date().valueOf(), 119 | }, 120 | ...orderList, 121 | ]); 122 | }; 123 | 124 | const changeOrderStatus = (taskId: string, status: string) => { 125 | if (typeof window !== "undefined") { 126 | try { 127 | const currentList = JSON.parse( 128 | window.localStorage.getItem("orderList") as string 129 | ); 130 | const newList = currentList.map((item: any) => { 131 | if (item.taskId === taskId) { 132 | return { 133 | ...item, 134 | status, 135 | }; 136 | } 137 | return item; 138 | }); 139 | setOrderList(newList); 140 | } catch (e) { 141 | console.log(e); 142 | } 143 | } 144 | }; 145 | 146 | const handleMint = async () => { 147 | setIsConfirmPay(true); 148 | }; 149 | 150 | const handleTransfer = async (priv: string) => { 151 | 152 | try { 153 | const secret = generatePrivateKey(); 154 | const _inscriptionAddress = generateInscribe( 155 | secret, 156 | tick, 157 | Number(amt), 158 | network, 159 | protocol 160 | ); 161 | let base = 546; 162 | if (protocol === "brc-100") { 163 | base = 294; 164 | } 165 | const fee = feeRate[speed] * 154 + base 166 | 167 | setIsInscribing(true); 168 | const taskId = uuidV4(); 169 | addOrderAndJumpToOrderList( 170 | taskId, 171 | secret, 172 | generateBrc20MintContent(tick, Number(amt), protocol), 173 | _inscriptionAddress, 174 | to, 175 | fee, 176 | protocol 177 | ); 178 | const txid = await sendBTCByPriv( 179 | priv, 180 | fee, 181 | feeRate[speed], 182 | _inscriptionAddress, 183 | generateAddressFromPubKey(wallet?.publicKey as string, network), 184 | network 185 | ); 186 | changeOrderStatus(taskId, "waiting_mint"); 187 | if (protocol === "brc-20") { 188 | await inscribeBrc20Mint( 189 | secret, 190 | generateBrc20MintContent(tick, Number(amt), protocol), 191 | txid, 192 | 0, 193 | fee, 194 | to, 195 | 546, 196 | network, 197 | ); 198 | changeOrderStatus(taskId, "minted"); 199 | router.push("/orders"); 200 | } else if (protocol === "brc-100") { 201 | await inscribeBrc20Mint( 202 | secret, 203 | generateBrc20MintContent(tick, Number(amt), protocol), 204 | txid, 205 | 0, 206 | fee, 207 | to, 208 | 294, 209 | network 210 | ); 211 | changeOrderStatus(taskId, "minted"); 212 | router.push("/orders"); 213 | } 214 | } catch (error: any) { 215 | console.log(error); 216 | toastError(error.message); 217 | } finally { 218 | setIsInscribing(false); 219 | } 220 | }; 221 | return ( 222 | <> 223 |
224 |
225 |

Minter

226 |
227 | 协议(protocol) 228 |
229 |
246 | 币种(tick) 247 | { 252 | setTick(e.target.value); 253 | }} 254 | onBlur={() => updateAmount(tick)} 255 | /> 256 |
257 |
258 | 259 | 铸造数量(amt) 260 | {isQueryTick && ( 261 | 262 | )} 263 | 264 | { 270 | setAmt(e.target.value ? Number(e.target.value) : 0); 271 | }} 272 | /> 273 |
274 |
275 | 接收地址(to) 276 | { 281 | setTo(e.target.value); 282 | }} 283 | /> 284 |
285 |
286 | { 291 | setSpeed("slow"); 292 | }} 293 | /> 294 | { 299 | setSpeed("average"); 300 | }} 301 | /> 302 | { 307 | setSpeed("fast"); 308 | }} 309 | /> 310 |
311 |
312 |
318 |
319 | setIsConfirmPay(false)} 323 | /> 324 | 325 |
326 | 327 | ); 328 | }; 329 | 330 | export default Brc20Minter; 331 | -------------------------------------------------------------------------------- /src/utils/browser-passworder.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer'; 2 | 3 | export type PlainObject = Record; 4 | 5 | export function isPlainObject(value: unknown): value is PlainObject { 6 | if (typeof value !== "object" || value === null) { 7 | return false; 8 | } 9 | 10 | try { 11 | let proto = value; 12 | while (Object.getPrototypeOf(proto) !== null) { 13 | proto = Object.getPrototypeOf(proto); 14 | } 15 | 16 | return Object.getPrototypeOf(value) === proto; 17 | } catch (_) { 18 | return false; 19 | } 20 | } 21 | 22 | export const hasProperty = < 23 | ObjectToCheck extends Object, 24 | Property extends PropertyKey 25 | >( 26 | objectToCheck: ObjectToCheck, 27 | name: Property 28 | ): objectToCheck is ObjectToCheck & 29 | Record< 30 | Property, 31 | Property extends keyof ObjectToCheck ? ObjectToCheck[Property] : unknown 32 | > => Object.hasOwnProperty.call(objectToCheck, name); 33 | 34 | export type DetailedEncryptionResult = { 35 | vault: string; 36 | exportedKeyString: string; 37 | }; 38 | 39 | export type PBKDF2Params = { 40 | iterations: number; 41 | }; 42 | 43 | export type KeyDerivationOptions = { 44 | algorithm: "PBKDF2"; 45 | params: PBKDF2Params; 46 | }; 47 | 48 | export type EncryptionKey = { 49 | key: CryptoKey; 50 | derivationOptions: KeyDerivationOptions; 51 | }; 52 | 53 | export type ExportedEncryptionKey = { 54 | key: JsonWebKey; 55 | derivationOptions: KeyDerivationOptions; 56 | }; 57 | 58 | export type EncryptionResult = { 59 | data: string; 60 | iv: string; 61 | salt?: string; 62 | // old encryption results will not have this 63 | keyMetadata?: KeyDerivationOptions; 64 | }; 65 | 66 | export type DetailedDecryptResult = { 67 | exportedKeyString: string; 68 | vault: unknown; 69 | salt: string; 70 | }; 71 | 72 | const EXPORT_FORMAT = "jwk"; 73 | const DERIVED_KEY_FORMAT = "AES-GCM"; 74 | const STRING_ENCODING = "utf-8"; 75 | const OLD_DERIVATION_PARAMS: KeyDerivationOptions = { 76 | algorithm: "PBKDF2", 77 | params: { 78 | iterations: 10_000, 79 | }, 80 | }; 81 | const DEFAULT_DERIVATION_PARAMS: KeyDerivationOptions = { 82 | algorithm: "PBKDF2", 83 | params: { 84 | iterations: 900_000, 85 | }, 86 | }; 87 | 88 | /** 89 | * Encrypts a data object that can be any serializable value using 90 | * a provided password. 91 | * 92 | * @param password - The password to use for encryption. 93 | * @param dataObj - The data to encrypt. 94 | * @param key - The CryptoKey to encrypt with. 95 | * @param salt - The salt to use to encrypt. 96 | * @param keyDerivationOptions - The options to use for key derivation. 97 | * @returns The encrypted vault. 98 | */ 99 | export async function encrypt( 100 | password: string, 101 | dataObj: R, 102 | key?: EncryptionKey | CryptoKey, 103 | salt: string = generateSalt(), 104 | keyDerivationOptions = DEFAULT_DERIVATION_PARAMS 105 | ): Promise { 106 | const cryptoKey = 107 | key || (await keyFromPassword(password, salt, false, keyDerivationOptions)); 108 | const payload = await encryptWithKey(cryptoKey, dataObj); 109 | payload.salt = salt; 110 | return JSON.stringify(payload); 111 | } 112 | 113 | /** 114 | * Encrypts a data object that can be any serializable value using 115 | * a provided password. 116 | * 117 | * @param password - A password to use for encryption. 118 | * @param dataObj - The data to encrypt. 119 | * @param salt - The salt used to encrypt. 120 | * @param keyDerivationOptions - The options to use for key derivation. 121 | * @returns The vault and exported key string. 122 | */ 123 | export async function encryptWithDetail( 124 | password: string, 125 | dataObj: R, 126 | salt = generateSalt(), 127 | keyDerivationOptions = DEFAULT_DERIVATION_PARAMS 128 | ): Promise { 129 | const key = await keyFromPassword(password, salt, true, keyDerivationOptions); 130 | const exportedKeyString = await exportKey(key); 131 | const vault = await encrypt(password, dataObj, key, salt); 132 | 133 | return { 134 | vault, 135 | exportedKeyString, 136 | }; 137 | } 138 | 139 | /** 140 | * Encrypts the provided serializable javascript object using the 141 | * provided CryptoKey and returns an object containing the cypher text and 142 | * the initialization vector used. 143 | * 144 | * @param encryptionKey - The CryptoKey to encrypt with. 145 | * @param dataObj - A serializable JavaScript object to encrypt. 146 | * @returns The encrypted data. 147 | */ 148 | export async function encryptWithKey( 149 | encryptionKey: EncryptionKey | CryptoKey, 150 | dataObj: R 151 | ): Promise { 152 | const data = JSON.stringify(dataObj); 153 | const dataBuffer = Buffer.from(data, STRING_ENCODING); 154 | const vector = global.crypto.getRandomValues(new Uint8Array(16)); 155 | const key = unwrapKey(encryptionKey); 156 | 157 | const buf = await global.crypto.subtle.encrypt( 158 | { 159 | name: DERIVED_KEY_FORMAT, 160 | iv: vector, 161 | }, 162 | key, 163 | dataBuffer 164 | ); 165 | 166 | const buffer = new Uint8Array(buf); 167 | const vectorStr = Buffer.from(vector).toString("base64"); 168 | const vaultStr = Buffer.from(buffer).toString("base64"); 169 | const encryptionResult: EncryptionResult = { 170 | data: vaultStr, 171 | iv: vectorStr, 172 | }; 173 | 174 | if (isEncryptionKey(encryptionKey)) { 175 | encryptionResult.keyMetadata = encryptionKey.derivationOptions; 176 | } 177 | 178 | return encryptionResult; 179 | } 180 | 181 | /** 182 | * Given a password and a cypher text, decrypts the text and returns 183 | * the resulting value. 184 | * 185 | * @param password - The password to decrypt with. 186 | * @param text - The cypher text to decrypt. 187 | * @param encryptionKey - The key to decrypt with. 188 | * @returns The decrypted data. 189 | */ 190 | export async function decrypt( 191 | password: string, 192 | text: string, 193 | encryptionKey?: EncryptionKey | CryptoKey 194 | ): Promise { 195 | const payload = JSON.parse(text); 196 | const { salt, keyMetadata } = payload; 197 | const cryptoKey = unwrapKey( 198 | encryptionKey || (await keyFromPassword(password, salt, false, keyMetadata)) 199 | ); 200 | 201 | const result = await decryptWithKey(cryptoKey, payload); 202 | return result; 203 | } 204 | 205 | /** 206 | * Given a password and a cypher text, decrypts the text and returns 207 | * the resulting value, keyString, and salt. 208 | * 209 | * @param password - The password to decrypt with. 210 | * @param text - The encrypted vault to decrypt. 211 | * @returns The decrypted vault along with the salt and exported key. 212 | */ 213 | export async function decryptWithDetail( 214 | password: string, 215 | text: string 216 | ): Promise { 217 | const payload = JSON.parse(text); 218 | const { salt, keyMetadata } = payload; 219 | const key = await keyFromPassword(password, salt, true, keyMetadata); 220 | const exportedKeyString = await exportKey(key); 221 | const vault = await decrypt(password, text, key); 222 | 223 | return { 224 | exportedKeyString, 225 | vault, 226 | salt, 227 | }; 228 | } 229 | 230 | /** 231 | * Given a CryptoKey and an EncryptionResult object containing the initialization 232 | * vector (iv) and data to decrypt, return the resulting decrypted value. 233 | * 234 | * @param encryptionKey - The CryptoKey to decrypt with. 235 | * @param payload - The payload to decrypt, returned from an encryption method. 236 | * @returns The decrypted data. 237 | */ 238 | export async function decryptWithKey( 239 | encryptionKey: EncryptionKey | CryptoKey, 240 | payload: EncryptionResult 241 | ): Promise { 242 | const encryptedData = Buffer.from(payload.data, "base64"); 243 | const vector = Buffer.from(payload.iv, "base64"); 244 | const key = unwrapKey(encryptionKey); 245 | 246 | let decryptedObj; 247 | try { 248 | const result = await crypto.subtle.decrypt( 249 | { name: DERIVED_KEY_FORMAT, iv: vector }, 250 | key, 251 | encryptedData 252 | ); 253 | 254 | const decryptedData = new Uint8Array(result); 255 | const decryptedStr = Buffer.from(decryptedData).toString(STRING_ENCODING); 256 | decryptedObj = JSON.parse(decryptedStr); 257 | } catch (e) { 258 | throw new Error("Incorrect password"); 259 | } 260 | 261 | return decryptedObj; 262 | } 263 | 264 | /** 265 | * Receives an exported CryptoKey string and creates a key. 266 | * 267 | * This function supports both JsonWebKey's and exported EncryptionKey's. 268 | * It will return a CryptoKey for the former, and an EncryptionKey for the latter. 269 | * 270 | * @param keyString - The key string to import. 271 | * @returns An EncryptionKey or a CryptoKey. 272 | */ 273 | export async function importKey( 274 | keyString: string 275 | ): Promise { 276 | const exportedEncryptionKey = JSON.parse(keyString); 277 | 278 | if (isExportedEncryptionKey(exportedEncryptionKey)) { 279 | return { 280 | key: await window.crypto.subtle.importKey( 281 | EXPORT_FORMAT, 282 | exportedEncryptionKey.key, 283 | DERIVED_KEY_FORMAT, 284 | true, 285 | ["encrypt", "decrypt"] 286 | ), 287 | derivationOptions: exportedEncryptionKey.derivationOptions, 288 | }; 289 | } 290 | 291 | return await window.crypto.subtle.importKey( 292 | EXPORT_FORMAT, 293 | exportedEncryptionKey, 294 | DERIVED_KEY_FORMAT, 295 | true, 296 | ["encrypt", "decrypt"] 297 | ); 298 | } 299 | 300 | /** 301 | * Exports a key string from a CryptoKey or from an 302 | * EncryptionKey instance. 303 | * 304 | * @param encryptionKey - The CryptoKey or EncryptionKey to export. 305 | * @returns A key string. 306 | */ 307 | export async function exportKey( 308 | encryptionKey: CryptoKey | EncryptionKey 309 | ): Promise { 310 | if (isEncryptionKey(encryptionKey)) { 311 | return JSON.stringify({ 312 | key: await window.crypto.subtle.exportKey( 313 | EXPORT_FORMAT, 314 | encryptionKey.key 315 | ), 316 | derivationOptions: encryptionKey.derivationOptions, 317 | }); 318 | } 319 | 320 | return JSON.stringify( 321 | await window.crypto.subtle.exportKey(EXPORT_FORMAT, encryptionKey) 322 | ); 323 | } 324 | 325 | /** 326 | * Generate a CryptoKey from a password and random salt. 327 | * 328 | * @param password - The password to use to generate key. 329 | * @param salt - The salt string to use in key derivation. 330 | * @param exportable - Whether or not the key should be exportable. 331 | * @returns A CryptoKey for encryption and decryption. 332 | */ 333 | export async function keyFromPassword( 334 | password: string, 335 | salt: string, 336 | exportable?: boolean 337 | ): Promise; 338 | /** 339 | * Generate a CryptoKey from a password and random salt, specifying 340 | * key derivation options. 341 | * 342 | * @param password - The password to use to generate key. 343 | * @param salt - The salt string to use in key derivation. 344 | * @param exportable - Whether or not the key should be exportable. 345 | * @param opts - The options to use for key derivation. 346 | * @returns An EncryptionKey for encryption and decryption. 347 | */ 348 | export async function keyFromPassword( 349 | password: string, 350 | salt: string, 351 | exportable?: boolean, 352 | opts?: KeyDerivationOptions 353 | ): Promise; 354 | // The overloads are already documented. 355 | export async function keyFromPassword( 356 | password: string, 357 | salt: string, 358 | exportable = false, 359 | opts: KeyDerivationOptions = OLD_DERIVATION_PARAMS 360 | ): Promise { 361 | const passBuffer = Buffer.from(password, STRING_ENCODING); 362 | const saltBuffer = Buffer.from(salt, "base64"); 363 | 364 | const key = await global.crypto.subtle.importKey( 365 | "raw", 366 | passBuffer, 367 | { name: "PBKDF2" }, 368 | false, 369 | ["deriveBits", "deriveKey"] 370 | ); 371 | 372 | const derivedKey = await global.crypto.subtle.deriveKey( 373 | { 374 | name: "PBKDF2", 375 | salt: saltBuffer, 376 | iterations: opts.params.iterations, 377 | hash: "SHA-256", 378 | }, 379 | key, 380 | { name: DERIVED_KEY_FORMAT, length: 256 }, 381 | exportable, 382 | ["encrypt", "decrypt"] 383 | ); 384 | 385 | return opts 386 | ? { 387 | key: derivedKey, 388 | derivationOptions: opts, 389 | } 390 | : derivedKey; 391 | } 392 | 393 | /** 394 | * Converts a hex string into a buffer. 395 | * 396 | * @param str - Hex encoded string. 397 | * @returns The string ecoded as a byte array. 398 | */ 399 | export function serializeBufferFromStorage(str: string): Uint8Array { 400 | const stripStr = str.slice(0, 2) === "0x" ? str.slice(2) : str; 401 | const buf = new Uint8Array(stripStr.length / 2); 402 | for (let i = 0; i < stripStr.length; i += 2) { 403 | const seg = stripStr.substr(i, 2); 404 | buf[i / 2] = parseInt(seg, 16); 405 | } 406 | return buf; 407 | } 408 | 409 | /** 410 | * Converts a buffer into a hex string ready for storage. 411 | * 412 | * @param buffer - Buffer to serialize. 413 | * @returns A hex encoded string. 414 | */ 415 | export function serializeBufferForStorage(buffer: Uint8Array): string { 416 | let result = "0x"; 417 | buffer.forEach((value) => { 418 | result += unprefixedHex(value); 419 | }); 420 | return result; 421 | } 422 | 423 | /** 424 | * Converts a number into hex value, and ensures proper leading 0 425 | * for single characters strings. 426 | * 427 | * @param num - The number to convert to string. 428 | * @returns An unprefixed hex string. 429 | */ 430 | function unprefixedHex(num: number): string { 431 | let hex = num.toString(16); 432 | while (hex.length < 2) { 433 | hex = `0${hex}`; 434 | } 435 | return hex; 436 | } 437 | 438 | /** 439 | * Generates a random string for use as a salt in CryptoKey generation. 440 | * 441 | * @param byteCount - The number of bytes to generate. 442 | * @returns A randomly generated string. 443 | */ 444 | export function generateSalt(byteCount = 32): string { 445 | const view = new Uint8Array(byteCount); 446 | global.crypto.getRandomValues(view); 447 | // Uint8Array is a fixed length array and thus does not have methods like pop, etc 448 | // so TypeScript complains about casting it to an array. Array.from() works here for 449 | // getting the proper type, but it results in a functional difference. In order to 450 | // cast, you have to first cast view to unknown then cast the unknown value to number[] 451 | // TypeScript ftw: double opt in to write potentially type-mismatched code. 452 | const b64encoded = btoa( 453 | String.fromCharCode.apply(null, view as unknown as number[]) 454 | ); 455 | return b64encoded; 456 | } 457 | 458 | /** 459 | * Updates the provided vault, re-encrypting 460 | * data with a safer algorithm if one is available. 461 | * 462 | * If the provided vault is already using the latest available encryption method, 463 | * it is returned as is. 464 | * 465 | * @param vault - The vault to update. 466 | * @param password - The password to use for encryption. 467 | * @param targetDerivationParams - The options to use for key derivation. 468 | * @returns A promise resolving to the updated vault. 469 | */ 470 | export async function updateVault( 471 | vault: string, 472 | password: string, 473 | targetDerivationParams = DEFAULT_DERIVATION_PARAMS 474 | ): Promise { 475 | if (isVaultUpdated(vault, targetDerivationParams)) { 476 | return vault; 477 | } 478 | 479 | return encrypt( 480 | password, 481 | await decrypt(password, vault), 482 | undefined, 483 | undefined, 484 | targetDerivationParams 485 | ); 486 | } 487 | 488 | /** 489 | * Updates the provided vault and exported key, re-encrypting 490 | * data with a safer algorithm if one is available. 491 | * 492 | * If the provided vault is already using the latest available encryption method, 493 | * it is returned as is. 494 | * 495 | * @param encryptionResult - The encrypted data to update. 496 | * @param password - The password to use for encryption. 497 | * @param targetDerivationParams - The options to use for key derivation. 498 | * @returns A promise resolving to the updated encrypted data and exported key. 499 | */ 500 | export async function updateVaultWithDetail( 501 | encryptionResult: DetailedEncryptionResult, 502 | password: string, 503 | targetDerivationParams = DEFAULT_DERIVATION_PARAMS 504 | ): Promise { 505 | if (isVaultUpdated(encryptionResult.vault, targetDerivationParams)) { 506 | return encryptionResult; 507 | } 508 | 509 | return encryptWithDetail( 510 | password, 511 | await decrypt(password, encryptionResult.vault), 512 | undefined, 513 | targetDerivationParams 514 | ); 515 | } 516 | 517 | /** 518 | * Checks if the provided key is an `EncryptionKey`. 519 | * 520 | * @param encryptionKey - The object to check. 521 | * @returns Whether or not the key is an `EncryptionKey`. 522 | */ 523 | function isEncryptionKey( 524 | encryptionKey: unknown 525 | ): encryptionKey is EncryptionKey { 526 | return ( 527 | isPlainObject(encryptionKey) && 528 | hasProperty(encryptionKey, "key") && 529 | hasProperty(encryptionKey, "derivationOptions") && 530 | encryptionKey.key instanceof CryptoKey && 531 | isKeyDerivationOptions(encryptionKey.derivationOptions) 532 | ); 533 | } 534 | 535 | /** 536 | * Checks if the provided object is a `KeyDerivationOptions`. 537 | * 538 | * @param derivationOptions - The object to check. 539 | * @returns Whether or not the object is a `KeyDerivationOptions`. 540 | */ 541 | function isKeyDerivationOptions( 542 | derivationOptions: unknown 543 | ): derivationOptions is KeyDerivationOptions { 544 | return ( 545 | isPlainObject(derivationOptions) && 546 | hasProperty(derivationOptions, "algorithm") && 547 | hasProperty(derivationOptions, "params") 548 | ); 549 | } 550 | 551 | /** 552 | * Checks if the provided key is an `ExportedEncryptionKey`. 553 | * 554 | * @param exportedKey - The object to check. 555 | * @returns Whether or not the object is an `ExportedEncryptionKey`. 556 | */ 557 | function isExportedEncryptionKey( 558 | exportedKey: unknown 559 | ): exportedKey is ExportedEncryptionKey { 560 | return ( 561 | isPlainObject(exportedKey) && 562 | hasProperty(exportedKey, "key") && 563 | hasProperty(exportedKey, "derivationOptions") && 564 | isKeyDerivationOptions(exportedKey.derivationOptions) 565 | ); 566 | } 567 | 568 | /** 569 | * Returns the `CryptoKey` from the provided encryption key. 570 | * If the provided key is a `CryptoKey`, it is returned as is. 571 | * 572 | * @param encryptionKey - The key to unwrap. 573 | * @returns The `CryptoKey` from the provided encryption key. 574 | */ 575 | function unwrapKey(encryptionKey: EncryptionKey | CryptoKey): CryptoKey { 576 | return isEncryptionKey(encryptionKey) ? encryptionKey.key : encryptionKey; 577 | } 578 | 579 | /** 580 | * Checks if the provided vault is an updated encryption format. 581 | * 582 | * @param vault - The vault to check. 583 | * @param targetDerivationParams - The options to use for key derivation. 584 | * @returns Whether or not the vault is an updated encryption format. 585 | */ 586 | export function isVaultUpdated( 587 | vault: string, 588 | targetDerivationParams = DEFAULT_DERIVATION_PARAMS 589 | ): boolean { 590 | const { keyMetadata } = JSON.parse(vault); 591 | return ( 592 | isKeyDerivationOptions(keyMetadata) && 593 | keyMetadata.algorithm === targetDerivationParams.algorithm && 594 | keyMetadata.params.iterations === targetDerivationParams.params.iterations 595 | ); 596 | } 597 | -------------------------------------------------------------------------------- /public/assets/telegram-widget.js: -------------------------------------------------------------------------------- 1 | (function(window) { 2 | (function(window){ 3 | window.__parseFunction = function(__func, __attrs) { 4 | __attrs = __attrs || []; 5 | __func = '(function(' + __attrs.join(',') + '){' + __func + '})'; 6 | return window.execScript ? window.execScript(__func) : eval(__func); 7 | } 8 | }(window)); 9 | (function(window){ 10 | 11 | function addEvent(el, event, handler) { 12 | var events = event.split(/\s+/); 13 | for (var i = 0; i < events.length; i++) { 14 | if (el.addEventListener) { 15 | el.addEventListener(events[i], handler); 16 | } else { 17 | el.attachEvent('on' + events[i], handler); 18 | } 19 | } 20 | } 21 | function removeEvent(el, event, handler) { 22 | var events = event.split(/\s+/); 23 | for (var i = 0; i < events.length; i++) { 24 | if (el.removeEventListener) { 25 | el.removeEventListener(events[i], handler); 26 | } else { 27 | el.detachEvent('on' + events[i], handler); 28 | } 29 | } 30 | } 31 | function getCssProperty(el, prop) { 32 | if (window.getComputedStyle) { 33 | return window.getComputedStyle(el, '').getPropertyValue(prop) || null; 34 | } else if (el.currentStyle) { 35 | return el.currentStyle[prop] || null; 36 | } 37 | return null; 38 | } 39 | function geById(el_or_id) { 40 | if (typeof el_or_id == 'string' || el_or_id instanceof String) { 41 | return document.getElementById(el_or_id); 42 | } else if (el_or_id instanceof HTMLElement) { 43 | return el_or_id; 44 | } 45 | return null; 46 | } 47 | 48 | var getWidgetsOrigin = function(default_origin, dev_origin) { 49 | var link = document.createElement('A'), origin; 50 | link.href = document.currentScript && document.currentScript.src || default_origin; 51 | origin = link.origin || link.protocol + '//' + link.hostname; 52 | if (origin == 'https://telegram.org') { 53 | origin = default_origin; 54 | } else if (origin == 'https://telegram-js.azureedge.net' || origin == 'https://tg.dev') { 55 | origin = dev_origin; 56 | } 57 | return origin; 58 | }; 59 | 60 | var getPageCanonical = function() { 61 | var a = document.createElement('A'), link, href; 62 | if (document.querySelector) { 63 | link = document.querySelector('link[rel="canonical"]'); 64 | if (link && (href = link.getAttribute('href'))) { 65 | a.href = href; 66 | return a.href; 67 | } 68 | } else { 69 | var links = document.getElementsByTagName('LINK'); 70 | for (var i = 0; i < links.length; i++) { 71 | if ((link = links[i]) && 72 | (link.getAttribute('rel') == 'canonical') && 73 | (href = link.getAttribute('href'))) { 74 | a.href = href; 75 | return a.href; 76 | } 77 | } 78 | } 79 | return false; 80 | }; 81 | 82 | function haveTgAuthResult() { 83 | var locationHash = '', re = /[#\?\&]tgAuthResult=([A-Za-z0-9\-_=]*)$/, match; 84 | try { 85 | locationHash = location.hash.toString(); 86 | if (match = locationHash.match(re)) { 87 | location.hash = locationHash.replace(re, ''); 88 | var data = match[1] || ''; 89 | data = data.replace(/-/g, '+').replace(/_/g, '/'); 90 | var pad = data.length % 4; 91 | if (pad > 1) { 92 | data += new Array(5 - pad).join('='); 93 | } 94 | return JSON.parse(window.atob(data)); 95 | } 96 | } catch (e) {} 97 | return false; 98 | } 99 | 100 | function getXHR() { 101 | if (navigator.appName == "Microsoft Internet Explorer"){ 102 | return new ActiveXObject("Microsoft.XMLHTTP"); 103 | } else { 104 | return new XMLHttpRequest(); 105 | } 106 | } 107 | 108 | if (!window.Telegram) { 109 | window.Telegram = {}; 110 | } 111 | if (!window.Telegram.__WidgetUuid) { 112 | window.Telegram.__WidgetUuid = 0; 113 | } 114 | if (!window.Telegram.__WidgetLastId) { 115 | window.Telegram.__WidgetLastId = 0; 116 | } 117 | if (!window.Telegram.__WidgetCallbacks) { 118 | window.Telegram.__WidgetCallbacks = {}; 119 | } 120 | 121 | function postMessageToIframe(iframe, event, data, callback) { 122 | if (!iframe._ready) { 123 | if (!iframe._readyQueue) iframe._readyQueue = []; 124 | iframe._readyQueue.push([event, data, callback]); 125 | return; 126 | } 127 | try { 128 | data = data || {}; 129 | data.event = event; 130 | if (callback) { 131 | data._cb = ++window.Telegram.__WidgetLastId; 132 | window.Telegram.__WidgetCallbacks[data._cb] = { 133 | iframe: iframe, 134 | callback: callback 135 | }; 136 | } 137 | iframe.contentWindow.postMessage(JSON.stringify(data), '*'); 138 | } catch(e) {} 139 | } 140 | 141 | function initWidget(widgetEl) { 142 | var widgetId, widgetElId, widgetsOrigin, existsEl, 143 | src, styles = {}, allowedAttrs = [], 144 | defWidth, defHeight, scrollable = false, onInitAuthUser, onAuthUser, onUnauth; 145 | if (!widgetEl.tagName || 146 | !(widgetEl.tagName.toUpperCase() == 'SCRIPT' || 147 | widgetEl.tagName.toUpperCase() == 'BLOCKQUOTE' && 148 | widgetEl.classList.contains('telegram-post'))) { 149 | return null; 150 | } 151 | if (widgetEl._iframe) { 152 | return widgetEl._iframe; 153 | } 154 | if (widgetId = widgetEl.getAttribute('data-telegram-post')) { 155 | var comment = widgetEl.getAttribute('data-comment') || ''; 156 | widgetsOrigin = getWidgetsOrigin('https://t.me', 'https://post.tg.dev'); 157 | widgetElId = 'telegram-post-' + widgetId.replace(/[^a-z0-9_]/ig, '-') + (comment ? '-comment' + comment : ''); 158 | src = widgetsOrigin + '/' + widgetId + '?embed=1'; 159 | allowedAttrs = ['comment', 'userpic', 'mode', 'single?', 'color', 'dark', 'dark_color']; 160 | defWidth = widgetEl.getAttribute('data-width') || '100%'; 161 | defHeight = ''; 162 | styles.minWidth = '320px'; 163 | } 164 | else if (widgetId = widgetEl.getAttribute('data-telegram-discussion')) { 165 | widgetsOrigin = getWidgetsOrigin('https://t.me', 'https://post.tg.dev'); 166 | widgetElId = 'telegram-discussion-' + widgetId.replace(/[^a-z0-9_]/ig, '-') + '-' + (++window.Telegram.__WidgetUuid); 167 | var websitePageUrl = widgetEl.getAttribute('data-page-url'); 168 | if (!websitePageUrl) { 169 | websitePageUrl = getPageCanonical(); 170 | } 171 | src = widgetsOrigin + '/' + widgetId + '?embed=1&discussion=1' + (websitePageUrl ? '&page_url=' + encodeURIComponent(websitePageUrl) : ''); 172 | allowedAttrs = ['comments_limit', 'color', 'colorful', 'dark', 'dark_color', 'width', 'height']; 173 | defWidth = widgetEl.getAttribute('data-width') || '100%'; 174 | defHeight = widgetEl.getAttribute('data-height') || 0; 175 | styles.minWidth = '320px'; 176 | if (defHeight > 0) { 177 | scrollable = true; 178 | } 179 | } 180 | else if (widgetEl.hasAttribute('data-telegram-login')) { 181 | widgetId = widgetEl.getAttribute('data-telegram-login'); 182 | widgetsOrigin = getWidgetsOrigin('https://oauth.telegram.org', 'https://oauth.tg.dev'); 183 | widgetElId = 'telegram-login-' + widgetId.replace(/[^a-z0-9_]/ig, '-'); 184 | src = widgetsOrigin + '/embed/' + widgetId + '?origin=' + encodeURIComponent(location.origin || location.protocol + '//' + location.hostname) + '&return_to=' + encodeURIComponent(location.href); 185 | allowedAttrs = ['size', 'userpic', 'init_auth', 'request_access', 'radius', 'min_width', 'max_width', 'lang']; 186 | defWidth = 186; 187 | defHeight = 28; 188 | if (widgetEl.hasAttribute('data-size')) { 189 | var size = widgetEl.getAttribute('data-size'); 190 | if (size == 'small') defWidth = 148, defHeight = 20; 191 | else if (size == 'large') defWidth = 238, defHeight = 40; 192 | } 193 | if (widgetEl.hasAttribute('data-onauth')) { 194 | onInitAuthUser = onAuthUser = __parseFunction(widgetEl.getAttribute('data-onauth'), ['user']); 195 | } 196 | else if (widgetEl.hasAttribute('data-auth-url')) { 197 | var a = document.createElement('A'); 198 | a.href = widgetEl.getAttribute('data-auth-url'); 199 | onAuthUser = function(user) { 200 | var authUrl = a.href; 201 | authUrl += (authUrl.indexOf('?') >= 0) ? '&' : '?'; 202 | var params = []; 203 | for (var key in user) { 204 | params.push(key + '=' + encodeURIComponent(user[key])); 205 | } 206 | authUrl += params.join('&'); 207 | location.href = authUrl; 208 | }; 209 | } 210 | if (widgetEl.hasAttribute('data-onunauth')) { 211 | onUnauth = __parseFunction(widgetEl.getAttribute('data-onunauth')); 212 | } 213 | var auth_result = haveTgAuthResult(); 214 | if (auth_result && onAuthUser) { 215 | onAuthUser(auth_result); 216 | } 217 | } 218 | else if (widgetId = widgetEl.getAttribute('data-telegram-share-url')) { 219 | widgetsOrigin = getWidgetsOrigin('https://t.me', 'https://post.tg.dev'); 220 | widgetElId = 'telegram-share-' + window.btoa(widgetId); 221 | src = widgetsOrigin + '/share/embed?origin=' + encodeURIComponent(location.origin || location.protocol + '//' + location.hostname); 222 | allowedAttrs = ['telegram-share-url', 'comment', 'size', 'text']; 223 | defWidth = 60; 224 | defHeight = 20; 225 | if (widgetEl.getAttribute('data-size') == 'large') { 226 | defWidth = 76; 227 | defHeight = 28; 228 | } 229 | } 230 | else { 231 | return null; 232 | } 233 | existsEl = document.getElementById(widgetElId); 234 | if (existsEl) { 235 | return existsEl; 236 | } 237 | for (var i = 0; i < allowedAttrs.length; i++) { 238 | var attr = allowedAttrs[i]; 239 | var novalue = attr.substr(-1) == '?'; 240 | if (novalue) { 241 | attr = attr.slice(0, -1); 242 | } 243 | var data_attr = 'data-' + attr.replace(/_/g, '-'); 244 | if (widgetEl.hasAttribute(data_attr)) { 245 | var attr_value = novalue ? '1' : encodeURIComponent(widgetEl.getAttribute(data_attr)); 246 | src += '&' + attr + '=' + attr_value; 247 | } 248 | } 249 | function getCurCoords(iframe) { 250 | var docEl = document.documentElement; 251 | var frect = iframe.getBoundingClientRect(); 252 | return { 253 | frameTop: frect.top, 254 | frameBottom: frect.bottom, 255 | frameLeft: frect.left, 256 | frameRight: frect.right, 257 | frameWidth: frect.width, 258 | frameHeight: frect.height, 259 | scrollTop: window.pageYOffset, 260 | scrollLeft: window.pageXOffset, 261 | clientWidth: docEl.clientWidth, 262 | clientHeight: docEl.clientHeight 263 | }; 264 | } 265 | function visibilityHandler() { 266 | if (isVisible(iframe, 50)) { 267 | postMessageToIframe(iframe, 'visible', {frame: widgetElId}); 268 | } 269 | } 270 | function focusHandler() { 271 | postMessageToIframe(iframe, 'focus', {has_focus: document.hasFocus()}); 272 | } 273 | function postMessageHandler(event) { 274 | if (event.source !== iframe.contentWindow || 275 | event.origin != widgetsOrigin) { 276 | return; 277 | } 278 | try { 279 | var data = JSON.parse(event.data); 280 | } catch(e) { 281 | var data = {}; 282 | } 283 | if (data.event == 'resize') { 284 | if (data.height) { 285 | iframe.style.height = data.height + 'px'; 286 | } 287 | if (data.width) { 288 | iframe.style.width = data.width + 'px'; 289 | } 290 | } 291 | else if (data.event == 'ready') { 292 | iframe._ready = true; 293 | focusHandler(); 294 | for (var i = 0; i < iframe._readyQueue.length; i++) { 295 | var queue_item = iframe._readyQueue[i]; 296 | postMessageToIframe(iframe, queue_item[0], queue_item[1], queue_item[2]); 297 | } 298 | iframe._readyQueue = []; 299 | } 300 | else if (data.event == 'visible_off') { 301 | removeEvent(window, 'scroll', visibilityHandler); 302 | removeEvent(window, 'resize', visibilityHandler); 303 | } 304 | else if (data.event == 'get_coords') { 305 | postMessageToIframe(iframe, 'callback', { 306 | _cb: data._cb, 307 | value: getCurCoords(iframe) 308 | }); 309 | } 310 | else if (data.event == 'scroll_to') { 311 | try { 312 | window.scrollTo(data.x || 0, data.y || 0); 313 | } catch(e) {} 314 | } 315 | else if (data.event == 'auth_user') { 316 | if (data.init) { 317 | onInitAuthUser && onInitAuthUser(data.auth_data); 318 | } else { 319 | onAuthUser && onAuthUser(data.auth_data); 320 | } 321 | } 322 | else if (data.event == 'unauthorized') { 323 | onUnauth && onUnauth(); 324 | } 325 | else if (data.event == 'callback') { 326 | var cb_data = null; 327 | if (cb_data = window.Telegram.__WidgetCallbacks[data._cb]) { 328 | if (cb_data.iframe === iframe) { 329 | cb_data.callback(data.value); 330 | delete window.Telegram.__WidgetCallbacks[data._cb]; 331 | } 332 | } else { 333 | console.warn('Callback #' + data._cb + ' not found'); 334 | } 335 | } 336 | } 337 | var iframe = document.createElement('iframe'); 338 | iframe.id = widgetElId; 339 | iframe.src = src; 340 | iframe.width = defWidth; 341 | iframe.height = defHeight; 342 | iframe.setAttribute('frameborder', '0'); 343 | if (!scrollable) { 344 | iframe.setAttribute('scrolling', 'no'); 345 | iframe.style.overflow = 'hidden'; 346 | } 347 | iframe.style.colorScheme = 'light dark'; 348 | iframe.style.border = 'none'; 349 | for (var prop in styles) { 350 | iframe.style[prop] = styles[prop]; 351 | } 352 | if (widgetEl.parentNode) { 353 | widgetEl.parentNode.insertBefore(iframe, widgetEl); 354 | if (widgetEl.tagName.toUpperCase() == 'BLOCKQUOTE') { 355 | widgetEl.parentNode.removeChild(widgetEl); 356 | } 357 | } 358 | iframe._ready = false; 359 | iframe._readyQueue = []; 360 | widgetEl._iframe = iframe; 361 | addEvent(iframe, 'load', function() { 362 | removeEvent(iframe, 'load', visibilityHandler); 363 | addEvent(window, 'scroll', visibilityHandler); 364 | addEvent(window, 'resize', visibilityHandler); 365 | visibilityHandler(); 366 | }); 367 | addEvent(window, 'focus blur', focusHandler); 368 | addEvent(window, 'message', postMessageHandler); 369 | return iframe; 370 | } 371 | function isVisible(el, padding) { 372 | var node = el, val; 373 | var visibility = getCssProperty(node, 'visibility'); 374 | if (visibility == 'hidden') return false; 375 | while (node) { 376 | if (node === document.documentElement) break; 377 | var display = getCssProperty(node, 'display'); 378 | if (display == 'none') return false; 379 | var opacity = getCssProperty(node, 'opacity'); 380 | if (opacity !== null && opacity < 0.1) return false; 381 | node = node.parentNode; 382 | } 383 | if (el.getBoundingClientRect) { 384 | padding = +padding || 0; 385 | var rect = el.getBoundingClientRect(); 386 | var html = document.documentElement; 387 | if (rect.bottom < padding || 388 | rect.right < padding || 389 | rect.top > (window.innerHeight || html.clientHeight) - padding || 390 | rect.left > (window.innerWidth || html.clientWidth) - padding) { 391 | return false; 392 | } 393 | } 394 | return true; 395 | } 396 | 397 | function getAllWidgets() { 398 | var widgets = []; 399 | if (document.querySelectorAll) { 400 | widgets = document.querySelectorAll('script[data-telegram-post],blockquote.telegram-post,script[data-telegram-discussion],script[data-telegram-login],script[data-telegram-share-url]'); 401 | } else { 402 | widgets = Array.prototype.slice.apply(document.getElementsByTagName('SCRIPT')); 403 | widgets = widgets.concat(Array.prototype.slice.apply(document.getElementsByTagName('BLOCKQUOTE'))); 404 | } 405 | return widgets; 406 | } 407 | 408 | function getWidgetInfo(el_or_id, callback) { 409 | var e = null, iframe = null; 410 | if (el = geById(el_or_id)) { 411 | if (el.tagName && 412 | el.tagName.toUpperCase() == 'IFRAME') { 413 | iframe = el; 414 | } else if (el._iframe) { 415 | iframe = el._iframe; 416 | } 417 | if (iframe && callback) { 418 | postMessageToIframe(iframe, 'get_info', {}, callback); 419 | } 420 | } 421 | } 422 | 423 | function setWidgetOptions(options, el_or_id) { 424 | var e = null, iframe = null; 425 | if (typeof el_or_id === 'undefined') { 426 | var widgets = getAllWidgets(); 427 | for (var i = 0; i < widgets.length; i++) { 428 | if (iframe = widgets[i]._iframe) { 429 | postMessageToIframe(iframe, 'set_options', {options: options}); 430 | } 431 | } 432 | } else { 433 | if (el = geById(el_or_id)) { 434 | if (el.tagName && 435 | el.tagName.toUpperCase() == 'IFRAME') { 436 | iframe = el; 437 | } else if (el._iframe) { 438 | iframe = el._iframe; 439 | } 440 | if (iframe) { 441 | postMessageToIframe(iframe, 'set_options', {options: options}); 442 | } 443 | } 444 | } 445 | } 446 | 447 | if (!document.currentScript || 448 | !initWidget(document.currentScript)) { 449 | var widgets = getAllWidgets(); 450 | for (var i = 0; i < widgets.length; i++) { 451 | initWidget(widgets[i]); 452 | } 453 | } 454 | 455 | var TelegramLogin = { 456 | popups: {}, 457 | options: null, 458 | auth_callback: null, 459 | _init: function(options, auth_callback) { 460 | TelegramLogin.options = options; 461 | TelegramLogin.auth_callback = auth_callback; 462 | var auth_result = haveTgAuthResult(); 463 | if (auth_result && auth_callback) { 464 | auth_callback(auth_result); 465 | } 466 | }, 467 | _open: function(callback) { 468 | TelegramLogin._auth(TelegramLogin.options, function(authData) { 469 | if (TelegramLogin.auth_callback) { 470 | TelegramLogin.auth_callback(authData); 471 | } 472 | if (callback) { 473 | callback(authData); 474 | } 475 | }); 476 | }, 477 | _auth: function(options, callback) { 478 | var bot_id = parseInt(options.bot_id); 479 | if (!bot_id) { 480 | throw new Error('Bot id required'); 481 | } 482 | var width = 550; 483 | var height = 470; 484 | var left = Math.max(0, (screen.width - width) / 2) + (screen.availLeft | 0), 485 | top = Math.max(0, (screen.height - height) / 2) + (screen.availTop | 0); 486 | var onMessage = function (event) { 487 | try { 488 | var data = JSON.parse(event.data); 489 | } catch(e) { 490 | var data = {}; 491 | } 492 | if (!TelegramLogin.popups[bot_id]) return; 493 | if (event.source !== TelegramLogin.popups[bot_id].window) return; 494 | if (data.event == 'auth_result') { 495 | onAuthDone(data.result); 496 | } 497 | }; 498 | var onAuthDone = function (authData) { 499 | if (!TelegramLogin.popups[bot_id]) return; 500 | if (TelegramLogin.popups[bot_id].authFinished) return; 501 | callback && callback(authData); 502 | TelegramLogin.popups[bot_id].authFinished = true; 503 | removeEvent(window, 'message', onMessage); 504 | }; 505 | var checkClose = function(bot_id) { 506 | if (!TelegramLogin.popups[bot_id]) return; 507 | if (!TelegramLogin.popups[bot_id].window || 508 | TelegramLogin.popups[bot_id].window.closed) { 509 | return TelegramLogin.getAuthData(options, function(origin, authData) { 510 | onAuthDone(authData); 511 | }); 512 | } 513 | setTimeout(checkClose, 100, bot_id); 514 | } 515 | var popup_url = Telegram.Login.widgetsOrigin + '/auth?bot_id=' + encodeURIComponent(options.bot_id) + '&origin=' + encodeURIComponent(location.origin || location.protocol + '//' + location.hostname) + (options.request_access ? '&request_access=' + encodeURIComponent(options.request_access) : '') + (options.lang ? '&lang=' + encodeURIComponent(options.lang) : '') + '&return_to=' + encodeURIComponent(location.href); 516 | var popup = window.open(popup_url, 'telegram_oauth_bot' + bot_id, 'width=' + width + ',height=' + height + ',left=' + left + ',top=' + top + ',status=0,location=0,menubar=0,toolbar=0'); 517 | TelegramLogin.popups[bot_id] = { 518 | window: popup, 519 | authFinished: false 520 | }; 521 | if (popup) { 522 | addEvent(window, 'message', onMessage); 523 | popup.focus(); 524 | checkClose(bot_id); 525 | } 526 | }, 527 | getAuthData: function(options, callback) { 528 | var bot_id = parseInt(options.bot_id); 529 | if (!bot_id) { 530 | throw new Error('Bot id required'); 531 | } 532 | var xhr = getXHR(); 533 | var url = Telegram.Login.widgetsOrigin + '/auth/get'; 534 | xhr.open('POST', url); 535 | xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); 536 | xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 537 | xhr.onreadystatechange = function() { 538 | if (xhr.readyState == 4) { 539 | if (typeof xhr.responseBody == 'undefined' && xhr.responseText) { 540 | try { 541 | var result = JSON.parse(xhr.responseText); 542 | } catch(e) { 543 | var result = {}; 544 | } 545 | if (result.user) { 546 | callback(result.origin, result.user); 547 | } else { 548 | callback(result.origin, false); 549 | } 550 | } else { 551 | callback('*', false); 552 | } 553 | } 554 | }; 555 | xhr.onerror = function() { 556 | callback('*', false); 557 | }; 558 | xhr.withCredentials = true; 559 | xhr.send('bot_id=' + encodeURIComponent(options.bot_id) + (options.lang ? '&lang=' + encodeURIComponent(options.lang) : '')); 560 | } 561 | }; 562 | 563 | window.Telegram.getWidgetInfo = getWidgetInfo; 564 | window.Telegram.setWidgetOptions = setWidgetOptions; 565 | window.Telegram.Login = { 566 | init: TelegramLogin._init, 567 | open: TelegramLogin._open, 568 | auth: TelegramLogin._auth, 569 | widgetsOrigin: getWidgetsOrigin('https://oauth.telegram.org', 'https://oauth.tg.dev') 570 | }; 571 | 572 | }(window)); 573 | })(window); --------------------------------------------------------------------------------