From de3933b04b13fc034f7eb15ed2144e2d569e8d3d Mon Sep 17 00:00:00 2001 From: PhilReact Date: Fri, 3 Jan 2025 15:15:30 +0200 Subject: [PATCH] added minting page --- package-lock.json | 105 +- package.json | 1 + src/App.tsx | 33 +- src/background-cases.ts | 140 +- src/background.ts | 14 +- src/components/Minting/Minting.tsx | 1191 +++++++++++++++++ src/components/TaskManager/TaskManger.tsx | 74 +- .../RemoveRewardShareTransaction.ts | 46 + src/transactions/RewardShareTransaction.ts | 60 + src/transactions/transactions.ts | 7 +- 10 files changed, 1622 insertions(+), 49 deletions(-) create mode 100644 src/components/Minting/Minting.tsx create mode 100644 src/transactions/RemoveRewardShareTransaction.ts create mode 100644 src/transactions/RewardShareTransaction.ts diff --git a/package-lock.json b/package-lock.json index fce63ce..3efb05f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "qortal-go", - "version": "0.3.9", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "qortal-go", - "version": "0.3.9", + "version": "0.4.0", "dependencies": { "@capacitor/android": "^6.1.2", "@capacitor/app": "^6.0.1", @@ -68,6 +68,7 @@ "react-frame-component": "^5.2.7", "react-infinite-scroller": "^1.2.6", "react-intersection-observer": "^9.13.0", + "react-loader-spinner": "^6.1.6", "react-qr-code": "^2.0.15", "react-quick-pinch-zoom": "^5.1.0", "react-quill": "^2.0.0", @@ -4771,6 +4772,11 @@ "resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz", "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==" }, + "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", @@ -5633,6 +5639,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.30001674", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001674.tgz", @@ -6138,6 +6152,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", @@ -11430,9 +11462,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==", + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "funding": [ { "type": "opencollective", @@ -11456,6 +11488,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", @@ -11967,6 +12004,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", @@ -12713,6 +12771,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", @@ -13029,6 +13092,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 133a04a..83cf662 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "react-frame-component": "^5.2.7", "react-infinite-scroller": "^1.2.6", "react-intersection-observer": "^9.13.0", + "react-loader-spinner": "^6.1.6", "react-qr-code": "^2.0.15", "react-quick-pinch-zoom": "^5.1.0", "react-quill": "^2.0.0", diff --git a/src/App.tsx b/src/App.tsx index b189cb4..6dff4f0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -82,6 +82,8 @@ import { LoadingButton } from "@mui/lab"; import { Label } from "./components/Group/AddGroup"; import { CustomizedSnackbars } from "./components/Snackbar/Snackbar"; import SettingsIcon from "@mui/icons-material/Settings"; +import EngineeringIcon from '@mui/icons-material/Engineering'; + import { cleanUrl, getFee, @@ -128,6 +130,8 @@ import { useHandleTutorials } from "./components/Tutorials/useHandleTutorials"; import { Tutorials } from "./components/Tutorials/Tutorials"; import BoundedNumericTextField from "./common/BoundedNumericTextField"; import { useHandleUserInfo } from "./components/Group/useHandleUserInfo"; +import { Minting } from "./components/Minting/Minting"; +import { isRunningGateway } from "./qortalRequests"; type extStates = @@ -389,6 +393,8 @@ function App() { const [authenticatePassword, setAuthenticatePassword] = useState(""); const [sendqortState, setSendqortState] = useState(null); const [isLoading, setIsLoading] = useState(false); + const [isOpenMinting, setIsOpenMinting] = useState(false) + const [ walletToBeDownloadedPasswordConfirm, setWalletToBeDownloadedPasswordConfirm, @@ -1618,6 +1624,29 @@ function App() { + + + { + try { + setIsOpenDrawerProfile(false); + 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 + }) + } + }}> + + + + { @@ -1688,7 +1717,6 @@ function App() { ); }; -console.log('openTutorialModal3', openTutorialModal) return ( )} + {isOpenMinting && ( + + )} ); } diff --git a/src/background-cases.ts b/src/background-cases.ts index 1fe2fa2..bcf673f 100644 --- a/src/background-cases.ts +++ b/src/background-cases.ts @@ -26,6 +26,7 @@ import { getGroupDataSingle, getKeyPair, getLTCBalance, + getLastRef, getNameInfo, getTempPublish, getTimestampEnterChat, @@ -41,6 +42,7 @@ import { makeAdmin, notifyAdminRegenerateSecretKey, pauseAllQueues, + processTransactionVersion2, registerName, removeAdmin, resumeAllQueues, @@ -56,8 +58,10 @@ import { } from "./background"; import { decryptGroupEncryption, encryptAndPublishSymmetricKeyGroupChat, encryptAndPublishSymmetricKeyGroupChatForAdmins, publishGroupEncryptedResource, publishOnQDN } from "./backgroundFunctions/encryption"; import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from "./constants/codes"; +import Base58 from "./deps/Base58"; import { encryptSingle } from "./qdn/encryption/group-encryption"; import { _createPoll, _voteOnPoll } from "./qortalRequests/get"; +import { createTransaction } from "./transactions/transactions"; import { getData, storeData } from "./utils/chromeStorage"; export function versionCase(request, event) { @@ -1899,4 +1903,138 @@ export async function publishGroupEncryptedResourceCase(request, event) { event.origin ); } - } \ No newline at end of file + } + + export async function createRewardShareCase(request, event) { + try { + const {recipientPublicKey} = request.payload; + const resKeyPair = await getKeyPair(); + const parsedData = 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"); + event.source.postMessage( + { + requestId: request.requestId, + action: "createRewardShare", + payload: res, + type: "backgroundMessageResponse", + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: "createRewardShare", + error: error?.message, + type: "backgroundMessageResponse", + }, + event.origin + ); + } + } + + export async function removeRewardShareCase(request, event) { + try { + const {rewardShareKeyPairPublicKey, recipient, percentageShare} = request.payload; + const resKeyPair = await getKeyPair(); + const parsedData = 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"); + event.source.postMessage( + { + requestId: request.requestId, + action: "removeRewardShare", + payload: res, + type: "backgroundMessageResponse", + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: "removeRewardShare", + error: error?.message, + type: "backgroundMessageResponse", + }, + event.origin + ); + } + } + + export async function getRewardSharePrivateKeyCase(request, event) { + try { + const {recipientPublicKey} = request.payload; + const resKeyPair = await getKeyPair(); + const parsedData = 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, + }); + + event.source.postMessage( + { + requestId: request.requestId, + action: "getRewardSharePrivateKey", + payload: tx?._base58RewardShareSeed, + type: "backgroundMessageResponse", + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: "getRewardSharePrivateKey", + error: error?.message, + type: "backgroundMessageResponse", + }, + event.origin + ); + } + } diff --git a/src/background.ts b/src/background.ts index 8fe5bb3..8cc4665 100644 --- a/src/background.ts +++ b/src/background.ts @@ -99,6 +99,9 @@ import { createPollCase, voteOnPollCase, encryptAndPublishSymmetricKeyGroupChatForAdminsCase, + createRewardShareCase, + getRewardSharePrivateKeyCase, + removeRewardShareCase, } from "./background-cases"; import { getData, removeKeysAndLogout, storeData } from "./utils/chromeStorage"; import {BackgroundFetch} from '@transistorsoft/capacitor-background-fetch'; @@ -1024,7 +1027,7 @@ export const sendQortFee = async (): Promise => { return qortFee; }; -async function getNameOrAddress(receiver) { +export async function getNameOrAddress(receiver) { try { const isAddress = validateAddress(receiver); if (isAddress) { @@ -2949,6 +2952,15 @@ function setupMessageListener() { case "setupGroupWebsocket": setupGroupWebsocketCase(request, event); break; + case "createRewardShare": + createRewardShareCase(request, event); + break; + case "getRewardSharePrivateKey": + getRewardSharePrivateKeyCase(request, event); + break; + case "removeRewardShare" : + removeRewardShareCase(request, event); + break; case "addEnteredQmailTimestamp": addEnteredQmailTimestampCase(request, event); break; diff --git a/src/components/Minting/Minting.tsx b/src/components/Minting/Minting.tsx new file mode 100644 index 0000000..070c085 --- /dev/null +++ b/src/components/Minting/Minting.tsx @@ -0,0 +1,1191 @@ +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) => { + window + .sendMessage( + "ADMIN_ACTION", + + { + type: "addmintingaccount", + value: val, + }, + 180000, + true + ) + .then((response) => { + if (!response?.error) { + res(response); + setMintingKey(""); + setTimeout(() => { + getMintingAccounts(); + }, 300); + return; + } + rej({ message: response.error }); + }) + .catch((error) => { + rej({ message: error.message || "An error occurred" }); + }); + }); + } 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) => { + window + .sendMessage( + "ADMIN_ACTION", + + { + type: "removemintingaccount", + value: val, + }, + 180000, + true + ) + .then((response) => { + if (!response?.error) { + res(response); + + setTimeout(() => { + getMintingAccounts(); + }, 300); + return; + } + rej({ message: response.error }); + }) + .catch((error) => { + rej({ message: error.message || "An error occurred" }); + }); + }); + } 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) => { + window + .sendMessage("createRewardShare", { + recipientPublicKey: publicKey, + }) + .then((response) => { + if (!response?.error) { + setTxList((prev) => [ + { + recipient, + ...response, + type: "add-rewardShare", + label: `Add rewardshare: awaiting confirmation`, + labelDone: `Add rewardshare: success!`, + done: false, + }, + ...prev, + ]); + res(response); + return; + } + rej({ message: response.error }); + }) + .catch((error) => { + rej({ message: error.message || "An error occurred" }); + }); + }); + }, []); + + const getRewardSharePrivateKey = useCallback(async (publicKey) => { + return await new Promise((res, rej) => { + window + .sendMessage("getRewardSharePrivateKey", { + recipientPublicKey: publicKey, + }) + .then((response) => { + if (!response?.error) { + res(response); + return; + } + rej({ message: response.error }); + }) + .catch((error) => { + rej({ message: error.message || "An error occurred" }); + }); + }); + }, []); + + 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) => { + window + .sendMessage("removeRewardShare", { + rewardShareKeyPairPublicKey: rewardShare.rewardSharePublicKey, + recipient: rewardShare.recipient, + percentageShare: -1, + }) + .then((response) => { + if (!response?.error) { + res(response); + setTxList((prev) => [ + { + ...rewardShare, + ...response, + type: "remove-rewardShare", + label: `Remove rewardshare: awaiting confirmation`, + labelDone: `Remove rewardshare: success!`, + done: false, + }, + ...prev, + ]); + return; + } + rej({ message: response.error }); + }) + .catch((error) => { + rej({ message: error.message || "An error occurred" }); + }); + }); + }, []); + + 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 51434c3..b2ef1bf 100644 --- a/src/components/TaskManager/TaskManger.tsx +++ b/src/components/TaskManager/TaskManger.tsx @@ -12,6 +12,7 @@ import PendingIcon from "@mui/icons-material/Pending"; import TaskAltIcon from "@mui/icons-material/TaskAlt"; import { MyContext, getBaseApiReact, isMobile } from "../../App"; import { getBaseApi } from "../../background"; +import { executeEvent } from "../../utils/events"; @@ -47,7 +48,7 @@ export const TaskManger = ({getUserInfo}) => { await new Promise((res)=> { setTimeout(() => { res(null) - }, 300000); + }, 60000); }) setTxList((prev)=> { let previousData = [...prev]; @@ -70,7 +71,7 @@ export const TaskManger = ({getUserInfo}) => { } } - intervals.current[signature] = setInterval(getAnswer, 120000) + intervals.current[signature] = setInterval(getAnswer, 60000) } useEffect(() => { @@ -104,48 +105,43 @@ export const TaskManger = ({getUserInfo}) => { } }) - prev.forEach((tx, index)=> { - - if(tx?.type === "created-common-secret" && tx?.signature && !tx.done){ - if(intervals.current[tx.signature]) return - - getStatus({signature: tx.signature}) - } - - }) - prev.forEach((tx, index)=> { - - if(tx?.type === "joined-group-request" && tx?.signature && !tx.done){ - if(intervals.current[tx.signature]) return - - getStatus({signature: tx.signature}) - } - - }) - prev.forEach((tx, index)=> { - - if(tx?.type === "join-request-accept" && tx?.signature && !tx.done){ - if(intervals.current[tx.signature]) return - - getStatus({signature: tx.signature}) - } - - }) - - prev.forEach((tx, index)=> { - - if(tx?.type === "register-name" && tx?.signature && !tx.done){ - if(intervals.current[tx.signature]) return - - getStatus({signature: tx.signature}, getUserInfo) - } - - }) + return previousData; }); }, [memberGroups, getUserInfo]); + useEffect(()=> { + + txList.forEach((tx) => { + if ( + ["created-common-secret", "joined-group-request", "join-request-accept"].includes( + tx?.type + ) && + tx?.signature && + !tx.done + ) { + if (!intervals.current[tx.signature]) { + getStatus({ signature: tx.signature }); + } + } + if (tx?.type === "register-name" && tx?.signature && !tx.done) { + if (!intervals.current[tx.signature]) { + 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); + } + } + }); + +}, [txList]) + if(isMobile) return null if (txList?.length === 0 || txList.filter((item) => !item?.done).length === 0) return null; diff --git a/src/transactions/RemoveRewardShareTransaction.ts b/src/transactions/RemoveRewardShareTransaction.ts new file mode 100644 index 0000000..375711a --- /dev/null +++ b/src/transactions/RemoveRewardShareTransaction.ts @@ -0,0 +1,46 @@ +// @ts-nocheck + +import { DYNAMIC_FEE_TIMESTAMP } from "../constants/constants" +import Base58 from "../deps/Base58" +import publicKeyToAddress from "../utils/generateWallet/publicKeyToAddress" +import TransactionBase from "./TransactionBase" + + +export default class RemoveRewardShareTransaction extends TransactionBase { + constructor() { + super() + this.type = 38 + } + + + set rewardShareKeyPairPublicKey(rewardShareKeyPairPublicKey) { + this._rewardShareKeyPairPublicKey = Base58.decode(rewardShareKeyPairPublicKey) + } + + set recipient(recipient) { + const _address = publicKeyToAddress(this._keyPair.publicKey) + this._recipient = recipient instanceof Uint8Array ? recipient : this.constructor.Base58.decode(recipient) + + if (new Date(this._timestamp).getTime() >= 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 }