From 2558d4af0174859140a4aceff903aa2b8155b579 Mon Sep 17 00:00:00 2001 From: Nicholas Shellabarger Date: Wed, 27 Mar 2024 10:23:05 -0600 Subject: [PATCH] 27 Mar 2023 SOD --- app/app-sandbox/package.json | 11 +- app/app-sandbox/public/CNAME | 2 +- app/app-sandbox/src/App.js | 817 +++++++++++++-- app/app-sandbox/src/abis/index.js | 937 +++++++++++++++++ .../src/components/AccountBalances/index.js | 249 +++-- app/app-sandbox/src/components/AppBar.tsx | 45 +- .../src/components/ApproveDialog/index.js | 63 +- .../src/components/BuyModal/index.js | 513 ++++++++++ .../src/components/Leaderboard/index.js | 77 ++ .../src/components/LoadingIndicator/index.js | 2 +- .../src/components/MainMenu/index.js | 104 +- .../src/components/NFTCard/index.js | 48 + .../src/components/NFTImage/index.js | 3 +- .../src/components/PoolForm/index.tsx | 91 +- .../src/components/PoolList/index.js | 5 +- .../src/components/SalesHistoryTable/index.js | 23 +- .../src/components/SearchInput/index.js | 28 + .../src/components/SellModal/index.js | 367 +++++++ .../src/components/SendDialog/index.js | 39 +- .../src/components/SwapForm/index.tsx | 288 ++++-- .../src/components/TokenDisplay/index.js | 322 ++++++ .../src/components/TokenSelect/index.jsx | 72 ++ .../src/components/TypeFilter/index.js | 33 + app/app-sandbox/src/constants/dex.js | 1 + app/app-sandbox/src/constants/mp.js | 22 + app/app-sandbox/src/custom.d.ts | 1 + app/app-sandbox/src/db.js | 53 + app/app-sandbox/src/index.js | 13 +- app/app-sandbox/src/pages/DEX/index.js | 142 +-- app/app-sandbox/src/pages/Home/index.js | 35 +- .../src/pages/NFTAuctions/index.js | 196 ++++ app/app-sandbox/src/pages/NFTClaim/index.js | 292 ++++++ .../src/pages/NFTCollection/index.js | 207 ++-- .../src/pages/NFTDiscover/index.js | 69 ++ app/app-sandbox/src/pages/NFTFeed/index.js | 195 ++++ .../src/pages/NFTMarketplace/index.js | 650 +----------- .../src/pages/NFTPortfolio/index.js | 199 +--- .../src/pages/NFTProjects/index.js | 97 ++ .../src/pages/NFTReverseAuctions/index.js | 234 +++++ app/app-sandbox/src/pages/NFTSales/index.js | 343 +++++++ app/app-sandbox/src/pages/NFTToken/index.js | 960 ++++++++++-------- app/app-sandbox/src/pages/Token/index.js | 104 +- .../src/services/HighForgeService.ts | 15 + .../src/services/NFTIndexerService.ts | 26 + .../src/store/MarketplaceContext.js | 321 ++++++ app/app-sandbox/src/utils/hf.js | 30 + app/app-sandbox/src/utils/mp.js | 126 +++ app/app-sandbox/yarn.lock | 98 +- 48 files changed, 6656 insertions(+), 1912 deletions(-) create mode 100644 app/app-sandbox/src/components/BuyModal/index.js create mode 100644 app/app-sandbox/src/components/Leaderboard/index.js create mode 100644 app/app-sandbox/src/components/NFTCard/index.js create mode 100644 app/app-sandbox/src/components/SearchInput/index.js create mode 100644 app/app-sandbox/src/components/SellModal/index.js create mode 100644 app/app-sandbox/src/components/TokenDisplay/index.js create mode 100644 app/app-sandbox/src/components/TokenSelect/index.jsx create mode 100644 app/app-sandbox/src/components/TypeFilter/index.js create mode 100644 app/app-sandbox/src/constants/dex.js create mode 100644 app/app-sandbox/src/constants/mp.js create mode 100644 app/app-sandbox/src/pages/NFTAuctions/index.js create mode 100644 app/app-sandbox/src/pages/NFTClaim/index.js create mode 100644 app/app-sandbox/src/pages/NFTDiscover/index.js create mode 100644 app/app-sandbox/src/pages/NFTFeed/index.js create mode 100644 app/app-sandbox/src/pages/NFTProjects/index.js create mode 100644 app/app-sandbox/src/pages/NFTReverseAuctions/index.js create mode 100644 app/app-sandbox/src/pages/NFTSales/index.js create mode 100644 app/app-sandbox/src/services/HighForgeService.ts create mode 100644 app/app-sandbox/src/services/NFTIndexerService.ts create mode 100644 app/app-sandbox/src/store/MarketplaceContext.js create mode 100644 app/app-sandbox/src/utils/hf.js create mode 100644 app/app-sandbox/src/utils/mp.js diff --git a/app/app-sandbox/package.json b/app/app-sandbox/package.json index 6087d3b..fbc0f4f 100644 --- a/app/app-sandbox/package.json +++ b/app/app-sandbox/package.json @@ -19,26 +19,30 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", - "@txnlab/use-wallet": "^2.3.0", + "@txnlab/use-wallet": "^2.7.0", "@walletconnect/modal-sign-html": "^2.5.5", "add": "^2.0.6", "algorand-walletconnect-qrcode-modal": "^1.8.0", "algosdk": "^2.7.0", "arc200js": "^2.3.3", - "arccjs": "^2.4.9", - "axios": "^1.4.0", + "arccjs": "^2.5.4", + "axios": "^1.6.7", "bignumber.js": "^9.1.2", "dexie": "^3.2.4", "dexie-react-hooks": "^1.1.7", "ethers": "^5.7.2", + "import": "^0.0.6", + "lute-connect": "^1.1.0", "moment": "^2.29.4", "nfdjs": "^1.1.0", "process": "^0.11.10", "prop-types": "^15.8.1", "react": "^18.2.0", + "react-confetti": "^6.1.0", "react-copy-to-clipboard": "^5.1.0", "react-csv": "^2.2.2", "react-dom": "^18.2.0", + "react-fast-marquee": "^1.6.3", "react-google-charts": "^4.0.1", "react-lazy-load-image-component": "^1.6.0", "react-router-dom": "^6.14.0", @@ -49,6 +53,7 @@ "swap200js": "^0.0.14", "tweetnacl": "^1.0.3", "typescript": "^5.0.4", + "ulujs": "^0.2.6", "use-debounce": "^9.0.4", "usehooks-ts": "^2.9.1", "web-vitals": "^2.1.4", diff --git a/app/app-sandbox/public/CNAME b/app/app-sandbox/public/CNAME index 9923255..ec35ab2 100644 --- a/app/app-sandbox/public/CNAME +++ b/app/app-sandbox/public/CNAME @@ -1 +1 @@ -shellyssandbox.xyz +shellyssandbox.xyz \ No newline at end of file diff --git a/app/app-sandbox/src/App.js b/app/app-sandbox/src/App.js index bad3010..9b67953 100644 --- a/app/app-sandbox/src/App.js +++ b/app/app-sandbox/src/App.js @@ -2,8 +2,6 @@ import React, { useEffect } from "react"; import "./App.css"; -import { Routes, Route } from "react-router-dom"; - import algosdk from "algosdk"; import { @@ -16,6 +14,7 @@ import { DeflyWalletConnect } from "@blockshake/defly-connect"; import { PeraWalletConnect } from "@perawallet/connect"; import { DaffiWalletConnect } from "@daffiwallet/connect"; import { WalletConnectModalSign } from "@walletconnect/modal-sign-html"; +import LuteConnect from "lute-connect"; import Home from "./pages/Home"; import DEX from "./pages/DEX"; @@ -35,6 +34,27 @@ import MainMenu from "./components/MainMenu"; import AppBar from "./components/AppBar.tsx"; import { MyCustomProvider } from "./wallet/CustomProvider"; import { getCurrentNode, getGenesisHash } from "./utils/reach"; +import NFTIndexerService from "./services/NFTIndexerService.ts"; +import { nftDb, mpDb, db } from "./db.js"; +import { + ctcInfoMp200, + ctcInfoMp201, + ctcInfoMp202, + ctcInfoMp203, + ctcInfoMp204, +} from "./constants/mp.js"; +import { getAlgorandClients } from "./utils/algorand.js"; +import { CONTRACT, arc200 } from "ulujs"; +import { mp200Schema, mp201Schema, mp202Schema } from "./abis/index.js"; +import { ctcInfoTri200 } from "./constants/dex.js"; +import triSchema from "./abis/triumvirate.json"; +import { MarketplaceProvider } from "./store/MarketplaceContext.js"; + +import { HashRouter, Routes, Route } from "react-router-dom"; +import { ToastContainer } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; + +const { algodClient, indexerClient } = getAlgorandClients(); function App() { const [node] = getCurrentNode(); @@ -47,24 +67,22 @@ function App() { id: "defly", clientStatic: DeflyWalletConnect, }, - /* - { - id: PROVIDER_ID.WALLETCONNECT, - clientStatic: WalletConnectModalSign, - clientOptions: { - projectId: "1da7d79da1e1c620ac717a04f1bf2cb4", - metadata: { - name: "ARC200", - description: "ARC200 Dapp", - url: "https://temptemp3.github.io/arc200.algo.xyz/", - icons: ["https://walletconnect.com/walletconnect-logo.png"], - }, - modalOptions: { - themeMode: "dark", - }, - }, - }, - */ + // { + // id: PROVIDER_ID.WALLETCONNECT, + // clientStatic: WalletConnectModalSign, + // clientOptions: { + // projectId: "1da7d79da1e1c620ac717a04f1bf2cb4", + // metadata: { + // name: "Shelly's Sandbox", + // description: "Shelly's Sandbox Dapp - DEX, NFTs, and more", + // url: "https://shellyssandbox.xyz", + // icons: ["https://walletconnect.com/walletconnect-logo.png"], + // }, + // modalOptions: { + // themeMode: "dark", + // }, + // }, + // }, { id: PROVIDER_ID.CUSTOM, clientOptions: { @@ -76,6 +94,12 @@ function App() { genesisHash: getGenesisHash(node), }, }, + { + id: PROVIDER_ID.LUTE, + clientStatic: LuteConnect, + clientOptions: { siteName: "Shell's Sandbox" }, + }, + { id: PROVIDER_ID.KIBISIS }, ]; break; default: @@ -85,85 +109,706 @@ function App() { { id: PROVIDER_ID.DEFLY, clientStatic: DeflyWalletConnect }, { id: PROVIDER_ID.PERA, clientStatic: PeraWalletConnect }, { id: PROVIDER_ID.DAFFI, clientStatic: DaffiWalletConnect }, - { - id: PROVIDER_ID.WALLETCONNECT, - clientStatic: WalletConnectModalSign, - clientOptions: { - projectId: "1da7d79da1e1c620ac717a04f1bf2cb4", - metadata: { - name: "ARC200", - description: "ARC200 Dapp", - url: "https://temptemp3.github.io/arc200.algo.xyz/", - icons: ["https://walletconnect.com/walletconnect-logo.png"], - }, - modalOptions: { - themeMode: "dark", - }, - }, - }, - //{ id: PROVIDER_ID.EXODUS }, - { - id: PROVIDER_ID.CUSTOM, - clientOptions: { - name: "kibisis", - icon: "https://avatars.githubusercontent.com/u/99801015?s=200&v=4", - getProvider: (params) => { - return new MyCustomProvider(params.algosdkStatic ?? algosdk); - }, - genesisHash: getGenesisHash(node), - }, - }, + // { + // id: PROVIDER_ID.WALLETCONNECT, + // clientStatic: WalletConnectModalSign, + // clientOptions: { + // projectId: "1da7d79da1e1c620ac717a04f1bf2cb4", + // metadata: { + // name: "ARC200", + // description: "ARC200 Dapp", + // url: "https://temptemp3.github.io/arc200.algo.xyz/", + // icons: ["https://walletconnect.com/walletconnect-logo.png"], + // }, + // modalOptions: { + // themeMode: "dark", + // }, + // }, + // }, + // { id: PROVIDER_ID.EXODUS }, + // { id: PROVIDER_ID.KIBISIS }, + // { + // id: PROVIDER_ID.CUSTOM, + // clientOptions: { + // name: "kibisis", + // icon: "https://avatars.githubusercontent.com/u/99801015?s=200&v=4", + // getProvider: (params) => { + // return new MyCustomProvider(params.algosdkStatic ?? algosdk); + // }, + // genesisHash: getGenesisHash(node), + // }, + // }, ]; break; } const providers = useInitializeProviders({ providers: networkProviders, nodeConfig: { - network: "testnet", - nodeServer: "https://testnet-api.algonode.cloud", + network: "voi-testnet", + nodeServer: "https://testnet-api.voi.nodly.io", nodeToken: "", nodePort: "443", }, algosdkStatic: algosdk, + debug: true, }); + + const [currentRound, setCurrentRound] = React.useState(0); + useEffect(() => { + algodClient + .status() + .do() + .then((s) => { + setCurrentRound(s["last-round"]); + }); + }, []); + + // EFFECT: update mp db + useEffect(() => { + if (!currentRound) return; + (async () => { + const mp = ctcInfoMp200; + const lastRound = localStorage.getItem("mp200-current-round"); + const ci = new CONTRACT(mp, algodClient, indexerClient, mp200Schema); + const evts = await ci.getEvents({ + minRound: lastRound, + }); + const listings = evts.find((el) => el.name === "ListEvent").events; + const sales = evts.find((el) => el.name === "BuyEvent").events; + const deletes = evts.find( + (el) => el.name === "DeleteNetListingEvent" + ).events; + await Promise.allSettled([ + ...listings.map((el) => { + const [txId, round, ts, lId, cId, tId, lAddr, lPrc] = el; + const pk = `${node}:${mp}:${lId}`; + const listing = { + pk, + mp, + txId, + round, + timestamp: ts, + lId, + cId, + tId, + lAddr, + lPrc, + }; + return mpDb.listings.put(listing, pk); + }), + ...sales.map((el) => { + const [txId, round, ts, lId, cId, tId, lAddr, lPrc, bAddr] = el; + const pk = `${node}:${mp}:${lId}`; + const sale = { + pk, + mp, + txId, + round, + timestamp: ts, + lId, + cId, + tId, + lAddr, + lPrc, + bAddr, + }; + return mpDb.sales.put(sale, pk); + }), + ...deletes.map((el) => { + const [txId, round, ts, lId] = el; + const pk = `${node}:${mp}:${lId}`; + const deletion = { + pk, + mp, + txId, + round, + timestamp: ts, + lId, + }; + return mpDb.deletions.put(deletion, pk); + }), + ]); + localStorage.setItem("mp200-current-round", currentRound + 1); + })(); + }, [currentRound]); + + // EFFECT: update mp201b db + // useEffect(() => { + // if (!currentRound) return; + // (async () => { + // const mp = 28368147; + // const lastRound = localStorage.getItem("mp201b-current-round"); + // const ci = new CONTRACT(mp, algodClient, indexerClient, mp201Schema); + // const evts = await ci.getEvents({ + // minRound: lastRound, + // }); + // const listings = evts.find((el) => el.name === "ListEvent").events; + // const sales = evts.find((el) => el.name === "BuyEvent").events; + // const deletes = evts.find( + // (el) => el.name === "DeleteListingEvent" + // ).events; + // const claims = evts.find((el) => el.name === "ClaimEvent").events; + // await Promise.allSettled([ + // ...listings.map((el) => { + // const [txId, round, ts, lId, cId, tId, lAddr, lPrc] = el; + // const pk = `${node}:${mp}:${lId}`; + // const listing = { + // pk, + // mp, + // txId, + // round, + // timestamp: ts, + // lId, + // cId, + // tId, + // lAddr, + // lPrc, + // }; + // return mpDb.listings.put(listing, pk); + // }), + // ...sales.map((el) => { + // const [txId, round, ts, lId, cId, tId, lAddr, lPrc, bAddr] = el; + // const pk = `${node}:${mp}:${lId}`; + // const sale = { + // pk, + // mp, + // txId, + // round, + // timestamp: ts, + // lId, + // // cId, + // // tId, + // // lAddr, + // // lPrc, + // // bAddr, + // }; + // return mpDb.sales.put(sale, pk); + // }), + // ...deletes.map((el) => { + // const [txId, round, ts, lId] = el; + // const pk = `${node}:${mp}:${lId}`; + // const deletion = { + // pk, + // mp, + // txId, + // round, + // timestamp: ts, + // lId, + // }; + // return mpDb.deletions.put(deletion, pk); + // }), + // ...claims.map((el) => { + // const [txId, round, ts, lId, cId, tId, lAddr, lPrc, bAddr] = el; + // const pk = `${node}:${mp}:${lId}`; + // const claim = { + // pk, + // mp, + // txId, + // round, + // timestamp: ts, + // lId, + // // cId, + // // tId, + // // lAddr, + // // lPrc, + // // bAddr, + // }; + // return mpDb.claims.put(claim, pk); + // }), + // ]); + // localStorage.setItem("mp201b-current-round", currentRound + 1); + // })(); + // }, [currentRound]); + + // EFFECT: update mp201 db + useEffect(() => { + if (!currentRound) return; + (async () => { + const mp = ctcInfoMp201; + const lastRound = localStorage.getItem("mp201-current-round"); + const ci = new CONTRACT(mp, algodClient, indexerClient, mp201Schema); + const evts = await ci.getEvents({ + minRound: lastRound, + }); + const listings = evts.find((el) => el.name === "ListEvent").events; + const sales = evts.find((el) => el.name === "BuyEvent").events; + const deletes = evts.find( + (el) => el.name === "DeleteListingEvent" + ).events; + const claims = evts.find((el) => el.name === "ClaimEvent").events; + await Promise.allSettled([ + ...listings.map((el) => { + const [txId, round, ts, lId, cId, tId, lAddr, lPrc] = el; + const pk = `${node}:${mp}:${lId}`; + const listing = { + pk, + mp, + txId, + round, + timestamp: ts, + lId, + cId, + tId, + lAddr, + lPrc, + }; + return mpDb.listings.put(listing, pk); + }), + ...sales.map((el) => { + const [txId, round, ts, lId, cId, tId, lAddr, lPrc, bAddr] = el; + const pk = `${node}:${mp}:${lId}`; + const sale = { + pk, + mp, + txId, + round, + timestamp: ts, + lId, + // cId, + // tId, + // lAddr, + // lPrc, + // bAddr, + }; + return mpDb.sales.put(sale, pk); + }), + ...deletes.map((el) => { + const [txId, round, ts, lId] = el; + const pk = `${node}:${mp}:${lId}`; + const deletion = { + pk, + mp, + txId, + round, + timestamp: ts, + lId, + }; + return mpDb.deletions.put(deletion, pk); + }), + ...claims.map((el) => { + const [txId, round, ts, lId, cId, tId, lAddr, lPrc, bAddr] = el; + const pk = `${node}:${mp}:${lId}`; + const claim = { + pk, + mp, + txId, + round, + timestamp: ts, + lId, + // cId, + // tId, + // lAddr, + // lPrc, + // bAddr, + }; + return mpDb.claims.put(claim, pk); + }), + ]); + localStorage.setItem("mp201-current-round", currentRound + 1); + })(); + }, [currentRound]); + + // EFFECT: update mp202 db + useEffect(() => { + if (!currentRound) return; + (async () => { + const mp = ctcInfoMp202; + const lastRound = localStorage.getItem("mp202-current-round"); + const ci = new CONTRACT(mp, algodClient, indexerClient, mp202Schema); + const evts = await ci.getEvents({ + minRound: lastRound, + }); + const listings = evts.find((el) => el.name === "ListEvent").events; + const sales = evts.find((el) => el.name === "BuyEvent").events; + const deletes = evts.find( + (el) => el.name === "DeleteListingEvent" + ).events; + const claims = evts.find((el) => el.name === "ClaimEvent").events; + await Promise.allSettled([ + ...listings.map((el) => { + const [txId, round, ts, lId, cId, tId, lAddr, lPrc] = el; + const pk = `${node}:${mp}:${lId}`; + const listing = { + pk, + mp, + txId, + round, + timestamp: ts, + lId, + cId, + tId, + lAddr, + lPrc, + }; + return mpDb.listings.put(listing, pk); + }), + ...sales.map((el) => { + const [txId, round, ts, lId] = el; + const pk = `${node}:${mp}:${lId}`; + const sale = { + pk, + mp, + txId, + round, + timestamp: ts, + lId, + }; + return mpDb.sales.put(sale, pk); + }), + ...deletes.map((el) => { + const [txId, round, ts, lId] = el; + const pk = `${node}:${mp}:${lId}`; + const deletion = { + pk, + mp, + txId, + round, + timestamp: ts, + lId, + }; + return mpDb.deletions.put(deletion, pk); + }), + ...claims.map((el) => { + const [txId, round, ts, lId] = el; + const pk = `${node}:${mp}:${lId}`; + const claim = { + pk, + mp, + txId, + round, + timestamp: ts, + lId, + }; + return mpDb.claims.put(claim, pk); + }), + ]); + localStorage.setItem("mp202-current-round", currentRound + 1); + })(); + }, [currentRound]); + + // EFFECT: update mp202 db + useEffect(() => { + if (!currentRound) return; + (async () => { + const mp = ctcInfoMp203; + const lastRound = localStorage.getItem("mp203-current-round"); + const ci = new CONTRACT(mp, algodClient, indexerClient, mp202Schema); + const evts = await ci.getEvents({ + minRound: lastRound, + }); + const listings = evts.find((el) => el.name === "ListEvent").events; + const sales = evts.find((el) => el.name === "BuyEvent").events; + const deletes = evts.find( + (el) => el.name === "DeleteListingEvent" + ).events; + const claims = evts.find((el) => el.name === "ClaimEvent").events; + const auctions = evts.find((el) => el.name === "AuctionEvent").events; + const auctionDeletes = evts.find( + (el) => el.name === "DeleteAuctionEvent" + ).events; + const bids = evts.find((el) => el.name === "BidEvent").events; + await Promise.allSettled([ + ...listings.map((el) => { + const [txId, round, ts, lId, cId, tId, lAddr, lPrc] = el; + const pk = `${node}:${mp}:${lId}`; + const listing = { + pk, + mp, + txId, + round, + timestamp: ts, + lId, + cId, + tId, + lAddr, + lPrc, + }; + return mpDb.listings.put(listing, pk); + }), + ...auctions.map((el) => { + const [txId, round, ts, lId, cId, tId, lAddr, lPrc, endTime] = el; + const pk = `${node}:${mp}:${lId}`; + const auction = { + pk, + mp, + txId, + round, + timestamp: ts, + lId, + cId, + tId, + lAddr, + lPrc, + endTime, + }; + return mpDb.auctions.put(auction, pk); + }), + ...sales.map((el) => { + const [txId, round, ts, lId] = el; + const pk = `${node}:${mp}:${lId}`; + const sale = { + pk, + mp, + txId, + round, + timestamp: ts, + lId, + }; + return mpDb.sales.put(sale, pk); + }), + ...deletes.map((el) => { + const [txId, round, ts, lId] = el; + const pk = `${node}:${mp}:${lId}`; + const deletion = { + pk, + mp, + txId, + round, + timestamp: ts, + lId, + }; + return mpDb.deletions.put(deletion, pk); + }), + ...auctionDeletes.map((el) => { + const [txId, round, ts, lId] = el; + const pk = `${node}:${mp}:${lId}`; + const deletion = { + pk, + mp, + txId, + round, + timestamp: ts, + lId, + }; + return mpDb.auctionDeletions.put(deletion, pk); + }), + ...claims.map((el) => { + const [txId, round, ts, lId] = el; + const pk = `${node}:${mp}:${lId}`; + const claim = { + pk, + mp, + txId, + round, + timestamp: ts, + lId, + }; + return mpDb.claims.put(claim, pk); + }), + ...bids.map((el) => { + const [txId, round, ts, lId, bAddr, bPrc] = el; + const pk = `${node}:${mp}:${lId}:${bAddr}`; + const bid = { + pk, + mp, + txId, + round, + timestamp: ts, + lId, + bAddr, + bPrc, + }; + return mpDb.bids.put(bid, pk); + }), + ]); + localStorage.setItem("mp203-current-round", currentRound + 1); + })(); + }, [currentRound]); + + // EFFECT: update mp204 db + useEffect(() => { + if (!currentRound) return; + (async () => { + const mp = ctcInfoMp204; + const lastRound = localStorage.getItem("mp204-current-round"); + const ci = new CONTRACT(mp, algodClient, indexerClient, mp202Schema); + const evts = await ci.getEvents({ + minRound: lastRound, + }); + const reverses = evts.find((el) => el.name === "ReverseListEvent").events; + await Promise.allSettled([ + ...reverses.map((el) => { + // reverseListings: + // "pk, mp, txId, round, timestamp, lId, cId, tId, lAddr, lPrc, fPrc, endTime", + const [txId, round, ts, lId, cId, tId, lAddr, lPrc, fPrc, endTime] = + el; + const pk = `${node}:${mp}:${lId}`; + const listing = { + pk, + mp, + txId, + round, + timestamp: ts, + lId, + cId, + tId, + lAddr, + lPrc, + fPrc, + endTime, + }; + return mpDb.reverseListings.put(listing, pk); + }), + ]); + localStorage.setItem("mp204-current-round", currentRound + 1); + })(); + }, [currentRound]); + + // EFFECT: update db pools + useEffect(() => { + if (!currentRound) return; + try { + (async () => { + const ctcInfo = ctcInfoTri200; + const lastRound = localStorage.getItem("tri200-current-round"); + const ci = new CONTRACT(ctcInfo, algodClient, indexerClient, triSchema); + const evts = await ci.getEvents({ + minRound: lastRound, + }); + const status = await algodClient.status().do(); + const currentRound = status["last-round"]; + const registerEvents = evts.find((el) => el.name === "Register").events; + if (registerEvents.length === 0) return; + const tokens = registerEvents + .map((e) => { + const [txId, round, timestamp, toks] = e; + return toks; + }) + .flat(); + await Promise.all( + registerEvents.map((e) => { + const [txId, round, timestamp, [poolId, tokA, tokB]] = e; + const pk = txId; + const dbPool = { + txId, + round, + timestamp, + poolId: Number(poolId), + tokA: Number(tokA), + tokB: Number(tokB), + }; + return db.registerEvents.put(dbPool, pk); + }) + ); + localStorage.setItem("tri200-current-round", currentRound + 1); + })(); + } catch (e) { + console.error(e); + } + }, [currentRound]); + + // EFFECT: update db collections + useEffect(() => { + if (!currentRound) return; + try { + (async () => { + const lastRound = localStorage.getItem("nft-current-round"); + const { collections } = await NFTIndexerService.getCollections({ + //"mint-min-round": lastRound, + }); + await Promise.all( + collections.map((c) => { + const pk = `${node}:${c.contractId}`; + const dbCollection = { + pk, + collectionId: c.contractId, + mintRound: c.mintRound, + totalSupply: c.totalSupply, + }; + return nftDb.collections.put(dbCollection, pk); + }) + ); + localStorage.setItem("nft-current-round", currentRound + 1); + })(); + } catch (e) { + console.error(e); + } + }, [currentRound]); + + // EFFECT: update nfts db + useEffect(() => { + if (!currentRound) return; + try { + (async () => { + const lastRound = localStorage.getItem("nft-current-round"); + const res = await NFTIndexerService.getTokens({ + "mint-min-round": lastRound, + }); + const { tokens, currentRound } = res; + await Promise.all( + tokens.map((t) => { + const pk = `${node}:${t.contractId}:${t.tokenId}`; + const metadata = JSON.parse(t.metadata); + const dbToken = { + pk, + name: metadata.name, + network: node, + symbol: String(metadata.name) + .replace(/[^A-Za-z]/, "") + .slice(0, 8) + .toUpperCase(), + decimals: 0, + supply: 1, + assetType: "arc72", + collectionId: t.contractId, + tokenId: t.tokenId, + mintRound: t["mint-round"], + metadata: t.metadata, + metadataURI: t.metadataURI, + owner: t.owner, + }; + return nftDb.nfts.put(dbToken, pk); + }) + ); + localStorage.setItem("nft-current-round", currentRound + 1); + })(); + } catch (e) { + console.error(e); + } + }, [currentRound]); + return ( -
- - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } - /> - - -
- } - /> - - - + + + + + {} />} + {/*} />*/} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } + /> + + + + } + /> + + + + +
); } diff --git a/app/app-sandbox/src/abis/index.js b/app/app-sandbox/src/abis/index.js index 4cefe53..96d847d 100644 --- a/app/app-sandbox/src/abis/index.js +++ b/app/app-sandbox/src/abis/index.js @@ -285,3 +285,940 @@ export const mp200Schema = { }, ], }; + +// ABI: { +// impure: [`_reachp_0((uint64,(address)))void`, `_reachp_2((uint64,(byte,byte[48])))void`, `buyNet(uint256)uint256`, `deleteNetListing(uint256)void`, `grant(address)void`, `listNet(uint64,uint256,uint64)uint256`, `lock()void`, `touch()void`], +// pure: [`listingByIndex(uint256)(uint64,uint256,address,(byte,byte[40]))`, `state()(address,byte,byte,uint256,address,address)`], +// sigs: [`_reachp_0((uint64,(address)))void`, `_reachp_2((uint64,(byte,byte[48])))void`, `buyNet(uint256)uint256`, `deleteNetListing(uint256)void`, `grant(address)void`, `listNet(uint64,uint256,uint64)uint256`, `listingByIndex(uint256)(uint64,uint256,address,(byte,byte[40]))`, `lock()void`, `state()(address,byte,byte,uint256,address,address)`, `touch()void`] +// } +export const mp201Schema = { + name: "mp200", + desc: "mp200", + methods: [ + // listNet(uint64,uint256,uint64)uint256 + { + name: "listNet", + args: [ + { + name: "collectionId", + type: "uint64", + }, + { + name: "tokenId", + type: "uint256", + }, + { + name: "listPrice", + type: "uint64", + }, + ], + returns: { + name: "listId", + type: "uint256", + }, + }, + { + name: "listSC", + args: [ + { + name: "collectionId", + type: "uint64", + }, + { + name: "tokenId", + type: "uint256", + }, + { + type: "uint64", + }, + { + type: "uint256", + }, + ], + returns: { + name: "listId", + type: "uint256", + }, + }, + { + name: "listSCA", + args: [ + { + name: "collectionId", + type: "uint64", + }, + { + name: "tokenId", + type: "uint256", + }, + { + type: "uint64", + }, + { + type: "uint256", + }, + ], + returns: { + name: "listId", + type: "uint256", + }, + }, + // buyNet(uint256)void + { + name: "buyNet", + args: [ + { + name: "listId", + type: "uint256", + }, + ], + returns: { + type: "void", + }, + }, + + { + name: "buySC", + args: [ + { + name: "listId", + type: "uint256", + }, + ], + returns: { + //name: "tokenId", + type: "void", + }, + }, + { + name: "buySCA", + args: [ + { + name: "listId", + type: "uint256", + }, + ], + returns: { + //name: "tokenId", + type: "void", + }, + }, + { + name: "claimSC", + args: [ + { + name: "listId", + type: "uint256", + }, + ], + returns: { + type: "void", + }, + }, + { + name: "wnt_stake", + args: [{ type: "uint64" }], + returns: { type: "void" }, + }, + { + name: "wnt_unstake", + args: [{ type: "uint64" }], + returns: { type: "void" }, + }, + // // deleteNetListing(uint256)void + // { + // name: "deleteNetListing", + // args: [ + // { + // name: "listId", + // type: "uint256", + // }, + // ], + // }, + // listingByIndex(uint256)(uint64,uint256,address,(byte,byte[40])) + { + name: "listingByIndex", + args: [ + { + type: "uint256", + }, + ], + returns: { + type: "(uint64,uint256,address,(byte,byte[40]),address)", + }, + }, + // state()(address,byte,byte,uint256,address,address) + { + name: "state", + args: [], + returns: { + type: "(address,byte,byte,uint256,address,address)", + }, + }, + // }, + // lock()void + // { + // name: "lock", + // }, + // touch()void + // { + // name: "touch", + // }, + // grant(address)void + // { + // name: "grant", + // args: [ + // { + // name: "newOwner", + // type: "address", + // }, + // ], + // }, + { + name: "arc200_transfer", + args: [ + { + type: "uint64", + }, + { + type: "address", + }, + { + type: "uint256", + }, + ], + returns: { type: "void" }, + }, + ], + // ListEvent: [UInt256, Contract, UInt256, Address, Price], // ListId, CollectionId, CollectionAddress, TokenId, ListPrice + // BuyEvent: [Contract, UInt256, Address, Price, Address], // CollectionId, TokenId, ListAddr, ListPrice, BuyAddr + // DeleteNetListingEvent: [UInt256], // ListId + events: [ + { + name: "ListEvent", + args: [ + { + type: "uint256", + }, + { + type: "uint64", + }, + { + type: "uint256", + }, + { + type: "address", + }, + { + type: "(byte,byte[40])", + }, + ], + }, + { + name: "BuyEvent", + args: [ + { + type: "uint256", + }, + { + type: "uint64", + }, + { + type: "uint256", + }, + { + type: "address", + }, + { + type: "(byte,byte[40])", + }, + { + type: "address", + }, + ], + }, + { + name: "ClaimEvent", + args: [ + { + type: "uint256", + }, + { + type: "uint64", + }, + { + type: "uint256", + }, + { + type: "address", + }, + { + type: "(byte,byte[40])", + }, + { + type: "address", + }, + ], + }, + { + name: "DeleteListingEvent", + args: [ + { + name: "listId", + type: "uint256", + }, + ], + }, + ], +}; + +// impure: [`_reachp_0((uint64,(address)))void`, `_reachp_2((uint64,(byte,byte[152])))void`, `arc200_approve(uint64,address,uint256)void`, `arc200_transfer(uint64,address,uint256)void`, `buyNet(uint256)void`, `buySC(uint256)void`, `claimSC(uint256)void`, `deleteListing(uint256)void`, `deregister()void`, `grant(address)void`, `listNet(uint64,uint256,uint64)uint256`, `listSC(uint64,uint256,uint64,uint256)uint256`, `lock()void`, `register(byte[32],byte[32],byte[64],uint64,uint64,uint64)void`, `touch()void`, `wnt_stake(uint64)void`, `wnt_unstake(uint64)void`], +// pure: [`keyInfo()(byte,byte[152])`, `listingByIndex(uint256)(uint64,uint256,address,(byte,byte[40]),address)`, `manager()address`, `state()(address,byte,uint256,address,address,byte,(byte,byte[152]))`], +// sigs: [`_reachp_0((uint64,(address)))void`, `_reachp_2((uint64,(byte,byte[152])))void`, `arc200_approve(uint64,address,uint256)void`, `arc200_transfer(uint64,address,uint256)void`, `buyNet(uint256)void`, `buySC(uint256)void`, `claimSC(uint256)void`, `deleteListing(uint256)void`, `deregister()void`, `grant(address)void`, `keyInfo()(byte,byte[152])`, `listNet(uint64,uint256,uint64)uint256`, `listSC(uint64,uint256,uint64,uint256)uint256`, `listingByIndex(uint256)(uint64,uint256,address,(byte,byte[40]),address)`, `lock()void`, `manager()address`, `register(byte[32],byte[32],byte[64],uint64,uint64,uint64)void`, `state()(address,byte,uint256,address,address,byte,(byte,byte[152]))`, `touch()void`, `wnt_stake(uint64)void`, `wnt_unstake(uint64)void`] +export const mp202Schema = { + name: "mp202", + desc: "mp202", + methods: [ + { name: "custom", args: [], returns: { type: "void" } }, + // mp204 + + // mp204 + + // reverseClaimSC(uint256)void + + { + name: "reverseClaimSC", + args: [ + { + name: "listId", + type: "uint256", + }, + ], + returns: { + type: "void", + }, + }, + + // reverseBuySC(uint256)void + + { + name: "reverseBuySC", + args: [ + { + name: "listId", + type: "uint256", + }, + ], + returns: { + type: "void", + }, + }, + + // v_reverseBuySC(uint256)(byte,byte[40]) + + { + name: "v_reverseBuySC", + args: [ + { + type: "uint256", + }, + ], + returns: { + type: "(byte,byte[40])", + }, + }, + + // reverseListSC: Fun( + // [Contract, UInt256, Contract, UInt256, UInt256, UInt], + // UInt256 + //), + // (CollectionId, TokenId, PaymentTokenId, ListPrice, FloorPrice, EndTime)ListId + // reverseListSC(uint64,uint256,uint64,uint256,uint256,uint64)uint256 + + { + name: "reverseListSC", + args: [ + { + name: "collectionId", + type: "uint64", + }, + { + name: "tokenId", + type: "uint256", + }, + { + name: "paymentTokenId", + type: "uint64", + }, + { + name: "listPrice", + type: "uint256", + }, + { + name: "floorPrice", + type: "uint256", + }, + { + name: "endTime", + type: "uint64", + }, + ], + returns: { + name: "listId", + type: "uint256", + }, + }, + + // mp203 + // const Auction = Struct([ + // ["collectionId", Contract], // arc72 application id + // ["tokenId", UInt256], // arc72 token id + // ["endTime", UInt], // end time + // ["auctionAddr", Address], // token owner + // ["reserveAddr", Address], // token owner + // ["highestBid", Price], // highest bid + // ["highestBidder", Address], // highest bidder + // ]); + // auctionByIndex(uint256)(uint64,uint256,uint64,address,address,(byte,byte[40]),address) + { + name: "auctionByIndex", + args: [ + { + type: "uint256", + }, + ], + returns: { + type: "(uint64,uint256,uint64,address,address,(byte,byte[40]),address)", + }, + }, + // mp202 + // listNet(uint64,uint256,uint64)uint256 + { + name: "listNet", + args: [ + { + name: "collectionId", + type: "uint64", + }, + { + name: "tokenId", + type: "uint256", + }, + { + name: "listPrice", + type: "uint64", + }, + ], + returns: { + name: "listId", + type: "uint256", + }, + }, + // buyNet(uint256)void + { + name: "buyNet", + args: [ + { + name: "listId", + type: "uint256", + }, + ], + returns: { + type: "void", + }, + }, + // listSC(uint64,uint256,uint64,uint256)uint256 + { + name: "listSC", + args: [ + { + name: "collectionId", + type: "uint64", + }, + { + name: "tokenId", + type: "uint256", + }, + { + name: "scTokenId", + type: "uint64", + }, + { + name: "listPrice", + type: "uint256", + }, + ], + returns: { + name: "listId", + type: "uint256", + }, + }, + + // bidSC(uint256,uint256)void + + { + name: "bidSC", + args: [ + { + name: "listId", + type: "uint256", + }, + { + name: "price", + type: "uint256", + }, + ], + returns: { + type: "void", + }, + }, + + // `buySC(uint256)void + { + name: "buySC", + args: [ + { + name: "listId", + type: "uint256", + }, + ], + returns: { + type: "void", + }, + }, + // claimSC(uint256)void + { + name: "claimSC", + args: [ + { + name: "listId", + type: "uint256", + }, + ], + returns: { + type: "void", + }, + }, + { + name: "wnt_stake", + args: [{ type: "uint64" }], + returns: { type: "void" }, + }, + { + name: "wnt_unstake", + args: [{ type: "uint64" }], + returns: { type: "void" }, + }, + // // deleteNetListing(uint256)void + // { + // name: "deleteNetListing", + // args: [ + // { + // name: "listId", + // type: "uint256", + // }, + // ], + // }, + // listingByIndex(uint256)(uint64,uint256,address,(byte,byte[40])) + { + name: "listingByIndex", + args: [ + { + type: "uint256", + }, + ], + returns: { + type: "(uint64,uint256,address,(byte,byte[40]),address)", + }, + }, + // state()(address,byte,byte,uint256,address,address) + { + name: "state", + args: [], + returns: { + type: "(address,byte,byte,uint256,address,address)", + }, + }, + // }, + // lock()void + // { + // name: "lock", + // }, + // touch()void + // { + // name: "touch", + // }, + // grant(address)void + // { + // name: "grant", + // args: [ + // { + // name: "newOwner", + // type: "address", + // }, + // ], + // }, + { + name: "arc200_transfer", + args: [ + { + type: "uint64", + }, + { + type: "address", + }, + { + type: "uint256", + }, + ], + returns: { type: "void" }, + }, + ], + events: [ + // mp204 + + // ReverseListEvent: [ + // UInt256, + // Contract, + // UInt256, + // Address, + // Price, + // Price, + // UInt, + // ], // ListId, CollectionId, TokenId, ListAddr, ListPrice, FloorPrice, EndTime + + { + name: "ReverseListEvent", + args: [ + { + type: "uint256", + }, + { + type: "uint64", + }, + { + type: "uint256", + }, + { + type: "address", + }, + { + type: "(byte,byte[40])", + }, + { + type: "(byte,byte[40])", + }, + { + type: "uint64", + }, + ], + }, + + // mp203 + // AuctionEvent: [UInt256, Contract, UInt256, Address, Price, UInt], // AuctionId, CollectionId, TokenId, ListAddr, ListPrice, EndTime + // DeleteAuctionEvent: [UInt256], // AuctionId + // BidEvent: [UInt256, Address, Price], // AuctionId, BidderAddr, Bid + // AnnounceEvent: [UInt256], // AuctionId + { + name: "AuctionEvent", + args: [ + { + type: "uint256", + }, + { + type: "uint64", + }, + { + type: "uint256", + }, + { + type: "address", + }, + { + type: "(byte,byte[40])", + }, + { + type: "uint64", + }, + ], + }, + { + name: "DeleteAuctionEvent", + args: [ + { + type: "uint256", + }, + ], + }, + { + name: "BidEvent", + args: [ + { + type: "uint256", + }, + { + type: "address", + }, + { + type: "(byte,byte[40])", + }, + ], + }, + { + name: "AnnounceEvent", + args: [ + { + type: "uint256", + }, + ], + }, + // mp202 + // ListEvent: [UInt256, Contract, UInt256, Address, Price], // ListId, CollectionId, CollectionAddress, TokenId, ListPrice + // BuyEvent: [Contract, UInt256, Address, Price, Address], // CollectionId, TokenId, ListAddr, ListPrice, BuyAddr + // DeleteNetListingEvent: [UInt256], // ListId + { + name: "ListEvent", + args: [ + { + type: "uint256", + }, + { + type: "uint64", + }, + { + type: "uint256", + }, + { + type: "address", + }, + { + type: "(byte,byte[40])", + }, + ], + }, + { + name: "BuyEvent", + args: [ + { + name: "listId", + type: "uint256", + }, + { + type: "address", + }, + ], + }, + { + name: "ClaimEvent", + args: [ + { + name: "listId", + type: "uint256", + }, + ], + }, + { + name: "DeleteListingEvent", + args: [ + { + name: "listId", + type: "uint256", + }, + ], + }, + ], +}; + +export const arc200Schema = { + name: "ARC-200", + desc: "Smart Contract Token Base Interface", + methods: [ + { + name: "arc200_name", + desc: "Returns the name of the token", + readonly: true, + args: [], + returns: { type: "byte[32]", desc: "The name of the token" }, + }, + { + name: "arc200_symbol", + desc: "Returns the symbol of the token", + readonly: true, + args: [], + returns: { type: "byte[8]", desc: "The symbol of the token" }, + }, + { + name: "arc200_decimals", + desc: "Returns the decimals of the token", + readonly: true, + args: [], + returns: { type: "uint8", desc: "The decimals of the token" }, + }, + { + name: "arc200_totalSupply", + desc: "Returns the total supply of the token", + readonly: true, + args: [], + returns: { type: "uint256", desc: "The total supply of the token" }, + }, + { + name: "arc200_balanceOf", + desc: "Returns the current balance of the owner of the token", + readonly: true, + args: [ + { + type: "address", + name: "owner", + desc: "The address of the owner of the token", + }, + ], + returns: { + type: "uint256", + desc: "The current balance of the holder of the token", + }, + }, + { + name: "arc200_transfer", + desc: "Transfers tokens", + readonly: false, + args: [ + { + type: "address", + name: "to", + desc: "The destination of the transfer", + }, + { + type: "uint256", + name: "value", + desc: "Amount of tokens to transfer", + }, + ], + returns: { type: "bool", desc: "Success" }, + }, + { + name: "arc200_transferFrom", + desc: "Transfers tokens from source to destination as approved spender", + readonly: false, + args: [ + { + type: "address", + name: "from", + desc: "The source of the transfer", + }, + { + type: "address", + name: "to", + desc: "The destination of the transfer", + }, + { + type: "uint256", + name: "value", + desc: "Amount of tokens to transfer", + }, + ], + returns: { type: "bool", desc: "Success" }, + }, + { + name: "arc200_approve", + desc: "Approve spender for a token", + args: [ + { type: "address", name: "spender" }, + { type: "uint256", name: "value" }, + ], + returns: { type: "bool", desc: "Success" }, + }, + { + name: "arc200_allowance", + desc: "Returns the current allowance of the spender of the tokens of the owner", + readonly: true, + args: [ + { type: "address", name: "owner" }, + { type: "address", name: "spender" }, + ], + returns: { type: "uint256", desc: "The remaining allowance" }, + }, + { + name: "hasBalance", + desc: "Transfers tokens from source to destination as approved spender", + readonly: true, + args: [ + { + type: "address", + name: "addr", + desc: "The source of the transfer", + }, + ], + returns: { type: "byte", desc: "Success" }, + }, + { + name: "hasAllowance", + desc: "Transfers tokens from source to destination as approved spender", + readonly: true, + args: [ + { type: "address", name: "owner" }, + { type: "address", name: "spender" }, + ], + returns: { type: "byte", desc: "Success" }, + }, + { + name: "touch", + desc: "Transfers tokens from source to destination as approved spender", + args: [], + returns: { type: "void" }, + }, + { + name: "state", + desc: "", + readonly: true, + args: [], + returns: { + type: "(byte[32],byte[8],uint64,uint256,address,address,byte,byte)", + desc: "", + }, + }, + ], + events: [ + { + name: "arc200_Transfer", + desc: "Transfer of tokens", + args: [ + { + type: "address", + name: "from", + desc: "The source of transfer of tokens", + }, + { + type: "address", + name: "to", + desc: "The destination of transfer of tokens", + }, + { + type: "uint256", + name: "value", + desc: "The amount of tokens transferred", + }, + ], + }, + { + name: "arc200_Approval", + desc: "Approval of tokens", + args: [ + { + type: "address", + name: "owner", + desc: "The owner of the tokens", + }, + { + type: "address", + name: "spender", + desc: "The approved spender of tokens", + }, + { + type: "uint256", + name: "value", + desc: "The amount of tokens approve", + }, + ], + }, + ], +}; diff --git a/app/app-sandbox/src/components/AccountBalances/index.js b/app/app-sandbox/src/components/AccountBalances/index.js index 1fe7fcc..59e8592 100644 --- a/app/app-sandbox/src/components/AccountBalances/index.js +++ b/app/app-sandbox/src/components/AccountBalances/index.js @@ -8,21 +8,16 @@ import { TableHead, TableRow, Tooltip, - Skeleton, Chip, + Dialog, + DialogContent, } from "@mui/material"; import { getCurrentNode, getGenesisHash } from "../../utils/reach"; -import ARC200Service from "../../services/ARC200Service.ts"; import { makeStdLib } from "../../utils/reach"; -import BoltIcon from "@mui/icons-material/Bolt"; import CurrencyExchangeIcon from "@mui/icons-material/CurrencyExchange"; import SendIcon from "@mui/icons-material/Send"; import DeleteIcon from "@mui/icons-material/Delete"; -import { - displayToken, - getAlgorandClients, - zeroAddress, -} from "../../utils/algorand.js"; +import { displayToken, getAlgorandClients } from "../../utils/algorand.js"; import SendDialog from "../SendDialog/index.js"; import ApproveDialog from "../ApproveDialog/index.js"; import SpendDialog from "../SpendDialog/index.js"; @@ -33,25 +28,29 @@ import { styled } from "@mui/material/styles"; import { Link } from "react-router-dom"; import defaultTokens from "../../config/defaultTokens.js"; import ThumbUpIcon from "@mui/icons-material/ThumbUp"; -import FireplaceIcon from "@mui/icons-material/Fireplace"; import { DEFAULT_NODE } from "../../config/defaultLocalStorage.js"; -import arc200 from "arc200js"; import LoadingIndicator from "../LoadingIndicator/index.js"; import AddIcon from "@mui/icons-material/Add"; import RemoveIcon from "@mui/icons-material/Remove"; -import * as wntBackend from "../../backend/wnt200/index.wNT200.mjs"; -import CONTRACT from "arccjs"; import { db } from "../../db"; import { useLiveQuery } from "dexie-react-hooks"; import algosdk, { waitForConfirmation } from "algosdk"; import { toast } from "react-toastify"; import CircularProgress from "@mui/material/CircularProgress"; +import { nt200, arc200 } from "ulujs"; +import { bigNumberToBigInt, bigNumberify } from "../../common/utils/bn.ts"; +import TokenDisplay from "../TokenDisplay/index.js"; const stdlib = makeStdLib(); const fawd = stdlib.formatWithDecimals; const [node] = (localStorage.getItem("node") || DEFAULT_NODE).split(":"); +const prepareStr = (b64Str) => + Buffer.from(b64Str, "base64") + //.reduce((acc, val) => (val > 0 ? [...acc, val] : acc), []) + .toString("utf8"); + // wVOI ------------------------------------------ const abi = { @@ -112,7 +111,7 @@ const abi = { events: [], }; -const ctcInfo = 24590664; +const WVOI = 24590664; const { algodClient, indexerClient } = getAlgorandClients(); const waitForTxn = async (txId, rounds = 4) => @@ -156,7 +155,7 @@ const networkToken = (accInfo) => ({ function AccountBalance(props) { const [node] = getCurrentNode(); - const { activeAccount } = useWallet(); + const { activeAccount, signTransactions } = useWallet(); const [sendDialogOpen, setSendDialogOpen] = useState(false); const [approveDialogOpen, setApproveDialogOpen] = useState(false); const [spendDialogOpen, setSpendDialogOpen] = useState(false); @@ -179,7 +178,7 @@ function AccountBalance(props) { const wallet = await algorand.enable({ genesisHash: getGenesisHash(node), }); - const { algodClient, indexerClient } = getAlgorandClients(); + const result = await window.algorand.signTxns({ txns: txns.map((el) => { return { @@ -194,27 +193,36 @@ function AccountBalance(props) { const res = await algodClient .sendRawTransaction(signedTransactionBytes) .do(); - return res.txId; + await waitForConfirmation(algodClient, res.txId, 4); + } else if ( + [PROVIDER_ID.KIBISIS, PROVIDER_ID.DEFLY, PROVIDER_ID.LUTE].includes( + activeAccount.providerId + ) + ) { + const stxns = await signTransactions( + txns.map((el) => new Uint8Array(Buffer.from(el, "base64"))) + ); + const res = await algodClient.sendRawTransaction(stxns).do(); + await waitForConfirmation(algodClient, res.txId, 4); } else { - const wtxns = txns.map((el) => { - return { - txn: el, - }; - }); - await stdlib.signSendAndConfirm({ addr: activeAccount.address }, wtxns); + throw new Error("Unsupported wallet"); } }, [activeAccount] ); - const reloadToken = useCallback(async () => { if (!activeAccount) return; if (props.token.assetType === "rc200") { const { algodClient, indexerClient } = getAlgorandClients(); const ci = new arc200(token.appId, algodClient, indexerClient); - const decimalsR = await ci.arc200_decimals(); - if (!decimalsR.success) return; /// TODO handle error - const decimals = decimalsR.returnValue; + + let decimals = token.decimals; + if (!decimals) { + const decimalsR = await ci.arc200_decimals(); + if (!decimalsR.success) return; + decimals = decimalsR.returnValue; + } + const balanceR = await ci.arc200_balanceOf(activeAccount.address); if (!balanceR.success) return; /// TODO handle error const amount = fawd(balanceR.returnValue, Number(decimals)); @@ -249,16 +257,11 @@ function AccountBalance(props) { }); } }, [activeAccount, token]); - useEffect(() => { - if (!activeAccount) return; - // realtime rc200 only - //ARC200Service.nextTransferEvent(token.appId) - // .then(reloadToken()) - // .catch(console.error); - // every interval - const interval = setInterval(reloadToken, 60_000); - return () => clearInterval(interval); - }, [activeAccount, token]); + // useEffect(() => { + // if (!activeAccount) return; + // const interval = setInterval(reloadToken, 60_000); + // return () => clearInterval(interval); + // }, [activeAccount, token]); return ( activeAccount && token && ( @@ -282,7 +285,6 @@ function AccountBalance(props) { reloadToken={reloadToken} /> - {/*{token.tokenId}*/} {token.name} {token.assetType !== "network" && ( @@ -291,11 +293,20 @@ function AccountBalance(props) { sx={{ ml: 1 }} label={`${( (token?.network ?? "")[0] + (token?.assetType ?? "") - ).toUpperCase()}:${token.appId}`} + ).toUpperCase()}:${token.appId || token.tokenId}`} /> )}
- Token Info + + Token Info +
Transactions for {activeAccount.address.slice(0, 4)}... @@ -308,15 +319,17 @@ function AccountBalance(props) { {[ - { - label: "S", - description: "Send", - icon: , - onClick: () => { - setToken(token); - setSendDialogOpen(true); - }, - }, + token.assetType !== "sa" + ? { + label: "S", + description: "Send", + icon: , + onClick: () => { + setToken(token); + setSendDialogOpen(true); + }, + } + : null, token.assetType === "rc200" ? { label: "A", @@ -363,28 +376,36 @@ function AccountBalance(props) { onClick: async () => { try { if (!activeAccount) return; - if (activeAccount.providerId != "custom") return; + const ci = new nt200(WVOI, algodClient, indexerClient, { + acc: { + addr: activeAccount.address, + }, + }); + const arc200_balanceOfR = await ci.arc200_balanceOf( + activeAccount.address + ); + if (!arc200_balanceOfR.success) + throw new Error("Failed to get balance"); + const balance = arc200_balanceOfR.returnValue; + if (balance === 0n) { + const createBalanceBoxR = await ci.createBalanceBox( + activeAccount.address + ); + if (createBalanceBoxR.success) { + await signTransaction(createBalanceBoxR.txns); + } + } const inputStr = window.prompt("Enter amount to mint"); if (!inputStr) return; setDoingMint(true); const num = Number(inputStr); if (isNaN(num)) return; const amt = num * 10 ** 6; - const ci = new CONTRACT( - ctcInfo, - algodClient, - indexerClient, - abi, - { - addr: activeAccount.address, - } + const res = await ci.deposit( + bigNumberToBigInt(bigNumberify(amt)) ); - ci.setFee(2000); - ci.setPaymentAmount(amt); - const res = await ci.deposit(amt); + console.log({ res }); const txId = await signTransaction(res.txns); - - await waitForTxn(txId); setToken({ ...token, amount: fawd(res.returnValue, Number(token.decimals)), @@ -414,13 +435,7 @@ function AccountBalance(props) { onClick: async () => { try { if (!activeAccount) return; - if (activeAccount.providerId != "custom") return; - const inputStr = window.prompt("Enter amount to burn"); - if (!inputStr) return; - const num = Number(inputStr); - if (isNaN(num)) return; - setDoingBurn(true); - const amt = num * 10 ** 6; + const ciArc200 = new arc200( token.appId, algodClient, @@ -429,40 +444,49 @@ function AccountBalance(props) { acc: { addr: activeAccount.address }, } ); + const totalSupplyR = + await ciArc200.arc200_totalSupply(); + if (!totalSupplyR.success) + throw new Error("Failed to get total supply"); + const totalSupply = totalSupplyR.returnValue; const approvalR = await ciArc200.arc200_allowance( activeAccount.address, - algosdk.getApplicationAddress(ctcInfo) + algosdk.getApplicationAddress(WVOI) ); - console.log({ approvalR }); - if (!approvalR.success) return; + if (!approvalR.success) + throw new Error("Failed to get allowance"); + + const inputStr = window.prompt("Enter amount to burn"); + if (!inputStr) return; + const num = Number(inputStr); + if (isNaN(num)) return; + setDoingBurn(true); + const amt = num * 10 ** 6; + const amtBi = bigNumberToBigInt(bigNumberify(amt)); + const approval = approvalR.returnValue; - if (approval < amt) { + if (approval < amtBi) { const res1 = await ciArc200.arc200_approve( - algosdk.getApplicationAddress(ctcInfo), - token.totalSupply + algosdk.getApplicationAddress(WVOI), + totalSupply ); - if (!res1.success) return; - const txId = await signTransaction(res1.txns); - await waitForTxn(txId); + if (!res1.success) + throw new Error("Failed to approve"); + await signTransaction(res1.txns); } - const ci = new CONTRACT( - ctcInfo, - algodClient, - indexerClient, - abi, - { + const ci = new nt200(WVOI, algodClient, indexerClient, { + acc: { addr: activeAccount.address, - } - ); - ci.setFee(2000); - const res2 = await ci.withdraw(amt); - if (!res2.success) return; - const txId = await signTransaction(res2.txns); - await waitForTxn(txId); + }, + }); + const withdrawR = await ci.withdraw(amt); + if (!withdrawR.success) + throw new Error("Failed to withdraw"); + await signTransaction(withdrawR.txns); setToken({ ...token, amount: fawd( - res2.returnValue, + withdrawR.returnValue, Number(token.decimals) ), }); @@ -506,6 +530,12 @@ function AccountBalance(props) { setToken(null); }, }, + token.assetType === "sa" && { + label: "V", + description: "VIA/VIASA Exchange", + icon: , + onClick: () => props?.setShowTokenDisplay(true), + }, ] .filter((el) => !!el) .map((el) => ( @@ -535,6 +565,7 @@ function AccountBalances(props) { const [nativeTokens, setNativeTokens] = useState(null); const [arc200Tokens, setArc200Tokens] = useState(null); const [sendDialogOpen, setSendDialogOpen] = useState(false); + const [showTokenDisplay, setShowTokenDisplay] = useState(false); const loading = useMemo(() => !props.tokens, [props.tokens]); // EFFECT: lookup account info and set network tokens useEffect(() => { @@ -551,7 +582,6 @@ function AccountBalances(props) { useEffect(() => { if (!activeAccount || nativeTokens) return; setNativeTokens([]); - /* if (!activeAccount || nativeTokens) return; (async () => { const { indexer } = await stdlib.getProvider(); @@ -564,8 +594,10 @@ function AccountBalances(props) { .lookupAssetByID(asset["asset-id"]) .do(); const decimals = assetDetails?.asset?.params?.decimals ?? 0; - const name = assetDetails?.asset?.params?.name ?? "Unknown"; - const symbol = assetDetails?.asset?.params?.["unit-name"] ?? "Unknown"; + const name = prepareStr(assetDetails?.asset?.params?.["name-b64"]); + const symbol = prepareStr( + assetDetails?.asset?.params?.["unit-name-b64"] + ); const amountAu = asset?.amount ?? 0; const amount = stdlib.formatWithDecimals( stdlib.bigNumberify(amountAu), @@ -580,16 +612,14 @@ function AccountBalances(props) { decimals, }); } - setNativeTokens(assetList); + setNativeTokens(assetList.filter((el) => el["asset-id"] === 27704545)); // only VIASA })(); - */ }, [activeAccount]); // EFFECT: lookup account assets and set arc200 tokens useEffect(() => { if (!activeAccount || !networkToken || !nativeTokens || !arc200Tokens) return; const tokens = []; - /* if (nativeTokens) nativeTokens.forEach((token) => { tokens.push({ @@ -599,7 +629,6 @@ function AccountBalances(props) { network: nodeNetwork(node), }); }); - */ if (arc200Tokens) arc200Tokens.forEach((token) => { tokens.push({ @@ -671,13 +700,14 @@ function AccountBalances(props) { totalSupply, }; } + console.log({ tm }); // --- // poolId, first poolId found const poolId = tm.symbol === "ARC200LT" ? 0 - : props.pools.find((el) => el.tokA == appId || el.tokB == appId) + : props?.pools?.find((el) => el.tokA == appId || el.tokB == appId) ?.poolId || 0; const balanceR = await ci.arc200_balanceOf(activeAccount.address); @@ -697,8 +727,9 @@ function AccountBalances(props) { // effect: reload tokens on account change // ------------------------------------------- useEffect(() => { - if (arc200Tokens) return; - reloadTokens(); + if (!activeAccount || arc200Tokens) return; + const timeout = setTimeout(() => reloadTokens(), 1000); + return () => clearTimeout(timeout); }, [props, activeAccount, arc200Tokens]); // ------------------------------------------- return loading ? ( @@ -727,11 +758,25 @@ function AccountBalances(props) { key={token.appId} token={token} manage={props.manage} + setShowTokenDisplay={setShowTokenDisplay} /> ))} + setShowTokenDisplay(false)} + > + + + + ); } diff --git a/app/app-sandbox/src/components/AppBar.tsx b/app/app-sandbox/src/components/AppBar.tsx index ee04079..2bf86d8 100644 --- a/app/app-sandbox/src/components/AppBar.tsx +++ b/app/app-sandbox/src/components/AppBar.tsx @@ -9,8 +9,8 @@ import MenuItem from "@mui/material/MenuItem"; import Divider from "@mui/material/Divider"; import Menu from "@mui/material/Menu"; import MenuIcon from "@mui/icons-material/Menu"; -import { useWallet } from "@txnlab/use-wallet"; -import { Button } from "@mui/material"; +import { PROVIDER_ID, useWallet } from "@txnlab/use-wallet"; +import { Button, Typography } from "@mui/material"; import { makeStdLib } from "../utils/reach.js"; import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import * as Copy from "react-copy-to-clipboard"; @@ -153,7 +153,7 @@ const MyAppBar = () => { providers .find((p) => p.isActive) .setActiveAccount(account.address); - window.location.reload(); + //window.location.reload(); }} > {account.address.slice(0, 4)}... @@ -168,9 +168,9 @@ const MyAppBar = () => { providers .find( (p) => - (p.metadata.id !== "custom" && - p.metadata.id === activeAccount?.providerId) || - p.metadata.id === "custom" + p.metadata.id !== "custom" && + p.metadata.id === activeAccount?.providerId + //|| p.metadata.id === "custom" ) .disconnect() .then(() => { @@ -185,16 +185,19 @@ const MyAppBar = () => { ); return ( + + Shelly's Sandbox + navigate("/")} > @@ -243,7 +246,11 @@ const MyAppBar = () => { > @@ -301,11 +308,10 @@ const MyAppBar = () => { activeAccount && providers.map( (provider) => - ((provider.metadata.id !== "custom" && - provider.metadata.id === activeAccount.providerId) || - (provider.metadata.id === "custom" && - provider.metadata.name === activeAccount.name)) && ( - { style={{ height: "30px", filter: - (provider.metadata.id !== "custom" && - provider.metadata.id === - activeAccount.providerId) || - provider.metadata.id === "custom" - ? "" + // provider.metadata.id !== PROVIDER_ID.CUSTOM && + provider.metadata.id === activeAccount.providerId + ? //|| provider.metadata.id === "custom" + "" : "grayscale(1)", }} src={ diff --git a/app/app-sandbox/src/components/ApproveDialog/index.js b/app/app-sandbox/src/components/ApproveDialog/index.js index 8fc92f3..8c762ed 100644 --- a/app/app-sandbox/src/components/ApproveDialog/index.js +++ b/app/app-sandbox/src/components/ApproveDialog/index.js @@ -14,31 +14,19 @@ import Typography from "@mui/material/Typography"; import Stack from "@mui/material/Stack"; import { toast } from "react-toastify"; -import { makeStdLib } from "../../utils/reach.js"; import { getAlgorandClients } from "../../utils/algorand.js"; -import arc200 from "arc200js"; +import { arc200 } from "ulujs"; +import { waitForConfirmation } from "algosdk"; +import { bigNumberToBigInt, bigNumberify } from "../../common/utils/bn.ts"; -function SendDialog(props) { - const { providers, activeAccount } = useWallet(); +function ApproveDialog(props) { + const { providers, activeAccount, signTransactions } = useWallet(); const [token, setToken] = useState(props.token); const [tokenAmount, setTokenAmount] = useState(""); const [accountAddress, setAccountAddress] = useState(""); const [doSubmit, setDoSubmit] = useState(false); const [pending, setPending] = useState(false); - const stdlib = makeStdLib(); - const fawd = stdlib.formatWithDecimals; - useEffect(() => { - if (!activeAccount) return; - if (token.amount) return; - (async () => { - const amount = fawd( - await ARC200Service.balanceOf(token.appId, activeAccount.address), - token.decimals - ); - setToken({ ...token, amount }); - })(); - }, [activeAccount, token, props.token]); const handleSubmit = async () => { if (!activeAccount) { providers @@ -53,9 +41,11 @@ function SendDialog(props) { (async () => { try { setPending(true); - const amount = stdlib - .parseCurrency(tokenAmount, Number(token.decimals)) - .toBigInt(); + const amount = bigNumberToBigInt( + bigNumberify( + Math.floor(Number(tokenAmount) * 10 ** Number(token.decimals)) + ) + ); let res; if ( activeAccount.providerId === PROVIDER_ID.CUSTOM && @@ -82,16 +72,28 @@ function SendDialog(props) { (stxn) => new Uint8Array(Buffer.from(stxn, "base64")) ); // send to the network - res = await algodClient + const res2 = await algodClient .sendRawTransaction(signedTransactionBytes) .do(); - } else { - res = await ARC200Service.approve( - props.token, - activeAccount.address, - accountAddress, - tokenAmount + } else if ( + [PROVIDER_ID.KIBISIS, PROVIDER_ID.DEFLY, PROVIDER_ID.LUTE].includes( + activeAccount.providerId + ) + ) { + const { algodClient, indexerClient } = getAlgorandClients(); + const ci = new arc200(token.appId, algodClient, indexerClient, { + acc: { addr: activeAccount.address }, + }); + + res = await ci.arc200_approve(accountAddress, amount); + if (!res.success) return; // TODO: handle error + const stxns = await signTransactions( + res.txns.map((el) => new Uint8Array(Buffer.from(el, "base64"))) ); + const res2 = await algodClient.sendRawTransaction(stxns).do(); + await waitForConfirmation(algodClient, res2.txId, 4); + } else { + throw new Error("Wallet not supported"); } if (res) { props.reloadToken(); @@ -106,13 +108,12 @@ function SendDialog(props) { ); setToken({ ...token, amount: undefined }); props.setOpen(false); - props.setTokens(null); } else { - alert("Transfer failed"); + throw new Error("Approval failed"); } // TODO catch others } catch (e) { - console.log(e); + toast.error(e.message); } finally { setPending(false); setDoSubmit(false); @@ -173,4 +174,4 @@ function SendDialog(props) { ); } -export default SendDialog; +export default ApproveDialog; diff --git a/app/app-sandbox/src/components/BuyModal/index.js b/app/app-sandbox/src/components/BuyModal/index.js new file mode 100644 index 0000000..ceddbe0 --- /dev/null +++ b/app/app-sandbox/src/components/BuyModal/index.js @@ -0,0 +1,513 @@ +// BuyModal.js +import React, { + useState, + Suspense, + lazy, + useMemo, + useEffect, + useCallback, + useContext, +} from "react"; +import { + Button, + Dialog, + DialogContent, + DialogTitle, + CircularProgress, +} from "@mui/material"; +import { swap200, arc200 } from "ulujs"; +import CONTRACT from "arccjs"; +import { getAlgorandClients } from "../../utils/algorand"; +import { useWallet, PROVIDER_ID } from "@txnlab/use-wallet"; +import { + mp201Schema, + arc200Schema, + mp202Schema, + mp200Schema, + arc72Schema, +} from "../../abis"; +import { bigNumberToBigInt, bigNumberify } from "../../common/utils/bn"; +import { getCurrentNode, getGenesisHash } from "../../utils/reach"; +import algosdk, { waitForConfirmation } from "algosdk"; +import { makeStdLib } from "../../utils/reach"; +import { decodeRoyalties } from "../../utils/hf.js"; +import { nftDb, db } from "../../db"; +import { useLiveQuery } from "dexie-react-hooks"; +import { + computeNFTSalePrice, + getPriceSymbol, + computeExtraPayment, + computeMarketplaceFee, + computeSalePrice, + decodePrice, + decodeDecimals, +} from "../../utils/mp.js"; +import Confetti from "react-confetti"; +import { useWindowSize } from "usehooks-ts"; +import { + ctcInfoMp201, + ctcInfoMp200, + fee, + feeBi, + ctcInfoMp202, +} from "../../constants/mp.js"; +import { toast } from "react-toastify"; +import { MarketplaceContext } from "../../store/MarketplaceContext"; + +const LazyNFTImage = lazy(() => import("../../components/NFTImage")); + +const { algodClient, indexerClient } = getAlgorandClients(); + +const BuyModal = ({ open, setOpen, mp, lId, cId, tId, lAddr, lPrc }) => { + const { isLoading, tokens, nfts, pools, listings, forSale } = + useContext(MarketplaceContext); + const { width, height } = useWindowSize(); + // const tokens = useLiveQuery(() => db.tokens.toArray()); + // const dbNfts = useLiveQuery(() => nftDb.nfts.toArray()); + // const pool = useLiveQuery(() => db.registerEvents.toArray()); + // const isLoading = useMemo( + // () => !pool || !tokens || !dbNfts, + // [pool, tokens, dbNfts] + // ); + const nft = useMemo(() => { + if (isLoading) return null; + return nfts.find( + (el) => el.collectionId === Number(cId) && el.tokenId === Number(tId) + ); + }, [isLoading, nfts, cId, tId]); + const token = useMemo(() => { + if (isLoading) return null; + const [pType, ...prc] = lPrc; + if (pType === "01") { + const [tok, tprc] = prc; + const token = tokens.find((el) => el.tokenId === Number("0x" + tok)); + return token; + } + }, [isLoading, tokens, lPrc]); + + // //const [nft, setNft] = useState(null); + //const [royalties, setRoyalties] = useState(null); + //const [token, setToken] = useState(null); + //const [tokens, setTokens] = useState(null); + //const [properties, setProperties] = useState(null); + const [poolPair, setPoolPair] = React.useState(null); + const [pool, setPool] = useState(null); + const [node] = getCurrentNode(); + const { activeAccount, signTransactions } = useWallet(); + const [step, setStep] = useState(0); + const [rate, setRate] = useState(1); + const [allowance, setAllowance] = useState(0n); + const [hasAllowance, setHasAllowance] = useState(false); + const [loading, setLoading] = useState(false); + const [message, setMessage] = useState(""); + const [showConfetti, setShowConfetti] = useState(false); + + const signTransaction = React.useCallback( + async (txns) => { + if (!activeAccount) return; + if ( + activeAccount.providerId === PROVIDER_ID.CUSTOM && + activeAccount.name === "kibisis" + ) { + const algorand = window.algorand; + if (!algorand) { + throw new Error("no wallets are installed!"); + } + const wallets = algorand.getWallets(); + const wallet = await algorand.enable({ + genesisHash: getGenesisHash(node), + }); + + const result = await window.algorand.signTxns({ + txns: txns.map((el) => { + return { + txn: el, + }; + }), + }); + let signedTransactionBytes; + signedTransactionBytes = result.stxns.map( + (stxn) => new Uint8Array(Buffer.from(stxn, "base64")) + ); + const res = await algodClient + .sendRawTransaction(signedTransactionBytes) + .do(); + await waitForConfirmation(algodClient, res.txId, 4); + } else if ( + [PROVIDER_ID.KIBISIS, PROVIDER_ID.DEFLY, PROVIDER_ID.LUTE].includes( + activeAccount.providerId + ) + ) { + const stxns = await signTransactions( + txns.map((el) => new Uint8Array(Buffer.from(el, "base64"))) + ); + const res = await algodClient.sendRawTransaction(stxns).do(); + await waitForConfirmation(algodClient, res.txId, 4); + } else { + throw new Error("Unsupported wallet"); + } + }, + [activeAccount] + ); + + const handleBuy = useCallback(async () => { + try { + setMessage("Please wait..."); + setLoading(true); + + const [pType, ...prc] = lPrc; + + const listing = forSale.find((el) => { + return Number(el.lId) === Number(lId) && Number(el.mp) === Number(mp); + }); + + // check collection escrow acc mbr + // // const accInfo = await algodClient + // .accountInformation(algosdk.getApplicationAddress(cId)) + // .do(); + // if (accInfo.amount - accInfo["min-balance"] < 28500) { + // const suggestedParams = await algodClient.getTransactionParams().do(); + // const paymentTxn = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ + // from: activeAccount?.address, + // to: algosdk.getApplicationAddress(cId), + // amount: 28500, + // suggestedParams, + // }); + // await signTransaction([ + // Buffer.from(paymentTxn.toByte()).toString("base64"), + // ]); + // } + + switch (pType) { + case "00": { + // TODO use customtxn + const ci = new CONTRACT( + mp, + algodClient, + indexerClient, + mp === ctcInfoMp202 + ? mp202Schema + : ctcInfoMp201 + ? mp201Schema + : mp200Schema, + { + addr: activeAccount.address, + } + ); + ci.setFee(4000); + ci.setPaymentAmount( + computeNFTSalePrice(tokens, lPrc, listing.nft.royalties, fee, true) + ); + ci.setAccounts([ + "G3MSA75OZEJTCCENOJDLDJK7UD7E2K5DNC7FVHCNOV7E3I4DTXTOWDUIFQ", + ]); + setMessage("Signing buy transaction..."); + const buyNetR = await ci.buyNet(lId); + if (!buyNetR.success) throw new Error(buyNetR.error); + await signTransaction(buyNetR.txns); + setShowConfetti(true); + toast("NFT purchase successful!"); + break; + } + case "01": { + // ----------------------------------------------- + // arc200_approve mp202 cId tId + // mp202_buySC tId + // mp202_claimSC tId + // ----------------------------------------------- + // TODO add preflight checks here + const [tId, tPrc] = prc; + const ptid = Number("0x" + tId); + const tprc = Number("0x" + tPrc); + const builder = { + mp202: new CONTRACT( + mp, + algodClient, + indexerClient, + mp202Schema, + { addr: activeAccount.address }, + undefined, + undefined, + true + ), + arc200: new CONTRACT( + ptid, + algodClient, + indexerClient, + arc200Schema, + { addr: activeAccount.address }, + undefined, + undefined, + true + ), + }; + const ciArc200 = new arc200(ptid, algodClient, indexerClient, { + acc: { addr: activeAccount.address }, + }); + const ciMp = new CONTRACT( + mp, + algodClient, + indexerClient, + mp202Schema, + { + addr: activeAccount?.address, + } + ); + const arc200_totalSupplyR = await ciArc200.arc200_totalSupply(); + if (!arc200_totalSupplyR.success) + throw new Error("Failed to get supply"); + const arc200_totalSupply = arc200_totalSupplyR.returnValue; + const arc200_allowanceR = await ciArc200.arc200_allowance( + activeAccount.address, + algosdk.getApplicationAddress(mp) + ); + if (!arc200_allowanceR.success) + throw new Error("Failed to get allowance"); + const arc200_allowance = arc200_allowanceR.returnValue; + if (arc200_allowance < bigNumberToBigInt(bigNumberify(tprc))) { + setMessage("Approving..."); + const arc200_approveR = await ciArc200.arc200_approve( + algosdk.getApplicationAddress(mp), + arc200_totalSupply + ); + if (!arc200_approveR.success) throw new Error("Failed to approve"); + await signTransaction(arc200_approveR.txns); + } + const customTxn = ( + await Promise.all([ + // builder.arc200.arc200_approve( + // algosdk.getApplicationAddress(mp), + // tprc + // ), + builder.mp202.buySC(lId), + builder.mp202.claimSC(lId), + ]) + ).map(({ obj }) => obj); + ciMp.setExtraTxns(customTxn); + const extraPaymentAmount = Math.max( + Math.ceil( + computeExtraPayment(tprc, listing.nft.royalties, fee, rate) + ), + 1e6 + ); + ciMp.setPaymentAmount(extraPaymentAmount); + ciMp.setFee(5000); + const customR = await ciMp.custom(); + if (!customR.success) { + throw new Error("failed in simulate"); + } + await signTransaction(customR.txns); + setShowConfetti(true); + toast("NFT purchase successful!"); + // ----------------------------------------------- + } + } + } catch (e) { + console.log(e); + toast.error(e.message); + } finally { + setLoading(false); + setMessage(""); + } + }, [activeAccount, rate, mp, lId, cId, tId, lAddr, lPrc, nfts]); + + // EFFECT: step 0 - check activeAccount + useEffect(() => { + if (activeAccount && step === 0 && !isLoading) setStep(1); + }, [step, activeAccount, pool]); + + // EFFECT: step 1 - check allowance + useEffect(() => { + if (activeAccount && step === 1) { + const [pType, ...prc] = lPrc; + switch (pType) { + case "00": { + return true; + } + case "01": { + const [tok, tprc] = prc; + const tokN = Number("0x" + tok); + const ciARC200 = new arc200(tokN, algodClient, indexerClient); + ciARC200 + .arc200_allowance( + activeAccount.address, + algosdk.getApplicationAddress(mp) + ) + .then((arc200_allowanceR) => { + if (!arc200_allowanceR.success) return; + const arc200_allowance = arc200_allowanceR.returnValue; + if (arc200_allowance > 0n) { + setHasAllowance(true); + setAllowance(arc200_allowance); + } + }); + } + } + } + }, [activeAccount, step]); + + // EFFECT: check rate + useEffect(() => { + if (!pool) return; + switch (lPrc[0]) { + case "00": + setPoolPair(null); + setPool(null); + setRate(1); + return; + case "01": + const [_, tok, tprc] = lPrc; + const tokN = Number("0x" + tok); + const wVOI = 24590664; + + if (tokN === wVOI) { + setPoolPair(null); + setPool(null); + setRate(1); + return; + } + + const pools = pool.filter((el) => { + return ( + [el.tokA, el.tokB].includes(wVOI) && + [el.tokA, el.tokB].includes(tokN) + ); + }); + + if (pools.length !== 1) { + setPoolPair(null); + setPool(null); + setRate(0.000001); + return; + } + + const [pool] = pools; + + setPool(pool); + + const { tokA, tokB, poolId } = pool; + + const tokASymbol = tokens.find((el) => el.tokenId === tokA)?.symbol; + const tokBSymbol = tokens.find((el) => el.tokenId === tokB)?.symbol; + + setPoolPair(`${tokASymbol}/${tokBSymbol}`); + } + }, [pool, lPrc]); + + useEffect(() => { + if (!pool) return; + const fallback = 0.000001; + const ci = new swap200(pool.poolId, algodClient, indexerClient); + ci.Info() + .then((InfoR) => { + if (!InfoR.success) return fallback; + return InfoR.returnValue; + }) + .then((Info) => { + const [lptBals, [balA, balB]] = Info; + const prec = 10000000n; + const rateAU = (prec * balA) / balB; + const rateSU = Number(rateAU) / Number(prec); + return rateSU; + }) + .then((r) => { + setRate(r); + }) + .catch((e) => { + console.log(e); + }); + }, [pool]); + + const handleClose = () => { + setOpen(false); + }; + + return isLoading && nft ? ( + "Loading..." + ) : ( + <> + + Buy NFT + + {showConfetti && } + }> + + + {[ + `ListAddress: ${lAddr.slice(0, 4) + "..." + lAddr.slice(-4)}`, + `ListPrice: ${( + decodePrice(lPrc) / + 10 ** decodeDecimals(lPrc, node, tokens) + ).toLocaleString()} ${getPriceSymbol(lPrc, node, tokens)}`, + // mp !== 28368147 + // ? `MarketplaceFee: ${computeMarketplaceFee(royalties, fee)}%` + // : null, + // mp !== 28368147 + // ? `Royalties: ${nft.royalties.royaltyPercent}%` + // : null, + mp !== 28368147 && pool ? `Rate (${poolPair}): ${rate}` : null, + mp !== 28368147 && lPrc[0] === "00" + ? "SalePrice: " + + ( + (decodePrice(lPrc) + + computeExtraPayment( + decodePrice(lPrc), + nft.royalties, + fee, + rate + )) / + 10 ** decodeDecimals(lPrc, node, tokens) + ).toLocaleString() + + " " + + getPriceSymbol(lPrc, node, tokens) + : null, + mp !== 28368147 && lPrc[0] !== "00" + ? "SalePrice: " + + ( + decodePrice(lPrc) / + 10 ** decodeDecimals(lPrc, node, tokens) + ).toLocaleString() + + " " + + getPriceSymbol(lPrc, node, tokens) + + " + " + + (( + Math.max( + computeExtraPayment( + decodePrice(lPrc), + nft.royalties, + fee, + rate + ), + 1e6 + ) / + 10 ** decodeDecimals(["00"], node, tokens) + ).toLocaleString() + + " " + + getPriceSymbol(["00"], node, tokens)) + : null, + ].map((el) => ( +

{el}

+ ))} + +
+
+ + ); +}; + +export default BuyModal; diff --git a/app/app-sandbox/src/components/Leaderboard/index.js b/app/app-sandbox/src/components/Leaderboard/index.js new file mode 100644 index 0000000..64d2715 --- /dev/null +++ b/app/app-sandbox/src/components/Leaderboard/index.js @@ -0,0 +1,77 @@ +// Leaderboard.js + +import React from "react"; +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Tooltip, + Chip, +} from "@mui/material"; +import NFTImage from "../NFTImage"; + +const Leaderboard = ({ collectionId, data }) => { + const [ranks, setRanks] = React.useState(null); + React.useEffect(() => { + if (!data) return; + const rank = {}; + for (let [key, value] of data) { + value.forEach((el) => { + if (!rank[key]) rank[key] = 1; + else rank[key]++; + }); + } + setRanks(Object.entries(rank).sort((a, b) => b[1] - a[1])); + }, [data]); + console.log({ ranks, data }); + return ( + ranks && ( + + + + + # + Address + Score + Collection + + + + {ranks.map((row, index) => ( + + {index + 1} + {row[0]} + {row[1]} + + {data.get(row[0]).map((el) => ( + + } + placement="left-end" + > + + + + + ))} + + + ))} + +
+
+ ) + ); +}; + +export default Leaderboard; diff --git a/app/app-sandbox/src/components/LoadingIndicator/index.js b/app/app-sandbox/src/components/LoadingIndicator/index.js index 5e5635c..478a155 100644 --- a/app/app-sandbox/src/components/LoadingIndicator/index.js +++ b/app/app-sandbox/src/components/LoadingIndicator/index.js @@ -11,7 +11,7 @@ const LoadingIndicator = ({ message }) => { flexDirection: "column", justifyContent: "center", alignItems: "center", - height: "100vh", // Adjust the height based on your layout + height: "90vh", // Adjust the height based on your layout }} > diff --git a/app/app-sandbox/src/components/MainMenu/index.js b/app/app-sandbox/src/components/MainMenu/index.js index f70d9b9..98e6748 100644 --- a/app/app-sandbox/src/components/MainMenu/index.js +++ b/app/app-sandbox/src/components/MainMenu/index.js @@ -1,10 +1,16 @@ -import React from "react"; +import React, { useContext, useMemo } from "react"; import Tabs from "@mui/material/Tabs"; import Tab from "@mui/material/Tab"; +import Stack from "@mui/material/Stack"; import { useLocation, useNavigate } from "react-router-dom"; import { useWallet } from "@txnlab/use-wallet"; +import { Box, Typography } from "@mui/material"; +import Marquee from "react-fast-marquee"; +import { MarketplaceContext } from "../../store/MarketplaceContext"; +import ExitToAppIcon from "@mui/icons-material/ExitToApp"; const MainMenu = () => { + const { isLoading, metrics } = useContext(MarketplaceContext); const { activeAccount } = useWallet(); const navigate = useNavigate(); const location = useLocation(); @@ -16,26 +22,88 @@ const MainMenu = () => { ? 2 : 0; const [value, setValue] = React.useState(index); + /* + const [volume, sales, avgSalePrice] = useMemo(() => { + const storedStats = localStorage.getItem("mp-stats"); + if (!storedStats) return [0, 0, 0]; + const stats = JSON.parse(storedStats); + const { volume, sales, avgSalePrice } = stats; + return [volume, sales, avgSalePrice]; + }, []); + */ + const handleChange = (event, newValue) => { setValue(newValue); }; - return ( - - {activeAccount && ( - navigate(`/account/${activeAccount.address}`)} - /> - )} - navigate(`/dex/`)} /> - navigate(`/nft/marketplace`)} /> - - ); + return !isLoading ? ( + <> +

+ {/*Nautilus - NFT Marketplace*/} + Shelly's Sandbox +

+ + {/* + + {Object.entries(metrics).map(([key, value], i) => ( + <> + + {key} + + {Object.entries(value).map(([key, value], i) => ( + + {key}: {value} + + ))} + + ))} + + News + + + + NFT Olympics + {" "} + is coming + + + */} + { + + {activeAccount && ( + navigate(`/account/${activeAccount.address}`)} + /> + )} + navigate(`/dex/`)} /> + + } + + + Nautilus - NFT Marketplace + + + + ) : null; }; export default MainMenu; diff --git a/app/app-sandbox/src/components/NFTCard/index.js b/app/app-sandbox/src/components/NFTCard/index.js new file mode 100644 index 0000000..d5e5037 --- /dev/null +++ b/app/app-sandbox/src/components/NFTCard/index.js @@ -0,0 +1,48 @@ +import React, { lazy, Suspense, useContext } from "react"; +import CircularProgress from "@mui/material/CircularProgress"; +import { Paper, Typography } from "@mui/material"; +import { MarketplaceContext } from "../../store/MarketplaceContext"; + +const LazyNFTImage = React.lazy(() => import("../../components/NFTImage")); + +const NFTCard = ({ cId, tId, image }) => { + const { forSale } = useContext(MarketplaceContext); + return ( + + }> + + +
+ + CollectionId:{" "} + {cId.toString()} + + + TokenId:{" "} + + {tId.toString()} + + + + List Status:{" "} + {forSale.find( + ({ cId: scId, tId: stId }) => + Number(scId) === Number(cId) && Number(stId) === Number(tId) + ) ? ( + For Sale + ) : ( + Not For Sale + )} + +
+
+ ); +}; + +export default NFTCard; diff --git a/app/app-sandbox/src/components/NFTImage/index.js b/app/app-sandbox/src/components/NFTImage/index.js index 1a15b16..ae006dd 100644 --- a/app/app-sandbox/src/components/NFTImage/index.js +++ b/app/app-sandbox/src/components/NFTImage/index.js @@ -53,6 +53,7 @@ const ipfsToGateway = (url) => { const NFTImage = ({ collectionId, tokenId, image }) => { const [url, setUrl] = React.useState(null); const resolveUrl = useCallback(async () => { + if (image) return; try { if (url) return; if (!collectionId || !tokenId) return; @@ -112,7 +113,7 @@ const NFTImage = ({ collectionId, tokenId, image }) => { attributes as props + src={image || url} // use normal attributes as props width={"100%"} effect="blur" delayTime={2000} diff --git a/app/app-sandbox/src/components/PoolForm/index.tsx b/app/app-sandbox/src/components/PoolForm/index.tsx index 4267c6e..5419e4b 100644 --- a/app/app-sandbox/src/components/PoolForm/index.tsx +++ b/app/app-sandbox/src/components/PoolForm/index.tsx @@ -16,7 +16,7 @@ import { toast } from "react-toastify"; import { useDebounce } from "usehooks-ts"; -import swap200 from "swap200js"; +//import swap200 from "swap200js"; import arc200Schema from "../../abis/arc200.json"; import { getCurrentNode, getGenesisHash, makeStdLib } from "../../utils/reach"; @@ -34,15 +34,28 @@ import BigNumber from "bignumber.js"; import HelpIcon from "@mui/icons-material/Help"; +import { swap200 } from "ulujs"; + type bals = [bigint, bigint]; const { indexerClient, algodClient } = getAlgorandClients(); const stdlib: any = makeStdLib(); -// contractjs funcs - -// arc200 +/* + * prepareString + * - prepare string (strip trailing null bytes) + * @param str: string to prepare + * @returns: prepared string + */ +const prepareString = (str: string) => { + const index = str.indexOf("\x00"); + if (index > 0) { + return str.slice(0, str.indexOf("\x00")); + } else { + return str; + } +}; const getMetadata = async (ctcInfo: number) => { const ci = new swap200(ctcInfo, algodClient, indexerClient); @@ -78,7 +91,7 @@ const doTransfer = async ( const ci = new swap200(ctcInfo, algodClient, indexerClient, { acc: { addr: addressFrom, sk: new Uint8Array(0) }, }); - const res = await ci.arc200_transfer(addrTo, amount, true, false); + const res = await ci.arc200_transfer(addrTo, amount, true, false); // ts-ignore if (!res.success) throw new Error("arc200_transfer failed"); return res; }; @@ -217,11 +230,12 @@ interface Tokens { } const PoolForm: React.FC = (props) => { - const { activeAccount } = useWallet(); + const { activeAccount, signTransactions } = useWallet(); const [tokens, setTokens] = React.useState({ tokenA: "", tokenB: "", }); + const [node] = getCurrentNode(); const debouncedValue = useDebounce(tokens, 500); const [swapDirection, setSwapDirection] = React.useState(true); const [balances, setBalances] = React.useState({}); @@ -382,7 +396,12 @@ const PoolForm: React.FC = (props) => { setTokenB(`${tokB}`); setMessage("Loading token metadata..."); const tokATM = await getMetadata(Number(tokA)); - const tokBTM = await getMetadata(Number(tokB)); + const tokBTM = ((tm) => ({ + ...tm, + name: prepareString(String(tm.name)), + symbol: prepareString(String(tm.symbol)), + }))(await getMetadata(Number(tokB))); + setTokenList({ [tokA as unknown as string]: { ...tokATM, @@ -448,18 +467,14 @@ const PoolForm: React.FC = (props) => { return () => clearTimeout(timeout); }, [activeAccount, version, balances, allowances, reserves]); - const signTransaction = useCallback( + const signTransaction = React.useCallback( async (txns: string[]) => { if (!activeAccount) return; - if ( - activeAccount.providerId === PROVIDER_ID.CUSTOM && - activeAccount.name === "kibisis" - ) { + if (activeAccount.providerId === PROVIDER_ID.CUSTOM) { const algorand = (window as any).algorand; if (!algorand) { throw new Error("no wallets are installed!"); } - const [node] = getCurrentNode(); const wallets = algorand.getWallets(); const wallet = await algorand.enable({ genesisHash: getGenesisHash(node), @@ -479,17 +494,22 @@ const PoolForm: React.FC = (props) => { const res = await algodClient .sendRawTransaction(signedTransactionBytes) .do(); - return res.txId; + await waitForConfirmation(algodClient, res.txId, 4); + } else if ( + [PROVIDER_ID.KIBISIS, PROVIDER_ID.DEFLY, PROVIDER_ID.LUTE].includes( + activeAccount.providerId + ) + ) { + const stxns = await signTransactions( + txns.map((el) => new Uint8Array(Buffer.from(el, "base64"))) + ); + const res = await algodClient.sendRawTransaction(stxns).do(); + await waitForConfirmation(algodClient, res.txId, 4); } else { - const wtxns = txns.map((el) => { - return { - txn: el, - }; - }); - await stdlib.signSendAndConfirm({ addr: activeAccount.address }, wtxns); + throw new Error("Unsupported wallet"); } }, - [activeAccount, allowance] + [activeAccount] ); const handleApproveChange = (e: React.ChangeEvent) => { @@ -537,9 +557,8 @@ const PoolForm: React.FC = (props) => { ol ); setMessage("Pending signature..."); - const txId = await signTransaction(txns); + await signTransaction(txns); setMessage("Waiting for confirmation..."); - await waitForTxn(txId); const msg = ""; toast(
@@ -549,7 +568,7 @@ const PoolForm: React.FC = (props) => {
); } catch (e) { - console.log(e); + toast.error(e.message); } finally { setLoading(false); setVersion(version + 1); @@ -589,8 +608,7 @@ const PoolForm: React.FC = (props) => { ctcAddr, allowance + outBi ); - const txId = await signTransaction(txns); - await waitForTxn(txId); + await signTransaction(txns); } const { returnValue: outsl } = await withdraw( ctcInfo, @@ -604,11 +622,10 @@ const PoolForm: React.FC = (props) => { outBi, outsl ); - const txId = await signTransaction(txns); - await waitForTxn(txId); + await signTransaction(txns); toast(
Remove successful!
); } catch (e) { - console.log(e); + toast.error(e.message); } finally { setLoading(false); setVersion(version + 1); @@ -640,9 +657,8 @@ const PoolForm: React.FC = (props) => { BigInt(0) ); setMessage("Signature pending (Transfer)..."); - const txId = await signTransaction(txns); + await signTransaction(txns); setMessage("Waiting for confirmation..."); - await waitForTxn(txId); setMessage("Signature pending (Deposit)..."); } const allowance = await getAllowance( @@ -658,9 +674,8 @@ const PoolForm: React.FC = (props) => { ctcAddr, inputABi ); - const txId = await signTransaction(txns); + await signTransaction(txns); setMessage("Waiting for confirmation..."); - await waitForTxn(txId); setMessage("Signature pending (Deposit)..."); } const { txns } = await depositReserve( @@ -669,9 +684,8 @@ const PoolForm: React.FC = (props) => { inputABi, isA ); - const txId = await signTransaction(txns); + await signTransaction(txns); setMessage("Waiting for confirmation..."); - await waitForTxn(txId); const msg = "+" + input + " " + token.symbol; toast(
@@ -682,7 +696,7 @@ const PoolForm: React.FC = (props) => { ); setVersion(version + 1); } catch (e) { - console.log(e); + toast.error(e.message); } finally { setLoading(false); } @@ -710,9 +724,8 @@ const PoolForm: React.FC = (props) => { inputABi, isA ); - const txId = await signTransaction(txns); + await signTransaction(txns); setMessage("Waiting for confirmation..."); - await waitForTxn(txId); const msg = "-" + input + " " + token.symbol; toast(
@@ -723,7 +736,7 @@ const PoolForm: React.FC = (props) => { ); setVersion(version + 1); } catch (e) { - console.log(e); + toast.error(e.message); } finally { setLoading(false); } diff --git a/app/app-sandbox/src/components/PoolList/index.js b/app/app-sandbox/src/components/PoolList/index.js index f6246f6..8cd2849 100644 --- a/app/app-sandbox/src/components/PoolList/index.js +++ b/app/app-sandbox/src/components/PoolList/index.js @@ -66,12 +66,14 @@ const StyledTableCell = styled(TableCell)(({ theme }) => ({ }, })); +export const badPools = [29138188, 29138375, 29146294, 29137625, 29138141]; + function PoolListItem(props) { const [tm, setTm] = useState(null); const token = useMemo(() => { return props.tokens.find((t) => t.tokenId === props.poolId); }, [props.poolId, props.tokens]); - return ( + return badPools.includes(props.poolId) ? null : ( {props?.pools ?.filter((p) => { + if (badPools.includes(p.poolId)) return false; const token = dbTokens.find((t) => t.tokenId === p.poolId); return token?.name ?.toLowerCase() diff --git a/app/app-sandbox/src/components/SalesHistoryTable/index.js b/app/app-sandbox/src/components/SalesHistoryTable/index.js index 2d74461..46709ea 100644 --- a/app/app-sandbox/src/components/SalesHistoryTable/index.js +++ b/app/app-sandbox/src/components/SalesHistoryTable/index.js @@ -1,17 +1,18 @@ -import React from 'react'; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableCell from '@mui/material/TableCell'; -import TableContainer from '@mui/material/TableContainer'; -import TableHead from '@mui/material/TableHead'; -import TableRow from '@mui/material/TableRow'; -import Paper from '@mui/material/Paper'; -import Typography from '@mui/material/Typography'; +import React from "react"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import Paper from "@mui/material/Paper"; +import Typography from "@mui/material/Typography"; +import { Tab } from "@mui/material"; const SalesHistoryTable = ({ salesData }) => { return (
- + Sales History @@ -21,6 +22,7 @@ const SalesHistoryTable = ({ salesData }) => { Date Seller Buyer + Symbol Amount @@ -30,6 +32,7 @@ const SalesHistoryTable = ({ salesData }) => { {sale.date} {sale.seller} {sale.buyer} + {sale.symbol} {sale.amount} ))} diff --git a/app/app-sandbox/src/components/SearchInput/index.js b/app/app-sandbox/src/components/SearchInput/index.js new file mode 100644 index 0000000..ed2ad70 --- /dev/null +++ b/app/app-sandbox/src/components/SearchInput/index.js @@ -0,0 +1,28 @@ +import React from "react"; +import TextField from "@mui/material/TextField"; +import InputAdornment from "@mui/material/InputAdornment"; +import SearchIcon from "@mui/icons-material/Search"; + +const SearchInput = ({onChange}) => { + return ( + + + + ), + }} + onChange={onChange} + + // You can add other props and event handlers as needed + /> + ); +}; + +export default SearchInput; diff --git a/app/app-sandbox/src/components/SellModal/index.js b/app/app-sandbox/src/components/SellModal/index.js new file mode 100644 index 0000000..7012b73 --- /dev/null +++ b/app/app-sandbox/src/components/SellModal/index.js @@ -0,0 +1,367 @@ +// BuyModal.js +import React, { useState, Suspense, lazy, useMemo, useCallback } from "react"; +import { + Grid, + Button, + Dialog, + DialogContent, + DialogTitle, + CircularProgress, + Typography, + TextField, +} from "@mui/material"; +import CONTRACT from "arccjs"; +import { arc72 } from "ulujs"; +import { getAlgorandClients } from "../../utils/algorand.js"; +import { useWallet, PROVIDER_ID, custom } from "@txnlab/use-wallet"; +import { mp201Schema, mp202Schema, arc72Schema } from "../../abis/index.js"; +import { getCurrentNode, getGenesisHash } from "../../utils/reach.js"; +import algosdk, { waitForConfirmation } from "algosdk"; +import { makeStdLib } from "../../utils/reach.js"; +import { decodeRoyalties } from "../../utils/hf.js"; +import { nftDb, db } from "../../db.js"; +import { useLiveQuery } from "dexie-react-hooks"; +import { computeSalePrice } from "../../utils/mp.js"; +import Confetti from "react-confetti"; +import { useWindowSize } from "usehooks-ts"; +import { ctcInfoMp202 as ctcInfoMp } from "../../constants/mp.js"; +import { toast } from "react-toastify"; +import TokenSelect from "../TokenSelect/index.jsx"; + +const LazyNFTImage = lazy(() => import("../NFTImage/index.js")); + +const { algodClient, indexerClient } = getAlgorandClients(); + +const SellModal = ({ open, setOpen, cid, tid }) => { + const { activeAccount, signTransactions } = useWallet(); + const [node] = getCurrentNode(); + const { width, height } = useWindowSize(); + const [showConfetti, setShowConfetti] = useState(false); + const [message, setMessage] = useState(""); + const dbTokens = useLiveQuery(() => db.tokens.toArray()); + const dbNfts = useLiveQuery(() => nftDb.nfts.toArray()); + const dbPools = useLiveQuery(() => db.registerEvents.toArray()); + const isLoading = useMemo( + () => !dbPools || !dbTokens || !dbNfts, + [dbPools, dbTokens, dbNfts] + ); + const nft = useMemo(() => { + if (!dbNfts) return null; + const nft = dbNfts.find( + (el) => el.collectionId === Number(cid) && el.tokenId === Number(tid) + ); + const metadata = JSON.parse(nft.metadata); + const royalties = decodeRoyalties(metadata.royalties); + return { ...nft, metadata, royalties }; + }, [dbNfts, cid, tid]); + const [token, setToken] = useState(null); + const [price, setPrice] = useState(""); + const salePrice = useMemo(() => { + const priceN = Number(price); + if (!price || !nft || !token || isNaN(priceN)) return null; + return computeSalePrice(priceN, nft.royalties, token); + }, [price, nft]); + + const signTransaction = React.useCallback( + async (txns) => { + if (!activeAccount) return; + if ( + activeAccount.providerId === PROVIDER_ID.CUSTOM && + activeAccount.name === "kibisis" + ) { + const algorand = window.algorand; + if (!algorand) { + throw new Error("no wallets are installed!"); + } + const wallets = algorand.getWallets(); + const wallet = await algorand.enable({ + genesisHash: getGenesisHash(node), + }); + + const result = await window.algorand.signTxns({ + txns: txns.map((el) => { + return { + txn: el, + }; + }), + }); + let signedTransactionBytes; + signedTransactionBytes = result.stxns.map( + (stxn) => new Uint8Array(Buffer.from(stxn, "base64")) + ); + const res = await algodClient + .sendRawTransaction(signedTransactionBytes) + .do(); + await waitForConfirmation(algodClient, res.txId, 4); + } else if ( + [PROVIDER_ID.KIBISIS, PROVIDER_ID.DEFLY, PROVIDER_ID.LUTE].includes( + activeAccount.providerId + ) + ) { + const stxns = await signTransactions( + txns.map((el) => new Uint8Array(Buffer.from(el, "base64"))) + ); + const res = await algodClient.sendRawTransaction(stxns).do(); + await waitForConfirmation(algodClient, res.txId, 4); + } else { + throw new Error("Unsupported wallet"); + } + }, + [activeAccount] + ); + + const handleClose = () => { + window.location.reload(); + }; + const handleSell = useCallback(async () => { + try { + if (!activeAccount) { + throw new Error("No active account"); + } + const builder = { + arc72: new CONTRACT( + Number(cid), + algodClient, + indexerClient, + arc72Schema, + { + addr: activeAccount?.address, + }, + true, + false, + true + ), + mp202: new CONTRACT( + ctcInfoMp, + algodClient, + indexerClient, + mp202Schema, + { + addr: activeAccount?.address, + }, + true, + false, + true + ), + }; + const ciMp = new CONTRACT( + ctcInfoMp, + algodClient, + indexerClient, + mp202Schema, + { + addr: activeAccount?.address, + } + ); + // const ciARC72 = new arc72(Number(cid), algodClient, indexerClient, { + // acc: { + // addr: activeAccount?.address, + // }, + // }); + // const arc72_ownerOfR = await ciARC72.arc72_ownerOf(Number(tid)); + // if (!arc72_ownerOfR.success) { + // throw new Error("arc72_ownerOf failed in simulate"); + // } + // if (arc72_ownerOfR.returnValue !== activeAccount?.address) { + // throw new Error("arc72_ownerOf returned wrong owner"); + // } + // const arc72_getApprovedrR = await ciARC72.arc72_getApproved(Number(tid)); + // if (!arc72_getApprovedrR.success) { + // throw new Error("arc72_getApproved failed in simulate"); + // } + // const arc72_getApproved = arc72_getApprovedrR.returnValue; + // if (arc72_getApproved !== algosdk.getApplicationAddress(ctcInfoMp)) { + // const arc72_approveR = await ciARC72.arc72_approve( + // algosdk.getApplicationAddress(ctcInfoMp), + // Number(tid) + // ); + // if (!arc72_approveR.success) { + // throw new Error("arc72_approve failed in simulate"); + // } + // await signTransaction(arc72_approveR.txns); + // } + if (token.tokenId === 0) { + const customPaymentAmount = [73700]; + const customTxns = ( + await Promise.all([ + builder.arc72.arc72_approve( + algosdk.getApplicationAddress(ctcInfoMp), + Number(tid) + ), + builder.mp202.listNet( + Number(cid), + Number(tid), + Number(price) * Math.pow(10, token.decimals) + ), + ]) + ).map(({ obj }) => obj); + ciMp.setPaymentAmount( + customPaymentAmount.reduce((acc, val) => acc + val, 0) + ); + ciMp.setExtraTxns(customTxns.reverse()); + const customR = await ciMp.custom(); + if (!customR.success) { + throw new Error("failed in simulate"); + } + const stxn = await signTransaction(customR.txns); + + // // listnet + // const listPrice = Number(price) * Math.pow(10, token.decimals); + // ciMp.setPaymentAmount(73700); + // const listNetR = await ciMp.listNet( + // Number(cid), + // Number(tid), + // listPrice + // ); + // if (!listNetR.success) { + // throw new Error("listNet failed in simulate"); + // } + // await signTransaction(listNetR.txns); + } else { + const customPaymentAmount = [73700]; + const customTxns = ( + await Promise.all([ + builder.arc72.arc72_approve( + algosdk.getApplicationAddress(ctcInfoMp), + Number(tid) + ), + builder.mp202.listSC( + Number(cid), + Number(tid), + token.tokenId, + Number(price) * Math.pow(10, token.decimals) + ), + ]) + ).map(({ obj }) => obj); + ciMp.setFee(2000); + ciMp.setPaymentAmount( + customPaymentAmount.reduce((acc, val) => acc + val, 0) + ); + ciMp.setExtraTxns(customTxns.reverse()); + const customR = await ciMp.custom(); + if (!customR.success) { + throw new Error("failed in simulate"); + } + const stxn = await signTransaction(customR.txns); + //listsc + // const boxCost = 28500; + // const accInfo = await algodClient + // .accountInformation(algosdk.getApplicationAddress(Number(cid))) + // .do(); + // if (accInfo.amount - accInfo["min-balance"] < boxCost) { + // const suggestedParams = await algodClient.getTransactionParams().do(); + // const paymentTxn = + // algosdk.makePaymentTxnWithSuggestedParamsFromObject({ + // from: activeAccount?.address, + // to: algosdk.getApplicationAddress(Number(cid)), + // amount: boxCost, + // suggestedParams, + // }); + // await signTransaction([ + // Buffer.from(paymentTxn.toByte()).toString("base64"), + // ]); + // } + // const listPrice = Number(price) * Math.pow(10, token.decimals); + // ciMp.setFee(2000); + // ciMp.setPaymentAmount(73700); + // const listSCR = await ciMp.listSC( + // Number(cid), + // Number(tid), + // token.tokenId, + // listPrice + // ); + // if (!listSCR.success) { + // throw new Error("listSC failed in simulate"); + // } + // await signTransaction(listSCR.txns); + } + toast("List successful!"); + } catch (e) { + toast.error(e.message); + } + }, [activeAccount, token, price]); + return isLoading ? ( + "Loading..." + ) : ( + <> + + Sell NFT + + {showConfetti && } + + + }> + + + {[ + `Name: ${nft.name}`, + `Royalty: ${nft.royalties?.royaltyPercent || 0}%`, + ].map((el) => ( + + {el} + + ))} + + + + 1. Select Token + + + {token && + [ + `Name: ${token.name}`, + `Symbol: ${token.symbol}`, + `Decimals: ${token.decimals}`, + ].map((el) =>

{el}

)} + + 2. Set Price + setPrice(e.target.value)} + /> + + {salePrice && + [`Sale Price: ${salePrice}`].map((el) =>

{el}

)} + + +
+
+
+
+ + ); +}; + +export default SellModal; diff --git a/app/app-sandbox/src/components/SendDialog/index.js b/app/app-sandbox/src/components/SendDialog/index.js index 484ba75..bb96e26 100644 --- a/app/app-sandbox/src/components/SendDialog/index.js +++ b/app/app-sandbox/src/components/SendDialog/index.js @@ -25,12 +25,15 @@ import { import ConfirmationComponent from "../ConfirmationComponent.js"; import { getAlgorandClients } from "../../utils/algorand.js"; import arc200 from "arc200js"; -import algosdk, { encodeUnsignedTransaction } from "algosdk"; +import algosdk, { + encodeUnsignedTransaction, + waitForConfirmation, +} from "algosdk"; import { encode as encodeBase64 } from "@stablelib/base64"; function SendDialog(props) { - const { providers, activeAccount } = useWallet(); + const { providers, activeAccount, signTransactions, sendTransactions } = useWallet(); const [token, setToken] = useState(props.token); const [tokenAmount, setTokenAmount] = useState(""); const [accountAddress, setAccountAddress] = useState(""); @@ -104,6 +107,12 @@ function SendDialog(props) { switch (props.token.assetType) { case "network": { if ( + [PROVIDER_ID.KIBISIS, PROVIDER_ID.DEFLY].includes( + activeAccount.providerId + ) + ) { + throw new Error("not supported"); + } else if ( activeAccount.providerId === PROVIDER_ID.CUSTOM && activeAccount.name === "kibisis" ) { @@ -177,6 +186,26 @@ function SendDialog(props) { .toBigInt(); let res2; if ( + [ + PROVIDER_ID.KIBISIS, + PROVIDER_ID.DEFLY, + PROVIDER_ID.LUTE, + ].includes(activeAccount.providerId) + ) { + const { algodClient, indexerClient } = getAlgorandClients(); + const ci = new arc200(token.appId, algodClient, indexerClient, { + acc: { addr: activeAccount.address }, + simulate: true, + formatBytes: true, + }); + const res = await ci.arc200_transfer(accountAddress, amount); + if (!res.success) throw new Error("failed to approve"); + const stxns = await signTransactions( + res.txns.map((el) => new Uint8Array(Buffer.from(el, "base64"))) + ).then(sendTransactions) + //const { txId } = await algodClient.sendRawTransaction(stxns).do(); + //await waitForConfirmation(algodClient, txId, 4); + } else if ( activeAccount.providerId === PROVIDER_ID.CUSTOM && activeAccount.name === "kibisis" ) { @@ -221,9 +250,9 @@ function SendDialog(props) { } if (res2) { // TODO confirm transaction - setTimeout(() => { - props.reloadToken(); - }, 5_000); + // setTimeout(() => { + // props.reloadToken(); + // }, 5_000); toast(
Transfer successful! diff --git a/app/app-sandbox/src/components/SwapForm/index.tsx b/app/app-sandbox/src/components/SwapForm/index.tsx index fa4e5cd..767db5b 100644 --- a/app/app-sandbox/src/components/SwapForm/index.tsx +++ b/app/app-sandbox/src/components/SwapForm/index.tsx @@ -17,15 +17,16 @@ import { InputLabel, } from "@mui/material"; import SwapVertIcon from "@mui/icons-material/SwapVert"; -import CONTRACT from "arccjs"; +// //import CONTRACT from "arccjs"; import { getAlgorandClients } from "../../utils/algorand"; import { PROVIDER_ID, useWallet } from "@txnlab/use-wallet"; import { toast } from "react-toastify"; import { useNavigate } from "react-router-dom"; +import algosdk from "algosdk"; import { useDebounce } from "usehooks-ts"; -import swap200 from "swap200js"; +//import swap200 from "swap200js"; import arc200Schema from "../../abis/arc200.json"; import swap200Schema from "../../abis/swap200.json"; @@ -39,11 +40,28 @@ import { formatWithDecimals, } from "../../common/utils/bn"; +import { CONTRACT, swap200, arc200 } from "ulujs"; + import convertToAtomicUnit from "../../common/utils/convertToAtomicUnit"; import convertToStandardUnit from "../../common/utils/convertToStandardUnit"; import BigNumber from "bignumber.js"; +/* + * prepareString + * - prepare string (strip trailing null bytes) + * @param str: string to prepare + * @returns: prepared string + */ +const prepareString = (str: string) => { + const index = str.indexOf("\x00"); + if (index > 0) { + return str.slice(0, str.indexOf("\x00")); + } else { + return str; + } +}; + const { indexerClient, algodClient } = getAlgorandClients(); const stdlib: any = makeStdLib(); @@ -61,10 +79,17 @@ const getEvents = async (ctcInfo: number) => { // arc200 const getMetadata = async (ctcInfo: number) => { - const ci = new swap200(ctcInfo, algodClient, indexerClient); + const ci = new arc200(ctcInfo, algodClient, indexerClient); const res = await ci.getMetadata(); if (!res.success) throw new Error("getMetadata failed"); - return res.returnValue; + const tm = res.returnValue; + const tmf = { + ...tm, + name: prepareString(String(tm.name)), + symbol: prepareString(String(tm.symbol)), + }; + console.log({ tmf }); + return tmf; }; const getBalance = async (ctcInfo: number, address: string) => { @@ -132,6 +157,158 @@ const swap = async ( amount: bigint, swapAForB = true ) => { + const abi: any = { + name: "", + desc: "", + methods: [ + { + name: "custom", + args: [], + returns: { + type: "void", + }, + }, + // Trader_swapAForB(byte,uint256,uint256)(uint256,uint256) + { + name: "Trader_swapAForB", + args: [{ type: "byte" }, { type: "uint256" }, { type: "uint256" }], + returns: { + type: "(uint256,uint256)", + }, + }, + // Trader_swapBForA(byte,uint256,uint256)(uint256,uint256) + { + name: "Trader_swapBForA", + args: [{ type: "byte" }, { type: "uint256" }, { type: "uint256" }], + returns: { + type: "(uint256,uint256)", + }, + }, + { + name: "Provider_withdrawA", + args: [{ type: "uint256" }], + returns: { type: "uint256" }, + }, + { + name: "Provider_withdrawB", + args: [{ type: "uint256" }], + returns: { type: "uint256" }, + }, + { + name: "createBalanceBoxes", + desc: "Creates a balance box", + args: [ + { + type: "address", + }, + ], + returns: { + type: "void", + }, + }, + ], + events: [], + }; + // const ci = new CONTRACT(ctcInfo, algodClient, indexerClient, abi, { + // addr: address, + // sk: new Uint8Array(0), + // }); + // console.log(ci); + // const ci2 = new CONTRACT( + // 24590664, + // //ctcInfo, + // algodClient, + // indexerClient, + // abi, + // { + // addr: address, + // sk: new Uint8Array(0), + // } + // ); + // const builder = { + // wnt200: new CONTRACT( + // 24590664, + // algodClient, + // indexerClient, + // { + // name: "", + // desc: "", + // methods: [ + // { + // name: "deposit", + // args: [ + // { + // name: "amount", + // type: "uint64", + // desc: "Amount to deposit", + // }, + // ], + // returns: { + // type: "uint256", + // desc: "Amount deposited", + // }, + // }, + // { + // name: "createBalanceBox", + // args: [ + // { + // type: "address", + // name: "Address to create balance box for", + // }, + // ], + // returns: { + // type: "byte", + // desc: "Success", + // }, + // }, + // ], + // events: [], + // }, + // { addr: address, sk: new Uint8Array(0) }, + // true, + // false, + // true + // ), + // swap200: new CONTRACT( + // ctcInfo, + // algodClient, + // indexerClient, + // abi, + // { addr: address, sk: new Uint8Array(0) }, + // true, + // false, + // true + // ), + // }; + // const buildP = []; + // ci.setFee(3000); + // let r; + // if (swapAForB) { + // r = await ci.Trader_swapAForB(1, Number(amount), 0); + // ci2.setPaymentAmount(Number(amount)); + // buildP.push(builder.wnt200.deposit(Number(amount))); + // buildP.push(builder.swap200.Trader_swapAForB(0, Number(amount), 0)); + // buildP.push(builder.swap200.Provider_withdrawB(r.returnValue[1])); + // } else { + // r = await ci.Trader_swapBForA(1, Number(amount), 0); + // ci2.setPaymentAmount(1); + // buildP.push(builder.swap200.Trader_swapBForA(0, Number(amount), 0)); + // buildP.push(builder.swap200.Provider_withdrawA(r.returnValue[0])); + // } + // const extraTxns = (await Promise.all(buildP)).map((el) => el.obj); + // console.log({ r, extraTxns }); + // ci2.setFee(3000); + // ci2.setEnableGroupResourceSharing(true); + // //ci2.setPaymetns([[1, algosdk.getApplicationAddress(ctcInfo)]]); + // ci2.setExtraTxns(extraTxns); + // ci2.setAccounts([algosdk.getApplicationAddress(ctcInfo)]); + // const customR = await ci2.custom(); + // console.log({ customR }); + // return { + // ...customR, + // returnValue: r.returnValue, + // }; + const ci = new swap200(ctcInfo, algodClient, indexerClient, { acc: { addr: address, sk: new Uint8Array(0) }, }); @@ -179,44 +356,6 @@ const doSwap = async ( return res; }; -// --- - -// end contractjs funcs - -// algsdk funcs - -const waitForTxn = async (txId: string, rounds = 4) => - await waitForConfirmation(algodClient, txId, rounds); - -// end algsdk funcs - -const poolList: any = { "23215100": { tokA: 6779767, tokB: 6778021 } }; -/* -const tokenList: any = { - "6778021": { - name: "VRC200", - symbol: "VRC200", - decimals: 8, - totalSupply: 1000000000000000, - }, - "6779767": { - name: "Voi Incentive Asset", - symbol: "VIA", - decimals: 6, - totalSupply: 10000000000000000, - }, - "23215100": { - name: "ARC200 LP - VIA/VRC200", - symbol: "ARC200LT", - decimals: 6, - totalSupply: 115792089237316195423570985008687907853269984665640564039457584007913129639935, - isPool: true, - }, -}; -const tokenA = "6779767"; -const tokenB = "6778021"; -*/ - interface SwapFormProps { ctcInfo: string; direction: boolean; @@ -231,11 +370,12 @@ interface Tokens { } const SwapForm: React.FC = (props) => { - const { activeAccount } = useWallet(); + const { activeAccount, signTransactions } = useWallet(); const [tokens, setTokens] = React.useState({ tokenA: "", tokenB: "", }); + const [node] = getCurrentNode(); const [pools, setPools] = React.useState([]); const debouncedValue = useDebounce(tokens, 500); const [swapDirection, setSwapDirection] = React.useState(true); @@ -448,18 +588,14 @@ const SwapForm: React.FC = (props) => { return `${feeBnStr} ${token.symbol}`; }, [protoInfo, swapDirection, tokens, token]); - const signTransaction = useCallback( + const signTransaction = React.useCallback( async (txns: string[]) => { if (!activeAccount) return; - if ( - activeAccount.providerId === PROVIDER_ID.CUSTOM && - activeAccount.name === "kibisis" - ) { + if (activeAccount.providerId === PROVIDER_ID.CUSTOM) { const algorand = (window as any).algorand; if (!algorand) { throw new Error("no wallets are installed!"); } - const [node] = getCurrentNode(); const wallets = algorand.getWallets(); const wallet = await algorand.enable({ genesisHash: getGenesisHash(node), @@ -479,17 +615,22 @@ const SwapForm: React.FC = (props) => { const res = await algodClient .sendRawTransaction(signedTransactionBytes) .do(); - return res.txId; + await waitForConfirmation(algodClient, res.txId, 4); + } else if ( + [PROVIDER_ID.KIBISIS, PROVIDER_ID.DEFLY, PROVIDER_ID.LUTE].includes( + activeAccount.providerId + ) + ) { + const stxns = await signTransactions( + txns.map((el) => new Uint8Array(Buffer.from(el, "base64"))) + ); + const res = await algodClient.sendRawTransaction(stxns).do(); + await waitForConfirmation(algodClient, res.txId, 4); } else { - const wtxns = txns.map((el) => { - return { - txn: el, - }; - }); - await stdlib.signSendAndConfirm({ addr: activeAccount.address }, wtxns); + throw new Error("Unsupported wallet"); } }, - [activeAccount, allowance] + [activeAccount] ); const handleApproveChange = (e: React.ChangeEvent) => { @@ -553,9 +694,8 @@ const SwapForm: React.FC = (props) => { ) */ ); - const txId = await signTransaction(txns); + await signTransaction(txns); setMessage("Waiting for confirmation..."); - await waitForTxn(txId); const msg = "+" + approval + " " + token.symbol; toast(
@@ -567,7 +707,7 @@ const SwapForm: React.FC = (props) => { setVersion(version + 1); setTokens({ tokenA: "", tokenB: "" }); } catch (e) { - console.log(e); + toast.error(e.message); } finally { setLoading(false); } @@ -608,7 +748,6 @@ const SwapForm: React.FC = (props) => { const res = await ci.createBalanceBox(activeAccount.address); const txId = await signTransaction(res.txns); setMessage("Waiting for confirmation (1 of 2)..."); - await waitForTxn(txId); } else { const res = await transfer( token.id, @@ -618,7 +757,6 @@ const SwapForm: React.FC = (props) => { ); const txId = await signTransaction(res.txns); setMessage("Waiting for confirmation (1 of 2)..."); - await waitForTxn(txId); } setMessage("Signature pending (2 of 2)..."); } @@ -636,7 +774,6 @@ const SwapForm: React.FC = (props) => { } else { setMessage("Waiting for confirmation..."); } - await waitForTxn(txId); setVersion(version + 1); toast(
@@ -649,7 +786,7 @@ const SwapForm: React.FC = (props) => {
); } catch (e) { - console.log(e); + toast.error(e.message); } finally { setLoading(false); } @@ -706,7 +843,6 @@ const SwapForm: React.FC = (props) => { tokenList[tokenB].symbol; const txId = await signTransaction(txns); setMessage("Waiting for confirmation..."); - await waitForTxn(txId); setVersion(version + 1); // refresh } else { // Swap from tokenB to tokenA @@ -737,7 +873,6 @@ const SwapForm: React.FC = (props) => { " " + tokenList[tokenA].symbol; setMessage("Waiting for confirmation..."); - await waitForTxn(txId); setVersion(version + 1); // refresh } toast( @@ -748,7 +883,7 @@ const SwapForm: React.FC = (props) => {
); } catch (e) { - console.log(e); + toast.error(e.message); } finally { setLoading(false); } @@ -793,7 +928,7 @@ const SwapForm: React.FC = (props) => { const w = String(token.id); const balanceBn = bigNumberify(balances[w]); const allowanceBn = bigNumberify(allowance); - if (v.gt(allowanceBn)) + if (true || v.gt(allowanceBn)) return (
Insufficient allowance @@ -1033,6 +1168,17 @@ const SwapForm: React.FC = (props) => { error={error} helperText={helperText} /> +
+ +
+ + + + ); +}; + +export default TokenDisplay; diff --git a/app/app-sandbox/src/components/TokenSelect/index.jsx b/app/app-sandbox/src/components/TokenSelect/index.jsx new file mode 100644 index 0000000..e431aa9 --- /dev/null +++ b/app/app-sandbox/src/components/TokenSelect/index.jsx @@ -0,0 +1,72 @@ +import * as React from "react"; +import { db } from "../../db.js"; +import { useLiveQuery } from "dexie-react-hooks"; +import { Autocomplete, TextField } from "@mui/material"; +import { getCurrentNode } from "../../utils/reach.js"; + +const getNentworkToken = (node) => { + switch (node) { + case "voi": + case "voi-testnet": + return { + name: "VOI", + tokenId: 0, + decimals: 6, + symbol: "VOI", + }; + case "algorand": + case "algorand-testnet": + return { + name: "Algo", + tokenId: 0, + decimals: 6, + symbol: "ALGO", + }; + case "ethereum": + case "ethereum-testnet": + return { + name: "Ether", + tokenId: 0, + decimals: 18, + symbol: "ETH", + }; + } +}; + +export default function TokenSelect({ onChange }) { + const [node] = getCurrentNode(); + const dbTokens = useLiveQuery(() => db.tokens.toArray()); + const isLoading = React.useMemo(() => !dbTokens, [dbTokens]); + const networkToken = getNentworkToken(node); + const tokens = React.useMemo(() => { + if (!dbTokens) return [networkToken]; + return [networkToken, ...dbTokens] + .map((token) => ({ + ...token, + decimals: Number(token.decimals), + totalSupply: Number(token.totalSupply), + })) + .filter((token) => token.name.indexOf("ARC200 LP") === -1) + .sort((a, b) => a.tokenId - b.tokenId); + }, [dbTokens]); + console.log(tokens); + return ( + !isLoading && ( +
+ false} + options={tokens.map((token) => ({ + label: token.name, + value: token, + }))} + renderInput={(params) => } + onChange={(e, value) => onChange(value?.value ?? null)} + /> +
+ ) + ); +} diff --git a/app/app-sandbox/src/components/TypeFilter/index.js b/app/app-sandbox/src/components/TypeFilter/index.js new file mode 100644 index 0000000..354efec --- /dev/null +++ b/app/app-sandbox/src/components/TypeFilter/index.js @@ -0,0 +1,33 @@ +import React, { useState } from "react"; +import { FormControl, InputLabel, Select, MenuItem } from "@mui/material"; + +function TypeFilter({ onChange }) { + const [selectedType, setSelectedType] = useState(""); + + const handleTypeChange = (event) => { + const type = event.target.value; + setSelectedType(type); + onChange(type); // Pass the selected type to the parent component for filtering + }; + + return ( + + + Type + + + + ); +} + +export default TypeFilter; diff --git a/app/app-sandbox/src/constants/dex.js b/app/app-sandbox/src/constants/dex.js new file mode 100644 index 0000000..cb6a3b9 --- /dev/null +++ b/app/app-sandbox/src/constants/dex.js @@ -0,0 +1 @@ +export const ctcInfoTri200 = 23223143; diff --git a/app/app-sandbox/src/constants/mp.js b/app/app-sandbox/src/constants/mp.js new file mode 100644 index 0000000..06482c2 --- /dev/null +++ b/app/app-sandbox/src/constants/mp.js @@ -0,0 +1,22 @@ +//------------------------------------------------------------------- +// mp.js +// - marketplace constants +//------------------------------------------------------------------- +export const fee = 5; +export const feeBi = 5n; +export const ctcInfoMp200 = 26944604; // mp200 active locked +export const ctcInfoMp201 = 28368532; // mp201 active locked +export const ctcInfoMp201b = 28368147; // mp201b inactive locked +export const ctcInfoMp202 = 28378489; // mp202 active +export const ctcInfoMp203 = 28380202; // mp203 dev +export const ctcInfoMp204 = 28382343; // mp204 dev +export const activeCtcInfo = ctcInfoMp202; +export const activeApps = [ + ctcInfoMp200, + ctcInfoMp201, + ctcInfoMp201b, + ctcInfoMp202, + ctcInfoMp203, + ctcInfoMp204, +]; +//------------------------------------------------------------------- \ No newline at end of file diff --git a/app/app-sandbox/src/custom.d.ts b/app/app-sandbox/src/custom.d.ts index e93b38d..367cf1b 100644 --- a/app/app-sandbox/src/custom.d.ts +++ b/app/app-sandbox/src/custom.d.ts @@ -13,3 +13,4 @@ declare module "nfdjs" { declare module "arccjs"; declare module "swap200js"; declare module "hsv2js"; +declare module "ulujs"; diff --git a/app/app-sandbox/src/db.js b/app/app-sandbox/src/db.js index 3f77bf2..c127a82 100644 --- a/app/app-sandbox/src/db.js +++ b/app/app-sandbox/src/db.js @@ -6,3 +6,56 @@ db.version(1).stores({ registerEvents: "txId, round, timestamp, poolId, tokA, tokB", // pools, tokenIds tokens: "pk, name, network, symbol, decimals, assetType, tokenId", // tokens }); + +// { +// "contractId": 26168759, +// "tokenId": 1, +// "owner": "AUSTSGSNXEGTUD3TIO7ORZADRB5BPAMHKMHQXGLWONMSJNZEP42XCHMVOI", +// "metadataURI": "https://prod.cdn.highforge.io/t/26168759/1.json#arc3", +// "metadata": "{\"name\":\"TestSkull #1\",\"description\":\"These skulls are just for testing.\",\"image\":\"https://prod.cdn.highforge.io/t/26168759/1.webp\",\"image_integrity\":\"CBl++f37p5FGhX+dhpGVhfcO3B86/DOzVybAE/iVpgU=\",\"image_mimetype\":\"image/png\",\"properties\":{\"Background\":\"Green\",\"Floor\":\"Disco\",\"Background Item\":\"Planets\",\"Base\":\"Ruby\"},\"royalties\":\"AfQnEAAAAAAFJTkaTbkNOg9zQ77o5AOIeheBh1MPC5l2c1kktyR/NQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}", +// "mint-round": 3183812 +// } + +export const nftDb = new Dexie("nftDatabase"); +nftDb.version(1).stores({ + nfts: "pk, name, network, symbol, decimals, supply, assetType, collectionId, tokenId, mintRound, metadata, metadataURI", + collections: "pk, contractId, mintRound, totalSupply", + transfers: "pk, txId, round, timestamp, collectionId, tokenId, from, to", +}); + +export const mpDb = new Dexie("mpDatabase"); +mpDb.version(1).stores({ + // ListEvent: [UInt256, Contract, UInt256, Address, Price], // ListId, CollectionId, TokenId, ListAddr, ListPrice + listings: "pk, mp, txId, round, timestamp, lId, cId, tId, lAddr, lPrc", + // BuyEvent: [UInt256, Address], // ListId BuyAddr + //sales: "pk, mp, txId, round, timestamp, lId, cId, tId, lAddr, lPrc, bAddr", + sales: "pk, mp, txId, round, timestamp, lId", + // ClaimEvent: [UInt256], // ListId + //claims: "pk, mp, txId, round, timestamp, lId, cId, tId, lAddr, lPrc, bAddr", + claims: "pk, mp, txId, round, timestamp, lId", + // DeleteListingEvent: [UInt256], // ListId + deletions: "pk, mp, txId, round, timestamp, lId", + // AuctionEvent: [UInt256, Contract, UInt256, Address, Price, UInt], // AuctionId, CollectionId, TokenId, ListAddr, ListPrice, EndTime + auctions: + "pk, mp, txId, round, timestamp, lId, cId, tId, lAddr, lPrc, endTime", + // BidEvent: [UInt256, Address, Price], // AuctionId, BidAddr, BidPrice + bids: "pk, mp, txId, round, timestamp, lId, bAddr, bPrc", + // DeleteAuctionEvent: [UInt256], // AuctionId + auctionDeletions: "pk, mp, txId, round, timestamp, lId", + // ReverseListEvent: [ + // UInt256, + // Contract, + // UInt256, + // Address, + // Price, + // Price, + // UInt, + // ], // ListId, CollectionId, TokenId, ListAddr, ListPrice, FloorPrice, EndTime + reverseListings: + "pk, mp, txId, round, timestamp, lId, cId, tId, lAddr, lPrc, fPrc, endTime", +}); + +// export const lpDb = new Dexie("lpDatabase"); +// lpDb.version(1).stores({ +// projects: "pk, lp, info", +// }); diff --git a/app/app-sandbox/src/index.js b/app/app-sandbox/src/index.js index 73cdd60..770ee7d 100644 --- a/app/app-sandbox/src/index.js +++ b/app/app-sandbox/src/index.js @@ -1,24 +1,13 @@ import React from "react"; import ReactDOM from "react-dom/client"; -import { HashRouter, Routes, Route } from "react-router-dom"; import "./index.css"; import App from "./App"; -import Token from "./pages/Token"; -import TokenAddress from "./pages/TokenAddress"; import reportWebVitals from "./reportWebVitals"; -import { ToastContainer } from "react-toastify"; -import "react-toastify/dist/ReactToastify.css"; - const root = ReactDOM.createRoot(document.getElementById("root")); root.render( - - - } /> - - - + ); diff --git a/app/app-sandbox/src/pages/DEX/index.js b/app/app-sandbox/src/pages/DEX/index.js index 8ea8cc4..d5d62d1 100644 --- a/app/app-sandbox/src/pages/DEX/index.js +++ b/app/app-sandbox/src/pages/DEX/index.js @@ -20,9 +20,9 @@ import { useNavigate, useParams } from "react-router-dom"; import { makeStdLib } from "../../utils/reach"; import { DEFAULT_NODE } from "../../config/defaultLocalStorage"; import { getAlgorandClients } from "../../utils/algorand"; +import CONTRACT from "arccjs"; import arc200 from "arc200js"; import triSchema from "../../abis/triumvirate.json"; -import CONTRACT from "arccjs"; import { db } from "../../db"; import { useLiveQuery } from "dexie-react-hooks"; import LoadingIndicator from "../../components/LoadingIndicator"; @@ -64,97 +64,6 @@ function Balances(props) { return ( <> - {!isLoading && ( - <> - {/* - - */} - {/* - - */} - {/* - */} - - )} - {props.links?.map((el) => ( - - - - - {el.name} - -
- {el.category} -
-
-
- ))} - - ); -} async function addRegisterEvent(evt) { try { @@ -227,15 +100,7 @@ function Home() { const dbPools = useLiveQuery(() => db.registerEvents.toArray()); const { activeAccount } = useWallet(); const [pools, setPools] = useState(null); - const [tokens, setTokens] = useState(null); - const [value, setValue] = React.useState(1); - const handleChange = (event, newValue) => { - setValue(newValue); - }; - useEffect(() => { - if (!activeAccount) setValue(1); - else setValue(0); - }, [activeAccount]); + // EFFECT: get all pools from db useEffect(() => { if (!dbPools) return; const lastRound = dbPools?.reduce((acc, cur) => { @@ -263,9 +128,6 @@ function Home() { setPools(newPools); }); }, [dbPools]); - const userTokens = JSON.parse( - localStorage.getItem("tokens") || `${JSON.stringify(defaultTokens)}` - ); return ( diff --git a/app/app-sandbox/src/pages/Home/index.js b/app/app-sandbox/src/pages/Home/index.js index 748e85d..83eafd8 100644 --- a/app/app-sandbox/src/pages/Home/index.js +++ b/app/app-sandbox/src/pages/Home/index.js @@ -2,7 +2,7 @@ import { Stack, Typography } from "@mui/material"; import { useWallet } from "@txnlab/use-wallet"; import React, { useEffect, useState, useMemo } from "react"; import AccountBalances from "../../components/AccountBalances"; -import Pools from "../../components/PoolList"; +import Pools, { badPools } from "../../components/PoolList"; import TokenList from "../../components/TokenList"; import Connect from "../../components/Connect"; import Box from "@mui/material/Box"; @@ -26,12 +26,15 @@ import CONTRACT from "arccjs"; import { db } from "../../db"; import { useLiveQuery } from "dexie-react-hooks"; import LoadingIndicator from "../../components/LoadingIndicator"; +import TokenDisplay from "../../components/TokenDisplay"; +import BuyModal from "../../components/BuyModal"; const stdlib = makeStdLib(); const [node] = (localStorage.getItem("node") || DEFAULT_NODE).split(":"); function Balances(props) { + const { activeAccount } = useWallet(); const dbTokens = useLiveQuery(() => db.tokens.toArray()); const navigate = useNavigate(); const params = useParams(); @@ -39,7 +42,6 @@ function Balances(props) { const [tokens, setTokens] = React.useState(null); const [, setAppId] = React.useState(0); const [manage, setManage] = React.useState(false); - const { activeAccount } = useWallet(); useEffect(() => { if (!dbTokens) return; const { algodClient, indexerClient } = getAlgorandClients(); @@ -156,14 +158,25 @@ function Balances(props) { )} - + + {/*( + + + + + + )*/} + + + + @@ -260,7 +273,7 @@ function Home() { })); rEvts.map(addRegisterEvent); const newPools = [...dbPools, ...rEvts]; - setPools(newPools); + setPools(newPools.filter((p) => !badPools.includes(p.poolId))); }); }, [dbPools]); const userTokens = JSON.parse( diff --git a/app/app-sandbox/src/pages/NFTAuctions/index.js b/app/app-sandbox/src/pages/NFTAuctions/index.js new file mode 100644 index 0000000..fb964d1 --- /dev/null +++ b/app/app-sandbox/src/pages/NFTAuctions/index.js @@ -0,0 +1,196 @@ +import React, { useEffect, lazy, Suspense, useContext } from "react"; +import { getCurrentNode } from "../../utils/reach"; +import CircularProgress from "@mui/material/CircularProgress"; +import { Button, Grid, Container, Paper, Chip } from "@mui/material"; +import { getAlgorandClients } from "../../utils/algorand"; +import { useNavigate } from "react-router-dom"; +import { db } from "../../db"; +import { arc200 } from "ulujs"; +import { MarketplaceContext } from "../../store/MarketplaceContext"; +import moment from "moment"; + +/* + * prepareString + * - prepare string (strip trailing null bytes) + * @param str: string to prepare + * @returns: prepared string + */ +const prepareString = (str) => { + const index = str.indexOf("\x00"); + if (index > 0) { + return str.slice(0, str.indexOf("\x00")); + } else { + return str; + } +}; + +const { algodClient, indexerClient } = getAlgorandClients("voi-testnet"); + +const LazyNFTImage = lazy(() => import("../../components/NFTImage")); + +function NFTSales() { + const { isLoading, tokens, forSale, nfts, listings, pools, liveAuctions } = + useContext(MarketplaceContext); + + const [node] = getCurrentNode(); + const navigate = useNavigate(); + + // EFFECT: update missing tokens + useEffect(() => { + if (isLoading) return; + (async () => { + const ptoks = new Set(); + liveAuctions.forEach((listing) => { + const { lPrc } = listing; + const [pType, ...prc] = lPrc; + switch (pType) { + case "00": { + return; + } + case "01": { + const [tId] = prc; + const tid = Number("0x" + tId); + ptoks.add(tid); + } + } + }); + for (const tok of Array.from(ptoks).filter( + (el) => !tokens.map((el) => el.tokenId).includes(el) + )) { + const ci = new arc200(tok, algodClient, indexerClient); + const res = await ci.getMetadata(); + if (!res.success) continue; + const metadata = res.returnValue; + const pk = `${node}:${tok}`; + const tm = { + ...res.returnValue, + pk, + tokenId: tok, + network: node, + decimals: Number(metadata.decimals), + name: prepareString(metadata.name), + symbol: prepareString(metadata.symbol), + }; + await db.tokens.put(tm); + } + })(); + }, [isLoading, forSale, tokens]); + + // get current block time + const [currentRound, setCurrentRound] = React.useState(0); + useEffect(() => { + algodClient + .status() + .do() + .then((s) => { + setCurrentRound(s["last-round"]); + }); + }, []); + + return !isLoading && currentRound ? ( + + + +   + + + {liveAuctions.length > 0 ? ( + + {liveAuctions.map( + ({ + mp, + txId, + round, + timestamp, + lId, + cId, + tId, + lAddr, + bAddr, + lPrc, + endTime, + }) => ( + + + }> + + +
+ MarketplaceId: {mp} +
+ ListId: {lId.toString()} +
+ CollectionId:{" "} + + {cId.toString()} + +
+ TokenId:{" "} + + {tId.toString()} + +
+ SellerAddr: {lAddr.slice(0, 4)}...{lAddr.slice(-4)} +
+ +
+
+ ) + )} +
+ ) : ( +
No NFTs for sale
+ )} +
+
+
+ ) : ( + "Loading..." + ); +} + +export default NFTSales; diff --git a/app/app-sandbox/src/pages/NFTClaim/index.js b/app/app-sandbox/src/pages/NFTClaim/index.js new file mode 100644 index 0000000..cea7856 --- /dev/null +++ b/app/app-sandbox/src/pages/NFTClaim/index.js @@ -0,0 +1,292 @@ +import React, { useEffect, useMemo, lazy, Suspense, useContext } from "react"; +import { useWallet, PROVIDER_ID } from "@txnlab/use-wallet"; +import { getCurrentNode, getGenesisHash } from "../../utils/reach"; +import CircularProgress from "@mui/material/CircularProgress"; +import { + Stack, + Button, + Grid, + Typography, + Container, + Tabs, + Tab, + createTheme, + Paper, + Tooltip, +} from "@mui/material"; +import { makeStdLib } from "../../utils/reach"; +import { makeStyles } from "@mui/styles"; +import algosdk, { waitForConfirmation } from "algosdk"; +import CONTRACT from "arccjs"; +import { getAlgorandClients } from "../../utils/algorand"; +import { useParams } from "react-router-dom"; +import { mp200Schema, mp201Schema, arc200Schema } from "../../abis"; +import NFTPortfolio from "../NFTPortfolio"; +import NFTFeed from "../NFTFeed"; +import NFTProjects from "../NFTProjects"; +import NFTSales from "../NFTSales"; +import BuyModal from "../../components/BuyModal"; +import { bigNumberToBigInt, bigNumberify } from "../../common/utils/bn"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; +import { decodeRoyalties } from "../../utils/hf"; +import { + computeExtraPayment, + computeNFTSalePrice, + decodeDecimals, + decodePrice, +} from "../../utils/mp"; +import { nftDb, db, mpDb } from "../../db"; +import { useLiveQuery } from "dexie-react-hooks"; +import { ctcInfoMp200, ctcInfoMp201, fee, feeBi } from "../../constants/mp"; +import Confetti from "react-confetti"; +import { useWindowSize } from "usehooks-ts"; +import { toast } from "react-toastify"; +import { MarketplaceContext } from "../../store/MarketplaceContext"; + +const stdlib = makeStdLib(); + +const theme = createTheme({ + palette: { + divider: "#C7C7C7", // Define the divider color + }, +}); + +const useStyles = makeStyles((theme) => ({ + textFieldRoot: { + paddingRight: 0, // Remove right padding for TextField + }, + selectRoot: { + border: 0, + borderBottom: `1px solid transparent`, + //backgroundColor: "transparent", + borderRadius: "0", + /* + padding: "8px 0", // Adjust padding as needed + "& .MuiSelect-select": { + paddingRight: 0, + }, + */ + }, +})); + +/* + * prepareString + * - prepare string (strip trailing null bytes) + * @param str: string to prepare + * @returns: prepared string + */ +const prepareString = (str) => { + const index = str.indexOf("\x00"); + if (index > 0) { + return str.slice(0, str.indexOf("\x00")); + } else { + return str; + } +}; + +function TabPanel(props) { + const { children, value, index, ...other } = props; + return value == index && children; +} + +const { algodClient, indexerClient } = getAlgorandClients("voi-testnet"); + +const LazyNFTImage = lazy(() => import("../../components/NFTImage")); + +const showTooltip = (lPrc) => { + const [pType, ...prc] = lPrc; + switch (pType) { + case "00": + return false; + default: + case "01": + return true; + } +}; + +function NFTMarketplace() { + const { + isLoading, + nfts, + tokens, + listins, + sales, + deletions, + claims, + properties, + unclaimed, + } = useContext(MarketplaceContext); + + console.log({ unclaimed }); + + const { width, height } = useWindowSize(); + const [showConfetti, setShowConfetti] = React.useState(false); + const [node] = getCurrentNode(); + const { activeAccount, signTransactions } = useWallet(); + const [value, setValue] = React.useState(0); + const [allNfts, setAllNfts] = React.useState([]); + + const [buyModalOpen, setBuyModalOpen] = React.useState(false); + const [buyModalListing, setBuyModalListing] = React.useState(null); + + // EEFFECT: when showing conffeti, hide after 5 seconds + useEffect(() => { + if (showConfetti) { + const timeout = setTimeout(() => { + window.location.reload(); + }, 5000); + return () => clearTimeout(timeout); + } + }, [showConfetti]); + + const handleChange = (event, newValue) => { + setValue(newValue); + }; + + const signTransaction = React.useCallback( + async (txns) => { + if (!activeAccount) return; + if ( + activeAccount.providerId === PROVIDER_ID.CUSTOM && + activeAccount.name === "kibisis" + ) { + const algorand = window.algorand; + if (!algorand) { + throw new Error("no wallets are installed!"); + } + const wallets = algorand.getWallets(); + const wallet = await algorand.enable({ + genesisHash: getGenesisHash(node), + }); + + const result = await window.algorand.signTxns({ + txns: txns.map((el) => { + return { + txn: el, + }; + }), + }); + let signedTransactionBytes; + signedTransactionBytes = result.stxns.map( + (stxn) => new Uint8Array(Buffer.from(stxn, "base64")) + ); + const res = await algodClient + .sendRawTransaction(signedTransactionBytes) + .do(); + await waitForConfirmation(algodClient, res.txId, 4); + } else if ( + [PROVIDER_ID.KIBISIS, PROVIDER_ID.DEFLY, PROVIDER_ID.LUTE].includes( + activeAccount.providerId + ) + ) { + const stxns = await signTransactions( + txns.map((el) => new Uint8Array(Buffer.from(el, "base64"))) + ); + const res = await algodClient.sendRawTransaction(stxns).do(); + await waitForConfirmation(algodClient, res.txId, 4); + } else { + throw new Error("Unsupported wallet"); + } + }, + [activeAccount] + ); + const handleClaim = React.useCallback( + async (mp, lId, cId, tId, lAddr, lPrc) => { + const [pType, ...prc] = lPrc; + console.log({ lId, cId, tId, lAddr, lPrc }); + switch (pType) { + case "00": { + return; // not applicable + } + case "01": { + // buySC + const [tId, tPrc] = prc; + const ci = new CONTRACT( + ctcInfoMp201, + algodClient, + indexerClient, + mp201Schema, + { + addr: activeAccount.address, + } + ); + // claimSC + ci.setFee(3000); + ci.setAccounts([ + "G3MSA75OZEJTCCENOJDLDJK7UD7E2K5DNC7FVHCNOV7E3I4DTXTOWDUIFQ", + ]); + const claimSCR = await ci.claimSC(lId); + if (!claimSCR.success) return; + toast.info("Signing transaction to claim NFT...", { + autoClose: true, + delay: 1000, + }); + await signTransaction(claimSCR.txns); + setShowConfetti(true); + toast(
NFT claim successful!
); + } + } + }, + [activeAccount] + ); + + return ( + <> + {showConfetti && } + + + {unclaimed.map(({ mp, lId, cId, tId, lAddr, lPrc, bAddr }) => ( + + + }> + + +
+ MarketplaceId: {mp} +
+ ListId: {lId.toString()} +
+ CollectionId:{" "} + + {cId.toString()} + +
+ TokenId:{" "} + + {tId.toString()} + +
+ +
+
+ ))} +
+
+ + ); +} + +export default NFTMarketplace; diff --git a/app/app-sandbox/src/pages/NFTCollection/index.js b/app/app-sandbox/src/pages/NFTCollection/index.js index 4c351af..752e048 100644 --- a/app/app-sandbox/src/pages/NFTCollection/index.js +++ b/app/app-sandbox/src/pages/NFTCollection/index.js @@ -25,6 +25,7 @@ import algosdk from "algosdk"; import CONTRACT from "arccjs"; import { getAlgorandClients } from "../../utils/algorand"; import { useParams } from "react-router-dom"; +import Leaderboard from "../../components/Leaderboard"; const stdlib = makeStdLib(); @@ -191,21 +192,6 @@ const useStyles = makeStyles((theme) => ({ }, })); -/* - * prepareString - * - prepare string (strip trailing null bytes) - * @param str: string to prepare - * @returns: prepared string - */ -const prepareString = (str) => { - const index = str.indexOf("\x00"); - if (index > 0) { - return str.slice(0, str.indexOf("\x00")); - } else { - return str; - } -}; - function TabPanel(props) { const { children, value, index, ...other } = props; return value == index && children; @@ -228,8 +214,9 @@ function NFTGallery() { const [value, setValue] = React.useState(1); const [nfts, setNfts] = React.useState([]); const [owners, setOwners] = React.useState(null); - const [tokens, setTokens] = React.useState(null); + const [ownerOf, setOwnerOf] = React.useState({}); const [ctcInfo, setCtcInfo] = React.useState(null); + const [ranks, setRanks] = React.useState(null); const handleChange = (event, newValue) => { setValue(newValue); }; @@ -239,52 +226,11 @@ function NFTGallery() { setCtcInfo(ctcInfo); }, []); - const signTransaction = React.useCallback( - async (txns) => { - if (!activeAccount) return; - if ( - activeAccount.providerId === PROVIDER_ID.CUSTOM && - activeAccount.name === "kibisis" - ) { - const algorand = window.algorand; - if (!algorand) { - throw new Error("no wallets are installed!"); - } - const wallets = algorand.getWallets(); - const wallet = await algorand.enable({ - genesisHash: getGenesisHash(node), - }); - const { algodClient, indexerClient } = getAlgorandClients(); - const result = await window.algorand.signTxns({ - txns: txns.map((el) => { - return { - txn: el, - }; - }), - }); - let signedTransactionBytes; - signedTransactionBytes = result.stxns.map( - (stxn) => new Uint8Array(Buffer.from(stxn, "base64")) - ); - const res = await algodClient - .sendRawTransaction(signedTransactionBytes) - .do(); - return res.txId; - } else { - const wtxns = txns.map((el) => { - return { - txn: el, - }; - }); - await stdlib.signSendAndConfirm({ addr: activeAccount.address }, wtxns); - } - }, - [activeAccount] - ); useEffect(() => { if (!activeAccount || !ctcInfo) setValue(0); else setValue(0); }, [activeAccount]); + useEffect(() => { if (!ctcInfo) return; (async () => { @@ -325,9 +271,24 @@ function NFTGallery() { }); setNfts(nfts); setOwners(owners); + const ownerOf = {}; + const rank = {}; + for (let [key, value] of owners) { + value.forEach((el) => { + ownerOf[el] = key; + if (!rank[key]) rank[key] = 1; + else rank[key]++; + }); + } + setRanks(Object.entries(rank).sort((a, b) => b[1] - a[1])); + setOwnerOf(ownerOf); })(); }, [ctcInfo]); - console.log({ ctcInfo, nfts, owners }); + console.log({ + ctcInfo, + nfts, + owners, + }); return !ctcInfo ? ( "Collection not found" ) : ( @@ -341,10 +302,9 @@ function NFTGallery() { allowScrollButtonsMobile aria-label="scrollable force tabs example" > - {/* - - - */} + + +

Collection AppId: {ctcInfo} @@ -352,29 +312,82 @@ function NFTGallery() { Collection Address:{" "} {algosdk.getApplicationAddress(ctcInfo).slice(0, 10)}...

-

- Your Collection: -

- {owners && owners.has(activeAccount?.address) && ( - - - {owners.get(activeAccount?.address).map((el) => ( - - }> - - - + + +

+ Collection: +

+ {nfts.length > 0 ? ( + + + {nfts.map((el) => ( + + }> + + +
TokenId:{" "} {el.toString()} -
- {/* +
+ Holder: + {((addr) => `${addr.slice(0, 4)}...${addr.slice(-4)}`)( + ownerOf[el] + )} +
+ ))} +
+
+ ) : ( + + )} + + + {ranks && ( + <> +

+ Leaderboard: +

+ + + )} +
+ + +

+ Your Collection: +

+ {owners && owners.has(activeAccount?.address) && ( + + + {owners.get(activeAccount?.address).map((el) => ( + + }> + + + + TokenId:{" "} + + {el.toString()} + + + {/* - )} - - - - ))} - - ) : ( - - )} - -
- - - {forSale.length > 0 ? ( - - {forSale.map(([lId, cId, tId, lAddr, lPrc]) => ( - - - }> - - -
- CollectionId:{" "} - - {cId.toString()} - -
- TokenId:{" "} - - {tId.toString()} - -
- SellerAddr: {lAddr.slice(0, 4)}...{lAddr.slice(-4)} -
- {sold.indexOf( - ([_, scId, stId, slAddr, ...rst]) => - cId === scId && tId === stId && lAddr === slAddr - ) === -1 && ( - - )} -
-
- ))} -
- ) : ( -
No NFTs for sale
- )} -
-
- - - {sold.length > 0 ? ( - - {sold.map(([lId, cId, tId, lAddr, lPrc, bAddr]) => ( - - - }> - - -
- CollectionId:{" "} - - {cId.toString()} - -
- TokenId:{" "} - - {tId.toString()} - -
- SellerAddr: {lAddr.slice(0, 4)}...{lAddr.slice(-4)} -
- ListPrice: {( - (Number(lPrc) * 1) / - 1e6 - ).toLocaleString()}{" "} - VOI -
- BuyerAddr: {bAddr.slice(0, 4)}...{bAddr.slice(-4)} -
-
-
- ))} -
- ) : ( -
No sold NFTs
- )} -
-
- - -
- {!loadingProjects ? ( - - ) : ( - - )} -
- {projects.length > 0 ? ( - - {projects.map((p) => ( - - }> - - -
- CollectionId:{" "} - - {p.applicationID} - -
- Title: {p.title} -
- Launchpad:{" "} - - High Forge - -
-
- ))} -
- ) : ( -
No NFT Projects
- )} -
-
- - - + {[ + , + , + , + , + , + ].map((child, i) => ( + + {child} + + ))} ); } diff --git a/app/app-sandbox/src/pages/NFTPortfolio/index.js b/app/app-sandbox/src/pages/NFTPortfolio/index.js index 6e5da11..db78fdc 100644 --- a/app/app-sandbox/src/pages/NFTPortfolio/index.js +++ b/app/app-sandbox/src/pages/NFTPortfolio/index.js @@ -1,178 +1,43 @@ -import React, { useEffect, useMemo, lazy, Suspense } from "react"; -import { useWallet, PROVIDER_ID } from "@txnlab/use-wallet"; -import { getCurrentNode, getGenesisHash } from "../../utils/reach"; -import { - TextField, - Button, - Grid, - Typography, - Container, - Tabs, - Tab, - InputAdornment, - Select, - MenuItem, - createTheme, - ThemeProvider, - Skeleton, - Paper, - Box, - ButtonGroup, -} from "@mui/material"; -import { makeStdLib } from "../../utils/reach"; -import { makeStyles } from "@mui/styles"; -import algosdk from "algosdk"; -import CONTRACT from "arccjs"; -import { getAlgorandClients } from "../../utils/algorand"; -import { useParams } from "react-router-dom"; -import { arc72Schema } from "../../abis"; - -const stdlib = makeStdLib(); - -const theme = createTheme({ - palette: { - divider: "#C7C7C7", // Define the divider color - }, -}); - -const useStyles = makeStyles((theme) => ({ - textFieldRoot: { - paddingRight: 0, // Remove right padding for TextField - }, - selectRoot: { - border: 0, - borderBottom: `1px solid transparent`, - //backgroundColor: "transparent", - borderRadius: "0", - /* - padding: "8px 0", // Adjust padding as needed - "& .MuiSelect-select": { - paddingRight: 0, - }, - */ - }, -})); - -/* - * prepareString - * - prepare string (strip trailing null bytes) - * @param str: string to prepare - * @returns: prepared string - */ -const prepareString = (str) => { - const index = str.indexOf("\x00"); - if (index > 0) { - return str.slice(0, str.indexOf("\x00")); - } else { - return str; - } -}; - -function TabPanel(props) { - const { children, value, index, ...other } = props; - return value == index && children; -} - -const ipfsToGateway = (url) => { - if (url.indexOf("ipfs://") !== 0) return url; - const gatewayURL = `https://ipfs.io/ipfs/${url.slice(7)}`; - return gatewayURL; -}; - -const { algodClient, indexerClient } = getAlgorandClients("voi-testnet"); - -const LazyNFTImage = React.lazy(() => import("../../components/NFTImage")); +import React, { useEffect, useContext } from "react"; +import { useWallet } from "@txnlab/use-wallet"; +import { Grid, Container } from "@mui/material"; +import { MarketplaceContext } from "../../store/MarketplaceContext"; +import NFTCard from "../../components/NFTCard"; function NFTPortfolio() { - const projects = JSON.parse(localStorage.getItem("hg-projects") || "[]"); - const ids = projects.map((el) => Number(el.applicationID)); + const { isLoading, nfts, forSale } = useContext(MarketplaceContext); + console.log({ forSale }); const { activeAccount } = useWallet(); - const [nfts, setNfts] = React.useState(null); + const [portfolio, setPortfolio] = React.useState([]); + + // EFFECT: get account holdings from db useEffect(() => { - if (!activeAccount) return; - (async () => { - const portfolio = []; - for (let i = 0; i < ids.length; i++) { - const ctcInfo = ids[i]; - const ci = new CONTRACT( - ctcInfo, - algodClient, - indexerClient, - arc72Schema - ); - const arc72_balanceOfR = await ci.arc72_balanceOf( - activeAccount?.address - ); - console.log({ arc72_balanceOfR }); - if (!arc72_balanceOfR.success) continue; - const arc72_balanceOf = Number(arc72_balanceOfR.returnValue); - console.log({ arc72_balanceOf }); - if (arc72_balanceOf === 0) continue; - const transfers = await ci.arc72_Transfer(); - console.log({ transfers }); - const owners = new Map(); - const tokens = new Map(); - transfers.forEach((el) => { - const [txId, round, ts, from, to, tokenId] = el; - if (!tokens.has(tokenId)) { - tokens.set(tokenId, to); - if (!owners.has(to)) { - owners.set(to, [tokenId]); - } else { - owners.set(to, [...owners.get(to), tokenId]); - } - } else { - const prevOwner = tokens.get(tokenId); - if (!owners.has(prevOwner)) { - owners.set(prevOwner, []); - } - const prevOwnerTokens = owners.get(prevOwner); - const index = prevOwnerTokens.indexOf(tokenId); - if (index !== -1) { - prevOwnerTokens.splice(index, 1); - } - tokens.set(tokenId, to); - if (!owners.has(to)) { - owners.set(to, [tokenId]); - } else { - owners.set(to, [...owners.get(to), tokenId]); - } - } - }); - const nfts = []; - for (const [key, value] of tokens.entries()) { - if (value === activeAccount?.address) { - console.log([key, value, activeAccount?.address]); - nfts.push(key); - } - } - portfolio.push(nfts.map((el) => [ctcInfo, Number(el)])); - } - setNfts(portfolio.flat()); - })(); - }, [activeAccount]); - console.log({ nfts }); - return nfts ? ( + if (!activeAccount || isLoading) return; + const partfolio = []; + nfts + .filter(({ owner }) => owner === activeAccount?.address) + .forEach(({ collectionId, tokenId }) => { + partfolio.push({ cId: Number(collectionId), tId: Number(tokenId) }); + }); + setPortfolio(partfolio.flat()); + }, [activeAccount, isLoading, nfts]); + + return !isLoading ? ( <> -

- Your Collection: -

- {nfts.map(([cId, tId], i) => ( - - }> - - - - TokenId:{" "} - {tId} - + +   + + + + {portfolio.map(({ cId, tId }, i) => ( + + + + ))} - ))} + diff --git a/app/app-sandbox/src/pages/NFTProjects/index.js b/app/app-sandbox/src/pages/NFTProjects/index.js new file mode 100644 index 0000000..756ca18 --- /dev/null +++ b/app/app-sandbox/src/pages/NFTProjects/index.js @@ -0,0 +1,97 @@ +import React, { useEffect, lazy, Suspense } from "react"; +import CircularProgress from "@mui/material/CircularProgress"; +import { Grid, Typography, Container, createTheme, Paper } from "@mui/material"; + +const LazyNFTImage = lazy(() => import("../../components/NFTImage")); + +function NFTProjects() { + const [projects, setProjects] = React.useState([]); + const updateProjects = React.useCallback(async (cache = true) => { + const exclude = []; + const key = `hg-projects`; + const storedProjects = localStorage.getItem(key); + if (cache && storedProjects) { + setProjects(JSON.parse(storedProjects)); + return; + } + await fetch(`https://test-voi.api.highforge.io/projects`) + .then((res) => res.json()) + .then((el) => el.results) + .then((res) => { + setProjects(res); + localStorage.setItem( + key, + JSON.stringify(res.filter((p) => !exclude.includes(p.applicationID))) + ); + }); + }, []); + useEffect(() => { + updateProjects(false); + }, []); + return ( + + + +   + + + {projects.length > 0 ? ( + + {projects.map((p) => ( + + +
+ }> + + +
+ + {p.title} + +
+ CollectionId:{" "} + + {p.applicationID} + +
+ Launchpad:{" "} + + High Forge + +
+
+
+ ))} +
+ ) : ( +
No NFT Projects
+ )} +
+
+
+ ); +} + +export default NFTProjects; diff --git a/app/app-sandbox/src/pages/NFTReverseAuctions/index.js b/app/app-sandbox/src/pages/NFTReverseAuctions/index.js new file mode 100644 index 0000000..085dec9 --- /dev/null +++ b/app/app-sandbox/src/pages/NFTReverseAuctions/index.js @@ -0,0 +1,234 @@ +import React, { + useEffect, + lazy, + Suspense, + useContext, + useCallback, +} from "react"; +import { getCurrentNode } from "../../utils/reach"; +import CircularProgress from "@mui/material/CircularProgress"; +import { Button, Grid, Container, Paper, Chip } from "@mui/material"; +import { getAlgorandClients } from "../../utils/algorand"; +import { useNavigate } from "react-router-dom"; +import { db } from "../../db"; +import { arc200 } from "ulujs"; +import { MarketplaceContext } from "../../store/MarketplaceContext"; +import moment from "moment"; +import { toast } from "react-toastify"; +import CONTRACT from "arccjs"; +import { mp202Schema } from "../../abis"; +import { useWallet } from "@txnlab/use-wallet"; + +/* + * prepareString + * - prepare string (strip trailing null bytes) + * @param str: string to prepare + * @returns: prepared string + */ +const prepareString = (str) => { + const index = str.indexOf("\x00"); + if (index > 0) { + return str.slice(0, str.indexOf("\x00")); + } else { + return str; + } +}; + +const { algodClient, indexerClient } = getAlgorandClients("voi-testnet"); + +const LazyNFTImage = lazy(() => import("../../components/NFTImage")); + +// ListId, CollectionId, TokenId, ListAddr, ListPrice, FloorPrice, EndTime +const NFTReverseAuctionCard = ({ + mp, + txId, + round, + timestamp, + lId, + cId, + tId, + lAddr, + lPrc, + fPrc, + endTime, +}) => { + const { activeAccount } = useWallet(); + const [price, setPrice] = React.useState(null); + const updatePrice = useCallback(() => { + if (!activeAccount) return; + const ciMp = new CONTRACT(mp, algodClient, indexerClient, mp202Schema, { + addr: activeAccount.address, + }); + const res = ciMp + .v_reverseBuySC(lId) + .then( + ({ returnValue: r }) => + Number("0x" + Buffer.from(r[1].slice(8)).toString("hex")) / 10 ** 6 + ) + .then(setPrice) + .catch(() => {}); + }, [activeAccount]); + useEffect(() => { + updatePrice(); + }, [activeAccount]); + useEffect(() => { + if (!activeAccount) return; + const interval = setInterval(() => updatePrice(), 3_000); + return () => clearInterval(interval); + }, [activeAccount]); + return ( + + }> + + +
+ MarketplaceId: {mp} +
+ ListId: {lId.toString()} +
+ CollectionId:{" "} + {cId.toString()} +
+ TokenId:{" "} + + {tId.toString()} + +
+ SellerAddr: {lAddr.slice(0, 4)}...{lAddr.slice(-4)} +
+ +
+ ); +}; + +function NFTSales() { + const { + isLoading, + tokens, + forSale, + nfts, + listings, + pools, + liveAuctions, + reverseAuctions, + } = useContext(MarketplaceContext); + + const [node] = getCurrentNode(); + const navigate = useNavigate(); + + // EFFECT: update missing tokens + useEffect(() => { + if (isLoading) return; + (async () => { + const ptoks = new Set(); + reverseAuctions.forEach((listing) => { + const { lPrc } = listing; + const [pType, ...prc] = lPrc; + switch (pType) { + case "00": { + return; + } + case "01": { + const [tId] = prc; + const tid = Number("0x" + tId); + ptoks.add(tid); + } + } + }); + for (const tok of Array.from(ptoks).filter( + (el) => !tokens.map((el) => el.tokenId).includes(el) + )) { + const ci = new arc200(tok, algodClient, indexerClient); + const res = await ci.getMetadata(); + if (!res.success) continue; + const metadata = res.returnValue; + const pk = `${node}:${tok}`; + const tm = { + ...res.returnValue, + pk, + tokenId: tok, + network: node, + decimals: Number(metadata.decimals), + name: prepareString(metadata.name), + symbol: prepareString(metadata.symbol), + }; + await db.tokens.put(tm); + } + })(); + }, [isLoading, forSale, tokens]); + + // get current block time + const [currentRound, setCurrentRound] = React.useState(0); + useEffect(() => { + algodClient + .status() + .do() + .then((s) => { + setCurrentRound(s["last-round"]); + }); + }, []); + + return !isLoading && currentRound ? ( + + + +   + + + {reverseAuctions.length > 0 ? ( + + {reverseAuctions.slice(1).map((props) => { + console.log({ props }); + return ( + + + + ); + })} + + ) : ( +
No NFTs for sale
+ )} +
+
+
+ ) : ( + "Loading..." + ); +} + +export default NFTSales; diff --git a/app/app-sandbox/src/pages/NFTSales/index.js b/app/app-sandbox/src/pages/NFTSales/index.js new file mode 100644 index 0000000..edb4b26 --- /dev/null +++ b/app/app-sandbox/src/pages/NFTSales/index.js @@ -0,0 +1,343 @@ +import React, { useEffect, useMemo, lazy, Suspense, useContext } from "react"; +import { useWallet, PROVIDER_ID } from "@txnlab/use-wallet"; +import { getCurrentNode, getGenesisHash } from "../../utils/reach"; +import CircularProgress from "@mui/material/CircularProgress"; +import { + Button, + Grid, + Container, + createTheme, + Paper, + Tooltip, +} from "@mui/material"; +import { makeStdLib } from "../../utils/reach"; +import { makeStyles } from "@mui/styles"; +import { getAlgorandClients } from "../../utils/algorand"; +import { useParams } from "react-router-dom"; +import { mp200Schema, mp201Schema, arc200Schema } from "../../abis"; +import BuyModal from "../../components/BuyModal"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; +import { decodeRoyalties } from "../../utils/hf"; +import { nftDb, db, mpDb } from "../../db"; +import { useLiveQuery } from "dexie-react-hooks"; +import { ctcInfoMp200, ctcInfoMp201, fee, feeBi } from "../../constants/mp"; +import Confetti from "react-confetti"; +import { useWindowSize } from "usehooks-ts"; +import { toast } from "react-toastify"; +import { arc200, CONTRACT } from "ulujs"; +import { MarketplaceContext } from "../../store/MarketplaceContext"; + +const stdlib = makeStdLib(); + +const theme = createTheme({ + palette: { + divider: "#C7C7C7", // Define the divider color + }, +}); + +const useStyles = makeStyles((theme) => ({ + textFieldRoot: { + paddingRight: 0, // Remove right padding for TextField + }, + selectRoot: { + border: 0, + borderBottom: `1px solid transparent`, + //backgroundColor: "transparent", + borderRadius: "0", + /* + padding: "8px 0", // Adjust padding as needed + "& .MuiSelect-select": { + paddingRight: 0, + }, + */ + }, +})); + +/* + * prepareString + * - prepare string (strip trailing null bytes) + * @param str: string to prepare + * @returns: prepared string + */ +const prepareString = (str) => { + const index = str.indexOf("\x00"); + if (index > 0) { + return str.slice(0, str.indexOf("\x00")); + } else { + return str; + } +}; + +function TabPanel(props) { + const { children, value, index, ...other } = props; + return value == index && children; +} + +const { algodClient, indexerClient } = getAlgorandClients("voi-testnet"); + +const LazyNFTImage = lazy(() => import("../../components/NFTImage")); + +const showTooltip = (lPrc) => { + const [pType] = lPrc; + switch (pType) { + case "00": + return false; + default: + case "01": + return true; + } +}; + +function NFTSales() { + const { isLoading, tokens, forSale, nfts, listings, pools } = + useContext(MarketplaceContext); + + // EFFECT: update missing tokens + useEffect(() => { + if (isLoading) return; + (async () => { + const ptoks = new Set(); + forSale.forEach((listing) => { + const { lPrc } = listing; + const [pType, ...prc] = lPrc; + switch (pType) { + case "00": { + return; + } + case "01": { + const [tId] = prc; + const tid = Number("0x" + tId); + ptoks.add(tid); + } + } + }); + for (const tok of Array.from(ptoks).filter( + (el) => !tokens.map((el) => el.tokenId).includes(el) + )) { + const ci = new arc200(tok, algodClient, indexerClient); + const res = await ci.getMetadata(); + if (!res.success) continue; + const metadata = res.returnValue; + const pk = `${node}:${tok}`; + const tm = { + ...res.returnValue, + pk, + tokenId: tok, + network: node, + decimals: Number(metadata.decimals), + name: prepareString(metadata.name), + symbol: prepareString(metadata.symbol), + }; + await db.tokens.put(tm); + } + })(); + }, [isLoading, forSale, tokens]); + + // EEFFECT: when showing conffeti, hide after 5 seconds + const [showConfetti, setShowConfetti] = React.useState(false); + useEffect(() => { + if (showConfetti) { + const timeout = setTimeout(() => { + window.location.reload(); + }, 5000); + return () => clearTimeout(timeout); + } + }, [showConfetti]); + + const { width, height } = useWindowSize(); + const [node] = getCurrentNode(); + const { activeAccount } = useWallet(); + const [allNfts, setAllNfts] = React.useState([]); + const [sold, setSold] = React.useState([]); + const [buyModalOpen, setBuyModalOpen] = React.useState(false); + const [buyModalListing, setBuyModalListing] = React.useState(null); + + const nftRoyalties = React.useCallback( + (cId, tId) => { + const token = nfts.find( + (el) => el.collectionId === Number(cId) && el.tokenId === Number(tId) + ); + return decodeRoyalties(token.metadata?.royalties); + }, + [allNfts, nfts] + ); + + const volume = useMemo(() => { + return ( + sold + .filter(([mp, txId, round, ts, lId, cId, tId, lAddr, lPrc, bAddr]) => { + const [pType, ...prc] = lPrc; + switch (pType) { + case "00": + return true; + case "01": + return false; + } + }) + .reduce( + (acc, [mp, txId, round, ts, lId, cId, tId, lAddr, lPrc, bAddr]) => { + const [pType, ...prc] = lPrc; + switch (pType) { + case "00": + return acc + Number(prc); + case "01": + return 0; + } + }, + 0 + ) / 1e6 + ); + }, [sold]); + const sales = useMemo(() => { + return sold.length; + }, [sold]); + const avgSalePrice = useMemo(() => { + return volume / sales; + }, [volume, sales]); + + // EFFECT: store mp stats + useEffect(() => { + if (!volume || !sales || !avgSalePrice) return; + localStorage.setItem( + "mp-stats", + JSON.stringify({ volume, sales, avgSalePrice }) + ); + }, [volume, sales, avgSalePrice]); + + return !isLoading ? ( + <> + {showConfetti && } + + + +   + + + {forSale.length > 0 ? ( + + {forSale.map( + ({ + mp, + txId, + round, + timestamp, + lId, + cId, + tId, + lAddr, + lPrc, + payment, + extraPayment, + decimals, + symbol, + royalties, + priceDisplay, + }) => ( + + + }> + + +
+ MarketplaceId: {mp} +
+ ListId: {lId.toString()} +
+ CollectionId:{" "} + + {cId.toString()} + +
+ TokenId:{" "} + + {tId.toString()} + +
+ SellerAddr: {lAddr.slice(0, 4)}...{lAddr.slice(-4)} +
+ +
+
+ ) + )} +
+ ) : ( +
No NFTs for sale
+ )} +
+
+
+ {buyModalListing && ( + + )} + + ) : ( + "Loading..." + ); +} + +export default NFTSales; diff --git a/app/app-sandbox/src/pages/NFTToken/index.js b/app/app-sandbox/src/pages/NFTToken/index.js index 041189d..0735c87 100644 --- a/app/app-sandbox/src/pages/NFTToken/index.js +++ b/app/app-sandbox/src/pages/NFTToken/index.js @@ -1,42 +1,55 @@ -import React, { lazy, useEffect, useMemo } from "react"; +import React, { lazy, useContext, useEffect, useMemo } from "react"; import { useWallet, PROVIDER_ID } from "@txnlab/use-wallet"; import { getCurrentNode, getGenesisHash } from "../../utils/reach"; import { Table, TableBody, - TableHead, TableRow, TableCell, - TextField, Button, Grid, - Typography, Container, - Tabs, - Tab, - InputAdornment, - Select, - MenuItem, createTheme, - ThemeProvider, - Skeleton, - Paper, - Box, + Chip, ButtonGroup, } from "@mui/material"; import { makeStdLib } from "../../utils/reach"; import { makeStyles } from "@mui/styles"; -import algosdk from "algosdk"; -import CONTRACT from "arccjs"; -import { getAlgorandClients } from "../../utils/algorand"; +import algosdk, { waitForConfirmation } from "algosdk"; +import { getAlgorandClients, zeroAddress } from "../../utils/algorand"; import { useParams } from "react-router-dom"; -import { mp200Schema, arc72Schema } from "../../abis"; +import { mp200Schema, mp201Schema, mp202Schema, arc72Schema } from "../../abis"; import SalesHistoryTable from "../../components/SalesHistoryTable"; +import { nftDb, db, mpDb } from "../../db"; +import { useLiveQuery } from "dexie-react-hooks"; +import { decodeRoyalties } from "../../utils/hf"; +import { + activeApps, + ctcInfoMp200, + ctcInfoMp201, + ctcInfoMp202, + ctcInfoMp203, + fee, + feeBi, +} from "../../constants/mp"; +import { arc72 } from "ulujs"; +import CONTRACT from "arccjs"; +import { toast } from "react-toastify"; +import { + computeExtraPayment, + computeNFTSalePrice, + computeSalePrice, + decodeDecimals, + decodePrice, + getPriceSymbol, +} from "../../utils/mp"; +import moment from "moment"; +import BuyModal from "../../components/BuyModal"; +import SellModal from "../../components/SellModal"; +import { MarketplaceContext } from "../../store/MarketplaceContext"; const stdlib = makeStdLib(); -const mp200CtcInfo = 26944604; - const theme = createTheme({ palette: { divider: "#C7C7C7", // Define the divider color @@ -61,62 +74,45 @@ const useStyles = makeStyles((theme) => ({ }, })); -/* - * prepareString - * - prepare string (strip trailing null bytes) - * @param str: string to prepare - * @returns: prepared string - */ -const prepareString = (str) => { - const index = str.indexOf("\x00"); - if (index > 0) { - return str.slice(0, str.indexOf("\x00")); - } else { - return str; - } -}; - -function TabPanel(props) { - const { children, value, index, ...other } = props; - return value == index && children; -} - -const ipfsToGateway = (url) => { - if (url.indexOf("ipfs://") !== 0) return url; - const gatewayURL = `https://ipfs.io/ipfs/${url.slice(7)}`; - return gatewayURL; -}; - const { algodClient, indexerClient } = getAlgorandClients("voi-testnet"); -const NFTInfo = ({ collectionId, tokenId, owner }) => { - // const [owner, setOwner] = React.useState(null); - // useEffect(() => { - // if (owner) return; - // (async () => { - // const ci = new CONTRACT(collectionId, algodClient, indexerClient, schema); - // const ownerR = await ci.arc72_ownerOf(tokenId); - // if (!ownerR.success) return; - // setOwner(ownerR.returnValue); - // })(); - // }); +const NFTInfo = ({ nftInfo, collectionId, tokenId, owner, controller }) => { return ( - - CollectionId - {collectionId} - - - TokenId - {tokenId} - - - Owner - - {owner?.slice(0, 10)}...{owner?.slice(-10)} - - + {[ + ["Name", nftInfo?.metadata?.name], + ["Description", nftInfo?.metadata?.description, "justify"], + ["CollectionId", collectionId], + ["TokenId", tokenId], + [ + "Owner", + owner ? owner?.slice(0, 10) + "..." + owner?.slice(-10) : "...", + ], + [ + "Controller", + controller + ? controller?.slice(0, 10) + "..." + controller?.slice(-10) + : "...", + ], + [ + "Traits", + Object.entries(nftInfo?.metadata?.properties || {}).map( + ([k, v]) => + ), + ], + [ + "Royalties", + nftInfo?.royalties ? `${nftInfo?.royalties.royaltyPercent}%` : null, + ], + ].map(([a, b, c]) => { + return !!b ? ( + + {a} + {b} + + ) : null; + })}
); @@ -124,105 +120,158 @@ const NFTInfo = ({ collectionId, tokenId, owner }) => { const LazyNFTImage = lazy(() => import("../../components/NFTImage")); -const fee = 5; -const feeBi = 5n; -const ctcInfoMp200 = 26944604; // mp200 - function NFTToken() { + const { + isLoading, + nfts, + tokens, + listings, + forSale, + projects, + auctions, + liveAuctions, + bids, + } = useContext(MarketplaceContext); + const { cid, tid } = useParams(); + const [node] = getCurrentNode(); - const { activeAccount } = useWallet(); + const { activeAccount, providers, signTransactions } = useWallet(); + const [value, setValue] = React.useState(1); - const [nfts, setNfts] = React.useState([]); - const [forSale, setForSale] = React.useState([]); const [sold, setSold] = React.useState([]); const [owners, setOwners] = React.useState(null); - const [tokens, setTokens] = React.useState(null); const [ctcInfo, setCtcInfo] = React.useState(null); const [owner, setOwner] = React.useState(null); const [controller, setController] = React.useState(null); - const handleBuy = React.useCallback( - async (lId, cId, tId, lAddr, lPrc) => { - const ciMp200 = new CONTRACT( - ctcInfoMp200, - algodClient, - indexerClient, - mp200Schema, - { - addr: activeAccount.address, - } - ); - ciMp200.setFee(4000); - ciMp200.setPaymentAmount((lPrc * (100n + feeBi)) / 100n); - ciMp200.setAccounts([ - "G3MSA75OZEJTCCENOJDLDJK7UD7E2K5DNC7FVHCNOV7E3I4DTXTOWDUIFQ", - ]); - const buyNetR = await ciMp200.buyNet(lId); - console.log({ buyNetR }); - if (!buyNetR.success) return; - console.log({ buyNetR }); - await signTransaction(buyNetR.txns); - alert("Purchase successful!"); - }, - [activeAccount] - ); + const [unclaimed, setUnclaimed] = React.useState(null); + const [showBuyModal, setShowBuyModal] = React.useState(false); + const [buyModalListing, setBuyModalListing] = React.useState(null); + const [showSellModal, setShowSellModal] = React.useState(false); + + // EFFECT: set nftInfo + const nft = useMemo(() => { + if (isLoading) return null; + return nfts.find( + ({ collectionId, tokenId }) => + Number(collectionId) === Number(cid) && Number(tokenId) === Number(tid) + ); + }, [isLoading, nfts]); + + // EFFECT: update controller useEffect(() => { - if (!owner) return; + if (controller) return; (async () => { - const ci = new CONTRACT( - ctcInfoMp200, - algodClient, - indexerClient, - mp200Schema - ); - const evts = await ci.getEvents(); - const listings = evts.find((el) => el.name === "ListEvent").events; - const sales = evts.find((el) => el.name === "BuyEvent").events; - const deletes = evts.find( - (el) => el.name === "DeleteNetListingEvent" - ).events; - const deleted = deletes.map(([txId, round, ts, lId]) => lId); - const nfts = []; - const sold = []; - listings.forEach((el) => { - const [txId, round, ts, lId, cId, tId, lAddr, [aT, lPrc]] = el; - nfts.push([lId, cId, tId, lAddr, lPrc]); + const ci = new arc72(Number(cid), algodClient, indexerClient); + const arc72_getApprovedrR = await ci.arc72_getApproved(Number(tid)); + if (!arc72_getApprovedrR.success) return; + const arc72_getApproved = arc72_getApprovedrR.returnValue; + setController(arc72_getApproved); + })(); + }, []); + + // EFFECT: update owner + useEffect(() => { + if (owner) return; + (async () => { + const ci = new arc72(Number(cid), algodClient, indexerClient); + const ownerR = await ci.arc72_ownerOf(Number(tid)); + if (!ownerR.success) return; + setOwner(ownerR.returnValue); + })(); + }, []); + + // set now to block time + const [now, setNow] = React.useState(null); + useEffect(() => { + (async () => { + const res = await algodClient.status().do(); + setNow(res["last-round"]); + })(); + }, []); + + // get bid info + const [token, setToken] = React.useState(null); + const [nextBid, setNextBid] = React.useState(null); + const [lastBid, setLastBid] = React.useState(null); + useEffect(() => { + if (isLoading) return; + const liveAuction = liveAuctions.find( + ({ cId, tId }) => + Number(cId) === Number(cid) && Number(tId) === Number(tid) + ); + if (!liveAuction) return; + const bid = bids + .filter((bid) => bid.lId === liveAuction.lId) + .reduce((acc, val) => { + if (val.round > acc.round) { + return val; + } + return acc; }); - nfts.reverse(); - sales.forEach((el) => { - const [txId, round, ts, lId, cId, tId, lAddr, [aT, lPrc], bAddr] = el; - sold.push([lId, cId, tId, lAddr, lPrc, bAddr]); + if (!bid) return; + const [ptid, pprc] = bid.bPrc.slice(1); + const ptidn = Number("0x" + ptid); + const pprcn = Number("0x" + pprc); + const token = tokens.find((t) => Number(t.tokenId) === ptidn); + if (!token) return; + const plusOne = 10 ** token.decimals; + const nextBid = pprcn + plusOne; + setLastBid(bid); + setNextBid(nextBid); + setToken(token); + }, [liveAuctions, bids, tokens, isLoading]); + + console.log({ liveAuctions, bids, tokens, lastBid, nextBid, token }); + + const handleBid = React.useCallback( + async (mp, txId, round, ts, lId, cId, tId, lAddr, lPrc, nextBid) => { + try { + const ciMp = new CONTRACT(mp, algodClient, indexerClient, mp202Schema, { + addr: activeAccount?.address, + }); + ciMp.setPaymentAmount(1000); + ciMp.setFee(2000); + const bidSCR = await ciMp.bidSC(lId, nextBid); + if (!bidSCR.success) { + throw new Error("Bid failed in simulate"); + } + const txns = bidSCR.txns; + const txId = await signTransaction(txns); + console.log({ txId }); + alert("Bid successful!"); + } catch (e) { + toast.error(e.message); + } + }, + [activeAccount, nft, token] + ); + + const handleBuy = React.useCallback( + async (mp, txId, round, ts, lId, cId, tId, lAddr, lPrc) => { + setShowBuyModal(true); + setBuyModalListing({ + mp, + txId, + round, + ts, + lId, + cId, + tId, + lAddr, + lPrc, }); - sold.reverse(); - setNfts( - nfts.filter( - ([lId, scId, stId, ...rst]) => - !deleted.includes(lId) && - Number(scId) === Number(cid) && - Number(tid) === Number(stId) - ) - ); - setForSale( - ((lIds) => - nfts.filter( - ([lId, scId, stId, lAddr, ...rst]) => - !lIds.includes(lId) && - !deleted.includes(lId) && - Number(scId) === Number(cid) && - Number(tid) === Number(stId) && - lAddr === owner - ))(sold.map(([slId, ...rst]) => slId)) - ); - setSold( - sold.filter( - ([lId, scId, stId, ...rst]) => - !deleted.includes(lId) && - Number(scId) === Number(cid) && - Number(tid) === Number(stId) - ) - ); - })(); - }, [owner]); + }, + [activeAccount, nft] + ); + + // EFFECT: check cid + useEffect(() => { + const ctcInfo = Number(cid); + if (isNaN(ctcInfo)) return; + setCtcInfo(ctcInfo); + }, []); + const signTransaction = React.useCallback( async (txns) => { if (!activeAccount) return; @@ -238,7 +287,7 @@ function NFTToken() { const wallet = await algorand.enable({ genesisHash: getGenesisHash(node), }); - const { algodClient, indexerClient } = getAlgorandClients(); + const result = await window.algorand.signTxns({ txns: txns.map((el) => { return { @@ -253,55 +302,105 @@ function NFTToken() { const res = await algodClient .sendRawTransaction(signedTransactionBytes) .do(); - return res.txId; + await waitForConfirmation(algodClient, res.txId, 4); + } else if ( + [PROVIDER_ID.KIBISIS, PROVIDER_ID.DEFLY, PROVIDER_ID.LUTE].includes( + activeAccount.providerId + ) + ) { + const stxns = await signTransactions( + txns.map((el) => new Uint8Array(Buffer.from(el, "base64"))) + ); + const res = await algodClient.sendRawTransaction(stxns).do(); + await waitForConfirmation(algodClient, res.txId, 4); } else { - const wtxns = txns.map((el) => { - return { - txn: el, - }; - }); - await stdlib.signSendAndConfirm({ addr: activeAccount.address }, wtxns); + throw new Error("Unsupported wallet"); } }, [activeAccount] ); - // useEffect(() => { - // if (controller) return; - // (async () => { - // const ci = new CONTRACT( - // Number(cid), - // algodClient, - // indexerClient, - // arc72Schema - // ); - // const arc72_getApprovedrR = await ci.arc72_getApproved(Number(tid)); - // if (!arc72_getApprovedrR.success) return; - // const arc72_getApproved = arc72_getApprovedrR.returnValue; - // setController(arc72_getApproved); - // })(); - // }); - console.log({ controller }); - useEffect(() => { - if (owner) return; - (async () => { - const ci = new CONTRACT( - Number(cid), - algodClient, - indexerClient, - arc72Schema + const handleUnlist = async () => { + try { + const ci = new arc72(Number(cid), algodClient, indexerClient, { + acc: { + addr: activeAccount?.address, + }, + }); + const arc72_approveR = await ci.arc72_approve( + activeAccount?.address, + Number(tid) ); - const ownerR = await ci.arc72_ownerOf(Number(tid)); - if (!ownerR.success) return; - setOwner(ownerR.returnValue); - })(); - }); - useEffect(() => { - const ctcInfo = Number(cid); - if (isNaN(ctcInfo)) return; - setCtcInfo(ctcInfo); - }, []); - return !ctcInfo ? ( - "Collection not found" + if (!arc72_approveR.success) { + throw new Error("arc72_approve failed in simulate"); + } + await signTransaction(arc72_approveR.txns); + alert("Unlist successful!"); + } catch (e) { + console.log(e); + } + }; + + const handleTransfer = async () => { + try { + const addr = prompt("Enter address to transfer to"); + if (!addr) return; + const ci = new arc72(ctcInfo, algodClient, indexerClient, { + acc: { addr: activeAccount?.address }, + }); + // const ci = new CONTRACT( + // ctcInfo, + // algodClient, + // indexerClient, + // arc72Schema, + // { + // addr: activeAccount?.address, + // } + // ); + const arc72_ownerOfR = await ci.arc72_ownerOf(Number(tid)); + if (!arc72_ownerOfR.success) { + throw new Error("arc72_ownerOf failed in simulate"); + } + if (arc72_ownerOfR.returnValue !== activeAccount?.address) { + throw new Error("arc72_ownerOf returned wrong owner"); + } + + // TODO check mbr of ctcAddr + + const accInfo = await algodClient + .accountInformation(algosdk.getApplicationAddress(Number(cid))) + .do(); + if (accInfo.amount - accInfo["min-balance"] < 28500) { + const suggestedParams = await algodClient.getTransactionParams().do(); + const paymentTxn = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ + from: activeAccount?.address, + to: algosdk.getApplicationAddress(Number(cid)), + amount: 28500, + suggestedParams, + }); + await signTransaction([ + Buffer.from(paymentTxn.toByte()).toString("base64"), + ]); + } + + const arc72_transferFromR = await ci.arc72_transferFrom( + activeAccount?.address, + addr, + Number(tid) + ); + if (!arc72_transferFromR.success) { + throw new Error("arc72_transferFrom failed in simulate"); + } + const txns = arc72_transferFromR.txns; + const txId = await signTransaction(txns); + console.log({ txId }); + alert("Transfer successful!"); + } catch (e) { + console.log(e); + } + }; + + return !ctcInfo || isLoading || !nft ? ( + "NFT not found" ) : ( <>

@@ -315,247 +414,260 @@ function NFTToken() { - + + + { + // owner is connected account + activeAccount?.address === owner && ( + + ) + } + { + // ower is connected account and controller is not a marketplace + owner === activeAccount?.address && + !activeApps + .map(algosdk.getApplicationAddress) + .includes(controller) && ( + + ) + } + { + // owner is connected accoujnt and controller is a marketplace + owner === activeAccount?.address && + activeApps + .map(algosdk.getApplicationAddress) + .includes(controller) && ( + + ) + } + - {activeAccount?.address === owner && ( - <> - - - - - )} - {controller === algosdk.getApplicationAddress(ctcInfoMp200) && - activeAccount?.address !== owner && - forSale && - forSale.length > 0 && ( - + > + Bid{" "} + {(nextBid / 10 ** token?.decimals || 0).toLocaleString()}{" "} + {token?.symbol || ""} + + `${addr.slice(0, 4)}...${addr.slice(-4)}`)( + lastBid?.bAddr + )}`} + /> + + diff > 0 ? `Ends ${diff} blocks` : "Ended")( + Number(endTime) - now + )} + /> + + ) + )} + {forSale + .filter( + ({ cId, tId }) => + Number(cId) === Number(cid) && Number(tId) === Number(tid) + ) + .map(({ mp, txId, round, ts, lId, cId, tId, lAddr, lPrc }) => + owner !== activeAccount?.address && lAddr === owner ? ( + + ) : null )} - {sold && sold.length > 0 && ( - - { + + {sold && sold.length > 0 && ( + + { return { id: lId, - date: "2022-01-01", + date: ((m) => `${m.format("llll")} (${m.fromNow()})`)( + moment.unix(ts) + ), seller: `${lAddr.slice(0, 4)}...${lAddr.slice(-4)}`, buyer: `${bAddr.slice(0, 4)}...${bAddr.slice(-4)}`, - amount: `${(Number(lPrc) / 1e6).toLocaleString()} VOI`, + amount: `${ + computeNFTSalePrice( + tokens, + lPrc, + nft.royalties.royaltyPercent, + fee, + false + ) / 1e6 + }`, + symbol: getPriceSymbol(lPrc, node, tokens), }; - })} - /> - - )} - + } + )} + /> + + )} + {showBuyModal && ( + setShowBuyModal(false)} + {...buyModalListing} + nfts={nfts} + /> + )} + {showSellModal && ( + setShowSellModal(false)} + cid={Number(cid)} + tid={Number(tid)} + /> + )} ); } diff --git a/app/app-sandbox/src/pages/Token/index.js b/app/app-sandbox/src/pages/Token/index.js index e9e0926..738af36 100644 --- a/app/app-sandbox/src/pages/Token/index.js +++ b/app/app-sandbox/src/pages/Token/index.js @@ -4,12 +4,15 @@ import { Accordion, AccordionDetails, AccordionSummary, + Button, Chip, + Grid, Modal, Skeleton, Stack, Tab, Tabs, + TextField, Typography, } from "@mui/material"; import { styled } from "@mui/material/styles"; @@ -51,6 +54,10 @@ import { formatWithDecimals } from "../../common/utils/bn"; import algosdk from "algosdk"; import { registeredToken, tokenURL } from "../../constants/json"; +import { CSVLink, CSVDownload } from "react-csv"; + +import TypeFilter from "../../components/TypeFilter"; + const stdlib = makeStdLib(); const bn = stdlib.bigNumberify; const fa = stdlib.formatAddress; @@ -532,8 +539,36 @@ const TokenTransactions = ({ }) => { const { CopyToClipboard } = Copy; const notify = (msg) => toast(msg); + const [addrTo, setAddrTo] = React.useState(""); + const [addrFrom, setAddrFrom] = React.useState(""); + const [txnType, setTxnType] = React.useState(""); const [pageTx, setTxPage] = React.useState(0); const [rowsTxPerPage, setTxRowsPerPage] = React.useState(10); + const filteredTransactions = React.useMemo(() => { + return transactions?.filter( + (tx) => + (addrTo === "" || tx[4] === addrTo) && + (addrFrom === "" || tx[3] === addrFrom) && + (txnType === "" || tx[6] === txnType) + ); + }); + const csvData = React.useMemo(() => { + const csvData = [ + ["txId", "block", "timestamp", "from", "to", "amount", "type"], + ...filteredTransactions.map( + ([txId, block, timestamp, from, to, amount, type]) => [ + txId, + block, + moment.unix(timestamp).format(), + from, + to, + Number(amount) / 10 ** token.decimals, + type, + ] + ), + ]; + return csvData; + }, [filteredTransactions]); if ((transactions?.length ?? 0) === 0) return null; const handleTxChangePage = (event, newPage) => { setTxPage(newPage); @@ -547,9 +582,46 @@ const TokenTransactions = ({

Transactions [ - {transactions?.length > 0 ? transactions?.length : "..."}] + {filteredTransactions?.length > 0 + ? filteredTransactions?.length + : "..."} + ]

- +

Filter by address

+ + + + setAddrFrom(e.target.value)} + /> + setAddrTo(e.target.value)} + /> + setTxnType(t)} /> + + + + + + + + + @@ -563,13 +635,13 @@ const TokenTransactions = ({ - {transactions?.length > 0 ? ( + {filteredTransactions?.length > 0 ? ( (rowsTxPerPage > 0 - ? transactions?.slice( + ? filteredTransactions?.slice( pageTx * rowsTxPerPage, pageTx * rowsTxPerPage + rowsTxPerPage ) - : transactions + : filteredTransactions ).map((row) => ( @@ -695,7 +767,7 @@ const TokenTransactions = ({ amount > 0)} - /> + <> + amount > 0)} + /> + ) : ( ); diff --git a/app/app-sandbox/src/services/HighForgeService.ts b/app/app-sandbox/src/services/HighForgeService.ts new file mode 100644 index 0000000..df9b29c --- /dev/null +++ b/app/app-sandbox/src/services/HighForgeService.ts @@ -0,0 +1,15 @@ +import axios from "axios"; + +const getProjects = async (query) => { + const response = await axios.get( + `https://test-voi.api.highforge.io/projects`, + { + params: query, + } + ); + return response.data; +}; + +export default { + getProjects, +}; diff --git a/app/app-sandbox/src/services/NFTIndexerService.ts b/app/app-sandbox/src/services/NFTIndexerService.ts new file mode 100644 index 0000000..5eb05b9 --- /dev/null +++ b/app/app-sandbox/src/services/NFTIndexerService.ts @@ -0,0 +1,26 @@ +import axios from "axios"; + +const getCollections = async (query = {}) => { + const response = await axios.get( + `https://arc72-idx.voirewards.com/nft-indexer/v1/collections`, + { + params: query, + } + ); + return response.data; +} + +const getTokens = async (query = {}) => { + const response = await axios.get( + `https://arc72-idx.voirewards.com/nft-indexer/v1/tokens`, + { + params: query, + } + ); + return response.data; +}; + +export default { + getCollections, + getTokens, +}; diff --git a/app/app-sandbox/src/store/MarketplaceContext.js b/app/app-sandbox/src/store/MarketplaceContext.js new file mode 100644 index 0000000..924188a --- /dev/null +++ b/app/app-sandbox/src/store/MarketplaceContext.js @@ -0,0 +1,321 @@ +// MyContext.js +import { createContext, useMemo } from "react"; +import { nftDb, db, mpDb } from "../db"; +import { decodeRoyalties } from "../utils/hf"; +import { useLiveQuery } from "dexie-react-hooks"; +import { + decodePrice, + getPriceSymbol, + decodeDecimals, + computeExtraPayment, + decodeTokenId, +} from "../utils/mp"; +import { getCurrentNode } from "../utils/reach"; +import { fee } from "../constants/mp.js"; + +const [node] = getCurrentNode(); + +const MarketplaceContext = createContext(); + +const MarketplaceProvider = ({ children }) => { + const dbNfts = useLiveQuery(() => nftDb.nfts.toArray()); + const dbCollections = useLiveQuery(() => nftDb.collections.toArray()); + const dbTokens = useLiveQuery(() => db.tokens.toArray()); + const dbPools = useLiveQuery(() => db.registerEvents.toArray()); + const dbMpListings = useLiveQuery(() => mpDb.listings.toArray()); + const dbMpAuctions = useLiveQuery(() => mpDb.auctions.toArray()); + const dbMpReverseAuctions = useLiveQuery(() => + mpDb.reverseListings.toArray() + ); + const dbMpSales = useLiveQuery(() => mpDb.sales.toArray()); + const dbMpDeletions = useLiveQuery(() => mpDb.deletions.toArray()); + const dbMpAuctionDeletions = useLiveQuery(() => + mpDb.auctionDeletions.toArray() + ); + const dbMpClaims = useLiveQuery(() => mpDb.claims.toArray()); + const dbMpBids = useLiveQuery(() => mpDb.bids.toArray()); + const isLoading = useMemo( + () => + !dbNfts || + !dbTokens || + !dbMpListings || + !dbMpSales || + !dbMpDeletions || + !dbMpAuctionDeletions || + !dbMpClaims || + !dbMpAuctions || + !dbMpBids || + !dbPools || + !dbCollections, + [ + dbNfts, + dbTokens, + dbMpListings, + dbMpAuctions, + dbMpAuctionDeletions, + dbMpSales, + dbMpDeletions, + dbMpClaims, + dbMpBids, + dbPools, + dbCollections, + ] + ); + const collections = useMemo(() => { + if (!dbCollections) return []; + return dbCollections; + }, [dbCollections]); + const pools = useMemo(() => { + if (!dbPools) return []; + return dbPools; + }, [dbPools]); + const nfts = useMemo(() => { + if (!dbNfts) return []; + return dbNfts.map((nft) => { + const metadata = JSON.parse(nft.metadata); + const royalties = decodeRoyalties(metadata.royalties); + return { + ...nft, + metadata, + royalties, + }; + }); + }, [dbNfts]); + const tokens = useMemo(() => { + if (!dbTokens) return []; + return dbTokens.map((token) => { + const decimals = Number(token.decimals); + return { + ...token, + decimals, + }; + }); + }, [dbTokens]); + const listings = useMemo(() => { + if (!dbMpListings) return []; + return dbMpListings; + }, [dbMpListings]); + const auctions = useMemo(() => { + if (!dbMpAuctions) return []; + return dbMpAuctions; + }, [dbMpAuctions]); + const reverseAuctions = useMemo(() => { + if (!dbMpReverseAuctions) return []; + return dbMpReverseAuctions; + }, [dbMpReverseAuctions]); + const bids = useMemo(() => { + if (!dbMpBids) return []; + return dbMpBids; + }, [dbMpBids]); + const sales = useMemo(() => { + if (!dbMpSales) return []; + return dbMpSales.map((sale) => { + if (sale?.lPrc === undefined) return sale; + const price = decodePrice(sale.lPrc); + return { + ...sale, + price, + }; + }); + }, [dbMpSales]); + const deletions = useMemo(() => { + if (!dbMpDeletions) return []; + return dbMpDeletions; + }, [dbMpDeletions]); + const auctionDeletions = useMemo(() => { + if (!dbMpAuctionDeletions) return []; + return dbMpAuctionDeletions; + }, [dbMpAuctionDeletions]); + const claims = useMemo(() => { + if (!dbMpClaims) return []; + return dbMpClaims; + }, [dbMpClaims]); + const liveAuctions = useMemo(() => { + if (isLoading) return []; + const deletionsK = auctionDeletions.map(({ mp, lId }) => `${mp}:${lId}`); + // TODO add closes + // TODO add announcments + return auctions + .filter(({ mp, lId }) => [...deletionsK].indexOf(`${mp}:${lId}`) < 0) + .map((auction) => { + const tokenId = decodeTokenId(auction.lPrc); + const payment = decodePrice(auction.lPrc); + const symbol = getPriceSymbol(auction.lPrc, node, tokens); + const decimals = decodeDecimals(auction.lPrc, node, tokens); + const lTokens = []; + const lToken = tokens.find((t) => t.tokenId === tokenId); + if (lToken) lTokens.push(lToken); + const wVOI = 24590664; + const lPools = pools.filter( + (p) => + [p.tokA, p.tokB].includes(tokenId) && + [p.tokA, p.tokB].includes(wVOI) + ); + const extraPayment = symbol === "wVOI" ? 0 : 0; + const priceDisplay = `${( + Number(payment + extraPayment) / + 10 ** decimals + ).toLocaleString()} ${symbol}`; + return { + ...auction, + tokens: lTokens, + pools: lPools, + tokenId, + payment, + extraPayment, + symbol, + decimals, + priceDisplay, + }; + }) + .sort((a, b) => b.timestamp - a.timestamp); + }, [isLoading, auctions, tokens, pools]); + const forSale = useMemo(() => { + if (isLoading) return []; + const deletionsK = deletions.map(({ mp, lId }) => `${mp}:${lId}`); + const claimsK = claims.map(({ mp, lId }) => `${mp}:${lId}`); + const salesK = sales.map(({ mp, lId }) => `${mp}:${lId}`); + return listings + .filter( + ({ mp, lId }) => + [...deletionsK, ...claimsK, ...salesK].indexOf(`${mp}:${lId}`) < 0 + ) + .map((listing) => { + const nft = nfts.find( + (n) => + n.collectionId === Number(listing.cId) && + n.tokenId === Number(listing.tId) + ); + const tokenId = decodeTokenId(listing.lPrc); + const payment = decodePrice(listing.lPrc); + const symbol = getPriceSymbol(listing.lPrc, node, tokens); + const decimals = decodeDecimals(listing.lPrc, node, tokens); + const lTokens = []; + const lToken = tokens.find((t) => t.tokenId === tokenId); + if (lToken) lTokens.push(lToken); + const wVOI = 24590664; + const lPools = pools.filter( + (p) => + [p.tokA, p.tokB].includes(tokenId) && + [p.tokA, p.tokB].includes(wVOI) + ); + const extraPayment = nft + ? listing.lPrc[0] === "00" + ? computeExtraPayment(payment, nft?.royalties, fee, 1) + : symbol === "wVOI" + ? computeExtraPayment(payment, nft?.royalties, fee, 1) + : computeExtraPayment(payment, nft?.royalties, fee, 1) + : 0; + const priceDisplay = + listing.lPrc[0] === "00" + ? `${( + Number(payment + extraPayment) / + 10 ** decimals + ).toLocaleString()} ${symbol}` + : `${( + Number(payment) / + 10 ** decimals + ).toLocaleString()} ${symbol}`; + return { + ...listing, + tokens: lTokens, + pools: lPools, + tokenId, + payment, + extraPayment, + symbol, + decimals, + nft, + priceDisplay, + }; + }) + .sort((a, b) => b.timestamp - a.timestamp); + }, [isLoading, listings, claims, sales, nfts, tokens, pools]); + const properties = useMemo(() => { + const properties = new Map(); + nfts.forEach((t) => { + if (t.metadata.properties) { + Object.keys(t.metadata.properties).forEach((k) => { + if (!properties.has(k)) { + properties.set(k, new Set([t.metadata.properties[k]])); + } else { + properties.set(k, properties.get(k).add(t.metadata.properties[k])); + } + }); + } + }); + return properties; + }, [isLoading, nfts]); + const unclaimed = useMemo(() => { + const soldK = sales + .filter(({ lPrc }) => { + if (lPrc === undefined) return false; + return lPrc[0] === "01"; + }) + .map(({ mp, lId }) => `${mp}:${lId}`); + const claimedK = claims.map(({ mp, lId }) => `${mp}:${lId}`); + const unclaimedListings = listings.filter( + ({ mp, lId }) => + soldK.indexOf(`${mp}:${lId}`) >= 0 && + claimedK.indexOf(`${mp}:${lId}`) < 0 + ); + return unclaimedListings.map((listing) => { + const { mp, lId } = listing; + const sale = sales.find( + ({ mp: smp, lId: slId }) => mp === smp && lId === slId + ); + return { ...listing, bAddr: sale.bAddr }; + }); + }, [isLoading, listings, claims, sales]); + const metrics = useMemo(() => { + if (isLoading) return {}; + const saleCount = sales.length; + const saleVolume = sales + .filter(({ lPrc }) => { + if (lPrc === undefined) return false; + return lPrc[0] === "00"; + }) + .reduce((acc, { price }) => acc + price, 0); + const saleAverage = saleVolume / saleCount; + const networkTokenSymbol = getPriceSymbol(["00"], node, tokens); + return { + ["All-time"]: { + ["Volume"]: `${( + saleVolume / 1e6 + ).toLocaleString()} ${networkTokenSymbol}`, + ["Sales"]: saleCount, + ["Average"]: `${( + saleAverage / 1e6 + ).toLocaleString()} ${networkTokenSymbol}`, + }, + }; + }, [isLoading, sales, tokens]); + return ( + + {children} + + ); +}; + +export { MarketplaceContext, MarketplaceProvider }; diff --git a/app/app-sandbox/src/utils/hf.js b/app/app-sandbox/src/utils/hf.js new file mode 100644 index 0000000..824fd5c --- /dev/null +++ b/app/app-sandbox/src/utils/hf.js @@ -0,0 +1,30 @@ +import { encodeAddress } from "algosdk"; +import { zeroAddress } from "./algorand"; + +export const decodeRoyalties = (royalties) => { + const buf = Buffer.from(royalties, "base64"); + const royaltyPoints = buf.slice(0, 2).readUInt16BE(0); + const creator1Points = buf.slice(2, 4).readUInt16BE(0); + const creator2Points = buf.slice(4, 6).readUInt16BE(0); + const creator3Points = buf.slice(6, 8).readUInt16BE(0); + const creator1Address = encodeAddress(buf.slice(8, 8 + 32 * 1)); + const creator2Address = encodeAddress(buf.slice(8 + 32, 8 + 32 * 2)); + const creator3Address = encodeAddress(buf.slice(8 + 32 * 2, 8 + 32 * 3)); + const creatorAddressCount = [ + creator1Address, + creator2Address, + creator3Address, + ].filter((el) => el !== zeroAddress).length; + const royaltyPercent = (royaltyPoints / 10000) * 100; + return { + royaltyPercent, + royaltyPoints, + creator1Address, + creator2Address, + creator3Address, + creator1Points, + creator2Points, + creator3Points, + creatorAddressCount, + }; +}; diff --git a/app/app-sandbox/src/utils/mp.js b/app/app-sandbox/src/utils/mp.js new file mode 100644 index 0000000..123f0d2 --- /dev/null +++ b/app/app-sandbox/src/utils/mp.js @@ -0,0 +1,126 @@ +import { fee } from "../constants/mp.js"; + +export const computeExtraPayment = (price, royalties, fee, rate = 1) => { + const { royaltyPercent } = royalties; + return ( + rate * + ((price * (royaltyPercent + computeMarketplaceFee(royalties, fee))) / 100) + ); +}; +export const computeMarketplaceFee = (royalties, fee) => + Math.max( + 1, + Math.min( + fee, + Math.ceil(royalties.royaltyPercent / royalties.creatorAddressCount) + ) + ); +export const getPriceSymbol = (price, node, tokens) => { + const [pType, ...prc] = price; + switch (pType) { + case "00": { + switch (node) { + case "voi": + case "voi-testnet": + return "VOI"; + default: + return "ALGO"; + } + } + case "01": { + const [_, tidr, tprcr] = price; + const tid = Number("0x" + tidr); + const token = tokens.find((el) => el.tokenId === tid); + return token?.symbol || "UNK"; + } + } +}; +export const decodeDecimals = (price, node, tokens) => { + const [pType, ...prc] = price; + switch (pType) { + case "00": { + switch (node) { + case "algorand": + case "algorand-testnet": + case "voi": + case "voi-testnet": + return 6; + default: + return 0; + } + } + case "01": { + const [, tidr] = price; + const tid = Number("0x" + tidr); + const token = tokens?.find((el) => el.tokenId === tid); + return Number(token?.decimals || 1); + } + } +}; + +export const decodeTokenId = (price) => { + const [pType, ...prc] = price; + switch (pType) { + case "00": { + return 0; + } + case "01": { + const [_, tidr] = price; + const tid = Number("0x" + tidr); + return tid; + } + } +}; + +export const decodePrice = (price) => { + const [pType, ...prc] = price; + switch (pType) { + case "00": { + const prcn = Number(prc); + return prcn; + } + case "01": { + const [_, tidr, tprcr] = price; + const tprc = Number("0x" + tprcr); + return tprc; + } + } +}; + +export const computeSalePrice = (price, royalties, token) => { + const salePrice = (prc) => prc + computeExtraPayment(prc, royalties, fee); + return salePrice(price); +}; +export const computeNFTSalePrice = ( + tokens, + price, + royalties, + fee, + addFee = false +) => { + console.log({ tokens, price, royalties, fee, addFee }); + const [pType, ...prc] = price; + const salePrice = (prc) => prc + computeExtraPayment(prc, royalties, fee); + switch (pType) { + case "00": { + const prcn = Number(prc); + return addFee ? salePrice(prcn) : prcn; /// 1e6; + } + case "01": { + /* + const l = nfts.find((el) => { + const [lmp, txId, round, ts, llId, cId, tId, lAddr, lPrc] = el; + return llId === lId && lmp === mp; + }); + const LPRC = 8; + */ + const [_, tidr, tprcr] = price; + const tid = Number("0x" + tidr); + const tprc = Number("0x" + tprcr.slice(0, tprcr.length - 6)); + //const token = tokens?.find((el) => el.tokenId === tid); + const au = tprc; + //const decimals = Number(token?.decimals || 1); + return au; /// decimals; + } + } +}; diff --git a/app/app-sandbox/yarn.lock b/app/app-sandbox/yarn.lock index e4947b1..a6ddd04 100644 --- a/app/app-sandbox/yarn.lock +++ b/app/app-sandbox/yarn.lock @@ -2800,10 +2800,10 @@ resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz" integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== -"@txnlab/use-wallet@^2.3.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@txnlab/use-wallet/-/use-wallet-2.3.0.tgz#d118f85ff60565cfc079ce795836da696e924b2c" - integrity sha512-nOY8tMnOsXjBqMOUeJdub/l+7Eu9GyURnsR99wVYyEOVP6OjpLyFLGGluITTCjPme6t+h/G58m1KkUz1qNHErQ== +"@txnlab/use-wallet@^2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@txnlab/use-wallet/-/use-wallet-2.7.0.tgz#03aad228f9fd72893ca811850892dd811c8e26fa" + integrity sha512-Nj0V9lqLAkPKdM1GO6qUJocnvf+WNWdL7N8NOBg+KdxZcpJLrABqf8OIR4C4OzL/tB2GqXUPTgq0cbv4fZB0Ow== dependencies: immer "^9.0.15" zustand "^4.3.8" @@ -3972,10 +3972,19 @@ arccjs@^2.3.3: buffer "^6.0.3" sha512 "^0.0.1" -arccjs@^2.4.9: - version "2.4.9" - resolved "https://registry.yarnpkg.com/arccjs/-/arccjs-2.4.9.tgz#70fb4f2d6f1d8b89e6b79925707fddaa23aee8c5" - integrity sha512-118gHWJPl7iecmLrxzveyxbWPm0jXythgSmNJuZ3K4GpKPXUs3mGA+amR2xqxGxNhUc7mwtiYDVKQl0Q0EBlRQ== +arccjs@^2.5.4: + version "2.5.4" + resolved "https://registry.yarnpkg.com/arccjs/-/arccjs-2.5.4.tgz#3704c98c8c3bd83affb5ebe5e49719f4dcaee8c1" + integrity sha512-rbZWJbwxYxnQLiE72Ualx+w4in9lxfi8yT75VDl7LG57ifo/JFbrKtyQl/rMiA9R4uU/Kf/ob56pNhM8stGncA== + dependencies: + algosdk "^2.7.0" + buffer "^6.0.3" + sha512 "^0.0.1" + +arccjs@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/arccjs/-/arccjs-2.7.0.tgz#ef13b771ec34ad3576c9f6fc7e276f90ad8c937a" + integrity sha512-7F7xgUXd7Afn8eyZkhp9XrcnpPCjFrEuJT15PlFicNcGz0nJWiNLSic6MpUcFvo82CovKDXhzsig+lzC8m2drg== dependencies: algosdk "^2.7.0" buffer "^6.0.3" @@ -4155,15 +4164,6 @@ axe-core@^4.6.2: resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz" integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== -axios@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz" - integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA== - dependencies: - follow-redirects "^1.15.0" - form-data "^4.0.0" - proxy-from-env "^1.1.0" - axios@^1.6.2: version "1.6.2" resolved "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz" @@ -4173,6 +4173,15 @@ axios@^1.6.2: form-data "^4.0.0" proxy-from-env "^1.1.0" +axios@^1.6.7: + version "1.6.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.7.tgz#7b48c2e27c96f9c68a2f8f31e2ab19f59b06b0a7" + integrity sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA== + dependencies: + follow-redirects "^1.15.4" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axobject-query@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz" @@ -6315,6 +6324,11 @@ follow-redirects@^1.0.0, follow-redirects@^1.15.0: resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== +follow-redirects@^1.15.4: + version "1.15.5" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" + integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz" @@ -6933,6 +6947,13 @@ import-local@^3.0.2: pkg-dir "^4.2.0" resolve-cwd "^3.0.0" +import@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/import/-/import-0.0.6.tgz#d0eb79df86aa2677c6db61578a5212b3031e6042" + integrity sha512-QPhTdjy9J4wUzmWSG7APkSgMFuPGPw+iJTYUblcfc2AfpqaatbwgCldK1HoLYx+v/+lWvab63GWZtNkcnj9JcQ== + dependencies: + optimist "0.3.x" + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" @@ -8283,6 +8304,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lute-connect@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/lute-connect/-/lute-connect-1.1.0.tgz#71b127bb7dfe85546fd9dce7d63b00f7a5e03095" + integrity sha512-j2Axs+Kk9LxG27VBXhTGXW0+TkJNbZ7GEU9RjUc42V0+M7A4RIq57AH8TvgW46Xq5+s0d/AYNvPDg952+R/Ylw== + lz-string@^1.4.4: version "1.5.0" resolved "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz" @@ -8725,6 +8751,13 @@ open@^8.0.9, open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" +optimist@0.3.x: + version "0.3.7" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.3.7.tgz#c90941ad59e4273328923074d2cf2e7cbc6ec0d9" + integrity sha512-TCx0dXQzVtSCg2OgY/bO9hjM9cV4XYx09TVK+s3+FhkjT6LovsLe+pPMzpWf+6yXK/hUizs2gUoTw3jHM0VaTQ== + dependencies: + wordwrap "~0.0.2" + optionator@^0.8.1: version "0.8.3" resolved "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz" @@ -9780,6 +9813,13 @@ react-app-polyfill@^3.0.0: regenerator-runtime "^0.13.9" whatwg-fetch "^3.6.2" +react-confetti@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/react-confetti/-/react-confetti-6.1.0.tgz#03dc4340d955acd10b174dbf301f374a06e29ce6" + integrity sha512-7Ypx4vz0+g8ECVxr88W9zhcQpbeujJAVqL14ZnXJ3I23mOI9/oBVTQ3dkJhUmB0D6XOtCZEM6N0Gm9PMngkORw== + dependencies: + tween-functions "^1.2.0" + react-copy-to-clipboard@^5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz" @@ -9790,7 +9830,7 @@ react-copy-to-clipboard@^5.1.0: react-csv@^2.2.2: version "2.2.2" - resolved "https://registry.npmjs.org/react-csv/-/react-csv-2.2.2.tgz" + resolved "https://registry.yarnpkg.com/react-csv/-/react-csv-2.2.2.tgz#5bbf0d72a846412221a14880f294da9d6def9bfb" integrity sha512-RG5hOcZKZFigIGE8LxIEV/OgS1vigFQT4EkaHeKgyuCbUAu9Nbd/1RYq++bJcJJ9VOqO/n9TZRADsXNDR4VEpw== react-dev-utils@^12.0.1: @@ -9836,6 +9876,11 @@ react-error-overlay@^6.0.11: resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz" integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== +react-fast-marquee@^1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/react-fast-marquee/-/react-fast-marquee-1.6.3.tgz#a1f3a0ce3b298c395766d19051aa8fa3af9f1a1c" + integrity sha512-oEISmNElv6lua/4i4uPYIteUKDxU0hAKKjH/tY2icje4GCns1rX6pIrkwVhjX0FMCIepUVqeyCchvqkiO/s2vw== + react-google-charts@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/react-google-charts/-/react-google-charts-4.0.1.tgz#b7713856a48009b77318f8951ddf2c3ba39f991b" @@ -11191,6 +11236,11 @@ tsutils@^3.21.0: dependencies: tslib "^1.8.1" +tween-functions@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/tween-functions/-/tween-functions-1.2.0.tgz#1ae3a50e7c60bb3def774eac707acbca73bbc3ff" + integrity sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA== + tweetnacl@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz" @@ -11266,6 +11316,13 @@ uint8arrays@^3.0.0, uint8arrays@^3.1.0: dependencies: multiformats "^9.4.2" +ulujs@^0.2.6: + version "0.2.6" + resolved "https://registry.yarnpkg.com/ulujs/-/ulujs-0.2.6.tgz#7de76c447435d1e2dd4dfe952da275da98a1233e" + integrity sha512-cbqFL+XgwhOBJtNvMmyDTS6IdsIjqXztzu69g/KfaLwjTz+LgtB2w5X6GxN4FM8fzBBAYVz7mgHVxk2fYZsf/A== + dependencies: + arccjs "^2.7.0" + unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz" @@ -11739,6 +11796,11 @@ word-wrap@^1.2.3, word-wrap@~1.2.3: resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + integrity sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw== + workbox-background-sync@6.5.4: version "6.5.4" resolved "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.5.4.tgz"