From 13f731250e35611065138cf062367bb3015fe299 Mon Sep 17 00:00:00 2001 From: Jianrong Date: Sat, 29 Jul 2023 22:35:07 +1000 Subject: [PATCH 1/3] Implement the login part --- .../services/grandmaster-manager/errors.ts | 17 +-- .../src/services/grandmaster-manager/index.ts | 104 ++++++++++-------- 2 files changed, 68 insertions(+), 53 deletions(-) diff --git a/frontend/src/services/grandmaster-manager/errors.ts b/frontend/src/services/grandmaster-manager/errors.ts index ced3c47..0261a85 100644 --- a/frontend/src/services/grandmaster-manager/errors.ts +++ b/frontend/src/services/grandmaster-manager/errors.ts @@ -1,13 +1,7 @@ - -export interface ManagerError { - errorType: ErrorTypes; - errorStatus: number; - errorMessage?: string; -} - export enum ErrorTypes { // Login section WALLET_NOT_INSTALLED = "WALLET_NOT_INSTALLED", + CONFLICT_WITH_METAMASK = "CONFLICT_WITH_METAMASK", WALLET_NOT_LOGIN = "WALLET_NOT_LOGIN", NOT_GRANDMASTER = "NOT_GRANDMASTER", // Transaction section @@ -15,4 +9,13 @@ export enum ErrorTypes { NOT_ENOUGH_BALANCE = "NOT_ENOUGH_BALANCE", // Everything else INTERNAL_ERROR = "INTERNAL_ERROR" +} + +export class ManagerError extends Error { + errorType: ErrorTypes; + + constructor(message?: string, errorType?: ErrorTypes) { + super(message); + this.errorType = errorType || ErrorTypes.INTERNAL_ERROR + } } \ No newline at end of file diff --git a/frontend/src/services/grandmaster-manager/index.ts b/frontend/src/services/grandmaster-manager/index.ts index 4962850..56252f0 100644 --- a/frontend/src/services/grandmaster-manager/index.ts +++ b/frontend/src/services/grandmaster-manager/index.ts @@ -1,6 +1,5 @@ -// import Web3 from 'web3'; - -import { ErrorTypes, ManagerError } from '@/services/grandmaster-manager/errors'; +import Web3 from "web3"; +import { ErrorTypes, ManagerError } from "@/services/grandmaster-manager/errors"; export interface AccountDetails { accountAddress: string; @@ -20,62 +19,75 @@ export interface CandidateDetails { export type CandidateDetailsStatus = 'MASTERNODE' | 'PROPOSED' | 'SLASHED'; export class GrandMasterManager { - // private _web3Client: any; - - // public get web3Client() { - // return this._web3Client; - // } - - // public set web3Client(value) { - // this._web3Client = value; - // } - - // constructor() { - // const win = window as any; - // this.web3Client = new Web3(win.xdc ? win.xdc : win.ethereum); - // } - + private web3Client; + constructor() { + const win = window as any; + this.web3Client = new Web3(win.xdc ? win.xdc : win.ethereum); + } + + private isXdcWalletInstalled() { + if (this.web3Client.currentProvider && (window as any).xdc) { + return true; + } + return false; + } + + private async getAccountDetails() { + const accounts = await this.web3Client.eth.getAccounts(); + if (!accounts || !accounts.length || !accounts[0].length) { + throw new Error("No wallet address found, have you logged in?"); + } + const accountAddress = accounts[0]; + const balance = await this.web3Client.eth.getBalance(accountAddress); + const networkId = await this.web3Client.eth.getChainId(); + // TODO: Get denom, rpcAddress + return { + accountAddress, balance, networkId + } + } + /** - * * This method will detect XDC-Pay and verify if customer has alraedy loggin. - * @returns Account details will be returned if all good, otherwise, relevant error message and type will be returned such as WALLET_NOT_LOGIN + * This method will detect XDC-Pay and verify if customer has alraedy loggin. + * @returns Account details will be returned if all good + * @throws ManagerError with type of "WALLET_NOT_INSTALLED" || "CONFLICT_WITH_METAMASK" || "WALLET_NOT_LOGIN" */ - async login(): Promise { - const mockError: ManagerError = { - errorStatus: 404, - errorType: ErrorTypes.NOT_GRANDMASTER - }; - return mockError; - - const chainId = 551; - return { - accountAddress: "xdc888c073313b36cf03cf1f739f39443551ff12bbe", - balance: "123", - networkId: chainId, - denom: "hxdc", - rpcAddress: "https://devnetstats.apothem.network/subnet" - }; + async login(): Promise { + if (!this.isXdcWalletInstalled) { + throw new ManagerError("XDC Pay Not Installed", ErrorTypes.WALLET_NOT_INSTALLED) + } + if ((this.web3Client.currentProvider as any).chainId) { + throw new ManagerError("Metamask need to be disabled", ErrorTypes.CONFLICT_WITH_METAMASK) + } + + try { + const { accountAddress, balance, networkId } = await this.getAccountDetails(); + return { + accountAddress, + balance, + networkId, + denom: "hxdc", + rpcAddress: "https://devnetstats.apothem.network/subnet" + } + } catch (err: any) { + throw new ManagerError(err.message, ErrorTypes.WALLET_NOT_LOGIN) + } } /** * Remove a masternode from the manager view list * @param address The master node to be removed - * @returns If transaction is successful, return. Otherwise, an error details will be returned + * @returns If transaction is successful, return. Otherwise, an error details will be thrown */ - async removeMasterNode(_address: string): Promise { - // const mockError: ManagerError = { - // errorStatus: 1, - // errorType: ErrorTypes.INTERNAL_ERROR - // }; - // return mockError; + async removeMasterNode(address: string): Promise { return true; } /** * Propose to add a new masternode for being a mining candidate from the next epoch * @param address The master node to be added - * @returns If transaction is successful, return. Otherwise, an error details will be returned + * @returns If transaction is successful, return. Otherwise, an error details will be thrown */ - async addNewMasterNode(_address: string): Promise { + async addNewMasterNode(address: string): Promise { return true; } @@ -83,9 +95,9 @@ export class GrandMasterManager { * Change the voting/ranking power/order of a particular masternode. * @param address The targeted masternode * @param value The xdc value that will be applied to the targeted address. Postive number means increase power, negative means decrease the power - * @returns If transaction is successful, return. Otherwise, an error details will be returned + * @returns If transaction is successful, return. Otherwise, an error details will be thrown */ - async changeVote(_address: string, _value: number): Promise { + async changeVote(address: string, value: number): Promise { return true; } From 40aa389d106e3c2434266b2d95375d91c62f662c Mon Sep 17 00:00:00 2001 From: Jianrong Date: Sun, 30 Jul 2023 22:26:56 +1000 Subject: [PATCH 2/3] implement the propose new masternode part --- .../grandmaster-manager/extensions.ts | 29 ++++ .../src/services/grandmaster-manager/index.ts | 156 ++++++++++++------ .../src/services/grandmaster-manager/utils.ts | 36 ++++ 3 files changed, 166 insertions(+), 55 deletions(-) create mode 100644 frontend/src/services/grandmaster-manager/extensions.ts create mode 100644 frontend/src/services/grandmaster-manager/utils.ts diff --git a/frontend/src/services/grandmaster-manager/extensions.ts b/frontend/src/services/grandmaster-manager/extensions.ts new file mode 100644 index 0000000..09f73db --- /dev/null +++ b/frontend/src/services/grandmaster-manager/extensions.ts @@ -0,0 +1,29 @@ +import Web3 from 'web3'; + +export interface Web3WithExtension extends Web3 { + xdcSubnet: { + getCandidates: (epochNum: "latest") => Promise<{ + candidates: { + [key: string]: { + capacity: number; + status: 'MASTERNODE' | 'PROPOSED' | 'SLASHED' + } + }; + epoch: number; + success: boolean; + }> + }; +} + +export const networkExtensions = (extensionName = 'xdcSubnet') => { + return { + property: extensionName, + methods: [ + { + name: 'getCandidates', + params: 1, + call: 'eth_getCandidates', + } + ], + }; +}; diff --git a/frontend/src/services/grandmaster-manager/index.ts b/frontend/src/services/grandmaster-manager/index.ts index 56252f0..922b813 100644 --- a/frontend/src/services/grandmaster-manager/index.ts +++ b/frontend/src/services/grandmaster-manager/index.ts @@ -1,5 +1,10 @@ import Web3 from "web3"; +import { + JsonRpcResponse +} from 'web3-core-helpers'; import { ErrorTypes, ManagerError } from "@/services/grandmaster-manager/errors"; +import { networkExtensions, Web3WithExtension } from "@/services/grandmaster-manager/extensions"; +import { getSigningMsg } from "@/services/grandmaster-manager/utils"; export interface AccountDetails { accountAddress: string; @@ -12,27 +17,50 @@ export interface AccountDetails { export interface CandidateDetails { address: string; delegation: number; - rank: number; - status: CandidateDetailsStatus; + status: 'MASTERNODE' | 'PROPOSED' | 'SLASHED' +} + +type ContractAvailableActions = 'propose' | 'resign' | 'vote' | 'unvote'; +interface ContractAvailableActionDetail { + name: string; + action: ContractAvailableActions; +} +const contractAvailableActions: {[key in ContractAvailableActions]: ContractAvailableActionDetail} = { + propose: { + action: "propose", + name: "Propose a new masternode" + }, + resign: { + action: "resign", + name: "Resign an existing masternode" + }, + vote: { + action: "vote", + name: "Increase masternode delegation", + }, + unvote: { + action: "unvote", + name: "Decrease masternode delegation", + } } -export type CandidateDetailsStatus = 'MASTERNODE' | 'PROPOSED' | 'SLASHED'; + +const CONTRACT_ADDRESS = "0x0000000000000000000000000000000000000088"; export class GrandMasterManager { - private web3Client; + private web3Client: Web3WithExtension; constructor() { - const win = window as any; - this.web3Client = new Web3(win.xdc ? win.xdc : win.ethereum); + this.web3Client = new Web3((window as any).ethereum).extend(networkExtensions()); } private isXdcWalletInstalled() { - if (this.web3Client.currentProvider && (window as any).xdc) { + if (this.web3Client.currentProvider) { return true; } return false; } - private async getAccountDetails() { + private async getGrandMasterAccountDetails() { const accounts = await this.web3Client.eth.getAccounts(); if (!accounts || !accounts.length || !accounts[0].length) { throw new Error("No wallet address found, have you logged in?"); @@ -41,6 +69,7 @@ export class GrandMasterManager { const balance = await this.web3Client.eth.getBalance(accountAddress); const networkId = await this.web3Client.eth.getChainId(); // TODO: Get denom, rpcAddress + // TODO: Check with the grand master info from the node. Make sure they are the same, otherwise NOT_GRANDMASTER error return { accountAddress, balance, networkId } @@ -49,18 +78,15 @@ export class GrandMasterManager { /** * This method will detect XDC-Pay and verify if customer has alraedy loggin. * @returns Account details will be returned if all good - * @throws ManagerError with type of "WALLET_NOT_INSTALLED" || "CONFLICT_WITH_METAMASK" || "WALLET_NOT_LOGIN" + * @throws ManagerError with type of "WALLET_NOT_INSTALLED" || "WALLET_NOT_LOGIN" */ async login(): Promise { if (!this.isXdcWalletInstalled) { throw new ManagerError("XDC Pay Not Installed", ErrorTypes.WALLET_NOT_INSTALLED) } - if ((this.web3Client.currentProvider as any).chainId) { - throw new ManagerError("Metamask need to be disabled", ErrorTypes.CONFLICT_WITH_METAMASK) - } try { - const { accountAddress, balance, networkId } = await this.getAccountDetails(); + const { accountAddress, balance, networkId } = await this.getGrandMasterAccountDetails(); return { accountAddress, balance, @@ -72,22 +98,55 @@ export class GrandMasterManager { throw new ManagerError(err.message, ErrorTypes.WALLET_NOT_LOGIN) } } - + + private encodeAbi(functionName: ContractAvailableActions, address: string) { + return this.web3Client.eth.abi.encodeFunctionCall({ + name: functionName, + type: 'function', + inputs: [{ + type: 'string', + name: 'address' + }] + }, [address]) + } + + private async signTransaction(action: ContractAvailableActionDetail, targetAddress: string, value: string) { + const { accountAddress, networkId } = await this.getGrandMasterAccountDetails(); + const nonce = await this.web3Client.eth.getTransactionCount(accountAddress); + const encodedData = this.encodeAbi(action.action, targetAddress); + const msg = getSigningMsg(action.name, networkId, nonce, encodedData, value); + const payload = { + method: "eth_signTypedData_v4", + params: [accountAddress, msg], + from: accountAddress + }; + (this.web3Client.currentProvider as any).sendAsync(payload, (err: any, result: JsonRpcResponse) => { + if (err) { + throw err; + } + if (result.error) { + throw new ManagerError("Received error when signing the transaction") + } + return result.result; + }) + } /** - * Remove a masternode from the manager view list - * @param address The master node to be removed + * Propose to add a new masternode for being a mining candidate from the next epoch + * @param address The master node to be added * @returns If transaction is successful, return. Otherwise, an error details will be thrown */ - async removeMasterNode(address: string): Promise { + async addNewMasterNode(address: string): Promise { + const signedTransaction = await this.signTransaction(contractAvailableActions.propose, address, "0x84595161401484a000000"); + return true; } - + /** - * Propose to add a new masternode for being a mining candidate from the next epoch - * @param address The master node to be added + * Remove a masternode from the manager view list + * @param address The master node to be removed * @returns If transaction is successful, return. Otherwise, an error details will be thrown */ - async addNewMasterNode(address: string): Promise { + async removeMasterNode(address: string): Promise { return true; } @@ -106,9 +165,11 @@ export class GrandMasterManager { * @param changeHandler The handler function to process when the accounts changed. The provided value will be the new wallet address. */ onAccountChange(changeHandler: (accounts: string) => any) { - changeHandler("0x3c03a0abac1da8f2f419a59afe1c125f90b506c5"); - // TODO: 1. Handle the account change via accountsChanged - // TODO: 2. Handle the chain change via chainChanged. This could happen if switch from testnet to mainnet etc. + (this.web3Client.currentProvider as any).on("accountsChanged", (accounts: string[]) => { + if (accounts && accounts.length) { + changeHandler(accounts[0]) + } + }) } /** @@ -119,37 +180,22 @@ export class GrandMasterManager { * 'SLASHED' means it's been taken out from the masternode list */ async getCandidates(): Promise { - return [ - { - address: "xdc25B4CBb9A7AE13feadC3e9F29909833D19D16dE5", - delegation: 5e+22, - rank: 0, - status: "MASTERNODE" - }, - { - address: "xdc2af0Cacf84899F504a6dC95e6205547bDfe28c2c", - delegation: 5e+22, - rank: 1, - status: "MASTERNODE" - }, - { - address: "xdc30f21E514A66732DA5Dff95340624fa808048601", - delegation: 5e+22, - rank: 2, - status: "MASTERNODE" - }, - { - address: "xdc3C03a0aBaC1DA8f2f419a59aFe1c125F90B506c5", - delegation: 5e+22, - rank: 3, - status: "PROPOSED" - }, - { - address: "xdc3D9fd0c76BB8B3B4929ca861d167f3e05926CB68", - delegation: 5e+22, - rank: 4, - status: "SLASHED" + try { + const { candidates, success } = await this.web3Client.xdcSubnet.getCandidates("latest"); + if (!success) { + throw new ManagerError("Fail to get list of candidates from xdc subnet"); } - ]; + return Object.entries(candidates).map(entry => { + const [ address, { capacity, status }] = entry; + return { + address, + delegation: capacity, + status + } + }).sort((a, b) => b.delegation - a.delegation); + + } catch (error: any) { + throw new ManagerError(error.message); + } } } \ No newline at end of file diff --git a/frontend/src/services/grandmaster-manager/utils.ts b/frontend/src/services/grandmaster-manager/utils.ts new file mode 100644 index 0000000..a07290d --- /dev/null +++ b/frontend/src/services/grandmaster-manager/utils.ts @@ -0,0 +1,36 @@ +export const getSigningMsg = (name: string, chainId: number, nonce: number, encodedData: string, value: string) => { + return JSON.stringify({ + domain: { + chainId: chainId.toString(), + name, + // verifyingContract: "0x0000000000000000000000000000000000000088", // To be replaced by the actual sm address + version: "1" + }, + message: { + nonce: Number(nonce), + gasPrice: 250000000, + gasLimit: 220000, + to: '0x0000000000000000000000000000000000000088', + value, + data: encodedData, + }, + primaryType: 'Grandmaster', + types: { + // This refers to the domain the contract is hosted on. + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + // { name: 'verifyingContract', type: 'address' }, + ], + Grandmaster: [ + { name: 'nonce', type: 'uint256' }, + { name: 'gasPrice', type: 'uint256' }, + { name: 'gasLimit', type: 'uint256' }, + { name: 'to', type: 'string' }, + { name: 'value', type: 'string' }, + { name: 'data', type: 'string' }, + ], + }, + }); +} \ No newline at end of file From adb236dc269a2c763b5c494950b8b8ef28502f55 Mon Sep 17 00:00:00 2001 From: Jianrong Date: Sun, 6 Aug 2023 14:00:09 +1000 Subject: [PATCH 3/3] Fix all the issues with using the management service --- backend/src/controllers/account.controller.ts | 12 +- backend/src/controllers/blocks.controller.ts | 2 +- backend/src/routes/index.ts | 1 + backend/swagger.yaml | 33 + frontend/package.json | 29 +- .../ManagementMasterCommitteePage.tsx | 2 +- .../grandmaster-manager/extensions.ts | 36 +- .../src/services/grandmaster-manager/index.ts | 86 +- frontend/vite.config.ts | 20 +- frontend/yarn.lock | 2150 +++-------------- 10 files changed, 446 insertions(+), 1925 deletions(-) diff --git a/backend/src/controllers/account.controller.ts b/backend/src/controllers/account.controller.ts index a2611cc..48a38e3 100644 --- a/backend/src/controllers/account.controller.ts +++ b/backend/src/controllers/account.controller.ts @@ -1,5 +1,5 @@ import { BlockService } from '../services/block.service'; -import { CHECKPOINT_CONTRACT, PARENTCHAIN_WALLET } from '../config'; +import { CHECKPOINT_CONTRACT, PARENTCHAIN_WALLET, SUBNET_URL } from '../config'; import { getService } from '../services'; import { AccountService } from '../services/account.service'; import { NextFunction, Request, Response } from 'express'; @@ -33,4 +33,14 @@ export class RelayerController { next(error); } }; + + // TODO: Fetch the chain grandmaster address etc. + public getChainDetails = async (req: Request, res: Response, next: NextFunction): Promise => { + const data = { + rpcUrl: SUBNET_URL, + grandmasterAddress: 'xxx', + minimumDelegation: '10000000000000000000000000', + }; + res.status(200).json(data); + }; } diff --git a/backend/src/controllers/blocks.controller.ts b/backend/src/controllers/blocks.controller.ts index 3f7ec2d..0c1cf51 100644 --- a/backend/src/controllers/blocks.controller.ts +++ b/backend/src/controllers/blocks.controller.ts @@ -14,7 +14,7 @@ export class BlocksController { } public loadRecentBlocks = async (req: Request, res: Response, next: NextFunction): Promise => { - const blockNumIndex = req.query.blockNumIndex? Number(req.query.blockNumIndex) : -1; + const blockNumIndex = req.query.blockNumIndex ? Number(req.query.blockNumIndex) : -1; try { const [latestMinedBlock, recentBlocks, chainStatus, lastSubnetCommittedBlock, parentchainSubnetBlock] = await Promise.all([ diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index 41c92d5..cbf20d7 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -21,6 +21,7 @@ export class Route implements Routes { this.router.get('/information/masternodes', this.masterNodeController.getMasternodesInfo); this.router.get('/information/network', this.blocksController.getBlockChainStats); this.router.get('/information/relayer', this.relayerController.getRelayerRelatedDetails); + this.router.get('/information/chainsetting', this.relayerController.getChainDetails); this.router.get('/confirmation', this.blocksController.confirmBlock); } } diff --git a/backend/swagger.yaml b/backend/swagger.yaml index f4b2a5e..4b23893 100644 --- a/backend/swagger.yaml +++ b/backend/swagger.yaml @@ -236,6 +236,39 @@ paths: description: 'Bad Request' '500': description: 'Server Error' + /information/chainsetting: + get: + summary: Get the subnet network metadata such as the RPC URL. This is particularly useful for help to validate the setting parameters from XDC pay/wallet in management page + tags: + - management + responses: + '200': + content: + 'application/json': + schema: + type: object + properties: + subnet: + type: object + properties: + rpcUrl: + description: Subnet Chain RPC URL + type: string + grandmasterAddress: + description: The grandmaster wallet address for verification + type: string + minimumDelegation: + description: Minimum delegation required when increase the candidates's voting/ranking power + required: + - rpcUrl + - grandmasterAddress + - minimumDelegation + required: + - subnet + '400': + description: 'Bad Request' + '500': + description: 'Server Error' /confirmation: get: diff --git a/frontend/package.json b/frontend/package.json index db69578..89e04cf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,34 +11,35 @@ }, "dependencies": { "@radix-ui/react-tooltip": "^1.0.6", - "@types/lodash.debounce": "^4.0.7", - "@types/react": "^18.0.37", - "@types/react-dom": "^18.0.11", - "@vitejs/plugin-react": "^4.0.0", - "autoprefixer": "^10.4.14", "axios": "^1.4.0", "formik": "^2.4.3", "lodash.debounce": "^4.0.8", - "postcss": "^8.4.24", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.14.0", - "sass": "^1.63.6", - "tailwind-merge": "^1.13.2", - "tailwindcss": "^3.3.2", - "typescript": "^5.0.2", - "vite": "^4.4.7", - "vite-tsconfig-paths": "^4.2.0", - "web3": "1.6.1", + "web3": "4.0.3", "yup": "^1.2.0" }, "devDependencies": { "@types/axios": "^0.14.0", + "@types/lodash.debounce": "^4.0.7", + "@types/react": "^18.0.37", + "@types/react-dom": "^18.0.11", "@types/web3": "^1.2.2", "@typescript-eslint/eslint-plugin": "^5.59.0", "@typescript-eslint/parser": "^5.59.0", + "@vitejs/plugin-react": "^4.0.0", + "autoprefixer": "^10.4.14", "eslint": "^8.38.0", "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.3.4" + "eslint-plugin-react-refresh": "^0.3.4", + "postcss": "^8.4.24", + "sass": "^1.63.6", + "tailwind-merge": "^1.13.2", + "tailwindcss": "^3.3.2", + "typescript": "^5.0.2", + "vite": "^4.4.7", + "vite-plugin-node-polyfills": "^0.9.0", + "vite-tsconfig-paths": "^4.2.0" } } diff --git a/frontend/src/pages/management-master-committee-page/ManagementMasterCommitteePage.tsx b/frontend/src/pages/management-master-committee-page/ManagementMasterCommitteePage.tsx index eae171f..25f2dea 100644 --- a/frontend/src/pages/management-master-committee-page/ManagementMasterCommitteePage.tsx +++ b/frontend/src/pages/management-master-committee-page/ManagementMasterCommitteePage.tsx @@ -143,7 +143,7 @@ export default function ManagementMasterCommitteePage() { {formatHash(row.address)} {row.delegation} xdc - {row.rank} + {i} {getDisplayStatus(row.status)}