15 |
16 |
22 |
23 |
24 | //
25 | );
26 |
27 | ClientAvatar.propTypes = {
28 | slug: PropTypes.string.isRequired,
29 | name: PropTypes.string,
30 | image: ImageType.isRequired,
31 | className: PropTypes.string,
32 | };
33 |
34 | export default ClientAvatar;
35 |
--------------------------------------------------------------------------------
/src/assets/scss/elements/_wallet-connect.scss:
--------------------------------------------------------------------------------
1 | .wallet-list-container {
2 | justify-content: center;
3 | display: flex;
4 | align-items: center;
5 |
6 | .wallet-list {
7 | display: flex;
8 | gap: 10px;
9 | text-align: center;
10 | flex-wrap: wrap;
11 | max-width: 280px;
12 | }
13 |
14 | .wallet-img-container {
15 | background-color: var(--background-color-1);
16 | align-items: center;
17 | justify-content: center;
18 | display: flex;
19 | padding: 10px;
20 | border-radius: 6px;
21 | width: 60px;
22 | height: 60px;
23 |
24 | margin-bottom: 10px;
25 | &:hover {
26 | box-shadow: 0 0 12px var(--color-primary);
27 | background-color: var(--color-primary);
28 | }
29 | }
30 |
31 | .wallet {
32 | border: 0px;
33 | text-align: center;
34 | cursor: pointer;
35 | display: flex;
36 |
37 | align-items: center;
38 | justify-content: center;
39 | flex-direction: column;
40 | width: 86px;
41 | }
42 |
43 | p {
44 | margin-bottom: 0px !important;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/countdown-timer/countdown-timer-text.js:
--------------------------------------------------------------------------------
1 | import Countdown, { zeroPad } from "react-countdown";
2 | import PropTypes from "prop-types";
3 | import clsx from "clsx";
4 |
5 | // TODO: Callback on complete, to remove item from live biddin
6 | const CountdownTimerText = ({ time, className }) => {
7 | const renderer = ({ days, hours, minutes, seconds, completed }) => {
8 | if (completed) return null;
9 | return (
10 |
11 | {Boolean(days) && {days} days}
12 | {Boolean(hours) && {hours} hours}
13 | {Boolean(minutes) && {minutes} minutes}
14 | {Boolean(seconds) && {seconds} seconds}
15 |
16 | );
17 | };
18 | return
;
19 | };
20 |
21 | CountdownTimerText.propTypes = {
22 | time: PropTypes.number.isRequired,
23 | className: PropTypes.string,
24 | };
25 |
26 | export default CountdownTimerText;
27 |
--------------------------------------------------------------------------------
/src/components/logo/index.js:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import Anchor from "@ui/anchor";
3 | import PropTypes from "prop-types";
4 | import clsx from "clsx";
5 |
6 | const Logo = ({ className, logo, path }) => (
7 |
8 | {logo?.[0]?.src && (
9 |
10 |
11 |
12 | )}
13 | {logo?.[1]?.src && (
14 |
15 |
16 |
17 | )}
18 |
19 | );
20 |
21 | Logo.propTypes = {
22 | className: PropTypes.string,
23 | path: PropTypes.string,
24 | logo: PropTypes.arrayOf(
25 | PropTypes.shape({
26 | src: PropTypes.string.isRequired,
27 | alt: PropTypes.string,
28 | })
29 | ),
30 | };
31 |
32 | export default Logo;
33 |
--------------------------------------------------------------------------------
/src/components/widgets/quicklink-widget/index.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import Anchor from "@ui/anchor";
3 |
4 | const QuicklinkWidget = ({ data }) => (
5 |
6 |
{data.title}
7 | {data?.menu && (
8 |
9 | {data.menu.map((nav) => (
10 | -
11 | {nav.text}
12 |
13 | ))}
14 |
15 | )}
16 |
17 | );
18 |
19 | QuicklinkWidget.propTypes = {
20 | data: PropTypes.shape({
21 | title: PropTypes.string,
22 | menu: PropTypes.arrayOf(
23 | PropTypes.shape({
24 | id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
25 | text: PropTypes.string.isRequired,
26 | path: PropTypes.string.isRequired,
27 | })
28 | ),
29 | }),
30 | };
31 |
32 | export default QuicklinkWidget;
33 |
--------------------------------------------------------------------------------
/src/lib/constants.config.js:
--------------------------------------------------------------------------------
1 | export const MAX_LIMIT_ONSALE = 15;
2 | export const MAX_FETCH_LIMIT = 200;
3 | export const MAX_ONSALE = 15;
4 | export const MIN_ONSALE = 5;
5 | export const ONSALE_BATCH_SIZE = 5;
6 | // Later we can sort by priority
7 | export const OUTXO_PRIOTITY = {
8 | "image/png": 0,
9 | "image/jpeg": 1,
10 | "image/webp": 2,
11 | "image/gif": 3,
12 | "video/webm": 4,
13 | "text/plain": 100,
14 | "application/json": 200,
15 | "text/html": 300,
16 | };
17 | export const DEFAULT_UTXO_TYPES = [
18 | "image/png",
19 | "image/jpeg",
20 | "image/webp",
21 | "image/gif",
22 | "video/webm",
23 | "text/plain;charset=utf-8",
24 | "application/json",
25 | "text/html;charset=utf-8",
26 | ];
27 |
28 | export const HIDE_TEXT_UTXO_OPTION = "hide .txt";
29 | export const OTHER_UTXO_OPTION = "other";
30 |
31 | export const DEFAULT_UTXO_OPTIONS = [
32 | HIDE_TEXT_UTXO_OPTION,
33 | ...DEFAULT_UTXO_TYPES,
34 | OTHER_UTXO_OPTION,
35 | ];
36 |
37 | export const MEMPOOL_API_URL = process.env.MEMPOOL_API_URL ||
38 | (process.env.TESTNET === 'true' ? 'https://mempool.space/testnet' : 'https://mempool.space');
39 |
40 |
--------------------------------------------------------------------------------
/src/pages/_app.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import PropTypes from "prop-types";
3 | import { useRouter } from "next/router";
4 | import sal from "sal.js";
5 | import { ThemeProvider } from "next-themes";
6 | import { GoogleAnalytics } from "nextjs-google-analytics";
7 |
8 | import "../assets/css/bootstrap.min.css";
9 | import "../assets/css/feather.css";
10 | import "react-toastify/dist/ReactToastify.css";
11 | import "../assets/scss/style.scss";
12 |
13 | const MyApp = ({ Component, pageProps }) => {
14 | const router = useRouter();
15 | useEffect(() => {
16 | sal({ threshold: 0.1, once: true });
17 | }, [router.asPath]);
18 |
19 | useEffect(() => {
20 | sal();
21 | }, []);
22 | useEffect(() => {
23 | document.body.className = `${pageProps.className}`;
24 | });
25 | return (
26 |
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | MyApp.propTypes = {
34 | Component: PropTypes.elementType,
35 | pageProps: PropTypes.shape({
36 | className: PropTypes.string,
37 | }),
38 | };
39 |
40 | export default MyApp;
41 |
--------------------------------------------------------------------------------
/public/images/logo/ordswap.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | const path = require("path");
3 |
4 | const withBundleAnalyzer = require("@next/bundle-analyzer")({
5 | enabled: process.env.ANALYZE === "true",
6 | });
7 | // import { Wasm as WasmIntegration } from "@sentry/wasm";
8 |
9 | const t = {
10 | reactStrictMode: false,
11 | sassOptions: {
12 | includePaths: [path.join(__dirname, "./src/assets/scss")],
13 | },
14 | images: {
15 | domains: [
16 | "ordinals.com",
17 | "d2v3k2do8kym1f.cloudfront.net",
18 | "https://ordinals.com",
19 | "https://explorer-signet.openordex.org",
20 | ],
21 | unoptimized: true,
22 | },
23 |
24 | webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
25 | // eslint-disable-next-line no-param-reassign
26 | (config.experiments = {
27 | asyncWebAssembly: true,
28 | layers: true,
29 | }),
30 | (config.ignoreWarnings = [
31 | {
32 | message:
33 | /(magic-sdk|@walletconnect\/web3-provider|@web3auth\/web3auth)/,
34 | },
35 | ]);
36 | return config;
37 | },
38 | env: {
39 | IS_TESTNET: process.env.IS_TESTNET || false,
40 | },
41 | };
42 |
43 | module.exports = t
44 |
--------------------------------------------------------------------------------
/src/hooks/use-is-spent.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useInterval } from "react-use";
3 | import { isSpent as isInscriptionSpent } from "@services/nosft";
4 |
5 | const delay = 5000;
6 |
7 | function useIsSpent(output) {
8 | const [currentOutput, setCurrentOutput] = useState(output);
9 | const [isSpent, setIsSpent] = useState(false);
10 | const [isPooling, setIsPooling] = useState(true);
11 |
12 | const fetchIsSpent = async () => {
13 | if (!currentOutput) return;
14 | // console.log("[useIsSpent]", currentOutput);
15 | const result = await isInscriptionSpent({
16 | output: currentOutput,
17 | });
18 | setIsSpent(result.spent);
19 | setIsPooling(!result.spent);
20 | };
21 |
22 | useInterval(
23 | async () => {
24 | await fetchIsSpent();
25 | },
26 | isPooling && currentOutput ? delay : null,
27 | );
28 |
29 | useEffect(() => {
30 | if (!output) return;
31 | // console.log("[useIsSpent] output [changed]", output);
32 | setCurrentOutput(output);
33 | setIsPooling(true);
34 | fetchIsSpent();
35 | }, [output]);
36 |
37 | return { isSpent, isPooling, setIsPooling };
38 | }
39 |
40 | export default useIsSpent;
41 |
--------------------------------------------------------------------------------
/public/images/logo/favicon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/services/local-storage.js:
--------------------------------------------------------------------------------
1 | const LocalStorageKeys = {
2 | INSCRIPTIONS_OUTPOINT: "INSCRIPTION_OUTPOINT",
3 | COLLECTION_INFO: "COLLECTION_INFO",
4 | };
5 |
6 | const LocalStorage = {
7 | set: (id, data) => {
8 | if (typeof window === "undefined") {
9 | return undefined;
10 | }
11 |
12 | window.localStorage.setItem(id, JSON.stringify(data));
13 | return undefined;
14 | },
15 | get: (id) => {
16 | if (typeof window === "undefined") {
17 | return undefined;
18 | }
19 |
20 | const value = window.localStorage.getItem(id);
21 |
22 | if (!value || value === "undefined") {
23 | return undefined;
24 | }
25 |
26 | return JSON.parse(value);
27 | },
28 | remove: (id) => {
29 | if (typeof window === "undefined") {
30 | return undefined;
31 | }
32 |
33 | return window.localStorage.removeItem(id);
34 | },
35 | clear: () => {
36 | if (typeof window === "undefined") {
37 | return undefined;
38 | }
39 |
40 | return window.localStorage.clear();
41 | },
42 | };
43 |
44 | export default LocalStorage;
45 | export { LocalStorageKeys };
46 |
--------------------------------------------------------------------------------
/src/pages/tools/sign.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-syntax, no-await-in-loop, no-continue */
2 |
3 | import React, { useRef } from "react";
4 | import Wrapper from "@layout/wrapper";
5 | import Header from "@layout/header";
6 | import Footer from "@layout/footer";
7 | import SEO from "@components/seo";
8 | import Sign from "@containers/Sign";
9 | import { WalletContext } from "@context/wallet-context";
10 | import { useWalletState, useHeaderHeight } from "@hooks";
11 |
12 | export async function getStaticProps() {
13 | return { props: { className: "template-color-1" } };
14 | }
15 |
16 | const App = () => {
17 | const walletState = useWalletState();
18 | const elementRef = useRef(null);
19 | const headerHeight = useHeaderHeight(elementRef);
20 |
21 | return (
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default App;
37 |
--------------------------------------------------------------------------------
/src/assets/scss/elements/_section-title.scss:
--------------------------------------------------------------------------------
1 | /*-------------------------
2 | Section Title
3 | --------------------------*/
4 | .section-title {
5 | display: flex;
6 | h3 {
7 | margin-right: 18px;
8 | }
9 | }
10 |
11 | /*-------------------------
12 | Section Title
13 | --------------------------*/
14 | .live-title {
15 | position: relative;
16 | margin-left: 19px;
17 | padding-left: 15px;
18 | &::before {
19 | width: 9px;
20 | height: 9px;
21 | background: var(--color-primary);
22 | position: absolute;
23 | left: -17px;
24 | top: 50%;
25 | content: "";
26 | transform: translateY(-50%);
27 | border-radius: 100%;
28 | opacity: 0.5;
29 | animation: customOne 1.5s infinite;
30 | }
31 | &::after {
32 | width: 15px;
33 | height: 15px;
34 | border: 1px solid var(--color-primary);
35 | position: absolute;
36 | left: -20px;
37 | top: 50%;
38 | content: "";
39 | transform: translateY(-50%);
40 | border-radius: 100%;
41 | opacity: 1;
42 | animation: customOne 1.2s infinite;
43 | }
44 | }
45 |
46 | .ordinals-counter {
47 | font-size: 12px;
48 | }
49 |
--------------------------------------------------------------------------------
/src/assets/scss/style.scss:
--------------------------------------------------------------------------------
1 | /* Default */
2 |
3 | @import "default/variables";
4 | @import "default/typography";
5 | @import "default/spacing";
6 | @import "default/reset";
7 | @import "default/forms";
8 | @import "default/mixins";
9 | @import "default/shortcode";
10 | @import "default/common";
11 | @import "default/animations";
12 | @import "default/text-animation";
13 | @import "default/sal";
14 |
15 | /* Header */
16 | @import "header/header";
17 | @import "header/nav";
18 | @import "header/mobilemenu";
19 |
20 | @import "elements/input-range";
21 | @import "elements/button";
22 | @import "elements/backtotop";
23 | @import "elements/cursor";
24 | @import "elements/banner";
25 | @import "elements/section-title";
26 | @import "elements/footer";
27 | @import "elements/portfolio";
28 | @import "elements/modal";
29 | @import "elements/slider";
30 | @import "elements/wallet-connect";
31 | @import "elements/product-details";
32 | @import "elements/tx-success";
33 | @import "elements/sign-message";
34 | @import "elements/countdown";
35 | @import "elements/wallet";
36 | @import "elements/animated-text";
37 | @import "elements/auction_modal";
38 | @import "elements/date-picker";
39 | @import "elements/bids";
40 | @import "elements/uninscribed_stats";
41 | @import "elements/live_offers";
42 |
--------------------------------------------------------------------------------
/src/components/animated-text/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { motion, AnimatePresence } from "framer-motion";
3 | import PropTypes from "prop-types";
4 |
5 | const AnimatedText = ({ text, className }) => {
6 | const characters = text.split("");
7 |
8 | return (
9 |
10 | {characters.map((char, index) => (
11 | // eslint-disable-next-line react/no-array-index-key
12 |
13 |
20 | {char}
21 |
22 |
23 | ))}
24 |
25 | );
26 | };
27 |
28 | AnimatedText.propTypes = {
29 | text: PropTypes.string,
30 | className: PropTypes.string,
31 | };
32 |
33 | export default AnimatedText;
34 |
--------------------------------------------------------------------------------
/src/components/menu/main-menu/megamenu.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import Anchor from "@ui/anchor";
3 |
4 | const MegaMenu = ({ menu }) => (
5 |
6 |
7 |
8 | {menu.map((nav) => (
9 |
10 | {nav?.submenu && (
11 |
12 | {nav.submenu.map((subnav) => (
13 | -
14 |
15 | {subnav.text}
16 | {subnav?.icon && }
17 |
18 |
19 | ))}
20 |
21 | )}
22 |
23 | ))}
24 |
25 |
26 |
27 | );
28 |
29 | MegaMenu.propTypes = {
30 | menu: PropTypes.arrayOf(PropTypes.shape({})),
31 | };
32 |
33 | export default MegaMenu;
34 |
--------------------------------------------------------------------------------
/src/components/menu/mobile-menu/megamenu.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import Anchor from "@ui/anchor";
3 |
4 | const MegaMenu = ({ menu }) => (
5 |
6 |
7 |
8 | {menu.map((nav) => (
9 |
10 | {nav?.submenu && (
11 |
12 | {nav.submenu.map((subnav) => (
13 | -
14 |
15 | {subnav.text}
16 | {subnav?.icon && }
17 |
18 |
19 | ))}
20 |
21 | )}
22 |
23 | ))}
24 |
25 |
26 |
27 | );
28 |
29 | MegaMenu.propTypes = {
30 | menu: PropTypes.arrayOf(PropTypes.shape({})),
31 | };
32 |
33 | export default MegaMenu;
34 |
--------------------------------------------------------------------------------
/src/data/general/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "home-01",
3 | "content": [
4 | {
5 | "section": "hero-section",
6 | "headings": [
7 | {
8 | "id": 1,
9 | "content": "Manage Your Ordinals"
10 | }
11 | ],
12 | "texts": [
13 | {
14 | "id": 1,
15 | "content": "Deprecation Notice: Please remove your UTXOs from this website when you can."
16 | }
17 | ],
18 | "buttons": [
19 | {
20 | "id": 1,
21 | "content": "Get Started",
22 | "path": "/"
23 | },
24 | {
25 | "id": 2,
26 | "color": "primary-alta",
27 | "content": "Mint",
28 | "path": "/astral-babe"
29 | }
30 | ],
31 | "images": [
32 | {
33 | "src": "/images/slider/bitcoinfrog.webp"
34 | }
35 | ]
36 | },
37 | {
38 | "section": "your-collection-section",
39 | "section_title": {
40 | "title": "Your collection"
41 | }
42 | }
43 | ]
44 | }
--------------------------------------------------------------------------------
/src/services/session-storage.js:
--------------------------------------------------------------------------------
1 | const SessionsStorageKeys = {
2 | ORDINALS_PUBLIC_KEY: "ORDINALS_PUBLIC_KEY",
3 | PAYMENT_PUBLIC_KEY: "PAYMENT_PUBLIC_KEY",
4 | ORDINALS_ADDRESS: "ORDINALS_ADDRESS",
5 | INSCRIPTIONS_ON_SALE: "INSCRIPTIONS_ON_SALE",
6 | INSCRIPTIONS_OWNED: "INSCRIPTIONS_OWNED",
7 | DOMAIN: "DOMAIN",
8 | WALLET_NAME: "WALLET_NAME",
9 | PAYMENT_ADDRESS: "PAYMENT_ADDRESS",
10 | };
11 |
12 | const SessionStorage = {
13 | set: (id, data) => {
14 | if (typeof window === "undefined") {
15 | return undefined;
16 | }
17 |
18 | window.sessionStorage.setItem(id, JSON.stringify(data));
19 | return undefined;
20 | },
21 | get: (id) => {
22 | if (typeof window === "undefined") {
23 | return undefined;
24 | }
25 |
26 | const value = window.sessionStorage.getItem(id);
27 |
28 | if (!value || value === "undefined") {
29 | return undefined;
30 | }
31 |
32 | return JSON.parse(value);
33 | },
34 | remove: (id) => {
35 | if (typeof window === "undefined") {
36 | return undefined;
37 | }
38 |
39 | return window.sessionStorage.removeItem(id);
40 | },
41 | clear: () => {
42 | if (typeof window === "undefined") {
43 | return undefined;
44 | }
45 |
46 | return window.sessionStorage.clear();
47 | },
48 | };
49 |
50 | export default SessionStorage;
51 | export { SessionsStorageKeys };
52 |
--------------------------------------------------------------------------------
/src/hooks/use-auction.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useInterval } from "react-use";
3 | import { getAuctionByInscription } from "@services/nosft";
4 |
5 | const delay = 5000;
6 |
7 | function useAuction(inscriptionId) {
8 | const [currentInscriptionId, setCurrentInscriptionId] =
9 | useState(inscriptionId);
10 | const [auction, setAuction] = useState(null);
11 | const [isPooling, setIsPooling] = useState(true);
12 |
13 | const fetchAuction = async () => {
14 | if (!currentInscriptionId) return;
15 | // console.log("[useAuction]", currentInscriptionId);
16 | const auctions = await getAuctionByInscription(currentInscriptionId);
17 | const _auction = auctions?.find(
18 | (a) => a.status === "RUNNING" || a.status === "PENDING",
19 | );
20 | setAuction(_auction);
21 | setIsPooling(Boolean(_auction));
22 | };
23 |
24 | useInterval(
25 | async () => {
26 | await fetchAuction();
27 | },
28 | isPooling && currentInscriptionId ? delay : null,
29 | );
30 |
31 | useEffect(() => {
32 | if (!inscriptionId) return;
33 | setCurrentInscriptionId(inscriptionId);
34 | setIsPooling(true);
35 | fetchAuction();
36 | }, [inscriptionId]);
37 |
38 | const reset = () => {
39 | setAuction(null);
40 | setIsPooling(false);
41 | };
42 |
43 | return { auction, isPooling, setIsPooling, reset };
44 | }
45 |
46 | export default useAuction;
47 |
--------------------------------------------------------------------------------
/src/pages/marketplace.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-syntax, no-await-in-loop, no-continue */
2 |
3 | import React, { useRef } from "react";
4 | import Wrapper from "@layout/wrapper";
5 | import Header from "@layout/header";
6 | import Footer from "@layout/footer";
7 | import SEO from "@components/seo";
8 | import NostrLiveAll from "@containers/NostrLiveAll";
9 | import { WalletContext } from "@context/wallet-context";
10 | import { useWalletState, useHeaderHeight, useDeezySockets } from "@hooks";
11 | import useMarketplace from "src/hooks/use-marketplace";
12 |
13 | export async function getStaticProps() {
14 | return { props: { className: "template-color-1" } };
15 | }
16 |
17 | const App = () => {
18 | const walletState = useWalletState();
19 | const elementRef = useRef(null);
20 | const headerHeight = useHeaderHeight(elementRef);
21 |
22 | const { openOrders, loading, sourse } = useMarketplace({ realtime: true });
23 |
24 | console.log({ sourse, openOrders: openOrders.length, loading });
25 |
26 | return (
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | );
39 | };
40 |
41 | export default App;
42 |
--------------------------------------------------------------------------------
/src/assets/scss/elements/_uninscribed_stats.scss:
--------------------------------------------------------------------------------
1 | .uninscribed-sats .container {
2 | border-radius: 8px;
3 | overflow: hidden;
4 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
5 | }
6 |
7 | .uninscribed-sats .table-dark {
8 | border-radius: 8px;
9 | padding: 16px;
10 | }
11 |
12 | .uninscribed-sats .table-dark th,
13 | .uninscribed-sats .table-dark td {
14 | padding: 12px;
15 | border-color: #454d5500 !important;
16 | }
17 |
18 | /* Slightly different colors for the headers */
19 | .uninscribed-sats .table-dark th {
20 | color: white;
21 | text-transform: none;
22 | }
23 |
24 | /* Slightly different colors for the body */
25 | .uninscribed-sats .table-dark td {
26 | color: #ddd;
27 | text-transform: none;
28 | }
29 |
30 | /* Add rounded corners to the first and last cell in the first row */
31 | .uninscribed-sats .table-dark thead tr:first-child th:first-child {
32 | border-top-left-radius: 8px;
33 | }
34 |
35 | .uninscribed-sats .table-dark thead tr:first-child th:last-child {
36 | border-top-right-radius: 8px;
37 | }
38 |
39 | /* Add rounded corners to the first and last cell in the last row */
40 | .uninscribed-sats .table-dark tbody tr:last-child td:first-child {
41 | border-bottom-left-radius: 8px;
42 | }
43 |
44 | .uninscribed-sats .table-dark tbody tr:last-child td:last-child {
45 | border-bottom-right-radius: 8px;
46 | }
47 |
48 | .uninscribed-sats .table-hover tbody tr:hover {
49 | background-color: #454d55;
50 | }
51 |
52 | .uninscribed-sats .badge {
53 | font-size: 100%;
54 | }
55 |
--------------------------------------------------------------------------------
/src/pages/auction.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-syntax, no-await-in-loop, no-continue */
2 |
3 | import React, { useRef } from "react";
4 | import Wrapper from "@layout/wrapper";
5 | import Header from "@layout/header";
6 | import Footer from "@layout/footer";
7 | import SEO from "@components/seo";
8 | import NostrLiveAll from "@containers/NostrLiveAll";
9 | import { WalletContext } from "@context/wallet-context";
10 | import { useWalletState, useHeaderHeight, useAuctions } from "@hooks";
11 |
12 | export async function getStaticProps() {
13 | return { props: { className: "template-color-1" } };
14 | }
15 |
16 | const App = () => {
17 | const walletState = useWalletState();
18 | const elementRef = useRef(null);
19 | const headerHeight = useHeaderHeight(elementRef);
20 |
21 | const {
22 | auctions: openOrders,
23 | loading,
24 | source,
25 | } = useAuctions({ realtime: true });
26 |
27 | console.log({ source, openOrders: openOrders.length, loading });
28 |
29 | return (
30 |
31 |
32 |
33 |
34 |
35 |
40 |
41 |
42 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default App;
49 |
--------------------------------------------------------------------------------
/src/components/ui/slider/index.js:
--------------------------------------------------------------------------------
1 | import dynamic from "next/dynamic";
2 | import PropTypes from "prop-types";
3 | import clsx from "clsx";
4 |
5 | const SlickSlider = dynamic(() => import("react-slick"), {
6 | ssr: false,
7 | });
8 |
9 | const Slider = ({ options, children, className }) => {
10 | const settings = {
11 | dots: false,
12 | arrows: false,
13 | infinite: false,
14 | speed: 500,
15 | slidesToShow: 1,
16 | slidesToScroll: 1,
17 | adaptiveHeight: true,
18 | cssEase: "linear",
19 |
20 | ...options,
21 | };
22 |
23 | return (
24 |
25 | {children}
26 |
27 | );
28 | };
29 |
30 | Slider.propTypes = {
31 | options: PropTypes.shape({
32 | dots: PropTypes.bool,
33 | infinite: PropTypes.bool,
34 | speed: PropTypes.number,
35 | slidesToShow: PropTypes.number,
36 | slidesToScroll: PropTypes.number,
37 | autoplay: PropTypes.bool,
38 | breakpoints: PropTypes.shape({}),
39 | }),
40 | children: PropTypes.node.isRequired,
41 | className: PropTypes.string,
42 | };
43 |
44 | export default Slider;
45 |
46 | export const SliderItem = ({ children, className, ...rest }) => (
47 |
48 | {children}
49 |
50 | );
51 |
52 | SliderItem.propTypes = {
53 | children: PropTypes.node.isRequired,
54 | className: PropTypes.string,
55 | };
56 |
--------------------------------------------------------------------------------
/src/hooks/use-runes.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { getRuneData } from '@services/nosft';
3 |
4 | const useRunes = (utxos) => {
5 | const [runeData, setRuneData] = useState({});
6 | const [loading, setLoading] = useState(false);
7 |
8 | useEffect(() => {
9 | if (!utxos || utxos.length === 0) {
10 | setRuneData({});
11 | return;
12 | }
13 |
14 | const fetchRuneData = async () => {
15 | setLoading(true);
16 | const newRuneData = {};
17 |
18 | // Only fetch for uninscribed UTXOs (those without inscriptionId)
19 | const uninscribedUtxos = utxos.filter(utxo => !utxo.inscriptionId);
20 |
21 | for (const utxo of uninscribedUtxos) {
22 | const outpoint = `${utxo.txid}:${utxo.vout}`;
23 | try {
24 | const data = await getRuneData(outpoint);
25 | if (data && data.runes && data.runes.length > 0) {
26 | newRuneData[outpoint] = data.runes;
27 | }
28 | } catch (error) {
29 | console.error(`Error fetching rune data for ${outpoint}:`, error);
30 | }
31 | }
32 |
33 | setRuneData(newRuneData);
34 | setLoading(false);
35 | };
36 |
37 | fetchRuneData();
38 | }, [utxos]);
39 |
40 | const getRunesForUtxo = (utxo) => {
41 | if (!utxo) return [];
42 | const outpoint = `${utxo.txid}:${utxo.vout}`;
43 | return runeData[outpoint] || [];
44 | };
45 |
46 | return {
47 | runeData,
48 | loading,
49 | getRunesForUtxo
50 | };
51 | };
52 |
53 | export default useRunes;
--------------------------------------------------------------------------------
/src/components/menu/main-menu/index.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import PropTypes from "prop-types";
3 | import Anchor from "@ui/anchor";
4 | import clsx from "clsx";
5 | import SubMenu from "./submenu";
6 | import MegaMenu from "./megamenu";
7 | import { useWallet } from "@context/wallet-context";
8 |
9 | const MainMenu = ({ menu }) => {
10 | const [currentPath, setCurrentPath] = useState("");
11 | const { nostrOrdinalsAddress } = useWallet();
12 |
13 | useEffect(() => {
14 | if (typeof window !== "undefined") {
15 | setCurrentPath(window.location.pathname);
16 | }
17 | }, []);
18 |
19 | return (
20 |
21 | {menu
22 | .filter(
23 | (optionMenu) =>
24 | !optionMenu.private || (optionMenu.private && nostrOrdinalsAddress)
25 | )
26 | .map((nav) => (
27 | -
34 |
38 | {nav.text}
39 |
40 | {nav?.submenu && }
41 | {nav?.megamenu && }
42 |
43 | ))}
44 |
45 | );
46 | };
47 |
48 | MainMenu.propTypes = {
49 | menu: PropTypes.arrayOf(PropTypes.shape({})),
50 | };
51 |
52 | export default MainMenu;
53 |
--------------------------------------------------------------------------------
/src/hooks/use-bid.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useInterval } from "react-use";
3 | import { getNostrBid } from "@services/nosft";
4 |
5 | const delay = 5000;
6 |
7 | function useBid({ inscriptionId, output }) {
8 | const [bids, setBids] = useState(null);
9 | const [isLoading, setIsLoading] = useState(true);
10 | const [isPooling, setIsPooling] = useState(false);
11 | const [currentInscriptionId, setCurrentInscriptionId] =
12 | useState(inscriptionId);
13 | const [currentOutput, setCurrentOutput] = useState(output);
14 |
15 | const fetchBid = async () => {
16 | if (!inscriptionId) return;
17 | // console.log("[useBid]", {
18 | // inscriptionId: currentInscriptionId,
19 | // output: currentOutput,
20 | // });
21 | const result = await getNostrBid({
22 | inscriptionId: currentInscriptionId,
23 | output: currentOutput,
24 | });
25 | setBids(result);
26 | setIsLoading(false);
27 | };
28 |
29 | useInterval(
30 | async () => {
31 | await fetchBid();
32 | },
33 | isPooling && currentInscriptionId && currentOutput ? delay : null,
34 | );
35 |
36 | useEffect(() => {
37 | if (!inscriptionId || !output) {
38 | setBids(null);
39 | }
40 | setCurrentInscriptionId(inscriptionId);
41 | setCurrentOutput(output);
42 | setIsPooling(true);
43 | setIsLoading(true);
44 | fetchBid();
45 | }, [inscriptionId, output]);
46 |
47 | const reset = () => {
48 | setBids(null);
49 | setIsPooling(false);
50 | };
51 |
52 | return { bids, isPooling, setIsPooling, reset, isLoading };
53 | }
54 |
55 | export default useBid;
56 |
--------------------------------------------------------------------------------
/src/assets/scss/elements/_bids.scss:
--------------------------------------------------------------------------------
1 | /* By default, hide all buttons */
2 | .bidListComponent .bid-wrapper .pd-react-area {
3 | opacity: 0;
4 | visibility: hidden;
5 | transition:
6 | opacity 0.3s ease,
7 | visibility 0.3s ease;
8 | }
9 |
10 | /* Show button on hover */
11 | .bidListComponent .bid-wrapper:hover .pd-react-area {
12 | opacity: 1;
13 | visibility: visible;
14 | }
15 |
16 | /* Using the class approach: */
17 | /* Show button for the bid-wrapper with the class .first-bid */
18 | .bidListComponent .bid-wrapper.first-bid .pd-react-area {
19 | opacity: 1;
20 | visibility: visible;
21 | }
22 |
23 | /* Using the nth-child approach: */
24 | /* Make the button of the first .bid-wrapper visible by default */
25 | .bidListComponent
26 | .rn-pd-content-area
27 | > .bid-wrapper:nth-child(2)
28 | .pd-react-area {
29 | opacity: 1;
30 | visibility: visible;
31 | }
32 |
33 | .bidListComponent .bid-wrapper,
34 | .bidListComponent .rn-pd-content-area > .d-flex {
35 | display: grid;
36 | grid-template-columns: 1fr 1fr 1fr auto; /* Distribute space for each column */
37 | gap: 10px; /* Gap between grid items */
38 | }
39 |
40 | .bidListComponent .pd-property-inner,
41 | .bidListComponent .rn-pd-content-area > .d-flex > .flex-grow-1 {
42 | display: flex;
43 | justify-content: flex-start; /* left-align content within each grid item */
44 | align-items: center;
45 | }
46 |
47 | @keyframes dotAnimation {
48 | 0%,
49 | 20% {
50 | opacity: 0;
51 | }
52 | 100% {
53 | opacity: 1;
54 | }
55 | }
56 |
57 | .loadingDot {
58 | animation: dotAnimation 1s infinite;
59 | }
60 |
61 | .bidListComponent .modal-content {
62 | min-width: max-content;
63 | }
64 |
--------------------------------------------------------------------------------
/src/hooks/use-marketplace.js:
--------------------------------------------------------------------------------
1 | import useSWR from "swr";
2 | import axios from "axios";
3 | import { useMemo } from "react";
4 | import { NOSFT_BASE_API_URL } from "@services/nosft";
5 | import useDeezySockets from "./use-sockets";
6 |
7 | const marketplaceApiUrl = `${NOSFT_BASE_API_URL}/marketplace`;
8 |
9 | const fetcher = async (url) => {
10 | const { data } = await axios.get(url);
11 | return data.marketplace;
12 | };
13 |
14 | export default function useMarketplace({ realtime = false }) {
15 | const { data: staticSaleOffers, isLoading: isLoadingStaticSaleOffers } =
16 | useSWR(marketplaceApiUrl, fetcher);
17 |
18 | const { sales: liveSaleOffers, loadingSales: isLoadingLiveSaleOffers } =
19 | useDeezySockets({
20 | onSale: true,
21 | limitSaleResults: false,
22 | });
23 |
24 | const { openOrders, sourse } = useMemo(() => {
25 | if (
26 | realtime &&
27 | liveSaleOffers &&
28 | liveSaleOffers.length > 0 &&
29 | !isLoadingLiveSaleOffers
30 | ) {
31 | return {
32 | openOrders: liveSaleOffers,
33 | sourse: "sockets",
34 | };
35 | } else if (staticSaleOffers && staticSaleOffers.length > 0) {
36 | return {
37 | openOrders: staticSaleOffers,
38 | sourse: "api",
39 | };
40 | } else {
41 | return {
42 | openOrders: [],
43 | sourse: "",
44 | };
45 | }
46 | }, [liveSaleOffers, staticSaleOffers]);
47 |
48 | const isLoading = isLoadingStaticSaleOffers && isLoadingLiveSaleOffers;
49 |
50 | return {
51 | loading: openOrders && openOrders.length > 0 ? false : isLoading,
52 | openOrders: openOrders || [],
53 | sourse,
54 | };
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/ui/anchor/index.js:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import PropTypes from "prop-types";
3 |
4 | const Anchor = ({ path, children, className, rel, label, target, onClick, ...rest }) => {
5 | if (!path) return
{children}
;
6 | const internal = /^\/(?!\/)/.test(path);
7 | if (!internal) {
8 | const isHash = path.startsWith("#");
9 | if (isHash) {
10 | return (
11 |
12 | {children}
13 |
14 | );
15 | }
16 | return (
17 |
26 | {children}
27 |
28 | );
29 | }
30 |
31 | return (
32 |
33 | {children}
34 |
35 | );
36 | };
37 |
38 | Anchor.defaultProps = {
39 | target: "_blank",
40 | rel: "noopener noreferrer",
41 | };
42 |
43 | Anchor.propTypes = {
44 | children: PropTypes.node.isRequired,
45 | path: PropTypes.string.isRequired,
46 | className: PropTypes.string,
47 | rel: PropTypes.string,
48 | label: PropTypes.string,
49 | target: PropTypes.oneOf(["_blank", "_self", "_parent", "_top"]),
50 | onClick: PropTypes.func,
51 | };
52 |
53 | Anchor.displayName = "Anchor";
54 |
55 | export default Anchor;
56 |
--------------------------------------------------------------------------------
/src/components/collection-card/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/forbid-prop-types */
2 | import { useContext } from "react";
3 | import dynamic from "next/dynamic";
4 | import PropTypes from "prop-types";
5 |
6 | import clsx from "clsx";
7 | import Anchor from "@ui/anchor";
8 | import Image from "next/image";
9 | import ClientAvatar from "@ui/client-avatar";
10 | import ProductBid from "@components/product-bid";
11 | import { useWallet } from "@context/wallet-context";
12 | import { ImageType } from "@utils/types";
13 | import { shortenStr, MEMPOOL_API_URL } from "@services/nosft";
14 | import { InscriptionPreview } from "@components/inscription-preview";
15 | import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
16 |
17 | const CollectionCard = ({ overlay, collection }) => {
18 | return (
19 |
20 |
21 |
32 |
33 |
34 | );
35 | };
36 |
37 | CollectionCard.propTypes = {
38 | collection: PropTypes.shape({
39 | name: PropTypes.string.isRequired,
40 | slug: PropTypes.string.isRequired,
41 | icon: PropTypes.string,
42 | description: PropTypes.string,
43 | }),
44 | };
45 |
46 | CollectionCard.defaultProps = {
47 | overlay: false,
48 | };
49 |
50 | export default CollectionCard;
51 |
--------------------------------------------------------------------------------
/src/components/rune-display/index.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import { Badge } from "react-bootstrap";
3 |
4 | const RuneDisplay = ({ runes }) => {
5 | if (!runes || runes.length === 0) {
6 | return null;
7 | }
8 |
9 | const formatRuneAmount = (amount, divisibility) => {
10 | if (divisibility === 0) {
11 | return amount.toLocaleString();
12 | }
13 |
14 | const divisor = Math.pow(10, divisibility);
15 | const formattedAmount = (amount / divisor).toFixed(divisibility);
16 |
17 | // Remove trailing zeros after decimal
18 | return formattedAmount.replace(/\.?0+$/, '');
19 | };
20 |
21 | return (
22 |
23 | {runes.map(([runeName, runeData], index) => (
24 |
25 |
31 | {runeData.symbol}
32 | {runeName}
33 |
34 | {formatRuneAmount(runeData.amount, runeData.divisibility)}
35 |
36 |
37 |
38 | ))}
39 |
40 | );
41 | };
42 |
43 | RuneDisplay.propTypes = {
44 | runes: PropTypes.arrayOf(
45 | PropTypes.arrayOf(
46 | PropTypes.oneOfType([
47 | PropTypes.string,
48 | PropTypes.shape({
49 | amount: PropTypes.number.isRequired,
50 | divisibility: PropTypes.number.isRequired,
51 | symbol: PropTypes.string.isRequired,
52 | }),
53 | ])
54 | )
55 | ),
56 | };
57 |
58 | export default RuneDisplay;
--------------------------------------------------------------------------------
/src/hooks/use-infinite-scroll.js:
--------------------------------------------------------------------------------
1 | // based on: https://www.npmjs.com/package/react-infinite-scroll-hook
2 | import { useEffect } from "react";
3 | import {
4 | useTrackVisibility,
5 | IntersectionObserverHookRefCallback as UseInfiniteScrollHookRefCallback,
6 | IntersectionObserverHookRootRefCallback as UseInfiniteScrollHookRootRefCallback,
7 | } from "react-intersection-observer-hook";
8 |
9 | const DEFAULT_DELAY_IN_MS = 100;
10 |
11 | export { UseInfiniteScrollHookRefCallback, UseInfiniteScrollHookRootRefCallback };
12 |
13 | function useInfiniteScroll({
14 | loading,
15 | hasNextPage,
16 | onLoadMore,
17 | rootMargin,
18 | disabled,
19 | delayInMs = DEFAULT_DELAY_IN_MS,
20 | }) {
21 | const [ref, { rootRef, isVisible }] = useTrackVisibility({
22 | rootMargin,
23 | });
24 |
25 | const shouldLoadMore = !disabled && !loading && isVisible && hasNextPage;
26 |
27 | // eslint-disable-next-line consistent-return
28 | useEffect(() => {
29 | if (shouldLoadMore) {
30 | // When we trigger 'onLoadMore' and new items are added to the list,
31 | // right before they become rendered on the screen, 'loading' becomes false
32 | // and 'isVisible' can be true for a brief time, based on the scroll position.
33 | // So, it triggers 'onLoadMore' just after the first one is finished.
34 | // We use a small delay here to prevent this kind of situations.
35 | // It can be configured by hook args.
36 | const timer = setTimeout(() => {
37 | onLoadMore();
38 | }, delayInMs);
39 | return () => {
40 | clearTimeout(timer);
41 | };
42 | }
43 | }, [onLoadMore, shouldLoadMore, delayInMs]);
44 |
45 | return [ref, { rootRef }];
46 | }
47 |
48 | export default useInfiniteScroll;
49 |
--------------------------------------------------------------------------------
/src/components/ui/scroll-to-top/index.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import clsx from "clsx";
3 | import { useScrollToTop } from "@hooks";
4 |
5 | const ScrollToTop = () => {
6 | const { stick, onClickHandler } = useScrollToTop();
7 |
8 | useEffect(() => {
9 | const progressPath = document.querySelector(".rn-progress-parent path");
10 | const pathLength = progressPath.getTotalLength();
11 | progressPath.style.transition = progressPath.style.WebkitTransition = "none";
12 | progressPath.style.strokeDasharray = `${pathLength} ${pathLength}`;
13 | progressPath.style.strokeDashoffset = pathLength;
14 | progressPath.getBoundingClientRect();
15 | progressPath.style.transition = progressPath.style.WebkitTransition = "stroke-dashoffset 10ms linear";
16 | const updateProgress = () => {
17 | const scroll = window.scrollY;
18 | const docHeight = document.body.offsetHeight;
19 | const winHeight = window.innerHeight;
20 | const height = docHeight - winHeight;
21 | const progress = pathLength - (scroll * pathLength) / height;
22 | progressPath.style.strokeDashoffset = progress;
23 | };
24 | updateProgress();
25 | window.addEventListener("scroll", updateProgress);
26 | });
27 |
28 | return (
29 |
e}
34 | tabIndex={-1}
35 | >
36 |
39 |
40 | );
41 | };
42 |
43 | export default ScrollToTop;
44 |
--------------------------------------------------------------------------------
/src/hooks/use-auctions.js:
--------------------------------------------------------------------------------
1 | import useSWR from "swr";
2 | import { useDeezySockets } from "@hooks";
3 | import { NOSFT_BASE_API_URL } from "@services/nosft";
4 | import axios from "axios";
5 | import { useMemo } from "react";
6 |
7 | const auctionsApiUrl = `${NOSFT_BASE_API_URL}/auctions`;
8 |
9 | const fetcher = async () => {
10 | const { data } = await axios.get(auctionsApiUrl);
11 | return {
12 | auctions: data.auctions.map((auction) => ({ ...auction, auction: true })),
13 | };
14 | };
15 |
16 | export default function useAuctions({ realtime = false }) {
17 | const { data: staticAuctions, isValidating: isLoadingStaticAuctions } =
18 | useSWR(auctionsApiUrl, fetcher);
19 |
20 | const { auctions: liveAuctions, loadingAuctions } = useDeezySockets({
21 | onAuction: true,
22 | limitSaleResults: false,
23 | });
24 |
25 | const auctionsResult = useMemo(() => {
26 | const hasLiveAuctions = liveAuctions && liveAuctions.length > 0;
27 | const hasStaticAuctions = staticAuctions && staticAuctions.auctions;
28 |
29 | // Prioritize real-time data from WebSocket if it's ready
30 | if (realtime && !loadingAuctions && hasLiveAuctions) {
31 | return {
32 | source: "sockets",
33 | auctions: liveAuctions,
34 | loading: false,
35 | };
36 | }
37 |
38 | // Fall back to staticAuctions data if WebSocket data isn't ready
39 | if (hasStaticAuctions) {
40 | return {
41 | source: "api",
42 | auctions: staticAuctions.auctions,
43 | loading: isLoadingStaticAuctions,
44 | };
45 | }
46 |
47 | // If neither is available, indicate loading state
48 | return {
49 | source: "",
50 | auctions: [],
51 | loading: isLoadingStaticAuctions || loadingAuctions,
52 | };
53 | }, [isLoadingStaticAuctions, loadingAuctions, liveAuctions, staticAuctions]);
54 |
55 | return auctionsResult;
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/countdown-timer/index.js:
--------------------------------------------------------------------------------
1 | import Countdown, { zeroPad } from "react-countdown";
2 | import PropTypes from "prop-types";
3 |
4 | // TODO: Callback on complete, to remove item from live biddin
5 | const CountdownTimer = ({ time }) => {
6 | const renderer = ({ days, hours, minutes, seconds, completed }) => {
7 | if (completed) return null;
8 | return (
9 |
10 | {days > 0 && (
11 |
12 | {days}
13 | Days
14 |
15 | )}
16 | {hours > 0 && (
17 |
18 | {hours}
19 | Hours
20 |
21 | )}
22 | {minutes > 0 && (
23 |
24 | {zeroPad(minutes)}
25 | Minutes
26 |
27 | )}
28 |
29 |
30 | {zeroPad(seconds)}
31 | Seconds
32 |
33 |
34 | );
35 | };
36 | if (!time) return null;
37 | return
;
38 | };
39 |
40 | CountdownTimer.propTypes = {
41 | time: PropTypes.number.isRequired,
42 | };
43 |
44 | export default CountdownTimer;
45 |
--------------------------------------------------------------------------------
/src/components/inscription-preview/index.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import PropTypes from "prop-types";
3 | import { isImageInscription, ORDINALS_EXPLORER_URL } from "@services/nosft";
4 | import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
5 |
6 | export const InscriptionPreview = ({ utxo }) => {
7 | const [loading, setLoading] = useState(true);
8 |
9 | const handleLoad = () => {
10 | setLoading(false);
11 | };
12 |
13 | const renderImage = () => {
14 | if (!utxo) {
15 | return
;
16 | }
17 |
18 | if (isImageInscription(utxo) || !utxo.inscriptionId) {
19 | let imgUrl = `${ORDINALS_EXPLORER_URL}/content/${utxo.inscriptionId}`;
20 | if (!utxo.inscriptionId) {
21 | imgUrl = "/images/logo/bitcoin.png";
22 | }
23 | return

;
24 | }
25 |
26 | return (
27 |
36 | );
37 | };
38 |
39 | return (
40 |
41 | {renderImage()}
42 |
43 | );
44 | };
45 |
46 | InscriptionPreview.propTypes = {
47 | utxo: PropTypes.shape({
48 | content_type: PropTypes.string,
49 | inscriptionId: PropTypes.string,
50 | txId: PropTypes.string,
51 | }),
52 | };
53 |
--------------------------------------------------------------------------------
/src/utils/nostr-relay.js:
--------------------------------------------------------------------------------
1 | import { Observable } from "rxjs";
2 | import {
3 | subscribeOrders as subscribeNosftOrders,
4 | unsubscribeOrders,
5 | subscribeAuctionOrders,
6 | subscribeMyAuctions,
7 | } from "@services/nosft";
8 |
9 | const Nostr = function () {
10 | const nostrModule = {
11 | subscriptionOrders: null,
12 | subscribeOrders: ({ address, limit, type = "live" }) =>
13 | new Observable(async (observer) => {
14 | try {
15 | nostrModule.unsubscribeOrders();
16 | const orderEvent = (err, event) => {
17 | if (err) {
18 | observer.error(err);
19 | } else {
20 | observer.next(event);
21 | }
22 | };
23 |
24 | if (type === "bidding") {
25 | nostrModule.subscriptionOrders = subscribeAuctionOrders({ callback: orderEvent, limit });
26 | return;
27 | }
28 |
29 | if (type === "my-bidding") {
30 | nostrModule.subscriptionOrders = subscribeMyAuctions({ callback: orderEvent, limit, address });
31 | return;
32 | }
33 |
34 | nostrModule.subscriptionOrders = subscribeNosftOrders({ callback: orderEvent, limit });
35 | } catch (error) {
36 | observer.error(error);
37 | }
38 | }),
39 | unsubscribeOrders: () => {
40 | if (nostrModule.subscriptionOrders) {
41 | unsubscribeOrders();
42 | if (nostrModule.subscriptionOrders.unsub) {
43 | nostrModule.subscriptionOrders.unsub();
44 | }
45 | nostrModule.subscriptionOrders = null;
46 | }
47 | },
48 | };
49 | return nostrModule;
50 | };
51 |
52 | const nostrPool = Nostr();
53 | export { nostrPool };
54 |
--------------------------------------------------------------------------------
/src/hooks/use-home.js:
--------------------------------------------------------------------------------
1 | import { useDeezySockets } from "@hooks";
2 | import useSWR from "swr";
3 | import { NOSFT_BASE_API_URL } from "@services/nosft";
4 |
5 | import axios from "axios";
6 | import { useMemo } from "react";
7 |
8 | const homeApiUrl = `${NOSFT_BASE_API_URL}/home`;
9 |
10 | const fetcher = async () => {
11 | const { data } = await axios.get(homeApiUrl);
12 | return {
13 | auctions: data.auctions.map((auction) => ({ ...auction, auction: true })),
14 | sales: data.marketplace,
15 | };
16 | };
17 |
18 | export default function useHome({ realtime = false }) {
19 | const {
20 | sales: liveSales,
21 | auctions: liveAuctions,
22 | loadingAuctions: isLoadingLiveAuctions,
23 | loadingSales: isLoadingLiveSales,
24 | } = useDeezySockets({
25 | onSale: true,
26 | onAuction: true,
27 | limitAuctionResults: true,
28 | limitSaleResults: true,
29 | });
30 |
31 | const { data: staticHome, isLoading } = useSWR(homeApiUrl, fetcher);
32 |
33 | const home = useMemo(() => {
34 | // Prioritize the sockets result if it's ready
35 | if (realtime && !isLoadingLiveAuctions && !isLoadingLiveSales) {
36 | return {
37 | sourse: "sockets",
38 | auctions: liveAuctions || [],
39 | sales: liveSales || [],
40 | loading: isLoadingLiveAuctions || isLoadingLiveSales,
41 | };
42 | }
43 |
44 | // If sockets result is not ready, use staticHome
45 | if (staticHome && staticHome.auctions && staticHome.sales) {
46 | return {
47 | auctions: staticHome.auctions || [],
48 | sales: staticHome.sales || [],
49 | sourse: "api",
50 | loading: isLoading,
51 | };
52 | }
53 |
54 | // Default return if none of the above conditions are met
55 | return {
56 | auctions: [],
57 | sales: [],
58 | sourse: "",
59 | loading: true,
60 | };
61 | }, [
62 | isLoadingLiveAuctions,
63 | isLoadingLiveSales,
64 | liveAuctions,
65 | liveSales,
66 | staticHome,
67 | isLoading,
68 | ]);
69 |
70 | return home;
71 | }
72 |
--------------------------------------------------------------------------------
/src/components/ui/button/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/button-has-type */
2 | import PropTypes from "prop-types";
3 | import clsx from "clsx";
4 | import Anchor from "../anchor";
5 |
6 | const Button = ({ children, type, label, onClick, className, path, size, color, shape, fullwidth, ...rest }) => {
7 | if (path) {
8 | return (
9 |
23 | {children}
24 |
25 | );
26 | }
27 |
28 | return (
29 |
45 | );
46 | };
47 |
48 | Button.propTypes = {
49 | children: PropTypes.node.isRequired,
50 | type: PropTypes.oneOf(["button", "submit", "reset"]),
51 | label: PropTypes.string,
52 | onClick: PropTypes.func,
53 | className: PropTypes.string,
54 | path: PropTypes.string,
55 | size: PropTypes.oneOf(["large", "small", "medium"]),
56 | color: PropTypes.oneOf(["primary", "primary-alta"]),
57 | shape: PropTypes.oneOf(["square", "ellipse"]),
58 | fullwidth: PropTypes.bool,
59 | };
60 |
61 | Button.defaultProps = {
62 | type: "button",
63 | size: "large",
64 | color: "primary",
65 | };
66 |
67 | export default Button;
68 |
--------------------------------------------------------------------------------
/src/components/transaction-sent-confirmation/index.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import Lottie from "lottie-react";
3 | import Button from "@ui/button";
4 | import { shortenStr, MEMPOOL_API_URL } from "@services/nosft";
5 | import { toast } from "react-toastify";
6 | import checkAnimation from "./check.json";
7 |
8 | const TransactionSent = ({ txId, onClose, title = "Transaction Sent" }) => {
9 | const submit = () => {
10 | window.open(`${MEMPOOL_API_URL}/tx/${txId}`, "_blank");
11 | };
12 | return (
13 |
14 |
15 |
16 |
{title}
17 |
18 |
29 |
30 |
31 |
32 |
42 |
43 |
46 |
47 | );
48 | };
49 |
50 | TransactionSent.propTypes = {
51 | title: PropTypes.string,
52 | txId: PropTypes.string,
53 | onClose: PropTypes.func.isRequired,
54 | };
55 |
56 | export default TransactionSent;
57 |
--------------------------------------------------------------------------------
/src/containers/HeroArea.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import Image from "next/image";
3 | import Button from "@ui/button";
4 | import { HeadingType, TextType, ButtonType, ImageType } from "@utils/types";
5 | import ConnectWallet from "@components/modals/connect-wallet";
6 | import { useWallet } from "@context/wallet-context";
7 |
8 | const HeroArea = ({ data }) => {
9 | const { nostrOrdinalsAddress, onShowConnectModal } = useWallet();
10 |
11 | return (
12 |
13 |
14 |
15 |
16 | {data?.headings[0]?.content && (
17 |
{data.headings[0].content}
18 | )}
19 | {data?.texts?.map((text) => (
20 |
21 | {text.content}
22 |
23 | ))}
24 |
25 | {!nostrOrdinalsAddress && (
26 |
27 |
28 |
29 |
30 |
31 | )}
32 |
33 |
34 | {data?.images?.[0]?.src && (
35 |
36 |
43 |
44 | )}
45 |
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | HeroArea.propTypes = {
53 | data: PropTypes.shape({
54 | headings: PropTypes.arrayOf(HeadingType),
55 | texts: PropTypes.arrayOf(TextType),
56 | buttons: PropTypes.arrayOf(ButtonType),
57 | images: PropTypes.arrayOf(ImageType),
58 | }),
59 | };
60 |
61 | export default HeroArea;
62 |
--------------------------------------------------------------------------------
/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import React, { useRef, useEffect } from "react";
2 | import { useRouter } from "next/router";
3 | import Wrapper from "@layout/wrapper";
4 | import Header from "@layout/header";
5 | import Footer from "@layout/footer";
6 | import SEO from "@components/seo";
7 | import HeroArea from "@containers/HeroArea";
8 | import OrdinalsArea from "@containers/OrdinalsArea";
9 | import { normalizedData } from "@utils/methods";
10 | import homepageData from "@data/general/home.json";
11 | import NostrLive from "@containers/NostrLive";
12 | import MainCollections from "@containers/MainCollections";
13 | import { useWalletState, useHeaderHeight, useHome } from "@hooks";
14 | import { WalletContext } from "@context/wallet-context";
15 |
16 | export async function getStaticProps() {
17 | return { props: { className: "template-color-1" } };
18 | }
19 |
20 | const App = () => {
21 | const walletState = useWalletState();
22 | const { ordinalsPublicKey, nostrOrdinalsAddress } = walletState;
23 | const elementRef = useRef(null);
24 | const headerHeight = useHeaderHeight(elementRef);
25 | const { sourse, sales, auctions, loading } = useHome({ realtime: true });
26 | const router = useRouter();
27 |
28 | useEffect(() => {
29 | if (ordinalsPublicKey && nostrOrdinalsAddress) {
30 | router.replace("/wallet");
31 | }
32 | }, [ordinalsPublicKey, nostrOrdinalsAddress, router]);
33 |
34 | const content = normalizedData(homepageData?.content || []);
35 |
36 | const userLoggedIn = ordinalsPublicKey && nostrOrdinalsAddress;
37 |
38 | if (userLoggedIn) {
39 | // Optionally, render nothing while redirecting
40 | return null;
41 | }
42 |
43 | return (
44 |
45 |
46 |
47 |
48 |
49 |
50 | {/*
51 |
52 | */}
53 |
54 |
55 |
56 |
57 | );
58 | };
59 |
60 | export default App;
61 |
--------------------------------------------------------------------------------
/src/pages/wallet.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-syntax, no-await-in-loop, no-continue */
2 |
3 | import React, { useRef } from "react";
4 | import Wrapper from "@layout/wrapper";
5 | import Header from "@layout/header";
6 | import Footer from "@layout/footer";
7 | import SEO from "@components/seo";
8 |
9 | import { useWalletState, useHeaderHeight } from "@hooks";
10 | import { WalletContext } from "@context/wallet-context";
11 | import WalletArea from "@containers/WalletArea";
12 |
13 | export async function getStaticProps() {
14 | return { props: { className: "template-color-1" } };
15 | }
16 |
17 | const App = () => {
18 | const walletState = useWalletState();
19 | const { ordinalsPublicKey, nostrOrdinalsAddress } = walletState;
20 | const elementRef = useRef(null);
21 | const headerHeight = useHeaderHeight(elementRef);
22 | return (
23 |
24 |
25 |
26 |
27 |
28 | {ordinalsPublicKey && nostrOrdinalsAddress ? (
29 |
30 | ) : (
31 |
40 |
46 | Please connect your wallet
47 |
48 |
53 | To access your ordinals, inscriptions, and runes, connect a supported wallet using the button in the top right.
54 |
55 |
56 | )}
57 |
58 |
59 |
60 |
61 | );
62 | };
63 |
64 | export default App;
65 |
--------------------------------------------------------------------------------
/src/components/card-options/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/forbid-prop-types */
2 | import PropTypes from "prop-types";
3 | import { useState } from "react";
4 | import Dropdown from "react-bootstrap/Dropdown";
5 | import SendModal from "@components/modals/send-modal";
6 |
7 | const CardOptions = ({ utxo, onSale }) => {
8 | const [showSendModal, setShowSendModal] = useState(false);
9 | const handleSendModal = () => {
10 | setShowSendModal((prev) => !prev);
11 | };
12 |
13 | return (
14 | <>
15 |
16 |
17 |
33 |
34 |
35 |
36 |
43 |
44 |
45 | {showSendModal && (
46 |
52 | )}
53 | >
54 | );
55 | };
56 |
57 | CardOptions.propTypes = {
58 | utxo: PropTypes.object, // TODO: DEFINE UXTO TYPE in @utils/types.js
59 | onSend: PropTypes.func,
60 | };
61 |
62 | export default CardOptions;
63 |
--------------------------------------------------------------------------------
/src/pages/output/[output].js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-syntax, no-await-in-loop, no-continue, react/forbid-prop-types */
2 | import React, { useRef, useEffect } from "react";
3 | import Wrapper from "@layout/wrapper";
4 | import Header from "@layout/header";
5 | import Footer from "@layout/footer";
6 | import SEO from "@components/seo";
7 | import ProductDetailsArea from "@containers/product-details";
8 | import { useRouter } from "next/router";
9 | import { WalletContext } from "@context/wallet-context";
10 | import { useWalletState, useHeaderHeight } from "@hooks";
11 | import "react-loading-skeleton/dist/skeleton.css";
12 | import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
13 | import useOutput from "src/hooks/use-output";
14 |
15 | const OutputPage = () => {
16 | const walletState = useWalletState();
17 | const router = useRouter();
18 | const { output } = router.query;
19 |
20 | const elementRef = useRef(null);
21 | const headerHeight = useHeaderHeight(elementRef);
22 | const { value: uninscribedSats, loading } = useOutput(output);
23 | const onAction = async () => {};
24 |
25 | return (
26 |
27 |
28 |
29 |
30 |
35 | {uninscribedSats && (
36 |
46 | )}
47 |
48 | {(loading || !uninscribedSats) && (
49 |
50 |
51 |
52 |
53 |
54 | )}
55 |
56 |
57 |
58 |
59 |
60 | );
61 | };
62 |
63 | OutputPage.propTypes = {};
64 |
65 | export default OutputPage;
66 |
--------------------------------------------------------------------------------
/src/containers/MainCollections.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/no-array-index-key */
2 |
3 | import { useState, useEffect } from "react";
4 | import PropTypes from "prop-types";
5 | import clsx from "clsx";
6 | import SectionTitle from "@components/section-title";
7 |
8 | import { getCollection } from "@services/nosft";
9 |
10 | import "react-loading-skeleton/dist/skeleton.css";
11 |
12 | import CollectionCard from "@components/collection-card";
13 |
14 | const mainCollections = [
15 | "astral-babes",
16 | "astralchads",
17 | "bitcoin-frogs",
18 | "bitcoinpunks",
19 | ];
20 |
21 | const MainCollections = ({ className, space }) => {
22 | const [collections, setCollections] = useState([]);
23 |
24 | useEffect(() => {
25 | const fetchCollections = async () => {
26 | // get all collections using promise.all
27 | const promises = mainCollections.map((collection) =>
28 | getCollection(collection),
29 | );
30 | const collections = await Promise.all(promises);
31 |
32 | setCollections(collections);
33 | };
34 | fetchCollections();
35 | }, []);
36 |
37 | const renderCards = () => {
38 | if (!collections.length) return null;
39 |
40 | return (
41 |
42 | {collections
43 | .filter((c) => c)
44 | .map((collection) => (
45 |
49 |
50 |
51 | ))}
52 |
53 | );
54 | };
55 |
56 | return (
57 |
58 |
59 |
64 |
65 |
66 |
{renderCards()}
67 |
68 |
69 |
70 | );
71 | };
72 |
73 | MainCollections.propTypes = {
74 | className: PropTypes.string,
75 | space: PropTypes.oneOf([1, 2]),
76 | };
77 |
78 | MainCollections.defaultProps = {
79 | space: 1,
80 | };
81 |
82 | export default MainCollections;
83 |
--------------------------------------------------------------------------------
/src/components/btc-transaction-tree/psbt.js:
--------------------------------------------------------------------------------
1 | import * as bitcoin from 'bitcoinjs-lib';
2 |
3 | const parseHexPsbt = (txHex, _metadata) => {
4 | const metadata = _metadata ? structuredClone(_metadata) : {};
5 | let psbt;
6 | try {
7 | psbt = bitcoin.Psbt.fromHex(txHex, { network: bitcoin.networks.bitcoin });
8 | } catch (error) {
9 | console.error('Failed to parse transaction hex:', error);
10 | return;
11 | }
12 |
13 | const inputValues = psbt.txInputs.map((input, index) => {
14 | const txid = Buffer.from(input.hash).reverse().toString('hex');
15 | const inputMetadata = Array.isArray(metadata.inputs) ? metadata.inputs[index] : undefined;
16 | const result = {
17 | name: `${txid.slice(0, 4)}...:${input.index}`,
18 | type: inputMetadata ? inputMetadata.type : '',
19 | fullName: `${txid}:${input.index}`,
20 | inputValue: inputMetadata ? inputMetadata.value : 0,
21 | };
22 | return result;
23 | });
24 |
25 | const outputNodes = psbt.txOutputs.map((output, index) => {
26 | let address;
27 | try {
28 | address = bitcoin.address.fromOutputScript(output.script, bitcoin.networks.bitcoin);
29 | } catch (e) {
30 | console.error('Failed to decode address from output script:', e);
31 | address = 'Unknown';
32 | }
33 | const outputMetadata = Array.isArray(metadata.outputs) ? metadata.outputs[index] : undefined;
34 | return {
35 | name: '',
36 | type: outputMetadata ? outputMetadata.type : '',
37 | value: output.value,
38 | address: address,
39 | };
40 | });
41 |
42 | const inputAmount = inputValues.reduce((acc, input) => acc + input.inputValue, 0);
43 | const outputAmount = outputNodes.reduce((acc, output) => acc + output.value, 0);
44 |
45 | outputNodes.push({
46 | name: '',
47 | value: inputAmount - outputAmount,
48 | type: 'Fee',
49 | address: '',
50 | });
51 |
52 | const data = {
53 | name: 'Transaction',
54 | children: [
55 | {
56 | name: 'Inputs',
57 | children: inputValues,
58 | },
59 | {
60 | name: 'Outputs',
61 | children: outputNodes,
62 | },
63 | ],
64 | };
65 | return data;
66 | }
67 |
68 | export { parseHexPsbt };
--------------------------------------------------------------------------------
/src/hooks/use-inscription.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { getLatestSellNostrInscription } from "@services/nosft";
3 | import { useInterval } from "react-use";
4 | import { getInscription } from "../services/nitrous-api";
5 |
6 | const delay = 5000;
7 |
8 | function useInscription(inscriptionId) {
9 | const [inscription, setInscription] = useState();
10 | const [currentInscriptionId, setCurrentInscriptionId] =
11 | useState(inscriptionId);
12 | const [nostrData, setNostrData] = useState();
13 | const [bids, setBids] = useState(null);
14 | const [auction, setAuction] = useState(null);
15 | const [isLoading, setIsLoading] = useState(true);
16 | const [collection, setCollection] = useState();
17 | const [isPooling, setIsPooling] = useState(true);
18 |
19 | const fetchInscription = async (id) => {
20 | const _currentInscriptionId = id || currentInscriptionId;
21 | if (!_currentInscriptionId) return;
22 | console.log("[useInscription]", _currentInscriptionId);
23 | const result = await getInscription(_currentInscriptionId);
24 | const {
25 | inscription: _inscription,
26 | collection: _collection,
27 | nostr: _nostr,
28 | bids: _bids,
29 | auction: _auction,
30 | } = result;
31 | console.log("[useInscription]", id, JSON.stringify({ _auction }));
32 |
33 | const output = _inscription
34 | ? _inscription.output || `${_inscription.txid}:${_inscription.vout}`
35 | : null;
36 | const utxo = {
37 | ..._inscription,
38 | output,
39 | value: parseInt(_inscription.value),
40 | };
41 |
42 | setIsLoading(false);
43 | setInscription(utxo);
44 | setCollection(_collection);
45 | setNostrData(_nostr);
46 | setBids(_bids);
47 | setAuction(_auction);
48 | };
49 |
50 | useInterval(
51 | async () => {
52 | await fetchInscription();
53 | },
54 | isPooling && currentInscriptionId ? delay : null,
55 | );
56 |
57 | useEffect(() => {
58 | if (!inscriptionId) return;
59 | setCurrentInscriptionId(inscriptionId);
60 | setIsPooling(true);
61 | fetchInscription(inscriptionId);
62 | // eslint-disable-next-line react-hooks/exhaustive-deps
63 | }, [inscriptionId]);
64 |
65 | return {
66 | inscription,
67 | collection,
68 | nostrData,
69 | isPooling,
70 | setIsPooling,
71 | bids,
72 | isLoading,
73 | auction,
74 | };
75 | }
76 |
77 | export default useInscription;
78 |
--------------------------------------------------------------------------------
/src/components/modals/bids-modal/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import { useState } from "react";
3 | import PropTypes from "prop-types";
4 | import Modal from "react-bootstrap/Modal";
5 | import { getPsbt, signAcceptBid } from "@services/nosft";
6 | import * as bitcoin from "bitcoinjs-lib";
7 | import * as ecc from "tiny-secp256k1";
8 | import { useWallet } from "@context/wallet-context";
9 | import { toast } from "react-toastify";
10 | import BidList from "@components/bids/bid-list";
11 |
12 | bitcoin.initEccLib(ecc);
13 |
14 | const BidsModal = ({
15 | show,
16 | handleModal,
17 | onAcceptBid,
18 | bids,
19 | shouldShowTakeBid,
20 | }) => {
21 | const { nostrOrdinalsAddress } = useWallet();
22 |
23 | const [isOnAcceptBid, setIsOnAcceptBid] = useState(false);
24 |
25 | const acceptBid = async (bid) => {
26 | setIsOnAcceptBid(true);
27 |
28 | try {
29 | const buyerSignedPsbt = getPsbt(bid.nostr.content);
30 | const txId = await signAcceptBid({
31 | psbt: buyerSignedPsbt,
32 | ordinalAddress: nostrOrdinalsAddress,
33 | });
34 |
35 | toast.success(`Transaction sent: ${txId}, copied to clipboard`);
36 | navigator.clipboard.writeText(txId);
37 | } catch (error) {
38 | console.error(error);
39 | toast.error(error.message);
40 | }
41 |
42 | setIsOnAcceptBid(false);
43 | onAcceptBid();
44 | handleModal();
45 | };
46 |
47 | return (
48 |
54 | {show && (
55 |
63 | )}
64 |
65 | Bids
66 |
67 |
68 |
74 |
75 |
76 | );
77 | };
78 |
79 | BidsModal.propTypes = {
80 | show: PropTypes.bool.isRequired,
81 | handleModal: PropTypes.func.isRequired,
82 | utxo: PropTypes.object,
83 | bids: PropTypes.array,
84 | onAcceptBid: PropTypes.func,
85 | };
86 | export default BidsModal;
87 |
--------------------------------------------------------------------------------
/src/components/loading-animation/loading-bar.json:
--------------------------------------------------------------------------------
1 | {"v":"4.11.1","fr":60,"ip":0,"op":68,"w":1920,"h":1080,"nm":"Comp 9","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":0,"s":[0],"e":[360]},{"t":60}],"ix":10},"p":{"a":0,"k":[926,542,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[94.718,102.5,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[590.451,561.677],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":99.5,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":1,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.066178865731,0.826732218266,0.973008573055,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":75,"ix":5},"lc":2,"lj":2,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[35.211,-11.951],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 2","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":68,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":0,"s":[0],"e":[-359]},{"t":60}],"ix":10},"p":{"a":0,"k":[962,540,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[116.344,347.531],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":62,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"g":{"p":3,"k":{"a":0,"k":[0.01,0.782,0.294,0.982,0.505,0.502,0.55,0.944,1,0.223,0.806,0.905],"ix":9}},"s":{"a":0,"k":[4.148,-124.836],"ix":5},"e":{"a":0,"k":[7.484,100.289],"ix":6},"t":1,"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-5.984,-11.859],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":68,"st":0,"bm":0}]}
--------------------------------------------------------------------------------
/public/images/logo/logo-white.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/fonts/Inter/README.txt:
--------------------------------------------------------------------------------
1 | Inter Variable Font
2 | ===================
3 |
4 | This download contains Inter as both a variable font and static fonts.
5 |
6 | Inter is a variable font with these axes:
7 | slnt
8 | wght
9 |
10 | This means all the styles are contained in a single file:
11 | Inter-VariableFont_slnt,wght.ttf
12 |
13 | If your app fully supports variable fonts, you can now pick intermediate styles
14 | that aren’t available as static fonts. Not all apps support variable fonts, and
15 | in those cases you can use the static font files for Inter:
16 | static/Inter-Thin.ttf
17 | static/Inter-ExtraLight.ttf
18 | static/Inter-Light.ttf
19 | static/Inter-Regular.ttf
20 | static/Inter-Medium.ttf
21 | static/Inter-SemiBold.ttf
22 | static/Inter-Bold.ttf
23 | static/Inter-ExtraBold.ttf
24 | static/Inter-Black.ttf
25 |
26 | Get started
27 | -----------
28 |
29 | 1. Install the font files you want to use
30 |
31 | 2. Use your app's font picker to view the font family and all the
32 | available styles
33 |
34 | Learn more about variable fonts
35 | -------------------------------
36 |
37 | https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
38 | https://variablefonts.typenetwork.com
39 | https://medium.com/variable-fonts
40 |
41 | In desktop apps
42 |
43 | https://theblog.adobe.com/can-variable-fonts-illustrator-cc
44 | https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
45 |
46 | Online
47 |
48 | https://developers.google.com/fonts/docs/getting_started
49 | https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
50 | https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
51 |
52 | Installing fonts
53 |
54 | MacOS: https://support.apple.com/en-us/HT201749
55 | Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
56 | Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
57 |
58 | Android Apps
59 |
60 | https://developers.google.com/fonts/docs/android
61 | https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
62 |
63 | License
64 | -------
65 | Please read the full license text (OFL.txt) to understand the permissions,
66 | restrictions and requirements for usage, redistribution, and modification.
67 |
68 | You can use them in your products & projects – print or digital,
69 | commercial or otherwise.
70 |
71 | This isn't legal advice, please consider consulting a lawyer and see the full
72 | license for all details.
73 |
--------------------------------------------------------------------------------
/src/hooks/use-sockets.js:
--------------------------------------------------------------------------------
1 | import { NOSFT_WSS } from "@services/nosft";
2 | import { useEffect, useRef, useState } from "react";
3 | import { io } from "socket.io-client";
4 |
5 | const wsLink = `${NOSFT_WSS}`;
6 |
7 | export default function useDeezySockets({
8 | onSale = false,
9 | limitSaleResults = false,
10 | onAuction = false,
11 | limitAuctionResults = false,
12 | }) {
13 | const socket = useRef(null);
14 | const [isConnected, setIsConnected] = useState(false);
15 | const [sales, setSales] = useState([]);
16 | const [auctions, setAuctions] = useState([]);
17 | const [loadingSales, setLoadingSales] = useState(true);
18 | const [loadingAuctions, setLoadingAuctions] = useState(true);
19 |
20 | useEffect(() => {
21 | const limitOnSale = limitSaleResults ? ":10" : "";
22 | const limitOnAuction = limitAuctionResults ? ":10" : "";
23 | if (!socket.current) {
24 | socket.current = io(wsLink);
25 |
26 | const onConnect = () => {
27 | console.log("Connected to the server");
28 | setIsConnected(true);
29 | if (onSale) {
30 | socket.current.emit("joinChannel", `onSale${limitOnSale}`);
31 | }
32 | if (onAuction) {
33 | socket.current.emit("joinChannel", `onAuction${limitOnAuction}`);
34 | }
35 | };
36 |
37 | const onDisconnect = () => {
38 | console.log("Disconnected from the server");
39 | setIsConnected(false);
40 | };
41 |
42 | const onUpdate = (data) => {
43 | console.log("[Received update]", data);
44 | if (data.channel.startsWith("onSale")) {
45 | setSales(data.payload);
46 | setLoadingSales(false);
47 | } else if (data.channel.startsWith("onAuction")) {
48 | setAuctions(
49 | data.payload.map((auction) => ({ ...auction, auction: true })),
50 | );
51 | setLoadingAuctions(false);
52 | }
53 | };
54 |
55 | const onConnectError = (err) => {
56 | setLoadingSales(false);
57 | console.log(`Connect Error: ${err.message}`);
58 | };
59 |
60 | socket.current.on("connect", onConnect);
61 | socket.current.on("disconnect", onDisconnect);
62 | socket.current.on("update", onUpdate);
63 | socket.current.on("connect_error", onConnectError);
64 |
65 | return () => {
66 | socket.current.off("connect", onConnect);
67 | socket.current.off("disconnect", onDisconnect);
68 | socket.current.off("update", onUpdate);
69 | socket.current.off("connect_error", onConnectError);
70 | };
71 | }
72 | }, []);
73 |
74 | return { isConnected, sales, auctions, loadingSales, loadingAuctions };
75 | }
76 |
--------------------------------------------------------------------------------
/src/assets/scss/elements/_cursor.scss:
--------------------------------------------------------------------------------
1 | //------ Cursore style----->
2 |
3 | .button-content {
4 | grid-area: content;
5 | display: flex;
6 | flex-direction: column;
7 | align-items: center;
8 | -webkit-touch-callout: none;
9 | -webkit-user-select: none;
10 | -moz-user-select: none;
11 | -ms-user-select: none;
12 | user-select: none;
13 | }
14 |
15 | .cursor {
16 | display: none;
17 | }
18 |
19 | @media (any-pointer: fine) {
20 | .cursor {
21 | position: fixed;
22 | top: 0;
23 | left: 0;
24 | display: block;
25 | pointer-events: none;
26 | z-index: 9;
27 | }
28 |
29 | .cursor__inner {
30 | fill: var(--cursor-fill);
31 | stroke: var(--cursor-stroke);
32 | stroke-width: var(--cursor-stroke-width);
33 | }
34 |
35 | .credits {
36 | padding-left: 25vw;
37 | }
38 | }
39 |
40 | /*---------------------------------------------------*/
41 | /* 09) SHANE CURSOR
42 | /*---------------------------------------------------*/
43 |
44 | .mouse-cursor {
45 | position: fixed;
46 | left: 0;
47 | top: 0;
48 | pointer-events: none;
49 | border-radius: 50%;
50 | -webkit-transform: translateZ(0);
51 | transform: translateZ(0);
52 | visibility: hidden;
53 | }
54 |
55 | .cursor-inner {
56 | margin-left: -3px;
57 | margin-top: -3px;
58 | width: 6px;
59 | height: 6px;
60 | z-index: 10000001;
61 | background-color: #4d535e;
62 | -webkit-transition: width 0.3s ease-in-out, height 0.3s ease-in-out,
63 | margin 0.3s ease-in-out, opacity 0.3s ease-in-out;
64 | transition: width 0.3s ease-in-out, height 0.3s ease-in-out,
65 | margin 0.3s ease-in-out, opacity 0.3s ease-in-out;
66 | }
67 |
68 | .cursor-inner.cursor-hover {
69 | margin-left: -30px;
70 | margin-top: -30px;
71 | width: 60px;
72 | height: 60px;
73 | background-color: #4d535e;
74 | opacity: 0.3;
75 | }
76 |
77 | .cursor-outer {
78 | margin-left: -15px;
79 | margin-top: -15px;
80 | width: 30px;
81 | height: 30px;
82 | border: 2px solid #4d535e;
83 | -webkit-box-sizing: border-box;
84 | box-sizing: border-box;
85 | z-index: 10000000;
86 | opacity: 0.5;
87 | -webkit-transition: all 0.08s ease-out;
88 | transition: all 0.08s ease-out;
89 | }
90 |
91 | .cursor-outer.cursor-hover {
92 | opacity: 0;
93 | }
94 |
95 | .main-wrapper[data-magic-cursor="hide"] .mouse-cursor {
96 | display: none;
97 | opacity: 0;
98 | visibility: hidden;
99 | position: absolute;
100 | z-index: -1111;
101 | }
102 |
--------------------------------------------------------------------------------
/src/layouts/footer.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import clsx from "clsx";
3 | import Image from "next/image";
4 | import LogoWidget from "@widgets/logo-widget";
5 | import QuicklinkWidget from "@widgets/quicklink-widget";
6 | import { ItemType } from "@utils/types";
7 |
8 | import footerData from "../data/general/footer.json";
9 |
10 | const Footer = ({ space, className, data }) => (
11 |
20 | {data?.items && (
21 |
22 |
23 |
24 |
25 | {data.items.map(({ id, image }) => (
26 | -
27 | {image?.src && (
28 |
37 | )}
38 |
39 | ))}
40 |
41 |
42 |
43 |
44 | )}
45 |
46 |
47 |
48 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | );
61 |
62 | Footer.propTypes = {
63 | space: PropTypes.oneOf([1, 2, 3]),
64 | className: PropTypes.string,
65 | data: PropTypes.shape({
66 | items: PropTypes.arrayOf(ItemType),
67 | }),
68 | };
69 |
70 | Footer.defaultProps = {
71 | space: 1,
72 | };
73 |
74 | export default Footer;
75 |
--------------------------------------------------------------------------------
/src/components/collection-info/index.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import Image from "next/image";
3 | import Anchor from "@ui/anchor";
4 | import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
5 |
6 | const CollectionInfo = ({ collection, isLoading }) => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 | {collection?.icon && (
15 |
21 | )}
22 |
23 | {!collection?.icon && isLoading && (
24 |
25 | )}
26 |
27 |
28 |
29 | {collection?.name && (
30 |
{collection?.name}
31 | )}
32 |
33 | {!collection?.name && isLoading && (
34 |
35 | )}
36 |
37 |
38 | {collection.description &&
{collection.description}
}
39 | {!collection.description && isLoading && (
40 |
41 | )}
42 |
43 |
44 | {/* Do we need the links here? */}
45 |
46 | {collection?.links?.length > 0 && (
47 |
48 | {collection.links.map((link) => (
49 |
50 | {link.name}
51 |
52 | ))}
53 |
54 | )}
55 | {!collection?.links?.length && isLoading && (
56 |
57 | )}
58 |
59 |
60 |
61 |
62 |
63 | );
64 | };
65 |
66 | CollectionInfo.propTypes = {
67 | isLoading: PropTypes.bool,
68 | collection: PropTypes.shape({
69 | name: PropTypes.string,
70 | description: PropTypes.string,
71 | icon: PropTypes.string,
72 | slug: PropTypes.string,
73 | links: PropTypes.arrayOf(
74 | PropTypes.shape({
75 | name: PropTypes.string,
76 | url: PropTypes.string,
77 | }),
78 | ),
79 | }),
80 | };
81 |
82 | export default CollectionInfo;
83 |
--------------------------------------------------------------------------------
/src/assets/scss/default/_text-animation.scss:
--------------------------------------------------------------------------------
1 | // text blinking start form hear
2 | @keyframes customOne {
3 | 0% {
4 | -webkit-transform: translateY(-50%) scale(0);
5 | transform: translateY(-50%) scale(1);
6 | opacity: 1;
7 | }
8 |
9 | 100% {
10 | -webkit-transform: translateY(-50%) scale(1.5);
11 | transform: translateY(-50%) scale(1.5);
12 | opacity: 0;
13 | }
14 | }
15 |
16 | @keyframes liveAuction {
17 | 0% {
18 | background: var(--color-white);
19 | }
20 |
21 | 100% {
22 | background: var(--color-danger);
23 | }
24 | }
25 |
26 | .cd-intro {
27 | margin: 4em auto;
28 | }
29 |
30 | @media only screen and (min-width: 768px) {
31 | .cd-intro {
32 | margin: 5em auto;
33 | }
34 | }
35 |
36 | @media only screen and (min-width: 1170px) {
37 | .cd-intro {
38 | margin: 6em auto;
39 | }
40 | }
41 |
42 | .cd-headline {
43 | font-size: 3rem;
44 | line-height: 1.2;
45 | }
46 |
47 | @media only screen and (min-width: 768px) {
48 | .cd-headline {
49 | font-size: 4.4rem;
50 | font-weight: 300;
51 | }
52 | }
53 |
54 | @media only screen and (min-width: 1170px) {
55 | .cd-headline {
56 | font-size: 48px;
57 | }
58 | }
59 |
60 | @media only screen and (max-width: 768px) {
61 | .cd-headline {
62 | font-size: 40px;
63 | }
64 | }
65 |
66 | @media only screen and (max-width: 479px) {
67 | .cd-headline {
68 | font-size: 26px;
69 | }
70 | }
71 |
72 | .cd-words-wrapper {
73 | display: inline-block;
74 | position: relative;
75 | text-align: left;
76 | }
77 |
78 | .cd-words-wrapper b {
79 | display: inline-block;
80 | position: absolute;
81 | white-space: nowrap;
82 | left: 0;
83 | top: 0;
84 | }
85 |
86 | .cd-words-wrapper b.is-visible {
87 | position: relative;
88 | }
89 |
90 | .no-js .cd-words-wrapper b {
91 | opacity: 0;
92 | }
93 |
94 | .no-js .cd-words-wrapper b.is-visible {
95 | opacity: 1;
96 | }
97 |
98 | /* --------------------------------
99 |
100 | xclip
101 |
102 | -------------------------------- */
103 |
104 | .cd-headline.clip span {
105 | display: inline-block;
106 | padding: 0;
107 | }
108 |
109 | .cd-headline.clip .cd-words-wrapper {
110 | overflow: hidden;
111 | vertical-align: middle;
112 | }
113 |
114 | .cd-headline.clip .cd-words-wrapper::after {
115 | content: "";
116 | position: absolute;
117 | top: 50%;
118 | right: 0;
119 | width: 2px;
120 | height: 80%;
121 | background-color: rgba(255, 255, 255, 0.3);
122 | transform: translateY(-50%);
123 | }
124 |
125 | .cd-headline.clip b {
126 | opacity: 0;
127 | }
128 |
129 | .cd-headline.clip b.is-visible {
130 | opacity: 1;
131 | }
132 |
--------------------------------------------------------------------------------
/src/hooks/use-wallet-state.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useMemo } from "react";
2 | import { getAddressInfo } from "@services/nosft";
3 | import useConnectWallet from "./use-connect-wallet";
4 |
5 | export const useWalletState = () => {
6 | const {
7 | ordinalsPublicKey,
8 | paymentPublicKey,
9 | onConnectHandler: onConnect,
10 | onDisconnectHandler: onDisconnect,
11 | walletName,
12 | ordinalsAddress,
13 | paymentAddress,
14 | } = useConnectWallet();
15 | const [nostrOrdinalsAddress, setNostrOrdinalsAddress] = useState("");
16 | const [nostrPaymentAddress, setNostrPaymentAddress] = useState("");
17 | const [ethProvider, setEthProvider] = useState();
18 | const [showConnectModal, setShowConnectModal] = useState(false);
19 |
20 | const onHideConnectModal = () => {
21 | setShowConnectModal(false);
22 | };
23 |
24 | const onShowConnectModal = () => {
25 | setShowConnectModal(true);
26 | };
27 |
28 | const onConnectHandler = (domain, callback) => {
29 | onConnect(domain, callback);
30 | onHideConnectModal();
31 | };
32 |
33 | const onDisconnectHandler = () => {
34 | onDisconnect();
35 | onHideConnectModal();
36 | };
37 |
38 | useEffect(() => {
39 | const syncAddress = async () => {
40 | if (!ordinalsPublicKey) {
41 | setNostrOrdinalsAddress("");
42 | setNostrPaymentAddress("");
43 | return;
44 | }
45 |
46 | if (ordinalsAddress && paymentAddress) {
47 | setNostrOrdinalsAddress(ordinalsAddress);
48 | setNostrPaymentAddress(paymentAddress);
49 | return;
50 | }
51 | const { address: nostrOrdinalsAddress } = await getAddressInfo(
52 | ordinalsPublicKey,
53 | );
54 | setNostrOrdinalsAddress(nostrOrdinalsAddress);
55 | setNostrPaymentAddress(nostrOrdinalsAddress);
56 | };
57 | syncAddress().catch(console.error);
58 | }, [ordinalsPublicKey, ordinalsAddress]);
59 |
60 | useEffect(() => {
61 | if (typeof window === "undefined") return;
62 | if (!window.ethereum) return;
63 | const provider = window.ethereum;
64 | setEthProvider(provider);
65 | }, []);
66 |
67 | const walletState = useMemo(
68 | () => ({
69 | walletName,
70 | ordinalsPublicKey,
71 | paymentPublicKey,
72 | nostrOrdinalsAddress,
73 | nostrPaymentAddress,
74 | ethProvider,
75 | showConnectModal,
76 | onConnectHandler,
77 | onDisconnectHandler,
78 | onHideConnectModal,
79 | onShowConnectModal,
80 | }),
81 | [
82 | walletName,
83 | ordinalsPublicKey,
84 | paymentPublicKey,
85 | nostrOrdinalsAddress,
86 | nostrPaymentAddress,
87 | ethProvider,
88 | showConnectModal,
89 | onConnectHandler,
90 | onDisconnectHandler,
91 | ],
92 | );
93 |
94 | return walletState;
95 | };
96 |
97 | export default useWalletState;
98 |
--------------------------------------------------------------------------------
/src/assets/scss/elements/_countdown.scss:
--------------------------------------------------------------------------------
1 | // count down
2 | .countdown-text-small {
3 | display: flex;
4 | gap: 4px;
5 | }
6 |
7 | .countdown {
8 | display: flex;
9 | margin: -4px;
10 | justify-content: center;
11 |
12 | .countdown-container {
13 | margin: 4px !important;
14 | position: relative;
15 |
16 | .countdown-heading {
17 | display: block;
18 | color: var(--color-body);
19 | font-size: 8px;
20 | text-align: center;
21 | text-transform: uppercase;
22 | margin-top: 0;
23 | display: block;
24 | }
25 |
26 | .countdown-value {
27 | display: block;
28 | font-size: 20px;
29 | color: var(--color-white);
30 | font-weight: 600;
31 | text-align: center;
32 | border-radius: 4px;
33 | padding: 2px 10px;
34 | position: relative;
35 | background-color: rgba(36, 36, 53, 0.3);
36 | backdrop-filter: blur(15px);
37 |
38 | &:after {
39 | content: ":";
40 | top: 50%;
41 | transform: translateY(-50%);
42 | right: -6px;
43 | position: absolute;
44 | font-size: 20px;
45 | color: var(--color-body);
46 | font-weight: 400;
47 | }
48 | }
49 |
50 | &:last-child {
51 | .countdown-value {
52 | &::after {
53 | display: none;
54 | }
55 | }
56 | }
57 | }
58 | }
59 |
60 | // count-down background variation
61 |
62 | body {
63 | &.count_bg--1 {
64 | .countdown {
65 | .countdown-container {
66 | .countdown-value {
67 | background-color: #0407a8;
68 | }
69 | }
70 | }
71 | }
72 | &.count_bg--2 {
73 | .countdown {
74 | .countdown-container {
75 | .countdown-value {
76 | background-color: var(--color-danger);
77 | }
78 | }
79 | }
80 | }
81 | &.count_bg--3 {
82 | .countdown {
83 | .countdown-container {
84 | .countdown-value {
85 | background-color: var(--color-primary-alta);
86 | }
87 | }
88 | }
89 | }
90 | &.count_bg--4 {
91 | .countdown {
92 | .countdown-container {
93 | .countdown-value {
94 | background-color: var(--color-primary);
95 | }
96 | }
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/components/product-bid/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/forbid-prop-types */
2 | import PropTypes from "prop-types";
3 | import Button from "@ui/button";
4 | import { satToBtc } from "@services/nosft";
5 |
6 | const ProductBid = ({ price, utxo, confirmed, date, type, onClick, alwaysNewTabOnView }) => {
7 | function onActionClicked(e) {
8 | e.preventDefault();
9 |
10 | if (onClick) {
11 | onClick(utxo.inscriptionId);
12 | return;
13 | }
14 |
15 | const path = utxo?.inscriptionId
16 | ? `/inscription/${utxo?.inscriptionId}`
17 | : `/output/${utxo?.txid}:${utxo?.vout}`;
18 |
19 | // Check if command/ctrl key is pressed for new tab
20 | if (e.metaKey || e.ctrlKey || alwaysNewTabOnView) {
21 | window.open(path, '_blank');
22 | } else {
23 | window.location.href = path;
24 | }
25 | return;
26 | }
27 |
28 | function renderMainAction(actionType) {
29 | if (actionType === "buy" || actionType === "sell") return null;
30 | let label = "View";
31 | switch (actionType) {
32 | case "buy":
33 | label = "Buy";
34 | break;
35 | case "sell":
36 | label = "Sell";
37 | break;
38 | case "send":
39 | label = "Send";
40 | case "view":
41 | label = "View";
42 | default:
43 | label = "View";
44 | }
45 |
46 | return (
47 |
50 | );
51 | }
52 | let minted = "";
53 | if (date) {
54 | minted = !confirmed
55 | ? "Unconfirmed"
56 | : new Date(date * 1000).toLocaleString();
57 | }
58 |
59 | const priceAmount = price?.amount?.replace(/,/g, "") || 0;
60 | const btcValue = satToBtc(Number(priceAmount));
61 | const textPrice = type === "buy" ? `Listed for: ${btcValue}` : btcValue;
62 |
63 | const renderLabelInfo = () => {
64 | if (type === "buy") {
65 | return (
66 | <>
67 |

72 |
{btcValue}
73 | >
74 | );
75 | }
76 |
77 | return
{minted};
78 | };
79 |
80 | return (
81 |
82 |
{renderLabelInfo()}
83 |
84 | {renderMainAction(type)}
85 |
86 | );
87 | };
88 |
89 | ProductBid.propTypes = {
90 | price: PropTypes.shape({
91 | amount: PropTypes.string.isRequired,
92 | currency: PropTypes.string.isRequired,
93 | }).isRequired,
94 | utxo: PropTypes.object,
95 | confirmed: PropTypes.bool,
96 | date: PropTypes.number,
97 | type: PropTypes.oneOf(["buy", "sell", "send", "view"]).isRequired,
98 | onClick: PropTypes.func,
99 | };
100 |
101 | export default ProductBid;
102 |
--------------------------------------------------------------------------------
/src/pages/inscription/[slug].js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-syntax, no-await-in-loop, no-continue, react/forbid-prop-types */
2 | import React, { useRef, useEffect } from "react";
3 | import Wrapper from "@layout/wrapper";
4 | import Header from "@layout/header";
5 | import Footer from "@layout/footer";
6 | import SEO from "@components/seo";
7 | import ProductDetailsArea from "@containers/product-details";
8 | import { useRouter } from "next/router";
9 | import { WalletContext } from "@context/wallet-context";
10 | import { useWalletState, useHeaderHeight } from "@hooks";
11 | import "react-loading-skeleton/dist/skeleton.css";
12 | import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
13 | import useAuction from "src/hooks/use-auction";
14 | import useInscription from "src/hooks/use-inscription";
15 | import useIsSpent from "src/hooks/use-is-spent";
16 | import useBid from "src/hooks/use-bid";
17 | import { useSimpleScrollTop } from "src/hooks/use-simple-scroll-top";
18 |
19 | const Inscription = () => {
20 | const walletState = useWalletState();
21 | const router = useRouter();
22 | const { slug } = router.query;
23 |
24 | const elementRef = useRef(null);
25 | const headerHeight = useHeaderHeight(elementRef);
26 | useSimpleScrollTop();
27 |
28 | const {
29 | inscription,
30 | collection,
31 | nostrData,
32 | bids,
33 | isLoading,
34 | auction: auctions,
35 | setIsPooling: setIsPoolingInscription,
36 | } = useInscription(slug);
37 |
38 | const { isSpent: isInscriptionSpent, setIsPooling: setIsPoolingIsSpent } =
39 | useIsSpent(inscription?.output);
40 |
41 | const onAction = async (startPooling) => {
42 | setIsPoolingInscription(startPooling);
43 | setIsPoolingIsSpent(startPooling);
44 | };
45 |
46 | useEffect(() => {
47 | if (isInscriptionSpent) {
48 | setIsPoolingIsSpent(false);
49 | }
50 | }, [isInscriptionSpent]);
51 |
52 | return (
53 |
54 |
55 |
56 |
57 |
62 | {inscription && (
63 |
73 | )}
74 |
75 | {!inscription && (
76 |
77 |
78 |
79 |
80 |
81 | )}
82 |
83 |
84 |
85 |
86 |
87 | );
88 | };
89 |
90 | Inscription.propTypes = {};
91 |
92 | export default Inscription;
93 |
--------------------------------------------------------------------------------
/src/containers/helpers.js:
--------------------------------------------------------------------------------
1 | import {
2 | DEFAULT_UTXO_TYPES,
3 | HIDE_TEXT_UTXO_OPTION,
4 | OTHER_UTXO_OPTION,
5 | } from "@lib/constants.config";
6 | import { matchSorter } from "match-sorter";
7 |
8 | export const collectionAuthor = [
9 | {
10 | name: "Danny Deezy",
11 | slug: "/deezy",
12 | image: {
13 | src: "/images/logo/nos-ft-logo.png",
14 | },
15 | },
16 | ];
17 |
18 | const isTextInscription = (utxo) => {
19 | return /(text\/plain|application\/json)/.test(utxo?.content_type);
20 | };
21 |
22 | const filterAscDate = (arr) => arr.sort((a, b) => a.created_at - b.created_at);
23 | const filterDescDate = (arr) => arr.sort((a, b) => b.created_at - a.created_at);
24 | const filterAscValue = (arr) => arr.sort((a, b) => a.value - b.value);
25 | const filterDescValue = (arr) => arr.sort((a, b) => b.value - a.value);
26 | const filterAscNum = (arr) => arr.sort((a, b) => a.num - b.num);
27 | const filterDescNum = (arr) => arr.sort((a, b) => b.num - a.num);
28 |
29 | const sortWithInscriptionId = (utxos, sortFunction) => {
30 | const withInscriptionId = [];
31 | const withoutInscriptionId = [];
32 |
33 | utxos.forEach((utxo) => {
34 | if (utxo.inscriptionId) {
35 | withInscriptionId.push(utxo);
36 | } else {
37 | withoutInscriptionId.push(utxo);
38 | }
39 | });
40 |
41 | return [
42 | ...sortFunction(withInscriptionId),
43 | ...sortFunction(withoutInscriptionId),
44 | ];
45 | };
46 |
47 | export const applyFilters = ({
48 | utxos,
49 | activeSort,
50 | sortAsc,
51 | utxosType,
52 | showOnlyOrdinals,
53 | searchQuery,
54 | }) => {
55 | let filtered = [...utxos];
56 | if (utxosType) {
57 | if (utxosType === OTHER_UTXO_OPTION) {
58 | filtered = filtered.filter(
59 | (utxo) => !DEFAULT_UTXO_TYPES.includes(utxo.content_type),
60 | );
61 | } else if (utxosType === HIDE_TEXT_UTXO_OPTION) {
62 | filtered = filtered.filter((utxo) => !isTextInscription(utxo));
63 | } else {
64 | filtered = filtered.filter((utxo) => utxo.content_type === utxosType);
65 | }
66 | }
67 |
68 | if (searchQuery && searchQuery.trim().length > 0) {
69 | filtered = matchSorter(filtered, searchQuery, {
70 | keys: [
71 | "inscriptionId",
72 | "key",
73 | "txid",
74 | "vout",
75 | "value",
76 | "num",
77 | "status.block_time",
78 | "status.block_height",
79 | "status.confirmed",
80 | ],
81 | });
82 | }
83 |
84 | if (showOnlyOrdinals) {
85 | filtered = filtered.filter((utxo) => utxo.inscriptionId);
86 | }
87 |
88 | if (activeSort === "value") {
89 | filtered = sortAsc
90 | ? sortWithInscriptionId(filtered, filterAscValue)
91 | : sortWithInscriptionId(filtered, filterDescValue);
92 | } else if (activeSort === "num") {
93 | filtered = sortAsc
94 | ? sortWithInscriptionId(filtered, filterAscNum)
95 | : sortWithInscriptionId(filtered, filterDescNum);
96 | } else {
97 | filtered = sortAsc
98 | ? sortWithInscriptionId(filtered, filterAscDate)
99 | : sortWithInscriptionId(filtered, filterDescDate);
100 | }
101 |
102 | return filtered;
103 | };
104 |
--------------------------------------------------------------------------------
/src/services/nitrous-api.js:
--------------------------------------------------------------------------------
1 | const axios = require("axios");
2 |
3 | const NITROUS_BASE_API_URL = "ordinals-api-lb.deezy.io";
4 |
5 | function mapInscription(obj) {
6 | console.log("[obj]", obj);
7 | const [txid, vout, offset] = obj.satpoint.split(':')
8 | return {
9 | content_length: String(obj.content_length),
10 | content_type: obj.content_type,
11 | created: obj.created ? Number(obj.timestamp) : 0,
12 | genesis_fee: String(obj.fee),
13 | genesis_height: String(obj.height),
14 | id: String(obj.id),
15 | num: String(obj.number),
16 | owner: obj.address,
17 | sats: String(obj.value),
18 | output: `${txid}:${vout}`,
19 | offset: offset,
20 | inscriptionId: obj.id,
21 | vout: Number(vout),
22 | txid: txid,
23 | value: obj.value,
24 | };
25 | }
26 |
27 | function mapNostrEvent(obj) {
28 | return {
29 | inscriptionId: obj.inscriptionId,
30 | content: obj.content,
31 | created_at: Number(obj.eventCreatedAt),
32 | id: obj.id,
33 | kind: obj.kind,
34 | pubkey: obj.pubkey,
35 | sig: obj.sig,
36 | tags: obj.tags,
37 | value: Number(obj.value),
38 | };
39 | }
40 |
41 | function mapBidEvent(obj) {
42 | return {
43 | price: Number(obj.price),
44 | bidOwner: obj.bidOwner,
45 | ordinalOwner: obj.ordinalOwner,
46 | output: obj.output,
47 | created_at: Number(obj.bidCreatedAt),
48 | nostr: mapNostrEvent(obj.nostr),
49 | };
50 | }
51 |
52 | function mapAuction(obj) {
53 | if (!obj) {
54 | return;
55 | }
56 |
57 | return {
58 | initialPrice: Number(obj.initialPrice),
59 | inscriptionId: obj.inscriptionId,
60 | secondsBetweenEachDecrease: Number(obj.secondsBetweenEachDecrease),
61 | collection: obj.collection,
62 | status: obj.status,
63 | decreaseAmount: Number(obj.decreaseAmount),
64 | btcAddress: obj.btcAddress,
65 | currentPrice: Number(obj.currentPrice),
66 | startTime: Number(obj.startTime),
67 | scheduledISODate: obj.scheduledISODate,
68 | output: obj.output,
69 | reservePrice: Number(obj.reservePrice),
70 | id: obj.id,
71 | metadata: obj.metadata,
72 | };
73 | }
74 |
75 | async function getInscription(inscriptionId) {
76 | try {
77 | const response = await axios.get(
78 | `https://${NITROUS_BASE_API_URL}/inscription/${inscriptionId}`, {
79 | headers: {
80 | accept: 'application/json'
81 | }
82 | }
83 | );
84 | const inscriptionData = await response.data;
85 |
86 | // Note: auctions are disabled
87 | const _auction = inscriptionData.auctions?.find(
88 | (a) => a.status === "RUNNING" || a.status === "PENDING",
89 | );
90 |
91 | return {
92 | ...inscriptionData,
93 | inscription: mapInscription(inscriptionData),
94 | // Note: sellEvents and bids are disabled
95 | nostr: inscriptionData.sellEvents?.[0]
96 | ? mapNostrEvent(inscriptionData.sellEvents?.[0])
97 | : undefined,
98 | bids: inscriptionData.bids?.map(mapBidEvent),
99 | auction: mapAuction(_auction),
100 | };
101 | } catch (error) {
102 | console.error(error);
103 | }
104 | }
105 |
106 | module.exports = { getInscription };
107 |
--------------------------------------------------------------------------------
/src/components/menu/mobile-menu/index.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import clsx from "clsx";
3 | import { Offcanvas, OffcanvasHeader, OffcanvasBody } from "@ui/offcanvas";
4 | import Anchor from "@ui/anchor";
5 | import Logo from "@components/logo";
6 | import { slideToggle, slideUp } from "@utils/methods";
7 | import SubMenu from "./submenu";
8 | import MegaMenu from "./megamenu";
9 |
10 | const MobileMenu = ({ isOpen, onClick, menu, logo }) => {
11 | const onClickHandler = (e) => {
12 | e.preventDefault();
13 | const { target } = e;
14 | const {
15 | parentElement: {
16 | parentElement: { childNodes },
17 | },
18 | nextElementSibling,
19 | } = target;
20 | slideToggle(nextElementSibling);
21 | childNodes.forEach((child) => {
22 | if (child.id === target.parentElement.id) return;
23 | if (child.classList.contains("has-children")) {
24 | slideUp(child.lastElementChild);
25 | }
26 | });
27 | };
28 | return (
29 |
30 |
31 |
32 |
33 |
34 |
62 |
63 |
64 | );
65 | };
66 |
67 | MobileMenu.propTypes = {
68 | isOpen: PropTypes.bool.isRequired,
69 | onClick: PropTypes.func.isRequired,
70 | menu: PropTypes.arrayOf(PropTypes.shape({})),
71 | logo: PropTypes.arrayOf(
72 | PropTypes.shape({
73 | src: PropTypes.string.isRequired,
74 | alt: PropTypes.string,
75 | })
76 | ),
77 | };
78 |
79 | export default MobileMenu;
80 |
--------------------------------------------------------------------------------
/src/assets/scss/elements/_backtotop.scss:
--------------------------------------------------------------------------------
1 | /*-------------------------
2 | Back To Top
3 | ---------------------------*/
4 | @-webkit-keyframes border-transform {
5 | 0%,
6 | 100% {
7 | border-radius: 63% 37% 54% 46% / 55% 48% 52% 45%;
8 | }
9 |
10 | 14% {
11 | border-radius: 40% 60% 54% 46% / 49% 60% 40% 51%;
12 | }
13 |
14 | 28% {
15 | border-radius: 54% 46% 38% 62% / 49% 70% 30% 51%;
16 | }
17 |
18 | 42% {
19 | border-radius: 61% 39% 55% 45% / 61% 38% 62% 39%;
20 | }
21 |
22 | 56% {
23 | border-radius: 61% 39% 67% 33% / 70% 50% 50% 30%;
24 | }
25 |
26 | 70% {
27 | border-radius: 50% 50% 34% 66% / 56% 68% 32% 44%;
28 | }
29 |
30 | 84% {
31 | border-radius: 46% 54% 50% 50% / 35% 61% 39% 65%;
32 | }
33 | }
34 |
35 | .paginacontainer {
36 | height: 3000px;
37 | }
38 |
39 | .rn-progress-parent {
40 | position: fixed;
41 | right: 30px;
42 | bottom: 30px;
43 | height: 46px;
44 | width: 46px;
45 | cursor: pointer;
46 | display: block;
47 | border-radius: 50px;
48 | box-shadow: inset 0 0 0 2px #0d0d12;
49 | z-index: 10000;
50 | opacity: 0;
51 | visibility: hidden;
52 | transform: translateY(15px);
53 | -webkit-transition: all 200ms linear;
54 | transition: all 200ms linear;
55 |
56 | &.rn-backto-top-active {
57 | opacity: 1;
58 | visibility: visible;
59 | transform: translateY(0);
60 | }
61 |
62 | &::after {
63 | position: absolute;
64 | font-family: "feather" !important;
65 | content: "\e914";
66 | text-align: center;
67 | line-height: 46px;
68 | font-size: 24px;
69 | color: var(--color-primary);
70 | left: 0;
71 | top: 0;
72 | height: 46px;
73 | width: 46px;
74 | cursor: pointer;
75 | display: block;
76 | z-index: 2;
77 | -webkit-transition: all 200ms linear;
78 | transition: all 200ms linear;
79 | }
80 |
81 | &:hover {
82 | &::after {
83 | color: var(--color-primary);
84 | }
85 | }
86 |
87 | &::before {
88 | position: absolute;
89 | font-family: "feather" !important;
90 | content: "\e914";
91 | text-align: center;
92 | line-height: 46px;
93 | font-size: 24px;
94 | opacity: 0;
95 | background: #0d0d12;
96 | -webkit-background-clip: text;
97 | -webkit-text-fill-color: transparent;
98 | left: 0;
99 | top: 0;
100 | height: 46px;
101 | width: 46px;
102 | cursor: pointer;
103 | display: block;
104 | z-index: 2;
105 | -webkit-transition: all 200ms linear;
106 | transition: all 200ms linear;
107 | }
108 | &:hover {
109 | &::before {
110 | opacity: 1;
111 | }
112 | }
113 | svg {
114 | path {
115 | fill: none;
116 | }
117 | &.rn-back-circle {
118 | path {
119 | stroke: var(--color-primary);
120 | stroke-width: 4;
121 | box-sizing: border-box;
122 | -webkit-transition: all 200ms linear;
123 | transition: all 200ms linear;
124 | }
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/assets/scss/default/_variables.scss:
--------------------------------------------------------------------------------
1 | :root {
2 | // themes color
3 | --color-primary: #fec823;
4 | --color-primary-alta: #212e48;
5 |
6 | --color-gray: #f6f6f6;
7 | --color-gray-2: #f5f8fa;
8 | --color-subtitle: #f9004d;
9 |
10 | // background-color
11 | --background-color-1: #3f3b38;
12 | --background-color-2: #13131d;
13 | --background-color-3: #151521;
14 | --background-color-4: #242435;
15 |
16 | // color gradient
17 | --gradient-one: linear-gradient(to right bottom, #2899d8, #00a3ff);
18 |
19 | // typo Color
20 | --color-heading: #ffffff;
21 | --color-body: #acacac;
22 | --color-dark: rgb(29, 29, 29);
23 |
24 | --color-light-heading: #181c32;
25 | --color-light-body: #65676b;
26 | --color-border-white: #00000024;
27 |
28 | --color-midgray: #878787;
29 | --color-light: #e4e6ea;
30 | --color-lighter: #ced0d4;
31 | --color-lightest: #f0f2f5;
32 | --color-border: #fec823;
33 | --color-white: #ffffff;
34 | --color-white-75: rgba(255, 255, 255, 0.75);
35 |
36 | // notify Colors
37 | --color-success: #3eb75e;
38 | --color-danger: #ff0003;
39 | --color-warning: #ff8f3c;
40 | --color-info: #1ba2db;
41 |
42 | //Social icon colors
43 | --color-facebook: #3b5997;
44 | --color-twitter: #1ba1f2;
45 | --color-youtube: #ed4141;
46 | --color-linkedin: #0077b5;
47 | --color-pinterest: #e60022;
48 | --color-instagram: #c231a1;
49 | --color-vimeo: #00adef;
50 | --color-twitch: #6441a3;
51 | --color-discord: #7289da;
52 |
53 | // Font weight
54 | --p-light: 300;
55 | --p-regular: 400;
56 | --p-medium: 500;
57 | --p-semi-bold: 600;
58 | --p-bold: 700;
59 | --p-extra-bold: 800;
60 | --p-black: 900;
61 |
62 | // Font weight
63 | --s-light: 300;
64 | --s-regular: 400;
65 | --s-medium: 500;
66 | --s-semi-bold: 600;
67 | --s-bold: 700;
68 | --s-extra-bold: 800;
69 | --s-black: 900;
70 |
71 | //transition easing
72 | --transition: 0.4s;
73 |
74 | // Font Family
75 | --font-primary: "Roboto", sans-serif;
76 | --font-secondary: "Roboto", sans-serif;
77 |
78 | //Fonts Size
79 | --font-size-b1: 14px;
80 | --font-size-b2: 18px;
81 | --font-size-b3: 22px;
82 |
83 | //Line Height
84 | --line-height-b1: 1.5;
85 | --line-height-b2: 1.6;
86 | --line-height-b3: 1.7;
87 |
88 | // Heading Font
89 | --h1: 50px;
90 | --h2: 36px;
91 | --h3: 32px;
92 | --h4: 26px;
93 | --h5: 22px;
94 | --h6: 20px;
95 |
96 | --toastify-color-progress-dark: #fec823;
97 |
98 | // skeleton
99 | --base-color: #13131d;
100 | --highlight-color: #242435;
101 | }
102 |
103 | // Layouts Variation
104 | $smlg-device: "only screen and (max-width: 1199px)";
105 | $extra-device: "only screen and (min-width: 1600px) and (max-width: 1919px)";
106 | $laptop-device: "only screen and (min-width: 1200px) and (max-width: 1599px)";
107 | $laptop-device2: "only screen and (min-width: 1200px) and (max-width: 1300px)";
108 | $lg-layout: "only screen and (min-width: 992px) and (max-width: 1199px)";
109 | $md-layout: "only screen and (min-width: 768px) and (max-width: 991px)";
110 | $sm-layout: "only screen and (max-width: 767px)";
111 | $large-mobile: "only screen and (max-width: 575px)";
112 | $small-mobile: "only screen and (max-width: 479px)";
113 |
--------------------------------------------------------------------------------
/src/components/bids/bid-list.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { toBTC } from "@containers/product-details";
3 | import { shortenStr } from "@services/nosft";
4 | import clsx from "clsx";
5 | import { TailSpin } from "react-loading-icons";
6 | import { useWallet } from "@context/wallet-context";
7 |
8 | function timeAgo(date) {
9 | const seconds = Math.floor((new Date() - date) / 1000);
10 | let interval = Math.floor(seconds / 31536000);
11 | if (interval > 1) {
12 | return interval + " years ago";
13 | }
14 | interval = Math.floor(seconds / 2592000);
15 | if (interval > 1) {
16 | return interval + " months ago";
17 | }
18 | interval = Math.floor(seconds / 86400);
19 | if (interval > 1) {
20 | return interval + " days ago";
21 | }
22 | interval = Math.floor(seconds / 3600);
23 | if (interval > 1) {
24 | return interval + " hours ago";
25 | }
26 | interval = Math.floor(seconds / 60);
27 | if (interval > 1) {
28 | return interval + " minutes ago";
29 | }
30 | return Math.floor(seconds) + " seconds ago";
31 | }
32 |
33 | const AcceptBidButton = ({ bid, onTakeBid, isOnAcceptBid }) => (
34 |
44 | );
45 |
46 | const gridStyles = {
47 | display: "grid",
48 | gridTemplateColumns: "repeat(3, 1fr) auto", // This sets up three equally wide columns and one auto-sized
49 | gap: "16px",
50 | alignItems: "center",
51 | };
52 |
53 | const BidList = ({ bids, onTakeBid, isOnAcceptBid, shouldShowTakeBid }) => {
54 | return (
55 |
56 |
57 | Price
58 | Buyer
59 | Time
60 |
61 | {bids.length === 0 &&
}
62 | {bids.map((bid, index) => (
63 |
71 | ))}
72 |
73 | );
74 | };
75 |
76 | const Bid = ({
77 | bid,
78 | onTakeBid,
79 | isOnAcceptBid,
80 | className,
81 | shouldShowTakeBid,
82 | }) => {
83 | const { nostrOrdinalsAddress } = useWallet();
84 | return (
85 |
90 |
{`${toBTC(bid.price)} BTC`}
91 |
92 | {nostrOrdinalsAddress !== bid.bidOwner
93 | ? shortenStr(bid.bidOwner || "", 4)
94 | : "You"}
95 |
96 |
97 | {timeAgo(new Date(bid.created_at * 1000))}
98 |
99 | {shouldShowTakeBid && (
100 |
105 | )}
106 |
107 | );
108 | };
109 |
110 | export default BidList;
111 |
--------------------------------------------------------------------------------
/src/assets/scss/elements/_toast.scss:
--------------------------------------------------------------------------------
1 | /*--------------------------
2 | Toast Styles
3 | ----------------------------*/
4 |
5 | .toast {
6 | .notification-container {
7 | font-size: 14px;
8 | box-sizing: border-box;
9 | position: fixed;
10 | z-index: 999999;
11 | }
12 |
13 | .top-right {
14 | top: 12px;
15 | right: 12px;
16 | transition: transform 0.6s ease-in-out;
17 | animation: toast-in-right 0.7s;
18 | }
19 |
20 | .bottom-right {
21 | bottom: 12px;
22 | right: 12px;
23 | transition: transform 0.6s ease-in-out;
24 | animation: toast-in-right 0.7s;
25 | }
26 |
27 | .top-left {
28 | top: 12px;
29 | left: 12px;
30 | transition: transform 0.6s ease-in;
31 | animation: toast-in-left 0.7s;
32 | }
33 |
34 | .bottom-left {
35 | bottom: 12px;
36 | left: 12px;
37 | transition: transform 0.6s ease-in;
38 | animation: toast-in-left 0.7s;
39 | }
40 |
41 | .notification {
42 | background: #fff;
43 | transition: 0.3s ease;
44 | position: relative;
45 | pointer-events: auto;
46 | overflow: hidden;
47 | margin: 0 0 6px;
48 | padding: 30px;
49 | margin-bottom: 15px;
50 | width: 300px;
51 | max-height: 100px;
52 | border-radius: 3px 3px 3px 3px;
53 | box-shadow: 0 0 10px #999;
54 | color: #000;
55 | opacity: 0.9;
56 | background-position: 15px;
57 | background-repeat: no-repeat;
58 | }
59 |
60 | .notification:hover {
61 | box-shadow: 0 0 12px #fff;
62 | opacity: 1;
63 | cursor: pointer;
64 | }
65 |
66 | .notification-title {
67 | font-weight: 700;
68 | font-size: 16px;
69 | text-align: left;
70 | margin-top: 0;
71 | margin-bottom: 6px;
72 | width: 300px;
73 | height: 18px;
74 | }
75 |
76 | .notification-message {
77 | margin: 0;
78 | text-align: left;
79 | height: 18px;
80 | margin-left: -1px;
81 | overflow: hidden;
82 | text-overflow: ellipsis;
83 | white-space: nowrap;
84 | }
85 |
86 | .notification-image {
87 | float: left;
88 | margin-right: 15px;
89 | }
90 |
91 | .notification-image img {
92 | width: 30px;
93 | height: 30px;
94 | }
95 |
96 | .toast {
97 | height: 50px;
98 | width: 365px;
99 | color: #fff;
100 | padding: 20px 15px 10px 10px;
101 | }
102 |
103 | .notification-container button {
104 | position: relative;
105 | right: -0.3em;
106 | top: -0.3em;
107 | float: right;
108 | font-weight: 700;
109 | color: #fff;
110 | outline: none;
111 | border: none;
112 | text-shadow: 0 1px 0 #fff;
113 | opacity: 0.8;
114 | line-height: 1;
115 | font-size: 16px;
116 | padding: 0;
117 | cursor: pointer;
118 | background: 0 0;
119 | border: 0;
120 | }
121 |
122 | @keyframes toast-in-right {
123 | from {
124 | transform: translateX(100%);
125 | }
126 | to {
127 | transform: translateX(0);
128 | }
129 | }
130 |
131 | @keyframes toast-in-left {
132 | from {
133 | transform: translateX(-100%);
134 | }
135 | to {
136 | transform: translateX(0);
137 | }
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nosft",
3 | "version": "0.2.17",
4 | "private": true,
5 | "dependencies": {
6 | "@emotion/react": "^11.11.1",
7 | "@emotion/styled": "^11.11.0",
8 | "@mui/x-date-pickers": "^6.10.1",
9 | "@next/bundle-analyzer": "^13.4.12",
10 | "@noble/hashes": "^1.3.1",
11 | "@scure/base": "^1.1.1",
12 | "@scure/btc-signer": "^1.0.1",
13 | "@stacks/common": "^6.5.5",
14 | "axios": "^1.4.0",
15 | "bip32": "^4.0.0",
16 | "bitcoin-address-validation": "^2.2.1",
17 | "bitcoinjs-lib": "github:deezy-inc/bitcoinjs-lib",
18 | "clsx": "^2.0.0",
19 | "d3": "^7.9.0",
20 | "dayjs": "^1.11.9",
21 | "ecpair": "^2.1.0",
22 | "ethers": "^5.7.2",
23 | "framer-motion": "^10.13.1",
24 | "install": "^0.13.0",
25 | "lottie-react": "^2.4.0",
26 | "match-sorter": "^6.3.1",
27 | "next": "^13.4.12",
28 | "next-themes": "^0.2.1",
29 | "nextjs-google-analytics": "^2.3.3",
30 | "nosft-core": "^2.5.13",
31 | "nostr-tools": "^1.13.1",
32 | "npm": "^9.8.1",
33 | "prop-types": "^15.8.1",
34 | "qrcode.react": "^4.2.0",
35 | "react": "^18.2.0",
36 | "react-bootstrap": "^2.8.0",
37 | "react-countdown": "^2.3.5",
38 | "react-dom": "^18.2.0",
39 | "react-icons": "^4.10.1",
40 | "react-intersection-observer-hook": "^2.1.1",
41 | "react-loading-icons": "^1.1.0",
42 | "react-loading-skeleton": "^3.3.1",
43 | "react-rotating-text": "^1.4.1",
44 | "react-scripts": "^5.0.1",
45 | "react-scroll": "^1.8.9",
46 | "react-slick": "^0.29.0",
47 | "react-slide-fade-in": "^1.0.9",
48 | "react-social-icons": "^5.15.0",
49 | "react-toastify": "^9.1.3",
50 | "react-transition-group": "^4.4.5",
51 | "react-use": "^17.4.0",
52 | "react-useinterval": "^1.0.2",
53 | "rxjs": "^7.8.1",
54 | "sal.js": "^0.8.5",
55 | "socket.io-client": "^4.7.2",
56 | "swr": "^2.2.2",
57 | "tiny-secp256k1": "^2.2.3",
58 | "varuint-bitcoin": "^1.1.2"
59 | },
60 | "scripts": {
61 | "prepare": "husky install",
62 | "dev": "next dev",
63 | "start": "next start",
64 | "build": "npm run copy-env && next build",
65 | "copy-env": "jq '.\"SENTRY_ENVIRONMENT\" = \"'\"$SENTRY_ENVIRONMENT\"'\"' env-variables.json >> tmp.json && mv tmp.json env-variables.json",
66 | "format": "prettier --ignore-path .prettierignore --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
67 | "lint": "eslint --ignore-path .eslintignore . --ext ts --ext tsx --ext js --ext jsx",
68 | "lint:fix": "yarn format & next lint --dir src --fix",
69 | "clean": "rimraf dist"
70 | },
71 | "browserslist": {
72 | "production": [
73 | ">0.2%",
74 | "not dead",
75 | "not op_mini all"
76 | ],
77 | "development": [
78 | "last 1 chrome version",
79 | "last 1 firefox version",
80 | "last 1 safari version"
81 | ]
82 | },
83 | "description": "Nosft",
84 | "main": "index.js",
85 | "keywords": [],
86 | "author": "",
87 | "license": "ISC",
88 | "devDependencies": {
89 | "@babel/core": "^7.22.9",
90 | "@babel/eslint-parser": "^7.22.9",
91 | "@babel/preset-react": "^7.22.5",
92 | "@next/eslint-plugin-next": "^13.4.12",
93 | "@types/node-fetch": "^2.6.4",
94 | "eslint": "8.45.0",
95 | "eslint-config-airbnb": "^19.0.4",
96 | "eslint-config-prettier": "^8.8.0",
97 | "eslint-import-resolver-alias": "^1.1.2",
98 | "eslint-plugin-import": "^2.27.5",
99 | "eslint-plugin-jsx-a11y": "^6.7.1",
100 | "eslint-plugin-prettier": "^5.0.0",
101 | "eslint-plugin-react": "^7.33.0",
102 | "eslint-plugin-react-hooks": "^4.6.0",
103 | "husky": "^8.0.3",
104 | "lint-staged": "^13.2.3",
105 | "prettier": "^3.0.0",
106 | "rimraf": "^5.0.1",
107 | "sass": "^1.64.1"
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/hooks/use-connect-wallet.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { connectWallet, onAccountChange } from "@services/nosft";
3 | import SessionStorage, { SessionsStorageKeys } from "@services/session-storage";
4 | import LocalStorage from "@services/local-storage";
5 | import { toast } from "react-toastify";
6 |
7 | function useConnectWallet() {
8 | const [ordinalsPublicKey, setOrdinalsPublicKey] = useState("");
9 | const [paymentPublicKey, setPaymentPublicKey] = useState("");
10 | const [ordinalsAddress, setOrdinalsAddress] = useState("");
11 | const [paymentAddress, setPaymentAddress] = useState("");
12 | const [walletName, setWalletName] = useState("");
13 |
14 | const onConnectHandler = async (domain, onConnected) => {
15 | try {
16 | const {
17 | ordinalsPublicKey: xOrdinalsPublicKey,
18 | paymentPublicKey: xPaymentPublicKey,
19 | walletName: xWalletName,
20 | ordinalsAddress: xOrdinalsAddress,
21 | paymentAddress: xPaymentAddress,
22 | } = await connectWallet(domain);
23 | SessionStorage.set(SessionsStorageKeys.WALLET_NAME, xWalletName);
24 | SessionStorage.set(SessionsStorageKeys.DOMAIN, domain);
25 | SessionStorage.set(SessionsStorageKeys.ORDINALS_ADDRESS, xOrdinalsAddress);
26 | SessionStorage.set(SessionsStorageKeys.PAYMENT_ADDRESS, xPaymentAddress);
27 | SessionStorage.set(
28 | SessionsStorageKeys.ORDINALS_PUBLIC_KEY,
29 | xOrdinalsPublicKey
30 | );
31 | SessionStorage.set(
32 | SessionsStorageKeys.PAYMENT_PUBLIC_KEY,
33 | xPaymentPublicKey
34 | );
35 | setOrdinalsAddress(xOrdinalsAddress);
36 | setPaymentAddress(xPaymentAddress);
37 | setWalletName(xWalletName);
38 | setOrdinalsPublicKey(xOrdinalsPublicKey);
39 | setPaymentPublicKey(xPaymentPublicKey);
40 |
41 | if (onConnected) {
42 | onConnected();
43 | }
44 | } catch(e) {
45 | toast.error(e.response?.data?.message || e.message);
46 | }
47 |
48 | };
49 |
50 | const onDisconnectHandler = async () => {
51 | SessionStorage.clear();
52 | LocalStorage.clear();
53 | setOrdinalsPublicKey("");
54 | setPaymentPublicKey("");
55 | setWalletName("");
56 | setPaymentAddress("");
57 | };
58 |
59 | onAccountChange(() => {
60 | onDisconnectHandler();
61 | // Reconnect with new address
62 | onConnectHandler(SessionStorage.get(SessionsStorageKeys.DOMAIN));
63 | });
64 |
65 | useEffect(() => {
66 | // TODO: We should ask the browser if we are connected to the wallet
67 | const xOrdinalsPublicKey = SessionStorage.get(
68 | SessionsStorageKeys.ORDINALS_PUBLIC_KEY
69 | );
70 | const xPaymentPublicKey = SessionStorage.get(
71 | SessionsStorageKeys.PAYMENT_PUBLIC_KEY
72 | );
73 | const xWalletName = SessionStorage.get(SessionsStorageKeys.WALLET_NAME);
74 | const ordinalsAddress = SessionStorage.get(
75 | SessionsStorageKeys.ORDINALS_ADDRESS
76 | );
77 | const paymentAddress = SessionStorage.get(
78 | SessionsStorageKeys.PAYMENT_ADDRESS
79 | );
80 | if (xOrdinalsPublicKey) {
81 | setOrdinalsPublicKey(xOrdinalsPublicKey);
82 | }
83 | if (xPaymentPublicKey) {
84 | setPaymentPublicKey(xPaymentPublicKey);
85 | }
86 | if (xWalletName) {
87 | setWalletName(xWalletName);
88 | }
89 | if (ordinalsAddress) {
90 | setOrdinalsAddress(ordinalsAddress);
91 | }
92 | if (paymentAddress) {
93 | setPaymentAddress(paymentAddress);
94 | }
95 | }, []);
96 |
97 | return {
98 | ordinalsAddress,
99 | paymentAddress,
100 | ordinalsPublicKey,
101 | paymentPublicKey,
102 | onConnectHandler,
103 | onDisconnectHandler,
104 | walletName,
105 | };
106 | }
107 |
108 | export default useConnectWallet;
109 |
--------------------------------------------------------------------------------
/src/containers/Collection.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/no-array-index-key */
2 | import { useState, useMemo } from "react";
3 | import PropTypes from "prop-types";
4 | import clsx from "clsx";
5 | import SectionTitle from "@components/section-title";
6 | import OrdinalFilter from "@components/ordinal-filter";
7 | import OrdinalCard from "@components/collection-inscription";
8 | import { collectionAuthor, applyFilters } from "@containers/helpers";
9 |
10 | import "react-loading-skeleton/dist/skeleton.css";
11 |
12 | const Collection = ({ className, space, collection }) => {
13 | const [filteredOwnedUtxos, setFilteredOwnedUtxos] = useState([]);
14 |
15 | const [activeSort, setActiveSort] = useState();
16 | const [sortAsc, setSortAsc] = useState(false);
17 |
18 | const [utxosType, setUtxosType] = useState("");
19 |
20 | useMemo(() => {
21 | const filteredUtxos = applyFilters({
22 | utxos: collection.inscriptions || [],
23 | activeSort,
24 | sortAsc,
25 | utxosType,
26 | });
27 |
28 | setFilteredOwnedUtxos(filteredUtxos);
29 | }, [collection?.inscriptions, activeSort, sortAsc, utxosType]);
30 |
31 | let { author: name, slug, icon } = collectionAuthor || {};
32 | if (name) {
33 | author = {
34 | name,
35 | slug,
36 | image: {
37 | src: icon,
38 | },
39 | };
40 | }
41 |
42 | return (
43 |
51 |
52 |
53 |
54 |
58 |
59 |
60 | {collection?.inscriptions?.length > 0 && (
61 |
62 |
72 |
73 | )}
74 |
75 |
76 |
77 | {collection?.inscriptions?.length > 0 && (
78 | <>
79 | {filteredOwnedUtxos?.map((inscription) => (
80 |
84 |
91 |
92 | ))}
93 |
94 | {filteredOwnedUtxos.length === 0 && (
95 |
96 |
97 |
No results found
98 |
99 |
100 | )}
101 | >
102 | )}
103 |
104 |
105 |
106 | );
107 | };
108 |
109 | Collection.propTypes = {
110 | className: PropTypes.string,
111 | space: PropTypes.oneOf([1, 2]),
112 | collection: PropTypes.any,
113 | };
114 |
115 | Collection.defaultProps = {
116 | space: 1,
117 | type: "live",
118 | };
119 |
120 | export default Collection;
121 |
--------------------------------------------------------------------------------
/src/components/modals/connect-wallet/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/forbid-prop-types */
2 | import PropTypes from "prop-types";
3 | import Modal from "react-bootstrap/Modal";
4 | import Image from "next/image";
5 | import { useWallet } from "@context/wallet-context";
6 |
7 | // Gets the callback function from the parent component to notify when the wallet get's connecteds
8 | const ConnectWallet = ({ callback }) => {
9 | const {
10 | ethProvider,
11 | onConnectHandler: onConnect,
12 | showConnectModal: show,
13 | onHideConnectModal,
14 | } = useWallet();
15 |
16 | const wallets = [
17 | {
18 | name: "MetaMask",
19 | image: "/images/logo/metamask.png",
20 | ethereum: true,
21 |
22 | onClick: () => {
23 | onConnect("nosft.xyz", callback);
24 | },
25 | },
26 | {
27 | name: "Ordswap",
28 | image: "/images/logo/ordswap.svg",
29 | ethereum: true,
30 |
31 | onClick: () => {
32 | onConnect("ordswap.io", callback);
33 | },
34 | },
35 | {
36 | name: "Generative",
37 | image: "/images/logo/generative.png",
38 | ethereum: true,
39 |
40 | onClick: () => {
41 | onConnect("generative.xyz", callback);
42 | },
43 | },
44 | {
45 | name: "UniSat",
46 | image: "/images/logo/unisat.png",
47 | provider: "unisat",
48 | onClick: () => {
49 | onConnect("unisat.io", callback);
50 | },
51 | },
52 | {
53 | name: "Alby",
54 | image: "/images/logo/alby.svg",
55 |
56 | onClick: () => {
57 | onConnect("alby", callback);
58 | },
59 | },
60 | {
61 | name: "Xverse",
62 | image: "/images/logo/xverse.png",
63 | provider: "xverse",
64 | onClick: () => {
65 | onConnect("xverse", callback);
66 | },
67 | },
68 | ];
69 |
70 | const getWallets = () => {
71 | const activeWallets = [];
72 | wallets.forEach((wallet) => {
73 | if (typeof window === "undefined") return;
74 | if (
75 | (wallet.provider === "xverse" && !window.BitcoinProvider) ||
76 | (!ethProvider && wallet.ethereum) ||
77 | (wallet.provider === "unisat" && !window.unisat)
78 | ) {
79 | return;
80 | }
81 | activeWallets.push(wallet);
82 | });
83 | return activeWallets;
84 | };
85 |
86 | return (
87 |
93 | {show && (
94 |
102 | )}
103 |
104 | Choose your wallet
105 |
106 |
107 |
108 |
109 | {getWallets().map((wallet) => (
110 |
127 | ))}
128 |
129 |
130 |
131 |
132 | );
133 | };
134 |
135 | ConnectWallet.propTypes = {
136 | callback: PropTypes.func,
137 | };
138 |
139 | export default ConnectWallet;
140 |
--------------------------------------------------------------------------------
/src/components/modals/send-bulk-modal/select-rate-step.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { InputGroup, Form } from "react-bootstrap";
3 | import Button from "@ui/button";
4 | import { TailSpin } from "react-loading-icons";
5 |
6 | import {
7 | shortenStr,
8 | } from "@services/nosft";
9 |
10 | const getTitle = (sendingInscriptions, sendingUtxos) => {
11 | const inscriptionCount = sendingInscriptions.length;
12 | const utxoCount = sendingUtxos.length;
13 | const inscriptionText = `inscription${inscriptionCount !== 1 ? 's' : ''}`;
14 | const utxoText = `UTXO${utxoCount !== 1 ? 's' : ''}`;
15 |
16 | const skipRunesText = "We are going to skip runes if there are inscriptions and runes selected";
17 |
18 | if (inscriptionCount && utxoCount) {
19 | return `You are about to send ${inscriptionCount} ${inscriptionText} and ${utxoCount} ${utxoText}. ${skipRunesText}.`;
20 | }
21 | if (inscriptionCount) {
22 | return `You are about to send ${inscriptionCount} ${inscriptionText}. ${skipRunesText}.`;
23 | }
24 | if (utxoCount) {
25 | return `You are about to send ${utxoCount} ${utxoText}. ${skipRunesText}.`;
26 | }
27 | return '';
28 | };
29 |
30 | export const SelectRateStep = ({ destinationBtcAddress, isBtcInputAddressValid, sendFeeRate, addressOnChange, feeRateOnChange, preparePsbt, isSending, sendingInscriptions, sendingUtxos }) => (
31 |
32 |
33 | {getTitle(sendingInscriptions, sendingUtxos)}
34 |
35 |
36 |
37 |
38 |
39 |
40 |
48 |
49 |
50 |
51 | That is not a valid BTC
52 | address
53 |
54 |
55 |
56 | Select a fee rate
57 |
63 |
64 |
65 |
66 |
67 |
68 | {!!destinationBtcAddress && Destination}
69 | Fee rate
70 |
71 |
72 | {!!destinationBtcAddress && (
73 | {shortenStr(destinationBtcAddress)}
74 | )}
75 | {sendFeeRate} sat/vbyte
76 |
77 |
78 |
79 |
80 |
81 |
90 |
91 |
92 |
93 | );
--------------------------------------------------------------------------------
/src/layouts/header.js:
--------------------------------------------------------------------------------
1 | /* eslint no-extra-boolean-cast: "off" */
2 |
3 | import React, { useState } from "react";
4 | import PropTypes from "prop-types";
5 | import clsx from "clsx";
6 |
7 | import Logo from "@components/logo";
8 | import MainMenu from "@components/menu/main-menu";
9 | import MobileMenu from "@components/menu/mobile-menu";
10 | import UserDropdown from "@components/user-dropdown";
11 | import { useOffcanvas } from "@hooks";
12 | import BurgerButton from "@ui/burger-button";
13 | import Button from "@ui/button";
14 | import { useWallet } from "@context/wallet-context";
15 | import ConnectWallet from "@components/modals/connect-wallet";
16 | import { TESTNET, INSCRIBOR_URL } from "@services/nosft";
17 | import menuData from "../data/general/menu";
18 | import headerData from "../data/general/header.json";
19 |
20 | const Header = React.forwardRef(({ className }, ref) => {
21 | const { ordinalsPublicKey, nostrOrdinalsAddress, onShowConnectModal } =
22 | useWallet();
23 |
24 | const { offcanvas, offcanvasHandler } = useOffcanvas();
25 |
26 | return (
27 | <>
28 |
35 | {TESTNET && (
36 |
37 |
YOU ARE USING TESTNET!
38 |
39 | )}
40 |
41 |
42 |
43 |
44 |
45 |
51 |
52 |
53 |
54 | {!Boolean(ordinalsPublicKey) && (
55 |
56 |
57 |
65 |
66 |
67 |
68 |
69 | )}
70 | {ordinalsPublicKey && nostrOrdinalsAddress && (
71 |
72 |
73 |
74 | )}
75 |
80 |
81 |
82 |
83 |
84 |
90 | >
91 | );
92 | });
93 |
94 | Header.propTypes = {
95 | className: PropTypes.string,
96 | };
97 |
98 | export default Header;
99 |
--------------------------------------------------------------------------------
/src/components/collection-inscription/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/forbid-prop-types */
2 | import { useState } from "react";
3 | import dynamic from "next/dynamic";
4 | import PropTypes from "prop-types";
5 |
6 | import clsx from "clsx";
7 | import Anchor from "@ui/anchor";
8 | import ClientAvatar from "@ui/client-avatar";
9 | import ProductBid from "@components/product-bid";
10 | import { useWallet } from "@context/wallet-context";
11 | import { shortenStr } from "@services/nosft";
12 | import { InscriptionPreview } from "@components/inscription-preview";
13 | import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
14 |
15 | const OrdinalCard = ({ overlay, inscription, auction, onClick }) => {
16 | const [onSale, setOnSale] = useState(null);
17 |
18 | const id = inscription?.id;
19 | const num = inscription?.num;
20 | const inscriptionId = inscription?.inscriptionId
21 | ? shortenStr(inscription?.inscriptionId)
22 | : "";
23 | const { name, slug, icon } = inscription?.collection || {};
24 | const hasCollection = Boolean(inscription?.collection);
25 |
26 | const inscriptionValue =
27 | auction?.currentPrice || inscription?.nostr?.value || inscription?.sats;
28 | const price = {
29 | amount: inscriptionValue?.toLocaleString("en-US"),
30 | currency: "Sats",
31 | };
32 | const type =
33 | auction?.currentPrice || inscription?.nostr?.value ? "buy" : "view";
34 | const date = auction?.startTime
35 | ? auction?.startTime / 1000
36 | : inscription?.created;
37 | const path = `/inscription/${id}`;
38 |
39 | return (
40 |
41 |
42 |
43 |
44 |
45 |
46 | {auction?.nextPriceDrop?.scheduledTime && (
47 | //
48 |
51 | )}
52 |
53 |
54 | {inscription && (
55 |
56 | {inscription?.meta?.name || shortenStr(id)}
57 |
58 | )}
59 | {!inscription &&
}
60 |
61 |
62 |
63 | {hasCollection && (
64 |
70 | )}
71 |
72 | {!hasCollection &&
}
73 |
74 |
75 | {id && (
76 |
77 | {inscriptionId ? `${inscriptionId}` : "\u00A0"}
78 |
79 | )}
80 | {!id &&
}
81 |
82 |
83 |
84 |
85 | {inscription && (
86 |
96 | )}
97 | {!inscription && (
98 |
99 |
100 |
101 | )}
102 |
103 |
104 |
105 | );
106 | };
107 |
108 | OrdinalCard.propTypes = {
109 | overlay: PropTypes.bool,
110 | inscription: PropTypes.object,
111 | auction: PropTypes.object,
112 | onClick: PropTypes.func,
113 | };
114 |
115 | OrdinalCard.defaultProps = {
116 | overlay: false,
117 | };
118 |
119 | export default OrdinalCard;
120 |
--------------------------------------------------------------------------------
/src/utils/bip322.js:
--------------------------------------------------------------------------------
1 | import * as bitcoin from "bitcoinjs-lib";
2 | import * as ecc from "tiny-secp256k1";
3 | import { signSigHash, toXOnly, getAddressInfo } from "@services/nosft";
4 | import { hexToBytes, utf8ToBytes } from "@stacks/common";
5 | import { serializeTaprootSignature } from "bitcoinjs-lib/src/psbt/bip371";
6 | import { encode } from "varuint-bitcoin";
7 | import { base64 } from "@scure/base";
8 | import SessionStorage, { SessionsStorageKeys } from "@services/session-storage";
9 | import { sha256 } from "@noble/hashes/sha256";
10 |
11 | bitcoin.initEccLib(ecc);
12 | const bip322MessageTag = "BIP0322-signed-message";
13 |
14 | // See tagged hashes section of BIP-340
15 | // https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#design
16 | const messageTagHash = Uint8Array.from([
17 | ...sha256(utf8ToBytes(bip322MessageTag)),
18 | ...sha256(utf8ToBytes(bip322MessageTag)),
19 | ]);
20 |
21 | function hashBip322Message(message) {
22 | return sha256(
23 | Uint8Array.from([
24 | ...messageTagHash,
25 | ...(typeof message === "string" || message instanceof String
26 | ? utf8ToBytes(message)
27 | : message),
28 | ]),
29 | );
30 | }
31 |
32 | // TODO: This function is NOT IN USE YET. It will be implemented in a different PR with a new UI.!
33 | // Used to prove ownership of address and associated ordinals
34 | // https://github.com/LegReq/bip0322-signatures/blob/master/BIP0322_signing.ipynb
35 | export async function signBip322MessageSimple(message) {
36 | const provider = SessionStorage.get(SessionsStorageKeys.DOMAIN);
37 | if (provider === "unisat.io") {
38 | return window.unisat.signMessage(message, "bip322-simple");
39 | }
40 |
41 | // const message = await prompt("Please enter BIP322 message to sign", "");
42 | const publicKey = SessionStorage.get(SessionsStorageKeys.ORDINALS_PUBLIC_KEY);
43 |
44 | const nostrScript = await getAddressInfo(toXOnly(publicKey.toString()));
45 | const scriptPubkey = nostrScript.output;
46 | const { pubkey } = nostrScript;
47 |
48 | // Generate a tagged hash of message to sign
49 | const prevoutHash = hexToBytes(
50 | "0000000000000000000000000000000000000000000000000000000000000000",
51 | );
52 | const prevoutIndex = 0xffffffff;
53 | const sequence = 0;
54 | const hash = hashBip322Message(message);
55 |
56 | // Create the virtual to_spend transaction
57 | const commands = [0, Buffer.from(hash)];
58 | const scriptSig = bitcoin.script.compile(commands);
59 | const virtualToSpend = new bitcoin.Transaction();
60 | virtualToSpend.version = 0;
61 | virtualToSpend.locktime = 0;
62 | virtualToSpend.addInput(
63 | Buffer.from(prevoutHash),
64 | prevoutIndex,
65 | sequence,
66 | scriptSig,
67 | );
68 | virtualToSpend.addOutput(Buffer.from(scriptPubkey), 0);
69 |
70 | // Create the virtual to_sign transaction
71 | const virtualToSign = new bitcoin.Psbt();
72 | virtualToSign.setLocktime(0);
73 | virtualToSign.setVersion(0);
74 | const prevTxHash = virtualToSpend.getHash(); // or id?
75 | const prevOutIndex = 0;
76 | const toSignScriptSig = bitcoin.script.compile([106]);
77 |
78 | virtualToSign.addInput({
79 | hash: prevTxHash,
80 | index: prevOutIndex,
81 | sequence: 0,
82 | witnessUtxo: { script: Buffer.from(scriptPubkey, "hex"), value: 0 },
83 | tapInternalKey: toXOnly(pubkey),
84 | });
85 | virtualToSign.addOutput({ script: toSignScriptSig, value: 0 });
86 |
87 | const sigHash = virtualToSign.__CACHE.__TX.hashForWitnessV1(
88 | 0,
89 | [virtualToSign.data.inputs[0].witnessUtxo.script],
90 | [virtualToSign.data.inputs[0].witnessUtxo.value],
91 | bitcoin.Transaction.SIGHASH_DEFAULT,
92 | );
93 |
94 | const sign = await signSigHash({ sigHash });
95 |
96 | virtualToSign.updateInput(0, {
97 | tapKeySig: serializeTaprootSignature(Buffer.from(sign, "hex")),
98 | });
99 | virtualToSign.finalizeAllInputs();
100 |
101 | const toSignTx = virtualToSign.extractTransaction();
102 |
103 | function encodeVarString(b) {
104 | return Buffer.concat([encode(b.byteLength), b]);
105 | }
106 |
107 | const len = encode(toSignTx.ins[0].witness.length);
108 | const result = Buffer.concat([
109 | len,
110 | ...toSignTx.ins[0].witness.map((w) => encodeVarString(w)),
111 | ]);
112 |
113 | const { signature } = {
114 | virtualToSpend,
115 | virtualToSign: toSignTx,
116 | signature: base64.encode(result),
117 | };
118 | return signature;
119 | }
120 |
--------------------------------------------------------------------------------
/src/utils/types.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 |
3 | export const IDType = PropTypes.oneOfType([PropTypes.string, PropTypes.number]);
4 |
5 | export const HeadingType = PropTypes.shape({
6 | id: IDType,
7 | content: PropTypes.string.isRequired,
8 | });
9 |
10 | export const TextType = PropTypes.shape({
11 | id: IDType,
12 | content: PropTypes.string.isRequired,
13 | });
14 |
15 | export const ImageType = PropTypes.shape({
16 | src: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]).isRequired,
17 | alt: PropTypes.string,
18 | width: PropTypes.number,
19 | height: PropTypes.number,
20 | layout: PropTypes.string,
21 | });
22 |
23 | export const ButtonComponentType = {
24 | children: PropTypes.node.isRequired,
25 | type: PropTypes.oneOf(["button", "submit", "reset"]),
26 | label: PropTypes.string,
27 | onClick: PropTypes.func,
28 | className: PropTypes.string,
29 | path: PropTypes.string,
30 | size: PropTypes.oneOf(["large", "small", "medium"]),
31 | color: PropTypes.oneOf(["primary", "primary-alta"]),
32 | fullwidth: PropTypes.bool,
33 | };
34 |
35 | // eslint-disable-next-line no-unused-vars
36 | const { children, ...restButtonTypes } = ButtonComponentType;
37 |
38 | export const ButtonType = PropTypes.shape({
39 | content: PropTypes.string.isRequired,
40 | ...restButtonTypes,
41 | });
42 |
43 | export const SectionTitleType = PropTypes.shape({
44 | title: PropTypes.string,
45 | subtitle: PropTypes.string,
46 | });
47 |
48 | export const ItemType = PropTypes.shape({
49 | id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
50 | title: PropTypes.string,
51 | subtitle: PropTypes.string,
52 | path: PropTypes.string,
53 | description: PropTypes.string,
54 | images: PropTypes.arrayOf(ImageType),
55 | image: ImageType,
56 | });
57 |
58 | export const OrdinalType = PropTypes.shape({
59 | id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
60 | title: PropTypes.string.isRequired,
61 | slug: PropTypes.string.isRequired,
62 | description: PropTypes.string.isRequired,
63 | price: PropTypes.shape({
64 | amount: PropTypes.number.isRequired,
65 | currency: PropTypes.string.isRequired,
66 | }).isRequired,
67 | likeCount: PropTypes.number,
68 | image: ImageType,
69 | authors: PropTypes.arrayOf(
70 | PropTypes.shape({
71 | name: PropTypes.string.isRequired,
72 | slug: PropTypes.string.isRequired,
73 | image: ImageType,
74 | })
75 | ),
76 | utxo: PropTypes.number,
77 | });
78 |
79 | export const SellerType = PropTypes.shape({
80 | id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
81 | name: PropTypes.string.isRequired,
82 | slug: PropTypes.string.isRequired,
83 | total_sale: PropTypes.number.isRequired,
84 | image: ImageType.isRequired,
85 | top_since: PropTypes.string,
86 | isVarified: PropTypes.bool,
87 | });
88 |
89 | export const CollectionType = PropTypes.shape({
90 | id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
91 | title: PropTypes.string.isRequired,
92 | slug: PropTypes.string.isRequired,
93 | total_item: PropTypes.number.isRequired,
94 | image: ImageType.isRequired,
95 | thumbnails: PropTypes.arrayOf(ImageType).isRequired,
96 | profile_image: ImageType.isRequired,
97 | });
98 |
99 | export const FeatureProductsType = PropTypes.shape({
100 | id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
101 | title: PropTypes.string.isRequired,
102 | slug: PropTypes.string.isRequired,
103 | author: PropTypes.shape({
104 | name: PropTypes.string.isRequired,
105 | slug: PropTypes.string,
106 | }),
107 | image: ImageType.isRequired,
108 | });
109 |
110 | export const NotifactionType = PropTypes.shape({
111 | id: IDType,
112 | title: PropTypes.string,
113 | description: PropTypes.string,
114 | path: PropTypes.string,
115 | date: PropTypes.string,
116 | time: PropTypes.string,
117 | image: ImageType,
118 | });
119 |
120 | export const NostrEvenType = PropTypes.shape({
121 | id: PropTypes.string,
122 | kind: PropTypes.number,
123 | pubkey: PropTypes.string,
124 | created_at: PropTypes.number,
125 | content: PropTypes.string,
126 | tags: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
127 | sig: PropTypes.string,
128 | value: PropTypes.number,
129 | });
130 |
--------------------------------------------------------------------------------