diff --git a/package-lock.json b/package-lock.json index 5ad262f..94634b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ "react-infinite-scroller": "^1.2.6", "react-intersection-observer": "^9.13.0", "react-json-view-lite": "^2.0.1", + "react-loader-spinner": "^6.1.6", "react-qr-code": "^2.0.15", "react-quill": "^2.0.0", "react-redux": "^9.1.2", @@ -2887,6 +2888,11 @@ "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, + "node_modules/@types/stylis": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -3593,6 +3599,14 @@ "node": ">=6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001599", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001599.tgz", @@ -4027,6 +4041,24 @@ "node": ">= 8" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -6372,7 +6404,6 @@ "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "devOptional": true, "funding": [ { "type": "github", @@ -9106,10 +9137,9 @@ } }, "node_modules/postcss": { - "version": "8.4.37", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.37.tgz", - "integrity": "sha512-7iB/v/r7Woof0glKLH8b1SPHrsX7uhdO+Geb41QpF/+mWZHU3uxxSlN+UXGVit1PawOYDToO+AbZzhBzWRDwbQ==", - "devOptional": true, + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "funding": [ { "type": "opencollective", @@ -9133,6 +9163,11 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -9631,6 +9666,27 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "node_modules/react-loader-spinner": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/react-loader-spinner/-/react-loader-spinner-6.1.6.tgz", + "integrity": "sha512-x5h1Jcit7Qn03MuKlrWcMG9o12cp9SNDVHVJTNRi9TgtGPKcjKiXkou4NRfLAtXaFB3+Z8yZsVzONmPzhv2ErA==", + "dependencies": { + "react-is": "^18.2.0", + "styled-components": "^6.1.2" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-loader-spinner/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, "node_modules/react-qr-code": { "version": "2.0.15", "resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.15.tgz", @@ -10258,6 +10314,11 @@ "node": ">= 0.4" } }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -10358,7 +10419,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", - "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -10491,6 +10551,38 @@ "devOptional": true, "license": "MIT" }, + "node_modules/styled-components": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.13.tgz", + "integrity": "sha512-M0+N2xSnAtwcVAQeFEsGWFFxXDftHUD7XrKla06QbpUMmbmtFBMMTcKWvFXtWxuD5qQkB8iU5gk6QASlx2ZRMw==", + "dependencies": { + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", + "css-to-react-native": "3.2.0", + "csstype": "3.1.3", + "postcss": "8.4.38", + "shallowequal": "1.1.0", + "stylis": "4.3.2", + "tslib": "2.6.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/styled-components/node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==" + }, "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", diff --git a/package.json b/package.json index 7d4c08e..cf326b5 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "file-saver": "^2.0.5", "html-to-text": "^9.0.5", "jssha": "3.3.1", + "lit": "^3.2.1", "lodash": "^4.17.21", "mime": "^4.0.4", "moment": "^2.30.1", @@ -58,6 +59,7 @@ "react-infinite-scroller": "^1.2.6", "react-intersection-observer": "^9.13.0", "react-json-view-lite": "^2.0.1", + "react-loader-spinner": "^6.1.6", "react-qr-code": "^2.0.15", "react-quill": "^2.0.0", "react-redux": "^9.1.2", @@ -65,7 +67,6 @@ "react-virtuoso": "^4.10.4", "recoil": "^0.7.7", "short-unique-id": "^5.2.0", - "lit": "^3.2.1", "slate": "^0.103.0", "slate-react": "^0.109.0", "tiptap-extension-resize-image": "^1.1.8", diff --git a/src/App.tsx b/src/App.tsx index 11b5341..c0043de 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -46,6 +46,7 @@ import CloseIcon from "@mui/icons-material/Close"; import { JsonView, allExpanded, darkStyles } from 'react-json-view-lite'; import 'react-json-view-lite/dist/index.css'; import HelpIcon from '@mui/icons-material/Help'; +import EngineeringIcon from '@mui/icons-material/Engineering'; import { createAccount, @@ -120,6 +121,8 @@ import { Wallets } from "./Wallets"; import './utils/seedPhrase/RandomSentenceGenerator'; import { test } from "vitest"; import { useHandleUserInfo } from "./components/Group/useHandleUserInfo"; +import { Minting } from "./components/Minting/Minting"; +import { isRunningGateway } from "./qortalRequests"; type extStates = | "not-authenticated" @@ -329,6 +332,8 @@ function App() { const {downloadResource} = useFetchResources() const [showSeed, setShowSeed] = useState(false) const [creationStep, setCreationStep] = useState(1) + const [isOpenMinting, setIsOpenMinting] = useState(false) + const setIsDisabledEditorEnter = useSetRecoilState(isDisabledEditorEnterAtom) const { isShow: isShowInfo, @@ -1651,6 +1656,54 @@ function App() { alignItems: 'center' }} > + {extState === "authenticated" && isMainWindow && ( + + + + )} + + { + try { + const res = await isRunningGateway() + if(res) throw new Error('Cannot view minting details on the gateway') + setIsOpenMinting(true) + + } catch (error) { + setOpenSnack(true) + setInfoSnack({ + type: 'error', + message: error?.message + }) + } + }}> + + + + + {(desktopViewMode === "apps" || desktopViewMode === "home") && ( { if(desktopViewMode === "apps"){ @@ -1755,17 +1808,7 @@ function App() { {!isMobile && renderProfile()} - - - + )} {isOpenSendQort && isMainWindow && ( @@ -3306,6 +3349,9 @@ function App() { }} /> )} + {isOpenMinting && ( + + )} ); } diff --git a/src/background-cases.ts b/src/background-cases.ts new file mode 100644 index 0000000..c1b64c2 --- /dev/null +++ b/src/background-cases.ts @@ -0,0 +1,82 @@ +import { getKeyPair, getLastRef, processTransactionVersion2 } from "./background"; +import Base58 from "./deps/Base58"; +import { createTransaction } from "./transactions/transactions"; + + +export async function createRewardShareCase(data) { + const {recipientPublicKey} = data; + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + let lastRef = await getLastRef(); + + const tx = await createTransaction(38, keyPair, { + recipientPublicKey, + percentageShare: 0, + lastReference: lastRef, + }); + + const signedBytes = Base58.encode(tx.signedBytes); + + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error("Transaction was not able to be processed"); + return res + + } + + export async function removeRewardShareCase(data) { + + const {rewardShareKeyPairPublicKey, recipient, percentageShare} = data; + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + let lastRef = await getLastRef(); + + const tx = await createTransaction(381, keyPair, { + rewardShareKeyPairPublicKey, + recipient, + percentageShare, + lastReference: lastRef, + }); + + const signedBytes = Base58.encode(tx.signedBytes); + + const res = await processTransactionVersion2(signedBytes); + if (!res?.signature) + throw new Error("Transaction was not able to be processed"); + return res + + } + + + export async function getRewardSharePrivateKeyCase(data) { + const {recipientPublicKey} = data + const resKeyPair = await getKeyPair(); + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); + const uint8PublicKey = Base58.decode(parsedData.publicKey); + const keyPair = { + privateKey: uint8PrivateKey, + publicKey: uint8PublicKey, + }; + let lastRef = await getLastRef(); + + const tx = await createTransaction(38, keyPair, { + recipientPublicKey, + percentageShare: 0, + lastReference: lastRef, + }); + + return tx?._base58RewardShareSeed + } diff --git a/src/background.ts b/src/background.ts index a772298..32dbe0f 100644 --- a/src/background.ts +++ b/src/background.ts @@ -32,6 +32,7 @@ import { Sha256 } from "asmcrypto.js"; import { TradeBotRespondMultipleRequest } from "./transactions/TradeBotRespondMultipleRequest"; import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from "./constants/resourceTypes"; import TradeBotRespondRequest from './transactions/TradeBotRespondRequest'; +import { createRewardShareCase, getRewardSharePrivateKeyCase, removeRewardShareCase } from './background-cases'; @@ -1258,7 +1259,7 @@ export const sendQortFee = async (): Promise => { return qortFee; }; -async function getNameOrAddress(receiver) { +export async function getNameOrAddress(receiver) { try { const isAddress = validateAddress(receiver); if (isAddress) { @@ -4633,6 +4634,48 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { break; } + case "createRewardShare": { + const { + recipientPublicKey + } = request.payload; + createRewardShareCase({recipientPublicKey}) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error?.message }); + }); + + break; + } + case "getRewardSharePrivateKey": { + const { + recipientPublicKey + } = request.payload; + getRewardSharePrivateKeyCase({recipientPublicKey}) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error?.message }); + }); + + break; + } + case "removeRewardShare": { + const { + rewardShareKeyPairPublicKey, recipient, percentageShare + } = request.payload; + removeRewardShareCase({rewardShareKeyPairPublicKey, recipient, percentageShare}) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error?.message }); + }); + + break; + } case "addEnteredQmailTimestamp": { addEnteredQmailTimestamp() diff --git a/src/components/Minting/Minting.tsx b/src/components/Minting/Minting.tsx new file mode 100644 index 0000000..87e0852 --- /dev/null +++ b/src/components/Minting/Minting.tsx @@ -0,0 +1,1198 @@ +import { + Alert, + Box, + Button, + Card, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + IconButton, + InputBase, + InputLabel, + Snackbar, + Typography, +} from "@mui/material"; +import React, { + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from "react"; +import CloseIcon from "@mui/icons-material/Close"; +import { MyContext, getBaseApiReact } from "../../App"; +import { + executeEvent, + subscribeToEvent, + unsubscribeFromEvent, +} from "../../utils/events"; +import { getFee, getNameOrAddress } from "../../background"; +import CopyToClipboard from "react-copy-to-clipboard"; +import { AddressBox } from "../../App-styles"; +import { Spacer } from "../../common/Spacer"; +import Copy from "../../assets/svgs/Copy.svg"; +import { Loader } from "../Loader"; +import { FidgetSpinner } from "react-loader-spinner"; +import { useModal } from "../../common/useModal"; + +export const Minting = ({ + setIsOpenMinting, + myAddress, + groups, + show, + setTxList, + txList, +}) => { + const [mintingAccounts, setMintingAccounts] = useState([]); + const [accountInfo, setAccountInfo] = useState(null); + const [rewardSharePublicKey, setRewardSharePublicKey] = useState(""); + const [mintingKey, setMintingKey] = useState(""); + const [rewardsharekey, setRewardsharekey] = useState(""); + const [rewardShares, setRewardShares] = useState([]); + const [nodeInfos, setNodeInfos] = useState({}); + const [openSnack, setOpenSnack] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const { isShow, onCancel, onOk, show: showKey, message } = useModal(); + const [info, setInfo] = useState(null); + const [names, setNames] = useState({}); + const [accountInfos, setAccountInfos] = useState({}); + + + + + const isPartOfMintingGroup = useMemo(() => { + if (groups?.length === 0) return false; + return !!groups?.find((item) => item?.groupId?.toString() === "694"); + }, [groups]); + const getMintingAccounts = useCallback(async () => { + try { + const url = `${getBaseApiReact()}/admin/mintingaccounts`; + const response = await fetch(url); + if (!response.ok) { + throw new Error("network error"); + } + const data = await response.json(); + setMintingAccounts(data); + } catch (error) {} + }, []); + + const accountIsMinting = useMemo(() => { + return !!mintingAccounts?.find( + (item) => item?.recipientAccount === myAddress + ); + }, [mintingAccounts, myAddress]); + + const getName = async (address) => { + try { + const response = await fetch( + `${getBaseApiReact()}/names/address/${address}` + ); + const nameData = await response.json(); + if (nameData?.length > 0) { + setNames((prev) => { + return { + ...prev, + [address]: nameData[0].name, + }; + }); + } else { + setNames((prev) => { + return { + ...prev, + [address]: null, + }; + }); + } + } catch (error) { + // error + } + }; + + const getAccountInfo = async (address: string, others?: boolean) => { + try { + if (!others) { + setIsLoading(true); + } + const url = `${getBaseApiReact()}/addresses/${address}`; + const response = await fetch(url); + if (!response.ok) { + throw new Error("network error"); + } + const data = await response.json(); + if (others) { + setAccountInfos((prev) => { + return { + ...prev, + [address]: data, + }; + }); + } else { + setAccountInfo(data); + } + } catch (error) { + } finally { + if (!others) { + setIsLoading(false); + } + } + }; + + const refreshRewardShare = () => { + if (!myAddress) return; + getRewardShares(myAddress); + }; + + useEffect(() => { + subscribeToEvent("refresh-rewardshare-list", refreshRewardShare); + + return () => { + unsubscribeFromEvent("refresh-rewardshare-list", refreshRewardShare); + }; + }, [myAddress]); + + const handleNames = (address) => { + if (!address) return undefined; + if (names[address]) return names[address]; + if (names[address] === null) return address; + getName(address); + return address; + }; + + const handleAccountInfos = (address, field) => { + if (!address) return undefined; + if (accountInfos[address]) return accountInfos[address]?.[field]; + if (accountInfos[address] === null) return undefined; + getAccountInfo(address, true); + return undefined; + }; + + const calculateBlocksRemainingToLevel1 = (address) => { + if (!address) return undefined; + if (!accountInfos[address]) return undefined; + return 7200 - accountInfos[address]?.blocksMinted || 0; + }; + + const getNodeInfos = async () => { + try { + const url = `${getBaseApiReact()}/admin/status`; + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const data = await response.json(); + setNodeInfos(data); + } catch (error) { + console.error("Request failed", error); + } + }; + + const getRewardShares = useCallback(async (address) => { + try { + const url = `${getBaseApiReact()}/addresses/rewardshares?involving=${address}`; + const response = await fetch(url); + if (!response.ok) { + throw new Error("network error"); + } + const data = await response.json(); + setRewardShares(data); + } catch (error) {} + }, []); + + const addMintingAccount = useCallback(async (val) => { + try { + setIsLoading(true); + return await new Promise((res, rej) => { + chrome?.runtime?.sendMessage( + { + action: "ADMIN_ACTION", + type: "qortalRequest", + payload: { + type: "addmintingaccount", + value: val, + }, + }, + (response) => { + if (response.error) { + rej({ message: response.error }); + return; + } else { + res(response); + setMintingKey(""); + setTimeout(() => { + getMintingAccounts(); + }, 300); + } + } + ); + }); + } catch (error) { + setInfo({ + type: "error", + message: error?.message || "Unable to add minting account", + }); + setOpenSnack(true); + } finally { + setIsLoading(false); + } + }, []); + + const removeMintingAccount = useCallback(async (val, acct) => { + try { + setIsLoading(true); + return await new Promise((res, rej) => { + + chrome?.runtime?.sendMessage( + { + action: "ADMIN_ACTION", + type: "qortalRequest", + payload: { + type: "removemintingaccount", + value: val, + }, + }, + (response) => { + if (response.error) { + rej({ message: response.error }); + return; + } else { + res(response); + + setTimeout(() => { + getMintingAccounts(); + }, 300); + } + } + ); + }); + } catch (error) { + setInfo({ + type: "error", + message: error?.message || "Unable to remove minting account", + }); + setOpenSnack(true); + } finally { + setIsLoading(false); + } + }, []); + + const createRewardShare = useCallback(async (publicKey, recipient) => { + const fee = await getFee("REWARD_SHARE"); + await show({ + message: "Would you like to perform an REWARD_SHARE transaction?", + publishFee: fee.fee + " QORT", + }); + return await new Promise((res, rej) => { + + + chrome?.runtime?.sendMessage( + { + action: "createRewardShare", + payload: { + recipientPublicKey: publicKey, + }, + }, + (response) => { + if (response?.error) { + rej({ message: response.error }); + return + } else { + setTxList((prev) => [ + { + recipient, + ...response, + type: "add-rewardShare", + label: `Add rewardshare: awaiting confirmation`, + labelDone: `Add rewardshare: success!`, + done: false, + }, + ...prev, + ]); + res(response); + return; + }; + } + ); + }); + }, []); + + const getRewardSharePrivateKey = useCallback(async (publicKey) => { + return await new Promise((res, rej) => { + chrome?.runtime?.sendMessage( + { + action: "getRewardSharePrivateKey", + payload: { + recipientPublicKey: publicKey, + }, + }, + (response) => { + if (response?.error) { + rej({ message: response.error }); + return + } else { + res(response); + return; + }; + } + ); + }); + }, []); + + const startMinting = async () => { + try { + setIsLoading(true); + const findRewardShare = rewardShares?.find( + (item) => + item?.recipient === myAddress && item?.mintingAccount === myAddress + ); + if (findRewardShare) { + const privateRewardShare = await getRewardSharePrivateKey( + accountInfo?.publicKey + ); + addMintingAccount(privateRewardShare); + } else { + await createRewardShare(accountInfo?.publicKey, myAddress); + const privateRewardShare = await getRewardSharePrivateKey( + accountInfo?.publicKey + ); + addMintingAccount(privateRewardShare); + } + } catch (error) { + setInfo({ + type: "error", + message: error?.message || "Unable to start minting", + }); + setOpenSnack(true); + } finally { + setIsLoading(false); + } + }; + + const getPublicKeyFromAddress = async (address) => { + const url = `${getBaseApiReact()}/addresses/publickey/${address}`; + const response = await fetch(url); + const data = await response.text(); + return data; + }; + + const checkIfMinterGroup = async (address) => { + const url = `${getBaseApiReact()}/groups/member/${address}`; + const response = await fetch(url); + const data = await response.json(); + return !!data?.find((grp) => grp?.groupId?.toString() === "694"); + }; + + const removeRewardShare = useCallback(async (rewardShare) => { + return await new Promise((res, rej) => { + + chrome?.runtime?.sendMessage( + { + action: "removeRewardShare", + payload: { + rewardShareKeyPairPublicKey: rewardShare.rewardSharePublicKey, + recipient: rewardShare.recipient, + percentageShare: -1, + }, + }, + (response) => { + if (response?.error) { + rej({ message: response.error }); + return + } else { + res(response); + setTxList((prev) => [ + { + ...rewardShare, + ...response, + type: "remove-rewardShare", + label: `Remove rewardshare: awaiting confirmation`, + labelDone: `Remove rewardshare: success!`, + done: false, + }, + ...prev, + ]); + return; + }; + } + ); + }); + }, []); + + const handleRemoveRewardShare = async (rewardShare) => { + try { + setIsLoading(true); + + const privateRewardShare = await removeRewardShare(rewardShare); + } catch (error) { + setInfo({ + type: "error", + message: error?.message || "Unable to remove reward share", + }); + setOpenSnack(true); + } finally { + setIsLoading(false); + } + }; + + const createRewardShareForPotentialMinter = async (receiver) => { + try { + setIsLoading(true); + const confirmReceiver = await getNameOrAddress(receiver); + if (confirmReceiver.error) + throw new Error("Invalid receiver address or name"); + const isInMinterGroup = await checkIfMinterGroup(confirmReceiver) + if(!isInMinterGroup) throw new Error('Account not in Minter Group') + const publicKey = await getPublicKeyFromAddress(confirmReceiver); + const findRewardShare = rewardShares?.find( + (item) => + item?.recipient === confirmReceiver && + item?.mintingAccount === myAddress + ); + if (findRewardShare) { + const privateRewardShare = await getRewardSharePrivateKey(publicKey); + setRewardsharekey(privateRewardShare); + } else { + await createRewardShare(publicKey, confirmReceiver); + const privateRewardShare = await getRewardSharePrivateKey(publicKey); + setRewardsharekey(privateRewardShare); + } + } catch (error) { + setInfo({ + type: "error", + message: error?.message || "Unable to create reward share", + }); + setOpenSnack(true); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + getNodeInfos(); + getMintingAccounts(); + }, []); + + useEffect(() => { + if (!myAddress) return; + getRewardShares(myAddress); + + getAccountInfo(myAddress); + }, [myAddress]); + + const _blocksNeed = () => { + if (accountInfo?.level === 0) { + return 7200; + } else if (accountInfo?.level === 1) { + return 72000; + } else if (accountInfo?.level === 2) { + return 201600; + } else if (accountInfo?.level === 3) { + return 374400; + } else if (accountInfo?.level === 4) { + return 618400; + } else if (accountInfo?.level === 5) { + return 964000; + } else if (accountInfo?.level === 6) { + return 1482400; + } else if (accountInfo?.level === 7) { + return 2173600; + } else if (accountInfo?.level === 8) { + return 3037600; + } else if (accountInfo?.level === 9) { + return 4074400; + } + }; + + const handleClose = () => { + setOpenSnack(false); + setTimeout(() => { + setInfo(null); + }, 250); + }; + + const _levelUpBlocks = () => { + if ( + accountInfo?.blocksMinted === undefined || + nodeInfos?.height === undefined + ) + return null; + let countBlocks = + _blocksNeed() - + (accountInfo?.blocksMinted + accountInfo?.blocksMintedAdjustment); + + let countBlocksString = countBlocks.toString(); + return "" + countBlocksString; + }; + + const showAndCopySponsorshipKey = async (rs) => { + try { + const sponsorshipKey = await getRewardSharePrivateKey( + rs?.rewardSharePublicKey + ); + await showKey({ + message: sponsorshipKey, + }); + } catch (error) {} + }; + + + return ( + + {"Manage your minting"} + + {isLoading && ( + + + + )} + + Account: {handleNames(accountInfo?.address)} + Level: {accountInfo?.level} + + blocks remaining until next level: {_levelUpBlocks()} + + + This node is minting: {nodeInfos?.isMintingPossible?.toString()} + + + + {accountInfo?.level >= 1 && !accountIsMinting && ( + + + {mintingAccounts?.length > 1 && ( + + Only 2 minting keys are allowed per node. Please remove one if + you would like to mint with this account. + + )} + + )} + + {mintingAccounts?.length > 0 && ( + Node's minting accounts + )} + + {accountIsMinting && ( + + + You currently have a minting key for this account attached to + this node + + + )} + + {mintingAccounts?.map((acct) => ( + + + Minting account: {handleNames(acct?.mintingAccount)} + + + Recipient account: {handleNames(acct?.recipientAccount)} + + {acct?.mintingAccount !== accountInfo?.address && + acct?.recipientAccount === accountInfo?.address && + (accountInfo?.level || 0) > 0 && ( + + You have reached level 1+. Remove this minting key and then + click "Start Minting". + + )} + + + + + ))} + + {mintingAccounts?.length > 1 && ( + + Only 2 minting keys are allowed per node. Please remove one if you + would like to add a different account. + + )} + + {txList?.filter( + (item) => + !item?.done && + (item?.type === "remove-rewardShare" || + item?.type === "add-rewardShare") + )?.length > 0 && ( + <> + + Ongoing transactions + + + {txList + ?.filter( + (item) => + !item.done && + (item?.type === "remove-rewardShare" || + item?.type === "add-rewardShare") + ) + ?.map((txItem) => ( + + {txItem?.type === "remove-rewardShare" && ( + Reward share being removed + )} + {txItem?.type === "add-rewardShare" && ( + Reward share being created + )} + + Recipient: {handleNames(txItem?.recipient)} + + + + + + ))} + + + + )} + + {!isPartOfMintingGroup && ( + + + + You are currently not part of the MINTER group + + + Visit the Q-Mintership app to apply to be a minter + + + + + + )} + {isPartOfMintingGroup && ( + <> + {accountInfo?.level >= 5 && ( + + {rewardShares?.filter((item) => item?.recipient !== myAddress) + ?.length > 0 && ( + <> + Active sponsorships + + {rewardShares + ?.filter((item) => item?.recipient !== myAddress) + .map((rs) => ( + + + Recipient: {handleNames(rs?.recipient)} + + + Level:{" "} + {handleAccountInfos(rs?.recipient, "level")} + + {handleAccountInfos(rs?.recipient, "level") !== + undefined && ( + <> + {handleAccountInfos(rs?.recipient, "level") === + 0 && ( + + Blocks remaining until level 1:{" "} + {calculateBlocksRemainingToLevel1( + rs?.recipient + )} + + )} + {(handleAccountInfos(rs?.recipient, "level") || + 0) > 0 && ( + + This account is above level 0. You may + remove this rewardshare + + )} + + )} + + + + + + ))} + + + )} + + + + Sponsor a new Minter + + {rewardShares?.filter((item) => item?.recipient !== myAddress) + ?.length > 0 ? ( + <> + + You are currently sponsoring one account. To sponsor + another account please remove the existing reward share. + + + ) : ( + <> + + Enter in the new Minter's address or name into the + input. Next, click on "Create reward share". If + successful, you will see a rewardshare key generated. + Copy the key and send it to your new Minter. + + + + setRewardSharePublicKey(e.target.value) + } + sx={{ + border: "0.5px solid var(--50-white, #FFFFFF80)", + padding: "0px 15px", + borderRadius: "5px", + height: "36px", + width: "350px", + maxWidth: "95%", + }} + placeholder="New minter's address or name" + inputProps={{ + "aria-label": "New minter's address or name", + fontSize: "14px", + fontWeight: 400, + }} + /> + + {rewardsharekey && ( + <> + + + + Click to copy the reward share key and share it with + your new minter + + + + + {rewardsharekey} + + + + )} + + )} + + + )} + {accountInfo?.level === 0 && !accountIsMinting && ( + <> + Become a minter! + + + + Ask a level 5+ minter to send you a minting key + + + Add the minting key in the input below and click "Add + minting key" + + + setMintingKey(e.target.value)} + sx={{ + border: "0.5px solid var(--50-white, #FFFFFF80)", + padding: "0px 15px", + borderRadius: "5px", + height: "36px", + width: "250px", + }} + placeholder="Add minting key" + inputProps={{ + "aria-label": "Add minting key", + fontSize: "14px", + fontWeight: 400, + }} + /> + + + + + )} + {accountInfo?.level === 0 && accountIsMinting && ( + + + You are currently on your way to level 1 + + + )} + + )} + {isShow && ( + + + {"Copy sponsorship key"} + + + + Click to copy the reward share key and share it with your new + minter + + + + + {message?.message} + + + + + + + + )} + + + + + + + {info?.message} + + + + ); +}; diff --git a/src/components/TaskManager/TaskManger.tsx b/src/components/TaskManager/TaskManger.tsx index 35e0a51..24eead1 100644 --- a/src/components/TaskManager/TaskManger.tsx +++ b/src/components/TaskManager/TaskManger.tsx @@ -12,6 +12,7 @@ import TaskAltIcon from "@mui/icons-material/TaskAlt"; import ExpandLess from "@mui/icons-material/ExpandLess"; import ExpandMore from "@mui/icons-material/ExpandMore"; import { MyContext, getBaseApiReact, isMobile } from "../../App"; +import { executeEvent } from "../../utils/events"; export const TaskManger = ({ getUserInfo }) => { const { txList, setTxList, memberGroups } = useContext(MyContext); @@ -39,7 +40,7 @@ export const TaskManger = ({ getUserInfo }) => { await new Promise((res) => setTimeout(() => { res(null); - }, 300000) + }, 60000) ); setTxList((prev) => { let previousData = [...prev]; @@ -62,7 +63,7 @@ export const TaskManger = ({ getUserInfo }) => { } }; - intervals.current[signature] = setInterval(getAnswer, 120000); + intervals.current[signature] = setInterval(getAnswer, 60000); }; useEffect(() => { @@ -96,7 +97,15 @@ export const TaskManger = ({ getUserInfo }) => { } }); - prev.forEach((tx) => { + + + return previousData; + }); + }, [memberGroups, getUserInfo]); + + useEffect(()=> { + + txList.forEach((tx) => { if ( ["created-common-secret", "joined-group-request", "join-request-accept"].includes( tx?.type @@ -113,11 +122,17 @@ export const TaskManger = ({ getUserInfo }) => { getStatus({ signature: tx.signature }, getUserInfo); } } + if((tx?.type === "remove-rewardShare" || tx?.type === "add-rewardShare") && tx?.signature && !tx.done){ + if (!intervals.current[tx.signature]) { + const sendEventForRewardShare = ()=> { + executeEvent('refresh-rewardshare-list', {}) + } + getStatus({ signature: tx.signature }, sendEventForRewardShare); + } + } }); - return previousData; - }); - }, [memberGroups, getUserInfo]); + }, [txList]) if (isMobile || txList?.length === 0 || txList.every((item) => item?.done)) return null; @@ -128,9 +143,9 @@ export const TaskManger = ({ getUserInfo }) => { = DYNAMIC_FEE_TIMESTAMP) { + this.fee = _address === recipient ? 0 : 0.01 + } else { + this.fee = _address === recipient ? 0 : 0.001 + } + } + + set percentageShare(share) { + this._percentageShare = share * 100 + this._percentageShareBytes = this.constructor.utils.int64ToBytes(this._percentageShare) + } + + get params() { + const params = super.params + params.push( + this._recipient, + this._rewardShareKeyPairPublicKey, + this._percentageShareBytes, + this._feeBytes + ) + return params + } +} diff --git a/src/transactions/RewardShareTransaction.ts b/src/transactions/RewardShareTransaction.ts new file mode 100644 index 0000000..8419432 --- /dev/null +++ b/src/transactions/RewardShareTransaction.ts @@ -0,0 +1,60 @@ +// @ts-nocheck + +import TransactionBase from './TransactionBase' + +import { Sha256 } from 'asmcrypto.js' +import nacl from '../deps/nacl-fast' +import ed2curve from '../deps/ed2curve' +import { DYNAMIC_FEE_TIMESTAMP } from '../constants/constants' +import publicKeyToAddress from '../utils/generateWallet/publicKeyToAddress' + +export default class RewardShareTransaction extends TransactionBase { + constructor() { + super() + this.type = 38 + } + + + + set recipientPublicKey(recipientPublicKey) { + this._base58RecipientPublicKey = recipientPublicKey instanceof Uint8Array ? this.constructor.Base58.encode(recipientPublicKey) : recipientPublicKey + this._recipientPublicKey = this.constructor.Base58.decode(this._base58RecipientPublicKey) + this.recipient = publicKeyToAddress(this._recipientPublicKey) + + const convertedPrivateKey = ed2curve.convertSecretKey(this._keyPair.privateKey) + const convertedPublicKey = ed2curve.convertPublicKey(this._recipientPublicKey) + const sharedSecret = new Uint8Array(32) + + nacl.lowlevel.crypto_scalarmult(sharedSecret, convertedPrivateKey, convertedPublicKey) + + this._rewardShareSeed = new Sha256().process(sharedSecret).finish().result + this._base58RewardShareSeed = this.constructor.Base58.encode(this._rewardShareSeed) + this._rewardShareKeyPair = nacl.sign.keyPair.fromSeed(this._rewardShareSeed) + + if (new Date(this._timestamp).getTime() >= DYNAMIC_FEE_TIMESTAMP) { + this.fee = (recipientPublicKey === this.constructor.Base58.encode(this._keyPair.publicKey) ? 0 : 0.01) + } else { + this.fee = (recipientPublicKey === this.constructor.Base58.encode(this._keyPair.publicKey) ? 0 : 0.001) + } + } + + set recipient(recipient) { + this._recipient = recipient instanceof Uint8Array ? recipient : this.constructor.Base58.decode(recipient) + } + + set percentageShare(share) { + this._percentageShare = share * 100 + this._percentageShareBytes = this.constructor.utils.int64ToBytes(this._percentageShare) + } + + get params() { + const params = super.params + params.push( + this._recipient, + this._rewardShareKeyPair.publicKey, + this._percentageShareBytes, + this._feeBytes + ) + return params + } +} diff --git a/src/transactions/transactions.ts b/src/transactions/transactions.ts index 400418c..cbf582f 100644 --- a/src/transactions/transactions.ts +++ b/src/transactions/transactions.ts @@ -17,7 +17,8 @@ import RegisterNameTransaction from './RegisterNameTransaction.js' import VoteOnPollTransaction from './VoteOnPollTransaction.js' import CreatePollTransaction from './CreatePollTransaction.js' import DeployAtTransaction from './DeployAtTransaction.js' - +import RewardShareTransaction from './RewardShareTransaction.js' +import RemoveRewardShareTransaction from './RemoveRewardShareTransaction.js' export const transactionTypes = { 3: RegisterNameTransaction, @@ -36,7 +37,9 @@ export const transactionTypes = { 29: GroupInviteTransaction, 30: CancelGroupInviteTransaction, 31: JoinGroupTransaction, - 32: LeaveGroupTransaction + 32: LeaveGroupTransaction, + 38: RewardShareTransaction, + 381: RemoveRewardShareTransaction }