diff --git a/FACTORY_VERSION.md b/FACTORY_VERSION.md index e41d299da..b818db8d4 100644 --- a/FACTORY_VERSION.md +++ b/FACTORY_VERSION.md @@ -1,5 +1,6 @@ |Version | Contract name | Address | Network | |--------|----------------|--------------|------------| +| 1.11.0 | FactoryAddress | 0.0.1137631 | Testnet | | 1.10.0 | FactoryAddress | 0.0.1137631 | Testnet | | 1.9.1 | FactoryAddress | 0.0.1137631 | Testnet | | 1.9.0 | FactoryAddress | 0.0.1137631 | Testnet | diff --git a/cli/package.json b/cli/package.json index be0cb07af..94716fac1 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@hashgraph/stablecoin-npm-cli", - "version": "1.10.0", + "version": "1.11.0", "description": "CLI for Hedera Stablecoin", "main": "./build/src/index.js", "bin": { diff --git a/contracts/package.json b/contracts/package.json index 3f9e4e1dd..dc385684c 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@hashgraph/stablecoin-npm-contracts", - "version": "1.10.0", + "version": "1.11.0", "description": "", "main": "./build/typechain-types/index.js", "module": "./build/typechain-types/index.js", @@ -122,7 +122,7 @@ "@chainlink/contracts": "^0.5.1", "@openzeppelin/contracts": "^4.8.2", "@openzeppelin/contracts-upgradeable": "^4.8.2", - "@hashgraph/smart-contracts": "github:hashgraph/hedera-smart-contracts#v0.4.0", + "@hashgraph/smart-contracts": "github:hashgraph/hedera-smart-contracts#v0.6.0", "chai-as-promised": "^7.1.1", "dotenv": "^16.0.3", "ethers": "5.7.2", diff --git a/contracts/test/rescuable.ts b/contracts/test/rescuable.ts index 986317314..79b9b311c 100644 --- a/contracts/test/rescuable.ts +++ b/contracts/test/rescuable.ts @@ -320,9 +320,9 @@ describe('Rescue Tests', function () { ) }) - it('Should rescue 10 HBAR', async function () { + it('Should rescue 1 HBAR', async function () { // Get the initial balance of the token owner and client - const AmountToRescue = BigNumber.from(10).mul(HBARFactor) + const AmountToRescue = BigNumber.from(1).mul(HBARFactor) const initialTokenOwnerBalance = await getHBARBalanceOf( proxyAddress.toString(), operatorClient, @@ -335,7 +335,6 @@ describe('Rescue Tests', function () { true, false ) - // rescue some tokens await rescueHBAR(proxyAddress, AmountToRescue, operatorClient) await delay(3000) @@ -356,12 +355,10 @@ describe('Rescue Tests', function () { const expectedTokenOwnerBalance = initialTokenOwnerBalance.sub(AmountToRescue) - const diffClientBalance = finalClientBalance.sub(initialClientBalance) - expect(finalTokenOwnerBalance.toString()).to.equals( expectedTokenOwnerBalance.toString() ) - expect(diffClientBalance.gt(BigNumber.from(0))).to.be.true + expect(finalClientBalance.gt(initialClientBalance)).to.be.true }) it('we cannot rescue more HBAR than the owner balance', async function () { diff --git a/defenders/package.json b/defenders/package.json index 38cf53b23..64c8f685d 100644 --- a/defenders/package.json +++ b/defenders/package.json @@ -1,6 +1,6 @@ { "name": "accelerator-service", - "version": "1.10.0", + "version": "1.11.0", "description": "Accelerator integration with OZ Defenders", "license": "MIT", "devDependencies": { diff --git a/hashconnect/lib/.npmignore b/hashconnect/lib/.npmignore index 2da193068..162be25fb 100644 --- a/hashconnect/lib/.npmignore +++ b/hashconnect/lib/.npmignore @@ -10,7 +10,6 @@ coverage *.log .gitlab-ci.yml -package-lock.json /*.tgz /tmp* /mnt/ diff --git a/hashconnect/lib/package.json b/hashconnect/lib/package.json index 210815808..4c23be4ac 100644 --- a/hashconnect/lib/package.json +++ b/hashconnect/lib/package.json @@ -1,6 +1,6 @@ { "name": "@hashgraph/hashconnect", - "version": "1.10.0", + "version": "1.11.0", "description": "hashconnect interoperability library", "author": "Tyler Coté , Nick Hanna ", "license": "MIT", @@ -87,7 +87,7 @@ "yarn": "^1.22.17" }, "dependencies": { - "@hashgraph/sdk": "^2.23.0", + "@hashgraph/sdk": "2.38.0", "@hashgraph/cryptography": "1.4.3", "buffer": "^6.0.3", "crypto-es": "^1.2.7", diff --git a/package.json b/package.json index 0527290ba..d95527ea7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hedera-stable-coin", - "version": "1.10.0", + "version": "1.11.0", "description": "stablecoin studio", "scripts": { "install": "node install.js && npm run build:cli:full && npm run prepare", diff --git a/sdk/__tests__/port/in/Proxy.test.ts b/sdk/__tests__/port/in/Proxy.test.ts index 3a830227c..514518055 100644 --- a/sdk/__tests__/port/in/Proxy.test.ts +++ b/sdk/__tests__/port/in/Proxy.test.ts @@ -18,6 +18,7 @@ * */ +/* eslint-disable jest/no-disabled-tests */ import Injectable from '../../../src/core/Injectable.js'; import { Account, diff --git a/sdk/package.json b/sdk/package.json index cda5ebdf7..05abf1b67 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@hashgraph/stablecoin-npm-sdk", - "version": "1.10.0", + "version": "1.11.0", "description": "stablecoin studio SDK", "main": "./build/cjs/src/index.js", "module": "./build/esm/src/index.js", @@ -71,7 +71,7 @@ "@hashgraph/cryptography": "1.4.3", "@hashgraph/hashconnect": "file:./../hashconnect/lib", "@hashgraph/hethers": "^1.2.5", - "@hashgraph/sdk": "^2.23.0", + "@hashgraph/sdk": "2.38.0", "@hashgraph/stablecoin-npm-contracts": "file:./../contracts", "@metamask/detect-provider": "^2.0.0", "@metamask/providers": "^10.2.1", diff --git a/sdk/src/port/out/hs/blade/BladeTransactionAdapter.ts b/sdk/src/port/out/hs/blade/BladeTransactionAdapter.ts index f5edd46f4..7c7d59529 100644 --- a/sdk/src/port/out/hs/blade/BladeTransactionAdapter.ts +++ b/sdk/src/port/out/hs/blade/BladeTransactionAdapter.ts @@ -88,17 +88,33 @@ export class BladeTransactionAdapter extends HederaTransactionAdapter { async init(network?: string): Promise { const currentNetwork = network ?? this.networkService.environment; - this.bc = await BladeConnector.init( - ConnectorStrategy.EXTENSION, // preferred strategy is optional - { - // dApp metadata options are optional, but are highly recommended to use - name: 'Stablecoin Studio', - description: - 'Stablecoin Studio is an open-source SDK that makes it easy for web3 stablecoin platforms, institutional issuers, enterprises, and payment providers to build stablecoin applications on the Hedera network.', - url: 'https://hedera.com/stablecoin-studio', - icons: [], - }, - ); + try { + this.bc = await BladeConnector.init( + ConnectorStrategy.EXTENSION, // preferred strategy is optional + { + // dApp metadata options are optional, but are highly recommended to use + name: 'Stablecoin Studio', + description: + 'Stablecoin Studio is an open-source SDK that makes it easy for web3 stablecoin platforms, institutional issuers, enterprises, and payment providers to build stablecoin applications on the Hedera network.', + url: 'https://hedera.com/stablecoin-studio', + icons: [], + }, + ); + this.signer = this.bc.getSigner(); + } catch (error: any) { + LogService.logTrace('Error initializing Blade', error); + return currentNetwork; + } + LogService.logTrace('Client Initialized'); + this.eventService.emit(WalletEvents.walletFound, { + wallet: SupportedWallets.BLADE, + name: SupportedWallets.BLADE, + }); + + return Promise.resolve(currentNetwork); + } + + async register(): Promise { LogService.logTrace('Checking for previously saved pairings: '); const params = { network: HederaNetwork.Testnet, @@ -117,18 +133,13 @@ export class BladeTransactionAdapter extends HederaTransactionAdapter { } } - this.setSigner(currentNetwork); - this.eventService.emit(WalletEvents.walletFound, { - wallet: SupportedWallets.BLADE, - name: SupportedWallets.BLADE, - }); const iniData: InitializationData = { account: this.account, }; this.eventService.emit(WalletEvents.walletPaired, { data: iniData, network: { - name: currentNetwork, + name: this.networkService.environment, recognized: true, factoryId: this.networkService.configuration ? this.networkService.configuration.factoryAddress @@ -138,14 +149,6 @@ export class BladeTransactionAdapter extends HederaTransactionAdapter { }); LogService.logTrace('Previous paring found: ', this.account); - return currentNetwork; - } - - private async setSigner(network: string): Promise { - this.signer = this.bc.getSigner(); - } - - async register(): Promise { Injectable.registerTransactionHandler(this); LogService.logTrace('Blade Registered as handler'); this.init(); @@ -159,9 +162,6 @@ export class BladeTransactionAdapter extends HederaTransactionAdapter { if (this.bc) await this.bc.killSession(); LogService.logTrace('Blade stopped'); - /*this.eventService.emit(WalletEvents.walletDisconnect, { - wallet: SupportedWallets.BLADE, - });*/ return Promise.resolve(true); } @@ -184,15 +184,6 @@ export class BladeTransactionAdapter extends HederaTransactionAdapter { signedT = await t.freezeWithSigner(this.signer); } const trx = await this.signer.signTransaction(signedT); - const hashPackTrx = { - //topic: this.initData.topic, - byteArray: trx.toBytes(), - metadata: { - accountToSign: this.account.id.toString(), - returnTransaction: false, - getRecord: true, - }, - }; let hashPackTransactionResponse; if ( t instanceof TokenCreateTransaction || @@ -213,22 +204,8 @@ export class BladeTransactionAdapter extends HederaTransactionAdapter { hashPackTransactionResponse = await t.executeWithSigner( this.signer, ); - /*this.logTransaction( - JSON.parse( - JSON.stringify(hashPackTransactionResponse), - ).response.transactionId.toString(), - this.networkService.environment, - );*/ } else { hashPackTransactionResponse = await this.signer.call(trx); - /*this.logTransaction( - hashPackTransactionResponse - ? (hashPackTransactionResponse as any).transactionId ?? - '' - : (hashPackTransactionResponse as any).transactionId ?? - '', - this.networkService.environment, - );*/ } return HashpackTransactionResponseAdapter.manageResponse( this.networkService.environment, @@ -243,9 +220,8 @@ export class BladeTransactionAdapter extends HederaTransactionAdapter { throw new SigningError(error); } } - public async restart(network: string): Promise { + public async restart(): Promise { await this.stop(); - //await this.init(network); } getAccount(): Account { diff --git a/sdk/src/port/out/rpc/RPCTransactionAdapter.ts b/sdk/src/port/out/rpc/RPCTransactionAdapter.ts index c1ed2aaf5..e9d66bc73 100644 --- a/sdk/src/port/out/rpc/RPCTransactionAdapter.ts +++ b/sdk/src/port/out/rpc/RPCTransactionAdapter.ts @@ -1273,7 +1273,7 @@ export default class RPCTransactionAdapter extends TransactionAdapter { name: SupportedWallets.METAMASK, }); if (ethProvider.isMetaMask) { - if (!ethereum.isConnected()) + if (pair && !ethereum.isConnected()) throw new WalletConnectError( 'Metamask is not connected!', ); diff --git a/web/package-lock.json b/web/package-lock.json index 59b74b76b..ff51c845f 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "@hashgraph/stablecoin-dapp", - "version": "1.9.2", + "version": "1.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@hashgraph/stablecoin-dapp", - "version": "1.9.2", + "version": "1.11.0", "dependencies": { "@chakra-ui/icons": "2.0.17", "@chakra-ui/react": "2.6.1", @@ -80,11 +80,11 @@ }, "../contracts": { "name": "@hashgraph/stablecoin-npm-contracts", - "version": "1.9.2", + "version": "1.11.0", "license": "ISC", "dependencies": { "@chainlink/contracts": "^0.5.1", - "@hashgraph/smart-contracts": "github:hashgraph/hedera-smart-contracts#v0.4.0", + "@hashgraph/smart-contracts": "github:hashgraph/hedera-smart-contracts#v0.6.0", "@openzeppelin/contracts": "^4.8.2", "@openzeppelin/contracts-upgradeable": "^4.8.2", "chai-as-promised": "^7.1.1", @@ -98,7 +98,7 @@ "@chainlink/contracts": "^0.5.1", "@hashgraph/hardhat-hethers": "1.0.3", "@hashgraph/hethers": "1.2.2", - "@hashgraph/sdk": "2.23.0", + "@hashgraph/sdk": "2.38.0", "@nomicfoundation/hardhat-chai-matchers": "^1.0.3", "@nomicfoundation/hardhat-network-helpers": "^1.0.6", "@nomicfoundation/hardhat-toolbox": "^2.0.0", @@ -13175,11 +13175,11 @@ }, "../hashconnect/lib": { "name": "@hashgraph/hashconnect", - "version": "1.9.2", + "version": "1.11.0", "license": "MIT", "dependencies": { "@hashgraph/cryptography": "1.4.3", - "@hashgraph/sdk": "^2.23.0", + "@hashgraph/sdk": "2.38.0", "buffer": "^6.0.3", "crypto-es": "^1.2.7", "crypto-js": "^4.1.1", @@ -21620,7 +21620,7 @@ }, "../sdk": { "name": "@hashgraph/stablecoin-npm-sdk", - "version": "1.9.2", + "version": "1.11.0", "license": "Apache-2.0", "dependencies": { "@bladelabs/blade-web3.js": "^1.2.0", diff --git a/web/package.json b/web/package.json index 7c2e76354..34f87b890 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "@hashgraph/stablecoin-dapp", - "version": "1.10.0", + "version": "1.11.0", "files": [ "build/" ], diff --git a/web/src/Router/NamedRoutes.ts b/web/src/Router/NamedRoutes.ts index a98de84f7..3c79b9593 100644 --- a/web/src/Router/NamedRoutes.ts +++ b/web/src/Router/NamedRoutes.ts @@ -28,6 +28,7 @@ export enum NamedRoutes { ProofOfReserve = 'proofOfReserve', FeesManagement = 'feesManagement', Settings = 'settings', + AppSettings = 'appSettings', StableCoinSettings = 'stableCoinSettings', FactorySettings = 'factorySettings', } diff --git a/web/src/Router/Router.tsx b/web/src/Router/Router.tsx index 69fdae2a2..3ba9cc715 100644 --- a/web/src/Router/Router.tsx +++ b/web/src/Router/Router.tsx @@ -41,6 +41,7 @@ import Settings from '../views/Settings'; import StableCoinSettings from '../views/Settings/StableCoin'; import FactorySettings from '../views/Settings/Factory'; import ModalWalletConnect from '../components/ModalWalletConnect'; +import AppSettings from '../views/AppSettings'; const LoginOverlayRoute = ({ show, loadingSC }: { show: boolean; loadingSC: boolean }) => { return ( @@ -189,6 +190,7 @@ const Router = () => { )} } /> + } /> } /> } /> @@ -196,6 +198,7 @@ const Router = () => { path={RoutesMappingUrl.stableCoinNotSelected} element={} /> + } /> } /> diff --git a/web/src/Router/RoutesMappingUrl.ts b/web/src/Router/RoutesMappingUrl.ts index 1c939f798..e7554ab37 100644 --- a/web/src/Router/RoutesMappingUrl.ts +++ b/web/src/Router/RoutesMappingUrl.ts @@ -33,6 +33,7 @@ export const RoutesMappingUrl: Record = { [NamedRoutes.CheckKyc]: '/operations/check-kyc', [NamedRoutes.FeesManagement]: '/feesManagement', [NamedRoutes.Settings]: '/settings', + [NamedRoutes.AppSettings]: '/appSettings', [NamedRoutes.StableCoinSettings]: '/settings/stableCoin', [NamedRoutes.FactorySettings]: '/settings/factory', }; diff --git a/web/src/components/ModalWalletConnect.tsx b/web/src/components/ModalWalletConnect.tsx index bb9f53915..024557162 100644 --- a/web/src/components/ModalWalletConnect.tsx +++ b/web/src/components/ModalWalletConnect.tsx @@ -281,16 +281,15 @@ const ModalWalletConnect = () => { ) : ( - - + + Blade - + )} diff --git a/web/src/i18n.ts b/web/src/i18n.ts index 00eb90412..bf4d5101c 100644 --- a/web/src/i18n.ts +++ b/web/src/i18n.ts @@ -21,6 +21,7 @@ import UNFREEZE_EN from './translations/en/unfreeze.json'; import CHECK_FROZEN_STATUS_EN from './translations/en/checkFrozen.json'; import PROOF_OF_RESERVE_EN from './translations/en/proofOfReserve.json'; import SETTINGS_EN from './translations/en/settings.json'; +import APPSETTINGS_EN from './translations/en/appSettings.json'; import GRANT_KYC_EN from './translations/en/grantKYC.json'; import REVOKE_KYC_EN from './translations/en/revokeKYC.json'; import CHECK_KYC_EN from './translations/en/checkKyc.json'; @@ -64,6 +65,7 @@ i18n checkKyc: CHECK_KYC_EN, feesManagement: FEES_MANAGEMENT_EN, updateToken: UPDATE_TOKEN_EN, + appSettings: APPSETTINGS_EN, }, es: { global: GLOBAL_ES, diff --git a/web/src/interfaces/IMirrorRPCNode.ts b/web/src/interfaces/IMirrorRPCNode.ts new file mode 100644 index 000000000..b8a7f5c6c --- /dev/null +++ b/web/src/interfaces/IMirrorRPCNode.ts @@ -0,0 +1,8 @@ +export interface IMirrorRPCNode { + name: string; + BASE_URL: string; + API_KEY: string; + Environment: string; + isInConfig: boolean; + HEADER: string; +} diff --git a/web/src/layout/sidebar/Sidebar.tsx b/web/src/layout/sidebar/Sidebar.tsx index 0b05ffd6e..b313cba84 100644 --- a/web/src/layout/sidebar/Sidebar.tsx +++ b/web/src/layout/sidebar/Sidebar.tsx @@ -56,6 +56,15 @@ const Sidebar = () => { route: NamedRoutes.Settings, }, ]; + const appProperties: optionsProps = { + icon: 'ShareNetwork', + title: t('sidebar.appSettings'), + route: NamedRoutes.AppSettings, + }; + + if (process.env.REACT_APP_SHOW_CONFIG === 'true') { + options.push(appProperties); + } return ( >; @@ -78,38 +79,41 @@ export class SDKService { return !!this.initData; } - public static async connectWallet(wallet: SupportedWallets, connectNetwork: string) { + public static async connectWallet( + wallet: SupportedWallets, + connectNetwork: string, + selectedMirror?: IMirrorRPCNode, + selectedRPC?: IMirrorRPCNode, + ) { let mirrorNode = []; // REACT_APP_MIRROR_NODE load from .env - - if (process.env.REACT_APP_MIRROR_NODE) + if (selectedMirror) { + mirrorNode = [selectedMirror]; + } else if (process.env.REACT_APP_MIRROR_NODE) { mirrorNode = JSON.parse(process.env.REACT_APP_MIRROR_NODE); - - const _mirrorNode = - mirrorNode.length !== 0 - ? mirrorNode.find((i: any) => i.Environment === connectNetwork) - ? { - baseUrl: mirrorNode.find((i: any) => i.Environment === connectNetwork).BASE_URL ?? '', - apiKey: mirrorNode.find((i: any) => i.Environment === connectNetwork).API_KEY ?? '', - headerName: - mirrorNode.find((i: any) => i.Environment === connectNetwork).HEADER ?? '', - } - : { baseUrl: '', apiKey: '', headerName: '' } - : { baseUrl: '', apiKey: '', headerName: '' }; + } + const mirrorNodeFiltered = mirrorNode.find((i: any) => i.Environment === connectNetwork); + const _mirrorNode = mirrorNodeFiltered + ? { + baseUrl: mirrorNodeFiltered.BASE_URL ?? '', + apiKey: mirrorNodeFiltered.API_KEY ?? '', + headerName: mirrorNodeFiltered.HEADER ?? '', + } + : { baseUrl: '', apiKey: '', headerName: '' }; let rpcNode = []; // REACT_APP_RPC_NODE load from .env - - if (process.env.REACT_APP_RPC_NODE) rpcNode = JSON.parse(process.env.REACT_APP_RPC_NODE); - - const _rpcNode = - rpcNode.length !== 0 - ? rpcNode.find((i: any) => i.Environment === connectNetwork) - ? { - baseUrl: rpcNode.find((i: any) => i.Environment === connectNetwork).BASE_URL ?? '', - apiKey: rpcNode.find((i: any) => i.Environment === connectNetwork).API_KEY ?? '', - headerName: rpcNode.find((i: any) => i.Environment === connectNetwork).HEADER ?? '', - } - : { baseUrl: '', apiKey: '', headerName: '' } - : { baseUrl: '', apiKey: '', headerName: '' }; + if (selectedRPC) { + rpcNode = [selectedRPC]; + } else if (process.env.REACT_APP_RPC_NODE) { + rpcNode = JSON.parse(process.env.REACT_APP_RPC_NODE); + } + const rpcNodeFiltered = rpcNode.find((i: any) => i.Environment === connectNetwork); + const _rpcNode = rpcNodeFiltered + ? { + baseUrl: rpcNodeFiltered.BASE_URL ?? '', + apiKey: rpcNodeFiltered.API_KEY ?? '', + headerName: rpcNodeFiltered.HEADER ?? '', + } + : { baseUrl: '', apiKey: '', headerName: '' }; await Network.setNetwork( new SetNetworkRequest({ diff --git a/web/src/store/slices/walletSlice.ts b/web/src/store/slices/walletSlice.ts index 9802eb759..58f54b282 100644 --- a/web/src/store/slices/walletSlice.ts +++ b/web/src/store/slices/walletSlice.ts @@ -1,5 +1,5 @@ -import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import { ConnectionState, GetListStableCoinRequest } from '@hashgraph/stablecoin-npm-sdk'; +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import SDKService from '../../services/SDKService'; import type { RootState } from '../store'; import type { IExternalToken } from '../../interfaces/IExternalToken'; @@ -13,6 +13,13 @@ import type { StableCoinViewModel, ProxyConfigurationViewModel, } from '@hashgraph/stablecoin-npm-sdk'; +import type { IMirrorRPCNode } from '../../interfaces/IMirrorRPCNode'; + +const LAST_WALLET_LS = 'lastWallet'; +const MIRROR_LIST_LS = 'mirrorList'; +const SELECTED_MIRROR_LS = 'selectedMirror'; +const RPC_LIST_LS = 'rpcList'; +const SELECTED_RPC_LS = 'selectedRPC'; export interface InitialStateProps { data?: InitializationData; @@ -43,6 +50,10 @@ export interface InitialStateProps { networkRecognized?: boolean; accountRecognized?: boolean; factoryId?: string; + mirrorList?: IMirrorRPCNode[]; + selectedMirror?: IMirrorRPCNode; + rpcList?: IMirrorRPCNode[]; + selectedRPC?: IMirrorRPCNode; } export const initialState: InitialStateProps = { @@ -58,7 +69,7 @@ export const initialState: InitialStateProps = { stableCoinList: undefined, externalTokenList: [], capabilities: undefined, - lastWallet: (localStorage?.getItem('lastWallet') as SupportedWallets) ?? undefined, + lastWallet: (localStorage?.getItem(LAST_WALLET_LS) as SupportedWallets) ?? undefined, status: ConnectionState.Disconnected, deletedToken: undefined, pausedToken: undefined, @@ -73,6 +84,10 @@ export const initialState: InitialStateProps = { isFactoryAcceptOwner: false, accountRecognized: true, factoryId: undefined, + mirrorList: [], + selectedMirror: undefined, + rpcList: [], + selectedRPC: undefined, }; export const getStableCoinList = createAsyncThunk( @@ -127,7 +142,6 @@ export const getExternalTokenList = createAsyncThunk( }, ); -const LAST_WALLET_LS = 'lastWallet'; export const walletSlice = createSlice({ name: 'wallet', initialState, @@ -209,6 +223,22 @@ export const walletSlice = createSlice({ setFactoryId: (state, action) => { state.factoryId = action.payload; }, + setMirrorList: (state, action) => { + state.mirrorList = action.payload; + localStorage.setItem(MIRROR_LIST_LS, JSON.stringify(action.payload)); + }, + setSelectedMirror: (state, action) => { + state.selectedMirror = action.payload; + localStorage.setItem(SELECTED_MIRROR_LS, JSON.stringify(action.payload)); + }, + setRPCList: (state, action) => { + state.rpcList = action.payload; + localStorage.setItem(RPC_LIST_LS, JSON.stringify(action.payload)); + }, + setSelectedRPC: (state, action) => { + state.selectedRPC = action.payload; + localStorage.setItem(SELECTED_RPC_LS, JSON.stringify(action.payload)); + }, clearData: (state) => { state.data = initialState.data; state.lastWallet = undefined; @@ -305,6 +335,30 @@ export const SELECTED_TOKEN_RESERVE_ADDRESS = (state: RootState) => state.wallet.selectedStableCoin?.reserveAddress; export const SELECTED_TOKEN_RESERVE_AMOUNT = (state: RootState) => state.wallet.selectedStableCoin?.reserveAmount; - export const SELECTED_TOKEN_ROLES = (state: RootState) => state.wallet.roles; +export const MIRROR_LIST = (state: RootState) => { + const list = localStorage?.getItem(MIRROR_LIST_LS); + if (list) { + return JSON.parse(list); + } else return state.wallet.mirrorList; +}; +export const SELECTED_MIRROR = (state: RootState) => { + const mirror = localStorage?.getItem(SELECTED_MIRROR_LS); + if (mirror) { + return JSON.parse(mirror); + } else return state.wallet.selectedMirror; +}; +export const RPC_LIST = (state: RootState) => { + const list = localStorage?.getItem(RPC_LIST_LS); + if (list) { + return JSON.parse(list); + } else return state.wallet.rpcList; +}; +export const SELECTED_RPC = (state: RootState) => { + const rpc = localStorage?.getItem(SELECTED_RPC_LS); + if (rpc) { + return JSON.parse(rpc); + } else return state.wallet.selectedRPC; +}; + export const walletActions = walletSlice.actions; diff --git a/web/src/translations/en/appSettings.json b/web/src/translations/en/appSettings.json new file mode 100644 index 000000000..1b10dffde --- /dev/null +++ b/web/src/translations/en/appSettings.json @@ -0,0 +1,14 @@ +{ + "title": "Configure custom RPC/MIRROR", + "subtitle": "Choose an option", + "name": "Name", + "rpc": "RPC", + "mirrorNode": "Mirror node", + "url": "Url", + "apiKey": "Api Key", + "addMirror": "Add new service", + "addRPC": "Add RPC", + "removeMirror": "Remove Mirror Node", + "removeRpc": "Remove RPC", + "header": "Header" +} diff --git a/web/src/translations/en/global.json b/web/src/translations/en/global.json index 9dd1211dd..88c1afb15 100644 --- a/web/src/translations/en/global.json +++ b/web/src/translations/en/global.json @@ -50,7 +50,8 @@ "details": "Stablecoin Details", "proofOfReserve": "Proof-of-Reserve", "feesManagement": "Fees Management", - "settings": "Settings" + "settings": "Settings", + "appSettings": "Network settings" }, "validations": { "required": "This field is required", diff --git a/web/src/views/AppSettings/constants.ts b/web/src/views/AppSettings/constants.ts new file mode 100644 index 000000000..81a517a9d --- /dev/null +++ b/web/src/views/AppSettings/constants.ts @@ -0,0 +1,4 @@ +export const networkOptions = [ + { value: 'testnet', label: 'Tesnet' }, + { value: 'mainnet', label: 'Mainnet' }, +]; diff --git a/web/src/views/AppSettings/index.tsx b/web/src/views/AppSettings/index.tsx new file mode 100644 index 000000000..14c78f2b9 --- /dev/null +++ b/web/src/views/AppSettings/index.tsx @@ -0,0 +1,440 @@ +import { + Box, + Heading, + Text, + Tabs, + TabList, + TabPanels, + Tab, + TabPanel, + Stack, + HStack, + Button, + RadioGroup, + Radio, + Flex, +} from '@chakra-ui/react'; +import { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import Icon from '../../components/Icon'; +import BaseContainer from '../../components/BaseContainer'; +import { SelectController } from '../../components/Form/SelectController'; +import { networkOptions } from './constants'; +import { type FieldValues, useForm, useWatch } from 'react-hook-form'; +import { propertyNotFound } from '../../constant'; +import InputController from '../../components/Form/InputController'; +import SwitchController from '../../components/Form/SwitchController'; +import { useDispatch, useSelector } from 'react-redux'; +import { + LAST_WALLET_SELECTED, + MIRROR_LIST, + RPC_LIST, + SELECTED_MIRROR, + SELECTED_RPC, + walletActions, +} from '../../store/slices/walletSlice'; +import SDKService from '../../services/SDKService'; +import { type IMirrorRPCNode } from '../../interfaces/IMirrorRPCNode'; + +const AppSettings = () => { + const { t } = useTranslation(['appSettings', 'errorPage']); + const dispatch = useDispatch(); + const form = useForm({ + mode: 'onChange', + }); + const { control, getValues } = form; + + const [defaultMirror, setDefaultMirror] = useState('0'); + const [defaultRPC, setDefaultRPC] = useState('0'); + const [showContentMirror, setShowContentMirror] = useState(false); + const [showContentRPC, setShowContentRPC] = useState(false); + const [arrayMirror, setArrayMirror] = useState([]); + const [arrayRPC, setArrayRPC] = useState([]); + + const selectedWallet = useSelector(LAST_WALLET_SELECTED); + const mirrorList: IMirrorRPCNode[] = useSelector(MIRROR_LIST); + const selectedMirror: IMirrorRPCNode = useSelector(SELECTED_MIRROR); + const rpcList: IMirrorRPCNode[] = useSelector(RPC_LIST); + const selectedRPC: IMirrorRPCNode = useSelector(SELECTED_RPC); + + const styles = { + menuList: { + maxH: '220px', + overflowY: 'auto', + bg: 'brand.white', + boxShadow: 'down-black', + p: 4, + }, + wrapper: { + border: '1px', + borderColor: 'brand.black', + borderRadius: '8px', + height: 'initial', + }, + }; + + function removeMirrorToArray(mirrorName: string) { + const newArray = arrayMirror.filter((obj: IMirrorRPCNode) => obj.name !== mirrorName); + setArrayMirror(newArray); + dispatch(walletActions.setMirrorList(newArray.filter((mirror) => mirror.isInConfig === false))); + } + function removeRPCToArray(rpcName: string) { + const newArray = arrayRPC.filter((obj: IMirrorRPCNode) => obj.name !== rpcName); + setArrayRPC(newArray); + dispatch(walletActions.setRPCList(newArray.filter((rpc) => rpc.isInConfig === false))); + } + + const apiKeyMirror = useWatch({ + control, + name: 'apiKeyMirror', + }); + const apiKeyRpc = useWatch({ + control, + name: 'apiKeyRpc', + }); + + function addMirror() { + const { nameMirror, urlMirror, apiKeyValueMirror, mirrorNetwork, apiKeyHeaderMirror } = + getValues(); + const newArray = [ + ...arrayMirror, + createOptionMirror( + nameMirror, + urlMirror, + apiKeyValueMirror, + mirrorNetwork.value, + false, + apiKeyHeaderMirror, + ), + ]; + setArrayMirror(newArray); + dispatch(walletActions.setMirrorList(newArray.filter((mirror) => mirror.isInConfig === false))); + } + function addRpc() { + const { nameRPC, urlRPC, apiKeyValueRPC, apiKeyHeaderRPC, mirrorNetwork } = getValues(); + const newArray = [ + ...arrayRPC, + createOptionMirror( + nameRPC, + urlRPC, + apiKeyValueRPC, + mirrorNetwork.value, + false, + apiKeyHeaderRPC, + ), + ]; + setArrayRPC(newArray); + dispatch(walletActions.setRPCList(newArray.filter((rpc) => rpc.isInConfig === false))); + } + + function createOptionMirror( + name: string, + BASE_URL: string, + API_KEY: string, + Environment: string, + isInConfig: boolean, + HEADER: string, + ): IMirrorRPCNode { + return { name, BASE_URL, API_KEY, Environment, isInConfig, HEADER }; + } + + useEffect(() => { + if (defaultMirror !== '0') { + const selectedDefaultMirror = arrayMirror.find( + (mirror: any) => mirror.name === defaultMirror, + ); + dispatch(walletActions.setSelectedMirror(selectedDefaultMirror)); + SDKService.connectWallet( + selectedWallet!, + selectedDefaultMirror!.Environment.toLocaleLowerCase(), + selectedDefaultMirror, + selectedRPC, + ); + } + }, [defaultMirror]); + + useEffect(() => { + if (defaultRPC !== '0') { + const selectedDefaultRPC = arrayRPC.find((rpc: any) => rpc.name === defaultRPC); + dispatch(walletActions.setSelectedRPC(selectedDefaultRPC)); + SDKService.connectWallet( + selectedWallet!, + selectedDefaultRPC!.Environment.toLocaleLowerCase(), + selectedMirror, + selectedDefaultRPC, + ); + } + }, [defaultRPC]); + + async function handleTypeChangeMirror(): Promise { + const { mirrorNetwork } = getValues(); + setShowContentMirror(true); + setArrayMirror([]); + let mirrors: IMirrorRPCNode[] = []; + + if (process.env.REACT_APP_MIRROR_NODE) { + mirrors = setNodeArrayByNetwork( + JSON.parse(process.env.REACT_APP_MIRROR_NODE), + mirrorNetwork.value, + ); + } + if (mirrorList) { + mirrorList + .filter((obj) => obj.Environment !== undefined) + .filter( + (obj) => obj.Environment.toLocaleLowerCase() === mirrorNetwork.value.toLocaleLowerCase(), + ) + .forEach((obj) => mirrors.push(obj)); + } + setArrayMirror(mirrors); + } + + async function handleTypeChangeRPC(): Promise { + const { rpcNetwork } = getValues(); + setShowContentRPC(true); + setArrayRPC([]); + let rpcs: IMirrorRPCNode[] = []; + + if (process.env.REACT_APP_RPC_NODE) { + rpcs = setNodeArrayByNetwork(JSON.parse(process.env.REACT_APP_RPC_NODE), rpcNetwork.value); + } + if (rpcList) { + rpcList + .filter((obj) => obj.Environment !== undefined) + .filter( + (obj) => obj.Environment.toLocaleLowerCase() === rpcNetwork.value.toLocaleLowerCase(), + ) + .forEach((obj) => rpcs.push(obj)); + } + setArrayRPC(rpcs); + } + + function setNodeArrayByNetwork(list: IMirrorRPCNode[], network: string): IMirrorRPCNode[] { + const nodes: IMirrorRPCNode[] = list + .filter((obj) => obj.Environment !== undefined) + .filter((obj) => obj.Environment.toLocaleLowerCase() === network.toLocaleLowerCase()) + .map((obj, index) => + createOptionMirror( + 'EnvConf' + String(index), + obj.BASE_URL, + obj.API_KEY, + obj.Environment, + true, + obj.HEADER, + ), + ); + return nodes; + } + + return ( + + + + + {t('MirrorNode')} + {t('rpc')} + + + + + handleTypeChangeMirror()} + /> + + + {arrayMirror.map((option: IMirrorRPCNode) => { + return ( + + + {option.name} -{option.BASE_URL} - Apikey: {option.API_KEY} -Header{' '} + {option.HEADER} + + + + removeMirrorToArray(option.name)} + marginLeft={{ base: 2 }} + display={option.isInConfig ? 'none' : 'block'} + /> + + + ); + })} + + + {t('addMirror')} + + + + + + + {t('apiKey')} + + + {apiKeyMirror === true && ( + + + + + )} + + + + + + + + handleTypeChangeRPC()} + /> + + + {arrayRPC.map((option: IMirrorRPCNode) => { + return ( + + + {option.name} -{option.BASE_URL} - Apikey: {option.API_KEY} -Header{' '} + {option.HEADER} + + + + removeRPCToArray(option.name)} + marginLeft={{ base: 2 }} + display={option.isInConfig ? 'none' : 'block'} + /> + + + ); + })} + + + {t('addRPC')} + + + + + + {t('apiKey')} + + + {apiKeyRpc === true && ( + + + + + )} + + + + + + + + + + ); +}; + +export default AppSettings; diff --git a/web/src/views/Roles/constants.ts b/web/src/views/Roles/constants.ts index 972c082ce..a74629f81 100644 --- a/web/src/views/Roles/constants.ts +++ b/web/src/views/Roles/constants.ts @@ -27,6 +27,12 @@ export const cashinLimitOptions = [ { value: 'CHECK', label: 'Check cash in limit' }, ]; +export const networkOptions = [ + { value: 'TESTNET', label: 'Tesnet' }, + { value: 'PREVIEWNET', label: 'Previewnet' }, + { value: 'MAINNET', label: 'Mainnet' }, +]; + export const fields = { amount: 'amount', account: 'account',