├── .eslintrc.json
├── .gitignore
├── .vscode
└── settings.json
├── README.md
├── next.config.js
├── package.json
├── postcss.config.js
├── prisma
└── schema.prisma
├── public
└── assets
│ ├── icon
│ └── outline
│ │ ├── back.svg
│ │ ├── check.svg
│ │ ├── close.svg
│ │ ├── copy.svg
│ │ ├── home.svg
│ │ ├── inscribe.svg
│ │ ├── orders.svg
│ │ ├── qrcode.svg
│ │ ├── setting.svg
│ │ └── wallet.svg
│ ├── loading.svg
│ ├── loading2.svg
│ ├── next.svg
│ ├── okx.svg
│ ├── telegram-web-app.js
│ ├── telegram-widget.js
│ ├── telegram.svg
│ └── vercel.svg
├── src
├── api
│ ├── brc20.ts
│ ├── chain.ts
│ └── mint.ts
├── app
│ ├── [lang]
│ │ ├── inscribe
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ ├── orders
│ │ │ └── page.tsx
│ │ ├── page.tsx
│ │ └── wallet
│ │ │ └── page.tsx
│ ├── api
│ │ ├── brc20
│ │ │ ├── inscribe
│ │ │ │ └── route.ts
│ │ │ ├── mint
│ │ │ │ ├── paid
│ │ │ │ │ └── route.ts
│ │ │ │ └── route.ts
│ │ │ ├── orders
│ │ │ │ └── route.ts
│ │ │ ├── tasks
│ │ │ │ ├── create
│ │ │ │ │ └── route.ts
│ │ │ │ └── route.ts
│ │ │ └── tick
│ │ │ │ ├── [tick]
│ │ │ │ └── route.ts
│ │ │ │ └── list
│ │ │ │ └── route.ts
│ │ └── telegram
│ │ │ └── validate
│ │ │ └── route.ts
│ ├── favicon.ico
│ └── globals.css
├── components
│ ├── Brc20Minter
│ │ ├── SendModal.tsx
│ │ ├── SpeedItem.tsx
│ │ ├── index.tsx
│ │ └── useMint.ts
│ ├── HomeView
│ │ ├── MintButton.tsx
│ │ └── index.tsx
│ ├── InitApp
│ │ └── index.tsx
│ ├── LanguageChanger
│ │ └── index.tsx
│ ├── Login
│ │ └── index.tsx
│ ├── Navigator
│ │ └── index.tsx
│ ├── OrderList
│ │ ├── TaskDisplay.tsx
│ │ ├── WalletSelectModal.tsx
│ │ ├── index.tsx
│ │ └── useOrders.ts
│ ├── TransactionConfirm
│ │ └── index.tsx
│ ├── TranslationsProvider
│ │ └── index.tsx
│ └── WalletManager
│ │ ├── ConfirmMnemonic.tsx
│ │ ├── CreateOrRestoreWallet.tsx
│ │ ├── Mnemonic.tsx
│ │ ├── ReceiveModal.tsx
│ │ ├── RestoreMnemonic.tsx
│ │ ├── SelectSource.tsx
│ │ ├── SendModal.tsx
│ │ ├── SetPassword.tsx
│ │ ├── ViewMnemonicModal.tsx
│ │ ├── WalletOperator.tsx
│ │ └── index.tsx
├── hooks
│ ├── useCopy.ts
│ ├── useLatest.ts
│ ├── useLoading.ts
│ ├── useLocalstorage.ts
│ ├── useNetwork.ts
│ ├── useTgInitData.ts
│ ├── useThrottleFn.ts
│ ├── useToast.ts
│ └── useWallet.ts
├── i18n-config.ts
├── locales
│ ├── en.json
│ ├── en
│ │ ├── common.json
│ │ └── home.json
│ ├── initI18n.ts
│ ├── zh-CN.json
│ └── zh-CN
│ │ ├── common.json
│ │ └── home.json
├── middleware.ts
├── server
│ └── btc.ts
├── types
│ └── wallet.ts
├── ui
│ ├── Button
│ │ └── index.tsx
│ ├── LoadingModal
│ │ └── index.tsx
│ └── Modal
│ │ └── index.tsx
└── utils
│ ├── address.ts
│ ├── browser-passworder.ts
│ ├── etc.ts
│ ├── formater.ts
│ ├── mint.ts
│ ├── transaction.ts
│ └── unibabel.ts
├── tailwind.config.ts
├── tsconfig.json
└── yarn.lock
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.inlineSuggest.showToolbar": "onHover",
3 | "svg.preview.background": "white"
4 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
37 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {}
3 |
4 | module.exports = nextConfig
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "brc20-inscribe-bot",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "postinstall": "prisma generate",
7 | "dev": "PORT=4987 next dev",
8 | "build": "next build",
9 | "start": "next start",
10 | "lint": "next lint"
11 | },
12 | "dependencies": {
13 | "@cmdcode/crypto-utils": "^2.4.6",
14 | "@cmdcode/tapscript": "^1.4.3",
15 | "@formatjs/intl-localematcher": "^0.5.2",
16 | "@headlessui/react": "^1.7.17",
17 | "@prisma/client": "^5.7.0",
18 | "@scure/bip32": "^1.3.2",
19 | "@vercel/postgres": "^0.5.1",
20 | "bip39": "^3.1.0",
21 | "bitcoinjs-lib": "^6.1.5",
22 | "browser-passworder": "^2.0.3",
23 | "buffer": "^6.0.3",
24 | "copy-to-clipboard": "^3.3.3",
25 | "crypto-js": "^4.2.0",
26 | "daisyui": "^4.4.17",
27 | "ecpair": "^2.1.0",
28 | "i18next": "^23.7.7",
29 | "i18next-resources-to-backend": "^1.2.0",
30 | "negotiator": "^0.6.3",
31 | "next": "14.0.3",
32 | "next-i18n-router": "^5.0.2",
33 | "node-fetch": "^3.3.2",
34 | "prisma": "^5.7.0",
35 | "qrcode.react": "^3.1.0",
36 | "react": "^18",
37 | "react-dom": "^18",
38 | "react-hot-toast": "^2.4.1",
39 | "react-i18next": "^13.5.0",
40 | "react-svg": "^16.1.31",
41 | "server-only": "^0.0.1",
42 | "tailwind-merge": "^2.0.0",
43 | "uuid": "^9.0.1"
44 | },
45 | "devDependencies": {
46 | "@types/crypto-js": "^4.2.1",
47 | "@types/negotiator": "^0.6.3",
48 | "@types/node": "^20",
49 | "@types/react": "^18",
50 | "@types/react-dom": "^18",
51 | "@types/uuid": "^9.0.7",
52 | "autoprefixer": "^10.0.1",
53 | "eslint": "^8",
54 | "eslint-config-next": "14.0.3",
55 | "postcss": "^8",
56 | "tailwindcss": "^3.3.0",
57 | "typescript": "^5"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/public/assets/icon/outline/back.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/icon/outline/check.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/icon/outline/close.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/icon/outline/copy.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/icon/outline/home.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/icon/outline/inscribe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/icon/outline/orders.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/icon/outline/qrcode.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/icon/outline/setting.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/icon/outline/wallet.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/loading.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/loading2.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/okx.svg:
--------------------------------------------------------------------------------
1 |
123 |
--------------------------------------------------------------------------------
/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);
--------------------------------------------------------------------------------
/public/assets/telegram.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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/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/api/mint.ts:
--------------------------------------------------------------------------------
1 | import { v4 as uuidv4 } from "uuid";
2 |
3 | export const fetchBrc20MintInscriptionAddress = async (
4 | tick: string,
5 | amt: number,
6 | receiveAddress: string
7 | ) => {
8 | const resp = await fetch("/api/brc20/mint", {
9 | method: "POST",
10 | body: JSON.stringify({
11 | tick,
12 | amt,
13 | receiveAddress,
14 | }),
15 | headers: {
16 | "Content-Type": "application/json",
17 | },
18 | });
19 | const data = await resp.json();
20 | return data?.data;
21 | };
22 |
23 | export const createOrder = async (
24 | priv: string,
25 | tick: string,
26 | amt: number,
27 | receiveAddress: string
28 | ) => {
29 | const resp = await fetch("/api/brc20/mint", {
30 | method: "POST",
31 | body: JSON.stringify({
32 | priv,
33 | tick,
34 | amt,
35 | receiveAddress,
36 | }),
37 | headers: {
38 | "Content-Type": "application/json",
39 | },
40 | });
41 | const data = await resp.json();
42 | return data?.data;
43 | }
44 |
45 | export const createTextInscriptionTask = async (
46 | userId: string,
47 | secret: string,
48 | text: string,
49 | receiveAddress: string,
50 | inscribeAddress: string,
51 | status: string,
52 | ) => {
53 | const resp = await fetch("/api/brc20/tasks/create", {
54 | method: "POST",
55 | body: JSON.stringify({
56 | id: uuidv4(),
57 | userId,
58 | secret,
59 | text,
60 | receiveAddress,
61 | inscribeAddress,
62 | status,
63 | createdAt: new Date(),
64 | updatedAt: new Date(),
65 | }),
66 | headers: {
67 | "Content-Type": "application/json",
68 | },
69 | });
70 | const data = await resp.json();
71 | return data?.data;
72 | }
73 |
74 | export const fetchBrc20MintPaid = async (
75 | taskId: string,
76 | txid: string,
77 | vout: number,
78 | amount: number
79 | ) => {
80 | const resp = await fetch("/api/brc20/mint/paid", {
81 | method: "POST",
82 | body: JSON.stringify({
83 | taskId,
84 | txid,
85 | vout,
86 | amount,
87 | }),
88 | headers: {
89 | "Content-Type": "application/json",
90 | },
91 | });
92 | const data = await resp.json();
93 | return data?.data;
94 | };
95 |
96 | export const inscribeBrc20Mint = async (
97 | secret: string,
98 | text: string,
99 | txid: string,
100 | vout: number,
101 | amount: number,
102 | receiveAddress: string,
103 | outputAmount: number,
104 | network: "main" | "testnet"
105 | ) => {
106 | const resp = await fetch("/api/brc20/inscribe", {
107 | method: "POST",
108 | body: JSON.stringify({
109 | secret,
110 | text,
111 | txid,
112 | vout,
113 | amount,
114 | receiveAddress,
115 | network,
116 | outputAmount,
117 | }),
118 | headers: {
119 | "Content-Type": "application/json",
120 | },
121 | });
122 | const data = await resp.json();
123 | return data?.data;
124 | };
125 |
126 |
127 | export const fetchTickInfo = async (tick: string) => {
128 | const resp = await fetch(`/api/brc20/tick/${tick}`, {
129 | method: "GET",
130 | headers: {
131 | "Content-Type": "application/json",
132 | },
133 | });
134 | const data = await resp.json();
135 | return data?.data;
136 | }
--------------------------------------------------------------------------------
/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/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 |
30 |
31 |
32 | {children}
33 |
34 |
35 |
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/src/app/[lang]/orders/page.tsx:
--------------------------------------------------------------------------------
1 | import Navigator from "@/components/Navigator";
2 | import OrderList from "@/components/OrderList";
3 | import initTranslations from "@/locales/initI18n";
4 | import TranslationsProvider from "@/components/TranslationsProvider";
5 |
6 | export default async function OrdersPage({ 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 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/[lang]/page.tsx:
--------------------------------------------------------------------------------
1 | import HomeView from "@/components/HomeView";
2 | import Navigator from "@/components/Navigator";
3 | import initTranslations from "@/locales/initI18n";
4 | import TranslationsProvider from "@/components/TranslationsProvider";
5 |
6 | async function getTicks() {
7 | const baseUrl = process.env.ALPHA_BOT_URL;
8 |
9 | if (!baseUrl) {
10 | throw new Error("ALPHA_BOT_URL not found");
11 | }
12 | const resp = await fetch(`${baseUrl}/api/brc20/ticks/list`, {
13 | method: "GET",
14 | headers: {
15 | "Content-Type": "application/json",
16 | },
17 | });
18 | const data = await resp.json();
19 | return data.data;
20 | }
21 |
22 | export default async function Home({ params: { lang } }: any) {
23 | const i18nNamespaces = ["common", "home"];
24 | const { resources } = await initTranslations(lang, i18nNamespaces);
25 |
26 | const ticks = await getTicks();
27 |
28 | return (
29 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/app/api/telegram/validate/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest } from "next/server";
2 | import CryptoJS from "crypto-js";
3 |
4 | const botToken = process.env.TELEGRAM_BOT_TOKEN;
5 |
6 | const verifyTelegramWebAppData = async (
7 | telegramInitData: string,
8 | botToken: string
9 | ): Promise => {
10 | const initData = new URLSearchParams(telegramInitData);
11 | const hash = initData.get("hash");
12 | let dataToCheck: string[] = [];
13 |
14 | initData.sort();
15 | initData.forEach(
16 | (val, key) => key !== "hash" && dataToCheck.push(`${key}=${val}`)
17 | );
18 |
19 | const secret = CryptoJS.HmacSHA256(botToken, "WebAppData");
20 | const _hash = CryptoJS.HmacSHA256(dataToCheck.join("\n"), secret).toString(
21 | CryptoJS.enc.Hex
22 | );
23 |
24 | return _hash === hash;
25 | };
26 |
27 | export async function POST(req: NextRequest): Promise {
28 | const requestData = await req.json();
29 | const initData = requestData.initData;
30 | const isValid = await verifyTelegramWebAppData(initData, botToken as string);
31 |
32 | if (!isValid) {
33 | return new Response(
34 | JSON.stringify({
35 | status: "error",
36 | message: "Invalid data",
37 | }),
38 | {
39 | status: 400,
40 | }
41 | );
42 | }
43 |
44 | return new Response(
45 | JSON.stringify({
46 | code: 0,
47 | message: "success"
48 | }),
49 | {
50 | status: 200,
51 | }
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RobotLivermore/brc20-inscribe-bot/e265ebe9c1f4e7e7fbed9f25fe3712d280302045/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/src/components/Brc20Minter/SendModal.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { FC, useCallback, useEffect, useState } from "react";
4 | import { WalletCore } from "@/types/wallet";
5 | import Modal from "@/ui/Modal";
6 | import { useTranslation } from "react-i18next";
7 | import Button from "@/ui/Button";
8 | import { getPrivFromMnemonic } from "@/utils/address";
9 | import { decrypt } from "@/utils/browser-passworder";
10 | import { sendBTC } from "@/utils/transaction";
11 |
12 | interface Props {
13 | visible: boolean;
14 | toAddress: string;
15 | wallet: WalletCore;
16 | onClose: () => void;
17 | }
18 |
19 | const SendModal: React.FC = ({ visible, toAddress, wallet, onClose }) => {
20 | const [password, setPassword] = useState("");
21 | const { t } = useTranslation();
22 |
23 | const confirmSend = useCallback(async () => {
24 | if (!wallet?.encryptedSeed) {
25 | return;
26 | }
27 | const decrypted = await decrypt(password, wallet?.encryptedSeed);
28 | const priv = getPrivFromMnemonic(decrypted as string);
29 |
30 | }, [password, wallet]);
31 | return (
32 | {
33 | onClose();
34 | setPassword("");
35 | }}>
36 |
37 |
38 |
39 | {t("wallet.password")}
40 |
41 | {
46 | setPassword(e.target.value);
47 | }}
48 | />
49 |
50 |
61 |
62 | );
63 | };
64 |
65 | export default SendModal;
66 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
317 |
318 |
319 |
setIsConfirmPay(false)}
323 | />
324 |
325 |
326 | >
327 | );
328 | };
329 |
330 | export default Brc20Minter;
331 |
--------------------------------------------------------------------------------
/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/components/HomeView/MintButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useCallback } from "react";
2 | import { useRouter } from "next/navigation";
3 | import Button from "@/ui/Button";
4 | import useToast from "@/hooks/useToast";
5 | import { useTranslation } from "react-i18next";
6 |
7 | interface Props {
8 | tick: string;
9 | limit: string;
10 | protocol: string;
11 | text: string;
12 | }
13 | const MintButton: FC = ({ tick, limit, text, protocol }) => {
14 | const { t } = useTranslation("home");
15 | const toast = useToast("error");
16 | const router = useRouter();
17 |
18 | const handleMint = useCallback(async () => {
19 | if (localStorage.getItem("localWallet")) {
20 | router.push(
21 | `/inscribe?tick=${tick}&amt=${Number(limit)}&protocol=${protocol}`
22 | );
23 | } else {
24 | toast(t("home.pleaseSetUpWallet"));
25 | router.push("/wallet");
26 | }
27 | }, [router, tick, limit, protocol, toast, t]);
28 | return (
29 |
35 | );
36 | };
37 |
38 | export default MintButton;
39 |
--------------------------------------------------------------------------------
/src/components/HomeView/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { FC, useState } from "react";
4 | import MintButton from "./MintButton";
5 | import { useTranslation } from "react-i18next";
6 |
7 | interface Props {
8 | ticks: any[];
9 | }
10 | const HomeView: FC = ({ ticks }) => {
11 | const { t } = useTranslation("home");
12 |
13 | return (
14 | <>
15 |
16 |
17 | {t("home.hotBRC20Mints")}
18 |
19 |
20 |
21 |
22 |
23 | {t("home.tick")} |
24 | {t("home.holders")} |
25 | {t("home.process")} |
26 | |
27 |
28 |
29 |
30 | {ticks.map((tick) => (
31 |
32 | {tick.tick} |
33 | {tick.holders} |
34 | {(Number(tick.mint_progress) * 100).toFixed(2)}% |
35 |
36 |
42 | |
43 |
44 | ))}
45 |
46 |
47 |
48 |
49 |
50 |
51 | {t("home.hotBRC100Mints")}
52 |
53 |
54 |
55 |
56 |
57 | {t("home.tick")} |
58 | {t("home.holders")} |
59 | {t("home.process")} |
60 | |
61 |
62 |
63 |
64 |
65 | bos |
66 | 3385 |
67 | 52.81% |
68 |
69 |
75 | |
76 |
77 |
78 |
79 |
80 |
81 | >
82 | );
83 | };
84 |
85 | export default HomeView;
86 |
--------------------------------------------------------------------------------
/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/components/LanguageChanger/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from "next/navigation";
4 | import { usePathname } from "next/navigation";
5 | import { useTranslation } from "react-i18next";
6 | import { i18n as i18nConfig } from "@/i18n-config";
7 | import React from 'react'
8 |
9 | export default function LanguageChanger() {
10 | const { i18n } = useTranslation();
11 | const currentLocale = i18n.language;
12 | const router = useRouter();
13 | const currentPathname = usePathname();
14 |
15 | const handleChange = (e: React.ChangeEvent) => {
16 | const newLocale = e.target.value;
17 |
18 | // set cookie for next-i18n-router
19 | const days = 30;
20 | const date = new Date();
21 | date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
22 | const expires = "; expires=" + date.toUTCString();
23 | document.cookie = `NEXT_LOCALE=${newLocale};expires=${expires};path=/`;
24 |
25 | // redirect to the new locale path
26 | if (
27 | currentLocale === i18nConfig.defaultLocale &&
28 | !i18nConfig.prefixDefault
29 | ) {
30 | router.push("/" + newLocale + currentPathname);
31 | } else {
32 | router.push(
33 | currentPathname.replace(`/${currentLocale}`, `/${newLocale}`)
34 | );
35 | }
36 |
37 | router.refresh();
38 | };
39 |
40 | return (
41 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/Login/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { FC } from "react";
4 | import Image from "next/image";
5 | import WalletManager from "../WalletManager";
6 | import LanguageChanger from '@/components/LanguageChanger';
7 | import useTgInitData from "@/hooks/useTgInitData";
8 |
9 | const Login: FC = () => {
10 | const initDataUnsafe = useTgInitData();
11 |
12 | return (
13 | <>
14 |
15 | {initDataUnsafe?.user && (
16 |
17 | {initDataUnsafe?.user?.photo_url ? (
18 |
19 | ) : (
20 |
21 | {initDataUnsafe?.user?.first_name.slice(0, 1)}
22 |
23 | )}
24 |
{`${initDataUnsafe?.user?.first_name}`}
25 |
26 | )}
27 |
28 |
29 |
30 |
31 | >
32 | );
33 | };
34 |
35 | export default Login;
36 |
--------------------------------------------------------------------------------
/src/components/Navigator/index.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import Link from "next/link";
3 |
4 | const Navigator: React.FC = () => {
5 | return (
6 |
9 |
10 |
11 | {/* Home */}
12 |
13 |
14 |
15 | {/* Inscribe */}
16 |
17 |
21 |
22 | {/* Orders */}
23 |
24 |
28 |
29 | {/* Wallet */}
30 |
31 |
32 | );
33 | };
34 |
35 | export default Navigator;
36 |
--------------------------------------------------------------------------------
/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 | {
56 | copy(taskId);
57 | }}
58 | />
59 |
60 |
61 |
62 |
63 |
64 | 铭刻地址:
65 | {
68 | copy(inscriptionAddress);
69 | }}
70 | >
71 |
72 | {abbreviateText(inscriptionAddress, 8, 8)}
73 | {
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 | {
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/components/OrderList/WalletSelectModal.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { Fragment } from "react";
4 | import { Dialog, Transition } from "@headlessui/react";
5 | import Link from "next/link";
6 | import Image from "next/image";
7 |
8 | interface Props {
9 | visible: boolean;
10 | onClose: () => void;
11 | }
12 |
13 | const WalletSelectModal: React.FC = ({ visible, onClose }) => {
14 | return (
15 |
16 |
78 |
79 | );
80 | };
81 |
82 | export default WalletSelectModal;
83 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/src/components/TransactionConfirm/index.tsx:
--------------------------------------------------------------------------------
1 | import Modal from "@/ui/Modal";
2 | import React, { FC } from "react";
3 | import Image from "next/image";
4 | import Button from "@/ui/Button";
5 | import { useTranslation } from "react-i18next";
6 | import useWallet from "@/hooks/useWallet";
7 | import toast from "react-hot-toast";
8 | import { getPrivFromMnemonic } from "@/utils/address";
9 | import { decrypt } from "@/utils/browser-passworder";
10 |
11 | interface TransactionConfirmProps {
12 | visible: boolean;
13 | onClose: () => void;
14 | onConfirm: (priv: string) => void;
15 | }
16 |
17 | const TransactionConfirm: FC = ({
18 | visible,
19 | onConfirm,
20 | onClose,
21 | }) => {
22 | const [password, setPassword] = React.useState("");
23 | const { t } = useTranslation();
24 | const { wallet } = useWallet();
25 |
26 | // const [errorTips, setErrorTips] = React.useState("");
27 |
28 | const confirmSend = async () => {
29 | if (!wallet) {
30 | return;
31 | }
32 | try {
33 | const memonic = (await decrypt(password, wallet.encryptedSeed)) as string;
34 | const priv = getPrivFromMnemonic(memonic);
35 | if (!memonic) {
36 | toast.error(t("wallet.passwordError"));
37 | return;
38 | }
39 | onConfirm(priv);
40 | onClose();
41 | } catch (e) {
42 | console.log(t("wallet.passwordError"));
43 | toast.error(t("wallet.passwordError"));
44 | }
45 | };
46 |
47 | return (
48 |
49 |
50 |
51 |
52 |
56 |
62 |
63 | {t("wallet.password")}
64 |
65 | {
70 | setPassword(e.target.value);
71 | }}
72 | />
73 |
74 |
{
80 | // setStage("success");
81 | confirmSend();
82 | }}
83 | />
84 |
85 |
86 | );
87 | };
88 |
89 | export default TransactionConfirm;
90 |
--------------------------------------------------------------------------------
/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/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 |
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 |
150 | {t("wallet.next")}
151 |
152 |
156 | {t("wallet.back")}
157 |
158 |
159 | );
160 | };
161 |
162 | export default ConfirmMnemonic;
163 |
--------------------------------------------------------------------------------
/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/WalletManager/Mnemonic.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useEffect } from "react";
4 | import { useTranslation } from "react-i18next";
5 | import { generateMnemonic } from "bip39";
6 | import useCopy from "@/hooks/useCopy";
7 |
8 | const MnemonicDisplay: React.FC<{ mnemonic: string }> = ({ mnemonic }) => {
9 | const wordList = mnemonic.split(" ");
10 | return (
11 |
12 | {wordList.map((word, index) => (
13 |
14 |
15 | {index + 1}
16 |
17 |
21 | {word}
22 |
23 |
24 | ))}
25 |
26 | );
27 | };
28 |
29 | interface Props {
30 | onConfirm: (mnemonic: string) => void;
31 | }
32 |
33 | const Mnemonic: React.FC = ({ onConfirm }) => {
34 | const [mnemonic, setMnemonic] = React.useState("");
35 | const { t } = useTranslation();
36 |
37 | useEffect(() => {
38 | const m = generateMnemonic();
39 | setMnemonic(m);
40 | }, []);
41 |
42 | const copy = useCopy();
43 |
44 |
45 | const handleConfirmMnemonic = () => {
46 | onConfirm(mnemonic);
47 | };
48 |
49 | return (
50 |
51 |
{t("wallet.secretRecoveryphrase")}
52 |
{t("wallet.mnemonicTips")}
53 |
54 | {t("wallet.mnemonicTips2")}
55 |
56 | {/*
57 | {mnemonic}
58 |
*/}
59 |
60 |
{
63 | copy(mnemonic);
64 | }}
65 | >
66 | {t("wallet.copyToClipboard")}
67 |
68 |
69 |
73 | {t("wallet.next")}
74 |
75 |
76 | );
77 | };
78 |
79 | export default Mnemonic;
80 |
--------------------------------------------------------------------------------
/src/components/WalletManager/ReceiveModal.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Modal from "@/ui/Modal";
4 | import React from "react";
5 | import Image from "next/image";
6 | import { useTranslation } from "react-i18next";
7 | import { QRCodeCanvas } from "qrcode.react";
8 | import { abbreviateText } from "@/utils/formater";
9 | import Button from "@/ui/Button";
10 | import useCopy from "@/hooks/useCopy";
11 |
12 | interface Props {
13 | visible: boolean;
14 | onClose: () => void;
15 | address: string;
16 | }
17 |
18 | const ReceiveModal: React.FC = ({ visible, address, onClose }) => {
19 | const { t } = useTranslation();
20 | const copy = useCopy();
21 | return (
22 |
23 |
24 |
25 |
31 |
32 |
{t("wallet.receive")}
33 |
{t("wallet.receiveTips")}
34 |
35 |
36 |
37 |
{
40 | copy(address);
41 | }}
42 | >
43 | {abbreviateText(address, 8, 8)}
44 |
45 |
46 |
52 |
53 |
54 | );
55 | };
56 |
57 | export default ReceiveModal;
58 |
--------------------------------------------------------------------------------
/src/components/WalletManager/RestoreMnemonic.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { FC } from "react";
4 | import { useTranslation } from "react-i18next";
5 | import { generateMnemonic, mnemonicToSeed, validateMnemonic } from "bip39";
6 |
7 | interface Props {
8 | onConfirm: (mnemonic: string) => void;
9 | }
10 |
11 | const RestoreMnemonic: FC = ({ onConfirm }) => {
12 | const { t } = useTranslation();
13 | const [mnemonic, setMnemonic] = React.useState("");
14 |
15 | const isMnemonicValid = validateMnemonic(mnemonic);
16 | return (
17 |
18 |
{t("wallet.secretRecoveryphrase")}
19 |
{t("wallet.mnemonicTips")}
20 |
38 | );
39 | };
40 |
41 | export default RestoreMnemonic;
42 |
--------------------------------------------------------------------------------
/src/components/WalletManager/SelectSource.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Button from "@/ui/Button";
4 | import React, { FC, useState } from "react";
5 | import { useTranslation } from 'react-i18next';
6 |
7 | interface Props {
8 | onCreateNewWallet: () => void;
9 | onRestoreWallet: () => void;
10 | }
11 |
12 | const SelectSource: FC = ({ onCreateNewWallet, onRestoreWallet }) => {
13 | const { t } = useTranslation();
14 |
15 | return (
16 |
17 |
18 |
{t('wallet.createOrRestore')}
19 |
20 | {t('wallet.createWalletTips')}
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 | };
30 |
31 | export default SelectSource;
32 |
--------------------------------------------------------------------------------
/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 |
{
228 | setStage("confirm");
229 | handleCalculateFee();
230 | }}
231 | />
232 | >
233 | )}
234 | {stage === "confirm" && (
235 | <>
236 |
237 |
238 | {
241 | setStage("input");
242 | }}
243 | >
244 |
250 |
251 | {t("wallet.signTransaction")}
252 |
253 |
254 |
255 | {t("wallet.receipientAddress")}:
256 |
257 |
258 | {abbreviateText(receipient, 10, 10)}
259 |
260 |
261 | {t("wallet.spendAmount")}:
262 |
263 |
264 | {((amount || 0) * 100000000 + fee) / 100000000} BTC
265 |
266 |
267 |
268 | {
273 | setStage("password");
274 | }}
275 | />
276 | >
277 | )}
278 | {stage === "password" && (
279 | <>
280 |
281 |
282 | {
285 | setStage("confirm");
286 | }}
287 | >
288 |
294 |
295 | {t("wallet.password")}
296 |
297 | {
302 | setPassword(e.target.value);
303 | }}
304 | />
305 |
306 | {
312 | // setStage("success");
313 | confirmSend();
314 | }}
315 | />
316 | >
317 | )}
318 |
319 |
320 | );
321 | };
322 |
323 | export default SendModal;
324 |
--------------------------------------------------------------------------------
/src/components/WalletManager/SetPassword.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Button from "@/ui/Button";
4 | import React, { FC } from "react";
5 | import { useTranslation } from "react-i18next";
6 |
7 | interface Props {
8 | onBack: () => void;
9 | onNext: (psw: string) => void;
10 | }
11 |
12 | // Create your password
13 | const SetPassword: FC = ({ onBack, onNext }) => {
14 | const { t } = useTranslation();
15 | const [password, setPassword] = React.useState("");
16 | const [confirmPassword, setConfirmPassword] = React.useState("");
17 |
18 | const [errorTips, setErrorTips] = React.useState("");
19 |
20 | const isPasswordValid = password.length >= 8 && password === confirmPassword;
21 |
22 | return (
23 |
24 |
{t("wallet.createPassword")}
25 |
{t("wallet.passwordTips")}
26 |
{
32 | setPassword(e.target.value);
33 | }}
34 | onBlur={() => {
35 | if (password.length < 8) {
36 | setErrorTips(t("wallet.passwordAtLeast"));
37 | } else {
38 | setErrorTips("");
39 | }
40 | }}
41 | />
42 |
{
48 | setConfirmPassword(e.target.value);
49 | }}
50 | onBlur={() => {
51 | if (password !== confirmPassword) {
52 | setErrorTips(t("wallet.passwordNotMatch"));
53 | } else {
54 | setErrorTips("");
55 | }
56 | }}
57 | />
58 |
{errorTips}
59 |
{
64 | onNext(password)
65 | }}
66 | disabled={!isPasswordValid}
67 | />
68 |
74 |
75 | );
76 | };
77 |
78 | export default SetPassword;
79 |
--------------------------------------------------------------------------------
/src/components/WalletManager/ViewMnemonicModal.tsx:
--------------------------------------------------------------------------------
1 | import Modal from "@/ui/Modal";
2 | import React, { FC, useCallback } from "react";
3 | import Image from "next/image";
4 | import Button from "@/ui/Button";
5 | import { useTranslation } from "react-i18next";
6 | import useWallet from "@/hooks/useWallet";
7 | import toast from "react-hot-toast";
8 | import { getPrivFromMnemonic } from "@/utils/address";
9 | import { decrypt } from "@/utils/browser-passworder";
10 |
11 | interface ViewMnemonicModalProps {
12 | visible: boolean;
13 | onClose: () => void;
14 | }
15 |
16 | const ViewMnemonicModal: FC = ({
17 | visible,
18 | onClose,
19 | }) => {
20 | const [password, setPassword] = React.useState("");
21 | const { t } = useTranslation();
22 | const { wallet } = useWallet();
23 | const [mnemonic, setMnemonic] = React.useState("");
24 |
25 | // const [errorTips, setErrorTips] = React.useState("");
26 |
27 | const confirmSend = async () => {
28 | if (!wallet) {
29 | return;
30 | }
31 | try {
32 | const _m = (await decrypt(password, wallet.encryptedSeed)) as string;
33 | setMnemonic(_m);
34 | if (!_m) {
35 | toast.error(t("wallet.passwordError"));
36 | return;
37 | }
38 | } catch (e) {
39 | console.log(t("wallet.passwordError"));
40 | toast.error(t("wallet.passwordError"));
41 | }
42 | };
43 |
44 | const handleClose = useCallback(() => {
45 | onClose();
46 | setPassword("");
47 | setMnemonic("");
48 | }, [onClose]);
49 |
50 | return (
51 |
52 |
53 |
54 | {t("wallet.viewMnemonic")}
55 |
56 | {!mnemonic && (
57 |
58 | {
63 | setPassword(e.target.value);
64 | }}
65 | />
66 | {
72 | // setStage("success");
73 | confirmSend();
74 | }}
75 | />
76 |
77 | )}
78 | {mnemonic && (
79 |
80 |
81 | {mnemonic}
82 |
83 |
{`Derivation path: m/86'/0'/0'/0/0`}
84 |
89 |
95 |
96 | )}
97 |
98 |
99 | );
100 | };
101 |
102 | export default ViewMnemonicModal;
103 |
--------------------------------------------------------------------------------
/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 | {
137 | setIsOpenSendModal(true);
138 | }}
139 | />
140 | {
145 | setIsOpenReceiveModal(true);
146 | }}
147 | />
148 |
149 |
150 | )}
151 | {page === "setting" && (
152 |
153 |
154 | {t("wallet.settingTitle")}
155 |
156 |
157 |
158 |
168 |
169 |
170 |
{
175 | setIsViewMnemonic(true);
176 | }}
177 | />
178 |
179 | {
184 | setIsConfirmDelete(true);
185 | }}
186 | />
187 |
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/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/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/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;
--------------------------------------------------------------------------------
/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;
--------------------------------------------------------------------------------
/src/hooks/useLocalstorage.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from "react";
2 |
3 | function useLocalStorage(key: string, initialValue: T) {
4 | // 使用 useState 来获取 localStorage 中的值,如果不存在则使用 initialValue
5 | const [storedValue, setStoredValue] = useState(() => {
6 | if (typeof window === 'undefined') {
7 | return initialValue
8 | }
9 | try {
10 | const item = window.localStorage.getItem(key);
11 | return item ? JSON.parse(item) : initialValue;
12 | } catch (error) {
13 | console.error("Error accessing localStorage:", error);
14 | return initialValue;
15 | }
16 | });
17 |
18 | // 封装一个函数来更新 localStorage 和本地 state
19 | const setValue = useCallback((value: T) => {
20 | try {
21 | // 更新本地 state
22 | setStoredValue(value);
23 | // 更新 localStorage
24 | window.localStorage.setItem(key, JSON.stringify(value));
25 | } catch (error) {
26 | console.error("Error updating localStorage:", error);
27 | }
28 | }, [key]);
29 |
30 | return [storedValue, setValue] as [T, (value: T) => void];
31 | }
32 |
33 | export default useLocalStorage;
34 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/hooks/useWallet.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react";
2 | import { WalletCore } from "@/types/wallet";
3 |
4 | const useWallet = () => {
5 | const [wallet, setWallet] = useState(null);
6 |
7 | const clearWallet = useCallback(() => {
8 | setWallet(null);
9 | if (typeof window === "undefined") {
10 | return;
11 | }
12 | try {
13 | window.localStorage.removeItem("localWallet");
14 | } catch (error) {
15 | console.error("Error accessing localStorage:", error);
16 | return;
17 | }
18 | }, []);
19 |
20 | useEffect(() => {
21 | if (typeof window === "undefined") {
22 | return;
23 | }
24 | try {
25 | const item = window.localStorage.getItem("localWallet");
26 | if (item) {
27 | setWallet(JSON.parse(item));
28 | }
29 | } catch (error) {
30 | console.error("Error accessing localStorage:", error);
31 | return;
32 | }
33 | }, []);
34 |
35 | useEffect(() => {
36 | if (wallet?.publicKey && wallet?.publicKey.length < 64) {
37 | clearWallet();
38 | }
39 | }, [wallet?.publicKey, clearWallet]);
40 |
41 | const saveWallet = useCallback((w: WalletCore | null) => {
42 | setWallet(w);
43 | if (typeof window === "undefined") {
44 | return;
45 | }
46 | try {
47 | window.localStorage.setItem("localWallet", JSON.stringify(w));
48 | } catch (error) {
49 | console.error("Error accessing localStorage:", error);
50 | return;
51 | }
52 | }, []);
53 | return {
54 | wallet,
55 | saveWallet,
56 | clearWallet,
57 | };
58 | };
59 |
60 | export default useWallet;
61 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/locales/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "products": {
3 | "cart": "Add to Cart"
4 | }
5 | }
--------------------------------------------------------------------------------
/src/locales/en/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "confirm": "Confirm",
4 | "cancel": "Cancel",
5 | "copySuccess": "Copied",
6 | "copyFail": "Copy Failed"
7 | },
8 | "wallet": {
9 | "createOrRestore": "Create or Restore Wallet",
10 | "createWalletTips": "Please ensure that your wallet is created by yourself, and we will not store your wallet information.",
11 | "create": "Create new Wallet",
12 | "restore": "Restore Wallet",
13 | "createPassword": "Create Password",
14 | "passwordTips": "Please keep your password safe as we do not store it. If you lose your password, you will be unable to unlock your wallet.",
15 | "password": "Password",
16 | "confirmPassword": "Confirm Password",
17 | "next": "Next",
18 | "back": "Back",
19 | "secretRecoveryphrase": "Secret Recovery Phrase",
20 | "mnemonicTips": "This is the only way to restore your wallet. Please keep it safe and do not share it with anyone.",
21 | "mnemonicTips2": "Store it somewhere safe and secret. If you lose it, you will lose access to your wallet.",
22 | "copyToClipboard": "Copy to clipboard",
23 | "copiedToClipboard": "Copied",
24 | "passwordNotMatch": "Password does not match",
25 | "passwordAtLeast": "Password must be at least 8 characters",
26 | "homeTitle": "BTC Balance",
27 | "send": "Send",
28 | "receive": "Receive",
29 | "receiveTips": "Only send BTC and Ordinals NFTs to this wallet.",
30 | "receipientAddress": "Receipient Address",
31 | "balance": "Balance",
32 | "available": "Available(safe to send)",
33 | "amount": "Amount",
34 | "fee": "Fee",
35 | "slow": "Slow",
36 | "fast": "Fast",
37 | "average": "Average",
38 | "signTransaction": "Sign Transaction",
39 | "sendSuccess": "Send Success",
40 | "sendFailed": "Send Failed, please try again later",
41 | "spendAmount": "Spend Amount",
42 | "passwordError": "Password Error",
43 | "settingTitle": "Settings",
44 | "networkSelector": "Network Selector",
45 | "deleteWallet": "Delete Wallet",
46 | "viewMnemonic": "View Recovery"
47 | },
48 | "inscribe": {
49 | "mint": "Mint"
50 | }
51 | }
--------------------------------------------------------------------------------
/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/locales/initI18n.ts:
--------------------------------------------------------------------------------
1 | import { createInstance } from "i18next";
2 | import { initReactI18next } from "react-i18next/initReactI18next";
3 | import resourcesToBackend from "i18next-resources-to-backend";
4 | import { i18n as i18nConfig } from "@/i18n-config";
5 |
6 | export default async function initTranslations(
7 | locale: string ,
8 | namespaces: string[],
9 | oldI18nInstance?: ReturnType,
10 | resources?: any,
11 | ) {
12 | const i18nInstance = oldI18nInstance || createInstance();
13 |
14 | i18nInstance.use(initReactI18next);
15 |
16 | if (!resources) {
17 | i18nInstance.use(
18 | resourcesToBackend(
19 | (language: string, namespace: string) =>
20 | import(`./${language}/${namespace}.json`)
21 | )
22 | );
23 | }
24 |
25 | await i18nInstance.init({
26 | lng: locale,
27 | resources,
28 | fallbackLng: i18nConfig.defaultLocale,
29 | supportedLngs: i18nConfig.locales,
30 | defaultNS: namespaces[0],
31 | fallbackNS: namespaces[0],
32 | ns: namespaces,
33 | preload: resources ? [] : i18nConfig.locales,
34 | });
35 |
36 | return {
37 | i18n: i18nInstance,
38 | resources: i18nInstance.services.resourceStore.data,
39 | t: i18nInstance.t,
40 | };
41 | }
42 |
--------------------------------------------------------------------------------
/src/locales/zh-CN.json:
--------------------------------------------------------------------------------
1 | {
2 | "products": {
3 | "cart": "Add to Cart"
4 | }
5 | }
--------------------------------------------------------------------------------
/src/locales/zh-CN/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "confirm": "确认",
4 | "cancel": "取消",
5 | "copySuccess": "复制成功",
6 | "copyFailed": "复制失败"
7 | },
8 | "wallet": {
9 | "createOrRestore": "创建或恢复钱包",
10 | "createWalletTips": "请确保您的钱包是由您自己创建的,我们不会保存您的钱包信息",
11 | "create": "新建钱包",
12 | "restore": "恢复钱包",
13 | "createPassword": "新建密码",
14 | "passwordTips": "请妥善保管您的密码,我们不会保存您的密码,一旦您的密码丢失,将无法解锁您的钱包。",
15 | "password": "密码",
16 | "confirmPassword": "输入密码",
17 | "next": "下一步",
18 | "back": "返回",
19 | "secretRecoveryphrase": "助记词",
20 | "mnemonicTips": "助记词是您的钱包的唯一凭证,如果您丢失了助记词,将无法恢复您的钱包,请妥善保管。",
21 | "mnemonicTips2": "请按顺序将您的助记词抄写在纸上,并妥善保管。",
22 | "copyToClipboard": "点击复制",
23 | "copiedToClipboard": "复制成功",
24 | "passwordNotMatch": "两次输入的密码不一致",
25 | "passwordAtLeast": "密码至少8位",
26 | "homeTitle": "BTC余额",
27 | "send": "发送",
28 | "receive": "接收",
29 | "receiveTips": "此地址仅能接受BTC及Ordinals资产",
30 | "receipientAddress": "接收地址",
31 | "balance": "余额",
32 | "available": "可用余额",
33 | "amount": "数量",
34 | "fee": "手续费",
35 | "slow": "慢",
36 | "fast": "快",
37 | "average": "中",
38 | "signTransaction": "签名交易",
39 | "sendSuccess": "发送成功",
40 | "sendFailed": "发送失败, 请稍后再试",
41 | "spendAmount": "花费金额",
42 | "passwordError": "密码错误",
43 | "settingTitle": "设置",
44 | "networkSelector": "网络选择",
45 | "viewMnemonic": "查看助记词"
46 | },
47 | "inscribe": {
48 | "mint": "铸造"
49 | }
50 | }
--------------------------------------------------------------------------------
/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/middleware.ts:
--------------------------------------------------------------------------------
1 | // import { NextResponse } from "next/server";
2 | import type { NextRequest } from "next/server";
3 |
4 | import { i18n } from "./i18n-config";
5 |
6 | // import { match as matchLocale } from "@formatjs/intl-localematcher";
7 | // import Negotiator from "negotiator";
8 |
9 | import { i18nRouter } from 'next-i18n-router';
10 |
11 | // function getLocale(request: NextRequest): string | undefined {
12 | // // Negotiator expects plain object so we need to transform headers
13 | // const negotiatorHeaders: Record = {};
14 | // request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));
15 |
16 | // // @ts-ignore locales are readonly
17 | // const locales: string[] = i18n.locales;
18 |
19 | // // Use negotiator and intl-localematcher to get best locale
20 | // let languages = new Negotiator({ headers: negotiatorHeaders }).languages(
21 | // locales
22 | // );
23 |
24 | // const locale = matchLocale(languages, locales, i18n.defaultLocale);
25 |
26 | // return locale;
27 | // }
28 |
29 | // export function middleware(request: NextRequest) {
30 | // const pathname = request.nextUrl.pathname;
31 |
32 | // console.log(pathname, i18n.locales.every(
33 | // (locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
34 | // ));
35 |
36 | // // // `/_next/` and `/api/` are ignored by the watcher, but we need to ignore files in `public` manually.
37 | // // // If you have one
38 | // if (
39 | // [
40 | // "/manifest.json",
41 | // "/favicon.ico",
42 | // // Your other files in `public`
43 | // ].includes(pathname)
44 | // ) {
45 | // return;
46 | // }
47 |
48 | // if (pathname.startsWith("/_next/")) {
49 | // return;
50 | // }
51 |
52 | // if (pathname.startsWith("/assets/")) {
53 | // return;
54 | // }
55 |
56 | // // Check if there is any supported locale in the pathname
57 | // const pathnameIsMissingLocale = i18n.locales.every(
58 | // (locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
59 | // );
60 |
61 | // // Redirect if there is no locale
62 | // if (pathnameIsMissingLocale) {
63 | // const locale = getLocale(request);
64 |
65 | // // e.g. incoming request is /products
66 | // // The new URL is now /en-US/products
67 | // return NextResponse.redirect(
68 | // new URL(
69 | // `/${locale}${pathname.startsWith("/") ? "" : "/"}${pathname}`,
70 | // request.url
71 | // )
72 | // );
73 | // }
74 | // }
75 |
76 | export function middleware(request: NextRequest) {
77 | return i18nRouter(request, i18n);
78 | }
79 |
80 | export const config = {
81 | // Matcher ignoring `/_next/` and `/api/`
82 | matcher: ['/((?!api|_next/static|_next/image|favicon.ico|assets).*)'],
83 | // matcher: i18n.locales.map((lang) => `/${lang}/:path*`),
84 | };
85 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/src/ui/Button/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from "react";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | // button component using tailwind
5 | /**
6 | * @Button
7 | */
8 |
9 | interface Props {
10 | theme: "primary" | "outline" | 'danger';
11 | text: string;
12 | disabled?: boolean;
13 | onClick?: () => void;
14 | className?: string;
15 | }
16 |
17 | const Button: FC = ({ theme, text, onClick, className, disabled }) => {
18 | const colorStyle = {
19 | primary: "bg-black text-white disabled:bg-gray-300",
20 | outline: "bg-white text-black disabled:text-gray-300 border border-black disabled:border-gray-300",
21 | danger: "bg-red-800 text-white disabled:bg-gray-300",
22 | }
23 | const cls = twMerge(
24 | colorStyle[theme],
25 | "flex justify-center items-center",
26 | "disabled:cursor-not-allowed ",
27 | "rounded-full ",
28 | "py-3 ",
29 | "hover:scale-[1.01] active:scale-[0.98] disabled:hover:scale-100 transition-all",
30 | className
31 | );
32 | return (
33 |
34 | {text}
35 |
36 | );
37 | };
38 |
39 | export default Button;
40 |
--------------------------------------------------------------------------------
/src/ui/LoadingModal/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Modal from "@/ui/Modal";
3 |
4 | interface Props {
5 | visible: boolean;
6 | }
7 |
8 | const LoadingModal: React.FC = ({ visible }) => (
9 | {}}>
10 |
11 |
12 |
28 |
Loading...
29 |
30 |
31 |
32 | );
33 |
34 | export default LoadingModal;
--------------------------------------------------------------------------------
/src/ui/Modal/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { Fragment } from "react";
4 | import { Dialog, Transition } from "@headlessui/react";
5 |
6 | interface Props {
7 | visible: boolean;
8 | onClose: () => void;
9 | children: React.ReactNode;
10 | }
11 |
12 | const Modal: React.FC = ({ visible, children, onClose }) => {
13 | return (
14 |
15 |
46 |
47 | );
48 | };
49 |
50 | export default Modal;
51 |
--------------------------------------------------------------------------------
/src/utils/address.ts:
--------------------------------------------------------------------------------
1 | import { HDKey } from "@scure/bip32";
2 | import { keys } from "@cmdcode/crypto-utils";
3 | import { Address, Tap } from "@cmdcode/tapscript";
4 | import { mnemonicToSeedSync } from "bip39";
5 | import { encrypt, decrypt } from "@/utils/browser-passworder";
6 | import { WalletCore } from "@/types/wallet";
7 |
8 | export const generateAddressFromPubKey = (pubkey: string, network?: 'main' | 'testnet') => {
9 | const [tpubkey] = Tap.getPubKey(pubkey);
10 | const address = Address.p2tr.encode(tpubkey, network);
11 | return address;
12 | }
13 |
14 | export const getPrivFromMnemonic = (mnemonic: string) => {
15 | const seed = mnemonicToSeedSync(mnemonic, "");
16 | console.log("seed", seed.toString("hex"));
17 | const hdWallet = HDKey.fromMasterSeed(seed);
18 | // "m/86'/0'/0'/0/0"
19 | const root = hdWallet.derive("m/86'/0'/0'/0/0");
20 |
21 | const seckey = keys.get_seckey(root.privateKey as Uint8Array);
22 |
23 | return seckey.to_hex();
24 | }
25 |
26 | export const generateWalletCore = async (
27 | mnemonic: string,
28 | password: string,
29 | network?: 'main' | 'testnet'
30 | ) => {
31 | const seed = mnemonicToSeedSync(mnemonic, "");
32 | console.log("seed", seed.toString("hex"));
33 | const hdWallet = HDKey.fromMasterSeed(seed);
34 | // "m/86'/0'/0'/0/0"
35 | const root = hdWallet.derive("m/86'/0'/0'/0/0");
36 |
37 | const seckey = keys.get_seckey(root.privateKey as Uint8Array);
38 | const pubkey = keys.get_pubkey(seckey, true);
39 | // const [tpubkey] = Tap.getPubKey(pubkey);
40 | // const address = Address.p2tr.encode(tpubkey, network);
41 | const address = generateAddressFromPubKey(pubkey.to_hex(), network);
42 |
43 | const payload = await encrypt(password, mnemonic);
44 |
45 | const newWallet = {
46 | encryptedSeed: payload,
47 | taprootAddress: address,
48 | publicKey: pubkey.to_hex(),
49 | network: network || 'main',
50 | } as WalletCore;
51 |
52 | return newWallet;
53 | };
54 |
55 | export const decryptSeed = async (encryptedSeed: string, password: string) => {
56 | try {
57 | const decryptedWallet = await decrypt(password, encryptedSeed);
58 | console.log(decryptedWallet);
59 | } catch (error) {
60 | console.log(error);
61 | }
62 | };
63 |
64 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/utils/etc.ts:
--------------------------------------------------------------------------------
1 | const str = (s: unknown): s is string => typeof s === 'string';
2 | const err = (m = ''): never => { throw new Error(m); }; // error helper, messes-up stack trace
3 | const u8n = (data?: any) => new Uint8Array(data); // creates Uint8Array
4 |
5 | export function bytesToHex(bytes: Uint8Array) {
6 | return bytes.reduce(
7 | (str, byte) => str + byte.toString(16).padStart(2, "0"),
8 | ""
9 | );
10 | }
11 |
12 | export const hexToBytes = (hex: string): Uint8Array => {
13 | // hex to bytes
14 | const l = hex.length; // error if not string,
15 | if (!str(hex) || l % 2) err("hex invalid 1"); // or has odd length like 3, 5.
16 | const arr = u8n(l / 2); // create result array
17 | for (let i = 0; i < arr.length; i++) {
18 | const j = i * 2;
19 | const h = hex.slice(j, j + 2); // hexByte. slice is faster than substr
20 | const b = Number.parseInt(h, 16); // byte, created from string part
21 | if (Number.isNaN(b) || b < 0) err("hex invalid 2"); // byte must be valid 0 <= byte < 256
22 | arr[i] = b;
23 | }
24 | return arr;
25 | };
26 |
27 | export const toXOnly = (pubKey: any) =>
28 | pubKey.length === 32 ? pubKey : pubKey.slice(1, 33);
--------------------------------------------------------------------------------
/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/utils/mint.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 | function bytesToHex(bytes: Uint8Array) {
9 | return bytes.reduce(
10 | (str, byte) => str + byte.toString(16).padStart(2, "0"),
11 | ""
12 | );
13 | }
14 |
15 | export const generatePrivateKey = (): string => {
16 | const privkey = keys.gen_seckey();
17 | const privString = bytesToHex(privkey);
18 | return privString;
19 | };
20 |
21 | export const generateBrc20MintContent = (
22 | tick: string,
23 | amt: number,
24 | protocol: string = "brc-20"
25 | ): string => {
26 | const text = `{"p":"${protocol}","op":"mint","tick":"${tick}","amt":"${Math.floor(
27 | amt
28 | )}"}`;
29 | return text;
30 | };
31 |
32 | /*
33 | 铭刻过程
34 | */
35 | export function generateInscribe(
36 | secret: string,
37 | tick: string,
38 | amt: number,
39 | network: "main" | "testnet",
40 | protocol: string = "brc-20"
41 | ): string {
42 | // 读取数据
43 | const text = generateBrc20MintContent(tick, amt, protocol);
44 | console.log(text);
45 |
46 | const seckey = keys.get_seckey(secret);
47 | const pubkey = keys.get_pubkey(seckey, true);
48 | // Basic format of an 'inscription' script.
49 | const ec = new TextEncoder();
50 | const content = ec.encode(text);
51 | const mimetype = ec.encode("text/plain;charset=utf-8");
52 |
53 | const script = [
54 | pubkey,
55 | "OP_CHECKSIG",
56 | "OP_0",
57 | "OP_IF",
58 | ec.encode("ord"),
59 | "01",
60 | mimetype,
61 | "OP_0",
62 | content,
63 | "OP_ENDIF",
64 | ];
65 |
66 | // For tapscript spends, we need to convert this script into a 'tapleaf'.
67 | const tapleaf = Tap.encodeScript(script);
68 | // Generate a tapkey that includes our leaf script. Also, create a merlke proof
69 | // (cblock) that targets our leaf and proves its inclusion in the tapkey.
70 | const [tpubkey] = Tap.getPubKey(pubkey, { target: tapleaf });
71 | console.log("tpubkey", tpubkey);
72 | // A taproot address is simply the tweaked public key, encoded in bech32 format.
73 | const address = Address.p2tr.fromPubKey(tpubkey, network);
74 | console.log("Your address:", address);
75 | return address;
76 | }
77 |
--------------------------------------------------------------------------------
/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/utils/unibabel.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | export function utf8ToBinaryString(str: string): string {
4 | const escstr = encodeURIComponent(str);
5 | let binstr = escstr.replace(/%([0-9A-F]{2})/g, function(match, p1) {
6 | return String.fromCharCode(parseInt(p1, 16));
7 | });
8 |
9 | return binstr;
10 | }
11 |
12 | export function utf8ToBuffer(str: string): Uint8Array {
13 | const binstr = utf8ToBinaryString(str);
14 | const buf = binaryStringToBuffer(binstr);
15 | return buf;
16 | }
17 |
18 | export function utf8ToBase64(str: string): string {
19 | const binstr = utf8ToBinaryString(str);
20 | return btoa(binstr);
21 | }
22 |
23 | export function binaryStringToUtf8(binstr: string): string {
24 | const escstr = binstr.replace(/(.)/g, function (m, p) {
25 | let code = p.charCodeAt(0).toString(16).toUpperCase();
26 | if (code.length < 2) {
27 | code = '0' + code;
28 | }
29 | return '%' + code;
30 | });
31 |
32 | return decodeURIComponent(escstr);
33 | }
34 |
35 | export function bufferToUtf8(buf: Uint8Array): string {
36 | const binstr = bufferToBinaryString(buf);
37 |
38 | return binaryStringToUtf8(binstr);
39 | }
40 |
41 | export function base64ToUtf8(b64: string): string {
42 | const binstr = atob(b64);
43 |
44 | return binaryStringToUtf8(binstr);
45 | }
46 |
47 | export function bufferToBinaryString(buf: Uint8Array): string {
48 | const binstr = Array.prototype.map.call(buf, function (ch) {
49 | return String.fromCharCode(ch);
50 | }).join('');
51 |
52 | return binstr;
53 | }
54 |
55 | export function bufferToBase64(arr: Uint8Array): string {
56 | const binstr = bufferToBinaryString(arr);
57 | return btoa(binstr);
58 | }
59 |
60 | export function binaryStringToBuffer(binstr: string): Uint8Array {
61 | const buf = new Uint8Array(binstr.length);
62 |
63 | Array.prototype.forEach.call(binstr, function (ch, i) {
64 | buf[i] = ch.charCodeAt(0);
65 | });
66 |
67 | return buf;
68 | }
69 |
70 | export function base64ToBuffer(base64: string): Uint8Array {
71 | const binstr = atob(base64);
72 | const buf = binaryStringToBuffer(binstr);
73 | return buf;
74 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------