From c4d79e90a5e0cfa90710e3bb214f938c35583c58 Mon Sep 17 00:00:00 2001 From: Aleksandr Kukharenko Date: Tue, 24 Oct 2023 14:11:27 +0300 Subject: [PATCH 1/7] integrate http polling for contracts --- public/main/client/handlers/single-core.js | 6 ++++++ public/main/client/index.js | 8 ++++++-- public/main/client/subscriptions/single-core.js | 1 + src/client/index.js | 4 ++++ src/components/contracts/BuyerHub.js | 2 +- src/store/reducers/contracts.js | 11 +++++++++++ src/subscriptions.js | 1 + 7 files changed, 30 insertions(+), 3 deletions(-) diff --git a/public/main/client/handlers/single-core.js b/public/main/client/handlers/single-core.js index 7521ed64..a2fe8f51 100644 --- a/public/main/client/handlers/single-core.js +++ b/public/main/client/handlers/single-core.js @@ -277,6 +277,11 @@ function refreshAllContracts({}, { api }) { return api.contracts.refreshContracts(null, walletId); } +function startWatchingContracts({}, { api }) { + const walletId = wallet.getAddress().address; + return api.contracts.startWatching(); +} + function refreshTransaction({ hash, address }, { api }) { return pTimeout( api.explorer.refreshTransaction(hash, address), @@ -372,6 +377,7 @@ function getPastTransactions({ address, page, pageSize }, { api }) { module.exports = { // refreshAllSockets, refreshAllContracts, + startWatchingContracts, purchaseContract, createContract, cancelContract, diff --git a/public/main/client/index.js b/public/main/client/index.js index 5f006aea..56df3fd3 100644 --- a/public/main/client/index.js +++ b/public/main/client/index.js @@ -29,7 +29,8 @@ function startCore({ chain, core, config: coreConfig }, webContent) { "transactions-scan-started", "transactions-scan-finished", "contracts-scan-started", - "contracts-scan-finished" + "contracts-scan-finished", + "contracts-updated", ); function send(eventName, data) { @@ -87,7 +88,10 @@ function startCore({ chain, core, config: coreConfig }, webContent) { }); } - emitter.on("open-wallet", syncTransactions); + emitter.on("open-wallet", (props) => { + syncTransactions(props); + api.contracts.startWatching({}); + }); emitter.on("wallet-error", function(err) { logger.warn( diff --git a/public/main/client/subscriptions/single-core.js b/public/main/client/subscriptions/single-core.js index 0a67e934..63362713 100644 --- a/public/main/client/subscriptions/single-core.js +++ b/public/main/client/subscriptions/single-core.js @@ -9,6 +9,7 @@ const listeners = { "login-submit": handlers.onLoginSubmit, // 'refresh-all-sockets': handlers.refreshAllSockets, "refresh-all-contracts": handlers.refreshAllContracts, + "start-watching-contracts": handlers.startWatchingContracts, "refresh-all-transactions": handlers.refreshAllTransactions, "refresh-transaction": handlers.refreshTransaction, "get-gas-limit": handlers.getGasLimit, diff --git a/src/client/index.js b/src/client/index.js index f3b40c3c..4ed01336 100644 --- a/src/client/index.js +++ b/src/client/index.js @@ -111,6 +111,10 @@ const createClient = function(createStore) { 'refresh-all-contracts', 120000 ), + startWatchingContracts: utils.forwardToMainProcess( + 'start-watching-contracts', + 120000 + ), onOnboardingCompleted: utils.forwardToMainProcess('onboarding-completed'), recoverFromMnemonic: utils.forwardToMainProcess('recover-from-mnemonic'), getTokenGasLimit: utils.forwardToMainProcess('get-token-gas-limit'), diff --git a/src/components/contracts/BuyerHub.js b/src/components/contracts/BuyerHub.js index 5d930bfe..396b4af3 100644 --- a/src/components/contracts/BuyerHub.js +++ b/src/components/contracts/BuyerHub.js @@ -87,7 +87,7 @@ function BuyerHub({ const [isHistoryModalOpen, setIsHistoryModalOpen] = useState(false); - const contractsWithHistory = contracts.filter(c => c.history.length); + const contractsWithHistory = contracts.filter(c => c.history?.length); const showHistory = contractsWithHistory.length; const onHistoryOpen = () => setIsHistoryModalOpen(true); diff --git a/src/store/reducers/contracts.js b/src/store/reducers/contracts.js index c29e4de4..e4e04966 100644 --- a/src/store/reducers/contracts.js +++ b/src/store/reducers/contracts.js @@ -44,6 +44,17 @@ const reducer = handleActions( }; }, + 'contracts-updated': (state, { payload }) => { + const idContractMap = keyBy(payload.actives, 'id'); + + return { + ...state, + actives: { ...state.actives, ...idContractMap }, + lastUpdated: parseInt(Date.now() / 1000, 10), + syncStatus: 'up-to-date' + }; + }, + 'remove-draft': (state, { payload }) => ({ ...state, drafts: Object.assign(state.drafts, []).filter( diff --git a/src/subscriptions.js b/src/subscriptions.js index 84f0cac5..3b09e672 100644 --- a/src/subscriptions.js +++ b/src/subscriptions.js @@ -10,6 +10,7 @@ export const subscribeToMainProcessMessages = function(store) { 'transactions-scan-finished', 'transactions-scan-started', 'contracts-scan-finished', + 'contracts-updated', 'contracts-scan-started', 'wallet-state-changed', 'coin-price-updated', From 1a1b34b9a6c9354abe28319c124e825ce2abd281 Mon Sep 17 00:00:00 2001 From: Aleksandr Kukharenko Date: Thu, 26 Oct 2023 14:48:20 +0300 Subject: [PATCH 2/7] transactions pool --- public/main/client/index.js | 4 ++-- src/store/reducers/wallet.js | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/public/main/client/index.js b/public/main/client/index.js index 56df3fd3..be9ce3be 100644 --- a/public/main/client/index.js +++ b/public/main/client/index.js @@ -60,10 +60,10 @@ function startCore({ chain, core, config: coreConfig }, webContent) { return api.explorer .syncTransactions( 0, - address, (number) => storage.setSyncBlock(number, chain), page, - pageSize + pageSize, + address ) .then(function() { send("transactions-scan-finished", { success: true }); diff --git a/src/store/reducers/wallet.js b/src/store/reducers/wallet.js index c0b5499f..ee8c75d1 100644 --- a/src/store/reducers/wallet.js +++ b/src/store/reducers/wallet.js @@ -26,6 +26,10 @@ export const initialState = { * Should filter transactions without receipt if we received ones */ const mergeTransactions = (stateTxs, payloadTxs) => { + console.log( + '🚀 ~ file: wallet.js:29 ~ mergeTransactions ~ payloadTxs:', + payloadTxs + ); const txWithReceipts = payloadTxs.filter(tx => tx.receipt); const newStateTxs = { ...stateTxs }; @@ -118,10 +122,7 @@ const reducer = handleActions( ...state, token: { ...state.token, - transactions: mergeTransactions( - state.token.transactions, - payload.transactions - ) + transactions: mergeTransactions(state.token.transactions, payload) } }), From e98ce6b53aed417f632b7ceee50ca9037173fb98 Mon Sep 17 00:00:00 2001 From: bohdan-titan Date: Tue, 7 Nov 2023 16:21:01 +0200 Subject: [PATCH 3/7] Fixed mapping --- public/main/client/handlers/single-core.js | 2 +- src/components/dashboard/tx-list/TxList.js | 1 + src/store/reducers/wallet.js | 72 ++++++++-------------- src/store/selectors/wallet.js | 6 +- src/store/utils/createTransactionParser.js | 72 +++++----------------- src/store/utils/index.js | 4 -- 6 files changed, 42 insertions(+), 115 deletions(-) diff --git a/public/main/client/handlers/single-core.js b/public/main/client/handlers/single-core.js index a2fe8f51..a8dc6221 100644 --- a/public/main/client/handlers/single-core.js +++ b/public/main/client/handlers/single-core.js @@ -371,7 +371,7 @@ const revealSecretPhrase = async (password) => { } function getPastTransactions({ address, page, pageSize }, { api }) { - return api.explorer.getPastCoinTransactions(0, undefined, address, page, pageSize); + return api.explorer.syncTransactions(0, undefined, page, pageSize, address); } module.exports = { diff --git a/src/components/dashboard/tx-list/TxList.js b/src/components/dashboard/tx-list/TxList.js index a8733a4e..41000d5c 100644 --- a/src/components/dashboard/tx-list/TxList.js +++ b/src/components/dashboard/tx-list/TxList.js @@ -73,6 +73,7 @@ export const TxList = ({ syncStatus, client }) => { + console.log('🚀 ~ file: TxList.js:76 ~ transactions:', transactions); const handleClick = e => { if (!window.isDev || !e.shiftKey || !e.altKey) return; diff --git a/src/store/reducers/wallet.js b/src/store/reducers/wallet.js index ee8c75d1..c12a9c18 100644 --- a/src/store/reducers/wallet.js +++ b/src/store/reducers/wallet.js @@ -30,51 +30,25 @@ const mergeTransactions = (stateTxs, payloadTxs) => { '🚀 ~ file: wallet.js:29 ~ mergeTransactions ~ payloadTxs:', payloadTxs ); - const txWithReceipts = payloadTxs.filter(tx => tx.receipt); - const newStateTxs = { ...stateTxs }; - - for (const tx of txWithReceipts) { - const key = `${tx.transaction.hash}_${tx.receipt.tokenSymbol || 'ETH'}`; - const oldStateTx = stateTxs[key]; - - const isDifferentLogIndex = - oldStateTx?.transaction?.logIndex && - tx?.transaction?.logIndex && - oldStateTx?.transaction?.logIndex !== tx?.transaction?.logIndex; // means that this is a second transaction within the same hash - - if (oldStateTx && !isDifferentLogIndex) { - continue; - } - newStateTxs[key] = tx; - // contract purchase emits 2 transactions with the same hash - // as of now we merge corresponding amount values. Temporary fix, until refactoring trasactions totally - - // we sum transaction value if it is transfers within the same transaction, but with different logIndex - // TODO: display both transactions in the UI either separately or as a single one with two outputs - if (oldStateTx && isDifferentLogIndex) { - if ( - newStateTxs[key].transaction.value && - oldStateTx.transaction.logIndex !== tx.transaction.logIndex - ) { - newStateTxs[key].transaction.value = String( - Number(oldStateTx.transaction.value) + Number(tx.transaction.value) - ); - } - - if (newStateTxs[key].transaction.input.amount) { - newStateTxs[key].transaction.input.amount = String( - Number(oldStateTx.transaction.input.amount) + - Number(tx.transaction.input.amount) - ); - } - if (newStateTxs[key].receipt.value) { - newStateTxs[key].receipt.value = String( - Number(oldStateTx.receipt.value) + Number(tx.receipt.value) - ); + const newStateTxs = { ...stateTxs }; + const txs = Object.values(payloadTxs).filter(x => typeof x == 'object'); + + for (const tx of txs) { + const flattenObjects = tx.transfers.map(x => ({ + ...tx, + ...x, + transfers: undefined + })); + for (const obj of flattenObjects) { + if (obj.amount == 0) { + continue; } + const key = `${obj.txhash}_${obj.token || 'ETH'}`; + newStateTxs[key] = obj; } } + return newStateTxs; }; @@ -118,13 +92,15 @@ const reducer = handleActions( } }), - 'token-transactions-changed': (state, { payload }) => ({ - ...state, - token: { - ...state.token, - transactions: mergeTransactions(state.token.transactions, payload) - } - }), + 'token-transactions-changed': (state, { payload }) => { + return { + ...state, + token: { + ...state.token, + transactions: mergeTransactions(state.token.transactions, payload) + } + }; + }, 'transactions-next-page': (state, { payload }) => ({ ...state, diff --git a/src/store/selectors/wallet.js b/src/store/selectors/wallet.js index 89d39952..0c57c927 100644 --- a/src/store/selectors/wallet.js +++ b/src/store/selectors/wallet.js @@ -66,11 +66,7 @@ export const getTransactions = createSelector(getWallet, walletData => { const transactions = Object.values(walletData?.token?.transactions) || []; - const sorted = sortBy(transactions, [ - 'receipt.blockNumber', - 'receipt.transactionIndex', - 'transaction.nonce' - ]).reverse(); + const sorted = sortBy(transactions, ['blockNumber']).reverse(); return sorted.map(transactionParser); }); diff --git a/src/store/utils/createTransactionParser.js b/src/store/utils/createTransactionParser.js index 11d804a6..6f8e1cc3 100644 --- a/src/store/utils/createTransactionParser.js +++ b/src/store/utils/createTransactionParser.js @@ -6,24 +6,17 @@ import { fromTokenBaseUnitsToLMR } from '../../utils/coinValue'; -function isSendTransaction({ transaction }, tokenData, myAddress) { - const from = transaction.input?.from || transaction.from; +function isSendTransaction(transaction, myAddress) { + const from = transaction.from; return from.toLowerCase() === myAddress.toLowerCase(); } -function isReceiveTransaction({ transaction }, tokenData, myAddress) { - const to = transaction.input?.to || transaction.to; +function isReceiveTransaction(transaction, myAddress) { + const to = transaction.to; return to?.toLowerCase() === myAddress.toLowerCase(); } -function isImportRequestTransaction(rawTx) { - return get(rawTx.meta, 'lumerin.importRequest', false); -} - function getTxType(rawTx, tokenData, myAddress) { - if (isImportRequestTransaction(rawTx)) { - return 'import-requested'; - } if (isSendTransaction(rawTx, tokenData, myAddress)) { return 'sent'; } @@ -33,28 +26,15 @@ function getTxType(rawTx, tokenData, myAddress) { return 'unknown'; } -function getFrom(rawTx, tokenData, txType) { - return rawTx.transaction.input?.from || rawTx.transaction.from; -} - -function getTo(rawTx, tokenData, txType) { - return rawTx.transaction.input?.to || rawTx.transaction.to; -} - -function getValue(rawTx, tokenData, txType) { +function getValue(rawTx, txType) { if (!['received', 'sent'].includes(txType)) { return '0'; } - const value = rawTx.transaction.input?.amount || rawTx.transaction.value; + const value = rawTx.amount; return value; } -function getSymbol(rawTx, tokenData, txType) { - const isLmr = typeof rawTx.transaction.input === 'object'; - return isLmr ? 'LMR' : 'ETH'; -} - function getConvertedFrom(rawTx, txType) { return txType === 'converted' ? new BigNumber(rawTx.transaction.value).isZero() @@ -91,10 +71,6 @@ function getIsPending(rawTx) { return !get(rawTx, 'receipt', null); } -function getContractCallFailed(rawTx) { - return get(rawTx, ['meta', 'contractCallFailed'], false); -} - function getGasUsed(rawTx) { return get(rawTx, ['receipt', 'gasUsed'], null); } @@ -107,47 +83,29 @@ function getBlockNumber(rawTx) { return get(rawTx, ['transaction', 'blockNumber'], null); } -// TODO: in the future other transaction types will include a timestamp -function getTimestamp(rawTx) { - const timestamp = get( - rawTx, - ['meta', 'lumerin', 'export', 'blockTimestamp'], - null - ); - return timestamp ? Number(timestamp) : null; -} - function getFormattedTime(timestamp) { return timestamp ? moment.unix(timestamp).format('LLLL') : null; } export const createTransactionParser = myAddress => rawTx => { - const tokenData = Object.values(rawTx.meta.token || {})[0] || null; - const txType = getTxType(rawTx, tokenData, myAddress); - const timestamp = getTimestamp(rawTx, txType); - const symbol = getSymbol(rawTx, tokenData, txType); - const value = getValue(rawTx, tokenData, txType); + const txType = getTxType(rawTx, myAddress); + const timestamp = Number(rawTx.timestamp); + const symbol = rawTx.token; + const value = getValue(rawTx, txType); return { - contractCallFailed: getContractCallFailed(rawTx), - isCancelApproval: getIsCancelApproval(tokenData), - approvedValue: getApprovedValue(tokenData), formattedTime: getFormattedTime(timestamp), - isProcessing: getIsProcessing(tokenData), - blockNumber: getBlockNumber(rawTx), - isApproval: getIsApproval(tokenData), - isPending: getIsPending(rawTx), + blockNumber: rawTx.blockNumber, timestamp, - gasUsed: getGasUsed(rawTx), + gasUsed: rawTx.transactionFee, txType, symbol, value: symbol === 'LMR' ? fromTokenBaseUnitsToLMR(value) : fromTokenBaseUnitsToETH(value), - from: getFrom(rawTx, tokenData, txType), - hash: getTransactionHash(rawTx), - meta: rawTx.meta, - to: getTo(rawTx, tokenData, txType) + from: rawTx.from, + hash: rawTx.txhash, + to: rawTx.to }; }; diff --git a/src/store/utils/index.js b/src/store/utils/index.js index f06b71cc..5add3170 100644 --- a/src/store/utils/index.js +++ b/src/store/utils/index.js @@ -42,10 +42,6 @@ export function isGreaterThanZero(client, amount) { return weiAmount.gt(client.toBN(0)); } -// export function isFailed(tx, confirmations) { -// return confirmations > 0 || tx.contractCallFailed -// } - export function isPending(tx, confirmations) { // return !isFailed(tx, confirmations) && confirmations < 6 return false; From b182f892fa943ed95111a2cea112334b45f8d1c8 Mon Sep 17 00:00:00 2001 From: bohdan-titan Date: Mon, 13 Nov 2023 13:37:22 +0200 Subject: [PATCH 4/7] Removed some comments --- public/main/client/index.js | 1 + src/components/dashboard/tx-list/TxList.js | 1 - src/store/reducers/wallet.js | 18 ++++++++---------- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/public/main/client/index.js b/public/main/client/index.js index be9ce3be..8fa36dd5 100644 --- a/public/main/client/index.js +++ b/public/main/client/index.js @@ -91,6 +91,7 @@ function startCore({ chain, core, config: coreConfig }, webContent) { emitter.on("open-wallet", (props) => { syncTransactions(props); api.contracts.startWatching({}); + //api.explorer.startWatching({}); }); emitter.on("wallet-error", function(err) { diff --git a/src/components/dashboard/tx-list/TxList.js b/src/components/dashboard/tx-list/TxList.js index 41000d5c..a8733a4e 100644 --- a/src/components/dashboard/tx-list/TxList.js +++ b/src/components/dashboard/tx-list/TxList.js @@ -73,7 +73,6 @@ export const TxList = ({ syncStatus, client }) => { - console.log('🚀 ~ file: TxList.js:76 ~ transactions:', transactions); const handleClick = e => { if (!window.isDev || !e.shiftKey || !e.altKey) return; diff --git a/src/store/reducers/wallet.js b/src/store/reducers/wallet.js index c12a9c18..81883814 100644 --- a/src/store/reducers/wallet.js +++ b/src/store/reducers/wallet.js @@ -26,11 +26,6 @@ export const initialState = { * Should filter transactions without receipt if we received ones */ const mergeTransactions = (stateTxs, payloadTxs) => { - console.log( - '🚀 ~ file: wallet.js:29 ~ mergeTransactions ~ payloadTxs:', - payloadTxs - ); - const newStateTxs = { ...stateTxs }; const txs = Object.values(payloadTxs).filter(x => typeof x == 'object'); @@ -102,11 +97,14 @@ const reducer = handleActions( }; }, - 'transactions-next-page': (state, { payload }) => ({ - ...state, - hasNextPage: payload.hasNextPage, - page: payload.page - }), + 'transactions-next-page': (state, { payload }) => { + console.log(payload); + return { + ...state, + hasNextPage: payload.hasNextPage, + page: payload.page + }; + }, 'token-state-changed': (state, { payload }) => ({ ...state, From 67f4978da2cb56d1aca0499cb13d4a3000c86ceb Mon Sep 17 00:00:00 2001 From: Aleksandr Kukharenko Date: Wed, 20 Dec 2023 14:49:34 +0200 Subject: [PATCH 5/7] update polling --- .devArbitrum.env | 3 +- .devGoerli.env | 25 -- .devSepolia.env | 5 +- .devSepoliaArbitrum.env | 27 +++ .github/workflows/auto-publish.yml | 73 +++--- .github/workflows/auto-release-dev.yml | 66 ++--- .github/workflows/auto-release-stg.yml | 69 +++--- .github/workflows/auto-tag.yml | 2 +- package.json | 7 +- public/config/index.js | 18 +- public/errorHandler.js | 2 +- public/main/client/contractsHashrateSyncer.js | 41 ++++ public/main/client/database.js | 1 + public/main/client/handlers/no-core.js | 37 ++- public/main/client/handlers/single-core.js | 46 ++-- public/main/client/index.js | 61 +++-- public/main/client/proxyRouter.js | 9 +- public/main/client/settings/index.js | 6 +- public/main/client/subscriptions/no-core.js | 2 +- src/client/index.js | 4 +- src/components/common/Root.js | 2 +- src/components/contracts/BuyerHub.js | 19 +- src/components/contracts/Marketplace.js | 1 + src/components/contracts/SellerHub.js | 22 +- .../contracts/contracts-list/BuyerHubRow.js | 15 +- .../contracts/contracts-list/ContractsList.js | 1 + .../contracts/contracts-list/Row.js | 2 - .../modals/ArchiveModal/ArchiveModal.js | 45 +++- .../modals/ArchiveModal/ArchiveRow.js | 42 +++- .../contracts/modals/CreateContractModal.js | 1 + .../modals/CreateContractModal.styles.js | 6 +- .../modals/HashrateModal/HashrateModal.js | 113 +++++++++ .../modals/HashrateModal/chartRenderer.js | 85 +++++++ .../contracts/modals/HashrateModal/utils.js | 26 ++ .../PurchaseModal/PurchaseContractModal.js | 4 +- .../PurchaseModal/PurchaseFormModalPage.js | 31 ++- .../PurchaseModal/PurchasePreviewModalPage.js | 14 +- .../SellerWhitelistModal.js | 59 +++++ src/components/hooks/useInterval.js | 26 ++ .../onboarding/ProxyRouterConfigStep.js | 6 +- src/components/toasts/Toast.js | 1 - src/components/tools/ExportPrivateKeyModal.js | 2 +- src/components/tools/ProxyConfigPanel.js | 229 ++++++++++++++++++ .../tools/RevealSecretPhraseModal.js | 7 +- src/components/tools/Tools.js | 209 +++------------- src/components/tools/common.js | 57 +++++ src/store/hocs/withContractsState.js | 6 +- src/store/hocs/withToolsState.js | 1 + src/store/reducers/contracts.js | 4 +- src/store/selectors/config.js | 3 + src/store/validators.js | 4 +- src/subscriptions.js | 1 + 52 files changed, 1098 insertions(+), 450 deletions(-) delete mode 100644 .devGoerli.env create mode 100644 .devSepoliaArbitrum.env create mode 100644 public/main/client/contractsHashrateSyncer.js create mode 100644 src/components/contracts/modals/HashrateModal/HashrateModal.js create mode 100644 src/components/contracts/modals/HashrateModal/chartRenderer.js create mode 100644 src/components/contracts/modals/HashrateModal/utils.js create mode 100644 src/components/contracts/modals/SellerWhitelistModal/SellerWhitelistModal.js create mode 100644 src/components/hooks/useInterval.js create mode 100644 src/components/tools/ProxyConfigPanel.js create mode 100644 src/components/tools/common.js diff --git a/.devArbitrum.env b/.devArbitrum.env index af15eb4c..6eb19781 100644 --- a/.devArbitrum.env +++ b/.devArbitrum.env @@ -3,8 +3,8 @@ CHAIN_ID=421613 SYMBOL_LMR=gaLMR SYMBOL_ETH=gaETH -PROXY_ROUTER_URL=http://proxy.dev.lumerin.io:8080 EXPLORER_URL=https://arbiscan.io/tx/{{hash}} +EXPLORER_API_URLS=["https://api.arbiscan.io/api"] ETH_NODE_ADDRESS= ETH_NODE_ADDRESS_HTTP= IP_LOOKUP_URL=https://ifconfig.io/ip @@ -14,7 +14,6 @@ LMR_DEFAULT_GAS_LIMIT=999999 DEFAULT_GAS_PRICE=1000000000 MAX_GAS_PRICE=20000000000000000 -REQUIRED_PASSWORD_ENTROPY=72 SENTRY_DSN= TRACKING_ID= diff --git a/.devGoerli.env b/.devGoerli.env deleted file mode 100644 index 4b340ab5..00000000 --- a/.devGoerli.env +++ /dev/null @@ -1,25 +0,0 @@ -DEV_TOOLS=true - -DISPLAY_NAME=Goerli -CHAIN_ID=5 -SYMBOL=LMR - -LUMERIN_TOKEN_ADDRESS=0xF3aCe2847F01D3ef1025c7070579611091A6422D -CLONE_FACTORY_ADDRESS=0xbF2A6EA18e2CF0846cE7FC9Fa9EB9bA22BF035fF - -PROXY_ROUTER_URL=http://proxy.dev.lumerin.io:8080 -EXPLORER_URL=https://goerli.etherscan.io/tx/{{hash}} -ETH_NODE_ADDRESS=wss://goerli.infura.io/ws/v3/API_KEY -IP_LOOKUP_URL=https://ifconfig.io/ip - -COIN_DEFAULT_GAS_LIMIT=999999 -LMR_DEFAULT_GAS_LIMIT=999999 -DEFAULT_GAS_PRICE=1000000000 -MAX_GAS_PRICE=20000000000000000 - -REQUIRED_PASSWORD_ENTROPY=72 -# SENTRY_DSN= -# TRACKING_ID= - -TITAN_LIGHTNING_POOL=pplp.titan.io:4141 -DEFAULT_SELLER_CURRENCY=BTC \ No newline at end of file diff --git a/.devSepolia.env b/.devSepolia.env index a056e812..2c65b94e 100644 --- a/.devSepolia.env +++ b/.devSepolia.env @@ -7,10 +7,9 @@ SYMBOL_ETH=sETH LUMERIN_TOKEN_ADDRESS=0x9072495AFc2D59d857118ee59b4EF6cfc407D103 CLONE_FACTORY_ADDRESS=0xBC2776b15F0CD5eF9f8E443b22B9690b579C0506 -FAUCET_ADDRESS=0xFE64cAE7Ca5166c8bb0e014e2D402f8d22764f24 -PROXY_ROUTER_URL=http://proxy.dev.lumerin.io:8080 EXPLORER_URL=https://sepolia.etherscan.io/tx/{{hash}} +EXPLORER_API_URLS=["https://api-sepolia.etherscan.io/api", "https://eth-sepolia.blockscout.com/api"] ETH_NODE_ADDRESS=wss://sepolia.infura.io/ws/v3/API_KEY ETH_NODE_ADDRESS_HTTP=https://rpc.ankr.com/eth_sepolia IP_LOOKUP_URL=https://ifconfig.io/ip @@ -20,8 +19,6 @@ LMR_DEFAULT_GAS_LIMIT=999999 DEFAULT_GAS_PRICE=1000000000 MAX_GAS_PRICE=20000000000000000 -REQUIRED_PASSWORD_ENTROPY=72 - RECAPTCHA_SITE_KEY=1 FAUCET_URL=https://faucet.dev.lumerin.io/ SHOW_FAUCET=true diff --git a/.devSepoliaArbitrum.env b/.devSepoliaArbitrum.env new file mode 100644 index 00000000..bf515962 --- /dev/null +++ b/.devSepoliaArbitrum.env @@ -0,0 +1,27 @@ +DISPLAY_NAME=Sepolia Arbitrum +CHAIN_ID=421614 +SYMBOL_LMR=saLMR +SYMBOL_ETH=saETH + +EXPLORER_URL=https://sepolia.arbiscan.io/tx/{{hash}} +EXPLORER_API_URLS=["https://api-sepolia.arbiscan.io/api"] +ETH_NODE_ADDRESS= +ETH_NODE_ADDRESS_HTTP=["https://arbitrum-sepolia.blockpi.network/v1/rpc/public","https://sepolia-rollup.arbitrum.io/rpc","https://arbitrum-sepolia.publicnode.com"] +IP_LOOKUP_URL=https://ifconfig.io/ip + +COIN_DEFAULT_GAS_LIMIT=999999 +LMR_DEFAULT_GAS_LIMIT=999999 +DEFAULT_GAS_PRICE=1000000000 +MAX_GAS_PRICE=20000000000000000 + +SENTRY_DSN= +TRACKING_ID= + +RECAPTCHA_SITE_KEY= +FAUCET_URL=https://faucet.dev.lumerin.io/ +SHOW_FAUCET=true +LUMERIN_TOKEN_ADDRESS=0xC27DafaD85F199FD50dD3FD720654875D6815871 +CLONE_FACTORY_ADDRESS=0x15437978300786aDe37f61e02Be1C061e51353D3 + +TITAN_LIGHTNING_POOL=pplp.titan.io:4141 +DEFAULT_SELLER_CURRENCY=BTC \ No newline at end of file diff --git a/.github/workflows/auto-publish.yml b/.github/workflows/auto-publish.yml index f45cffaa..b6057752 100644 --- a/.github/workflows/auto-publish.yml +++ b/.github/workflows/auto-publish.yml @@ -29,26 +29,26 @@ jobs: os: macos-latest runs-on: ${{ matrix.os }} environment: ${{ matrix.environment }} - env: - CHAIN_ID: ${{ secrets.CHAIN_ID }} - EXPLORER_URL: ${{ secrets.EXPLORER_URL }} - DISPLAY_NAME: ${{ secrets.DISPLAY_NAME }} - SYMBOL: ${{ secrets.SYMBOL }} - SYMBOL_ETH: ${{ secrets.SYMBOL_ETH }} - SYMBOL_LMR: ${{ secrets.SYMBOL_LMR }} - PROXY_ROUTER_URL: ${{ secrets.PROXY_ROUTER_URL }} - COIN_DEFAULT_GAS_LIMIT: ${{ secrets.COIN_DEFAULT_GAS_LIMIT }} - MAX_GAS_PRICE: ${{ secrets.MAX_GAS_PRICE }} - LMR_DEFAULT_GAS_LIMIT: ${{ secrets.LMR_DEFAULT_GAS_LIMIT }} - DEFAULT_GAS_PRICE: ${{ secrets.DEFAULT_GAS_PRICE }} - REQUIRED_PASSWORD_ENTROPY: ${{ secrets.REQUIRED_PASSWORD_ENTROPY }} - RECAPTCHA_SITE_KEY: ${{ secrets.RECAPTCHA_SITE_KEY }} - FAUCET_URL: ${{ secrets.FAUCET_URL }} - SHOW_FAUCET: ${{ secrets.SHOW_FAUCET }} + env: # keep alphabetical order + CHAIN_ID: ${{ vars.CHAIN_ID }} + COIN_DEFAULT_GAS_LIMIT: ${{ vars.COIN_DEFAULT_GAS_LIMIT }} + DEFAULT_GAS_PRICE: ${{ vars.DEFAULT_GAS_PRICE }} + DISPLAY_NAME: ${{ vars.DISPLAY_NAME }} ETH_NODE_ADDRESS_HTTP: ${{ vars.ETH_NODE_ADDRESS_HTTP }} - TITAN_LIGHTNING_POOL: ${{ secrets.TITAN_LIGHTNING_POOL }} - PORT_CHECKER_URL: ${{ vars.PORT_CHECKER_URL }} + EXPLORER_API_URLS: ${{ vars.EXPLORER_API_URLS }} + EXPLORER_URL: ${{ vars.EXPLORER_URL }} + FAUCET_URL: ${{ vars.FAUCET_URL }} + LMR_DEFAULT_GAS_LIMIT: ${{ vars.LMR_DEFAULT_GAS_LIMIT }} + MAX_GAS_PRICE: ${{ vars.MAX_GAS_PRICE }} PORT_CHECK_ERROR_LINK: ${{ vars.PORT_CHECK_ERROR_LINK }} + PORT_CHECKER_URL: ${{ vars.PORT_CHECKER_URL }} + RECAPTCHA_SITE_KEY: ${{ secrets.RECAPTCHA_SITE_KEY }} + SELLER_WHITELIST_URL: ${{ vars.SELLER_WHITELIST_URL }} + SHOW_FAUCET: ${{ vars.SHOW_FAUCET }} + SYMBOL_ETH: ${{ vars.SYMBOL_ETH }} + SYMBOL_LMR: ${{ vars.SYMBOL_LMR }} + TITAN_LIGHTNING_DASHBOARD: ${{ vars.TITAN_LIGHTNING_DASHBOARD }} + TITAN_LIGHTNING_POOL: ${{ vars.TITAN_LIGHTNING_POOL }} steps: - name: Checkout uses: actions/checkout@v3 @@ -60,28 +60,33 @@ jobs: - name: Post install if: matrix.target == 'macos-arm' run: npm run postinstallMacDist - - name: Set env from github - run: | + - name: Set env from github + # keep alphabetical order + run: | echo "CHAIN_ID=$CHAIN_ID" >> .env - echo "EXPLORER_URL=$EXPLORER_URL" >> .env - echo "DISPLAY_NAME=$DISPLAY_NAME" >> .env - echo "SYMBOL=$SYMBOL" >> .env - echo "SYMBOL_ETH=$SYMBOL_ETH" >> .env - echo "SYMBOL_LMR=$SYMBOL_LMR" >> .env - echo "PROXY_ROUTER_URL=$PROXY_ROUTER_URL" >> .env echo "COIN_DEFAULT_GAS_LIMIT=$COIN_DEFAULT_GAS_LIMIT" >> .env - echo "MAX_GAS_PRICE=$MAX_GAS_PRICE" >> .env - echo "LMR_DEFAULT_GAS_LIMIT=$LMR_DEFAULT_GAS_LIMIT" >> .env echo "DEFAULT_GAS_PRICE=$DEFAULT_GAS_PRICE" >> .env - echo "REQUIRED_PASSWORD_ENTROPY=$REQUIRED_PASSWORD_ENTROPY" >> .env - echo "RECAPTCHA_SITE_KEY=$RECAPTCHA_SITE_KEY" >> .env - echo "FAUCET_URL=$FAUCET_URL" >> .env - echo "SHOW_FAUCET=$SHOW_FAUCET" >> .env + echo "DEV_TOOLS=true" >> .env + echo "DISPLAY_NAME=$DISPLAY_NAME" >> .env echo "ETH_NODE_ADDRESS_HTTP=$ETH_NODE_ADDRESS_HTTP" >> .env - echo "TITAN_LIGHTNING_POOL=$TITAN_LIGHTNING_POOL" >> .env + echo "EXPLORER_API_URLS=$EXPLORER_API_URLS" >> .env + echo "EXPLORER_URL=$EXPLORER_URL" >> .env + echo "FAUCET_URL=$FAUCET_URL" >> .env + echo "LMR_DEFAULT_GAS_LIMIT=$LMR_DEFAULT_GAS_LIMIT" >> .env + echo "MAX_GAS_PRICE=$MAX_GAS_PRICE" >> .env echo "NODE_ENV=production" >> .env - echo "PORT_CHECKER_URL=$PORT_CHECKER_URL" >> .env echo "PORT_CHECK_ERROR_LINK=$PORT_CHECK_ERROR_LINK" >> .env + echo "PORT_CHECKER_URL=$PORT_CHECKER_URL" >> .env + echo "PROXY_ROUTER_URL=$PROXY_ROUTER_URL" >> .env + echo "RECAPTCHA_SITE_KEY=$RECAPTCHA_SITE_KEY" >> .env + echo "REQUIRED_PASSWORD_ENTROPY=$REQUIRED_PASSWORD_ENTROPY" >> .env + echo "SELLER_WHITELIST_URL=$SELLER_WHITELIST_URL" >> .env + echo "SHOW_FAUCET=$SHOW_FAUCET" >> .env + echo "SYMBOL_ETH=$SYMBOL_ETH" >> .env + echo "SYMBOL_LMR=$SYMBOL_LMR" >> .env + echo "SYMBOL=$SYMBOL" >> .env + echo "TITAN_LIGHTNING_DASHBOARD=$TITAN_LIGHTNING_DASHBOARD" >> .env + echo "TITAN_LIGHTNING_POOL=$TITAN_LIGHTNING_POOL" >> .env shell: bash - name: Set env from gitlab run: | diff --git a/.github/workflows/auto-release-dev.yml b/.github/workflows/auto-release-dev.yml index bb89c4c3..19179180 100644 --- a/.github/workflows/auto-release-dev.yml +++ b/.github/workflows/auto-release-dev.yml @@ -35,26 +35,26 @@ jobs: asset_name: lumerin_v${{github.ref_name}}_arm.dmg runs-on: ${{ matrix.os }} environment: ${{ matrix.environment }} - env: - CHAIN_ID: ${{ secrets.CHAIN_ID }} - EXPLORER_URL: ${{ secrets.EXPLORER_URL }} - DISPLAY_NAME: ${{ secrets.DISPLAY_NAME }} - SYMBOL: ${{ secrets.SYMBOL }} - SYMBOL_ETH: ${{ secrets.SYMBOL_ETH }} - SYMBOL_LMR: ${{ secrets.SYMBOL_LMR }} - PROXY_ROUTER_URL: ${{ secrets.PROXY_ROUTER_URL }} - COIN_DEFAULT_GAS_LIMIT: ${{ secrets.COIN_DEFAULT_GAS_LIMIT }} - MAX_GAS_PRICE: ${{ secrets.MAX_GAS_PRICE }} - LMR_DEFAULT_GAS_LIMIT: ${{ secrets.LMR_DEFAULT_GAS_LIMIT }} - DEFAULT_GAS_PRICE: ${{ secrets.DEFAULT_GAS_PRICE }} - REQUIRED_PASSWORD_ENTROPY: ${{ secrets.REQUIRED_PASSWORD_ENTROPY }} - RECAPTCHA_SITE_KEY: ${{ secrets.RECAPTCHA_SITE_KEY }} - FAUCET_URL: ${{ secrets.FAUCET_URL }} - SHOW_FAUCET: ${{ secrets.SHOW_FAUCET }} + env: # keep alphabetical order + CHAIN_ID: ${{ vars.CHAIN_ID }} + COIN_DEFAULT_GAS_LIMIT: ${{ vars.COIN_DEFAULT_GAS_LIMIT }} + DEFAULT_GAS_PRICE: ${{ vars.DEFAULT_GAS_PRICE }} + DISPLAY_NAME: ${{ vars.DISPLAY_NAME }} ETH_NODE_ADDRESS_HTTP: ${{ vars.ETH_NODE_ADDRESS_HTTP }} - TITAN_LIGHTNING_POOL: ${{ secrets.TITAN_LIGHTNING_POOL }} - PORT_CHECKER_URL: ${{ vars.PORT_CHECKER_URL }} + EXPLORER_API_URLS: ${{ vars.EXPLORER_API_URLS }} + EXPLORER_URL: ${{ vars.EXPLORER_URL }} + FAUCET_URL: ${{ vars.FAUCET_URL }} + LMR_DEFAULT_GAS_LIMIT: ${{ vars.LMR_DEFAULT_GAS_LIMIT }} + MAX_GAS_PRICE: ${{ vars.MAX_GAS_PRICE }} PORT_CHECK_ERROR_LINK: ${{ vars.PORT_CHECK_ERROR_LINK }} + PORT_CHECKER_URL: ${{ vars.PORT_CHECKER_URL }} + RECAPTCHA_SITE_KEY: ${{ secrets.RECAPTCHA_SITE_KEY }} + SELLER_WHITELIST_URL: ${{ vars.SELLER_WHITELIST_URL }} + SHOW_FAUCET: ${{ vars.SHOW_FAUCET }} + SYMBOL_ETH: ${{ vars.SYMBOL_ETH }} + SYMBOL_LMR: ${{ vars.SYMBOL_LMR }} + TITAN_LIGHTNING_DASHBOARD: ${{ vars.TITAN_LIGHTNING_DASHBOARD }} + TITAN_LIGHTNING_POOL: ${{ vars.TITAN_LIGHTNING_POOL }} steps: - name: Checkout uses: actions/checkout@v3 @@ -67,28 +67,32 @@ jobs: if: matrix.target == 'macos-arm' run: npm run postinstallMacDist - name: Set env from github + # keep alphabetical order run: | echo "CHAIN_ID=$CHAIN_ID" >> .env + echo "COIN_DEFAULT_GAS_LIMIT=$COIN_DEFAULT_GAS_LIMIT" >> .env + echo "DEFAULT_GAS_PRICE=$DEFAULT_GAS_PRICE" >> .env echo "DEV_TOOLS=true" >> .env echo "DISPLAY_NAME=$DISPLAY_NAME" >> .env + echo "ETH_NODE_ADDRESS_HTTP=$ETH_NODE_ADDRESS_HTTP" >> .env + echo "EXPLORER_API_URLS=$EXPLORER_API_URLS" >> .env echo "EXPLORER_URL=$EXPLORER_URL" >> .env - echo "SYMBOL=$SYMBOL" >> .env - echo "SYMBOL_ETH=$SYMBOL_ETH" >> .env - echo "SYMBOL_LMR=$SYMBOL_LMR" >> .env - echo "PROXY_ROUTER_URL=$PROXY_ROUTER_URL" >> .env - echo "COIN_DEFAULT_GAS_LIMIT=$COIN_DEFAULT_GAS_LIMIT" >> .env - echo "MAX_GAS_PRICE=$MAX_GAS_PRICE" >> .env + echo "FAUCET_URL=$FAUCET_URL" >> .env echo "LMR_DEFAULT_GAS_LIMIT=$LMR_DEFAULT_GAS_LIMIT" >> .env - echo "DEFAULT_GAS_PRICE=$DEFAULT_GAS_PRICE" >> .env - echo "REQUIRED_PASSWORD_ENTROPY=$REQUIRED_PASSWORD_ENTROPY" >> .env + echo "MAX_GAS_PRICE=$MAX_GAS_PRICE" >> .env + echo "NODE_ENV=production" >> .env + echo "PORT_CHECK_ERROR_LINK=$PORT_CHECK_ERROR_LINK" >> .env + echo "PORT_CHECKER_URL=$PORT_CHECKER_URL" >> .env + echo "PROXY_ROUTER_URL=$PROXY_ROUTER_URL" >> .env echo "RECAPTCHA_SITE_KEY=$RECAPTCHA_SITE_KEY" >> .env - echo "FAUCET_URL=$FAUCET_URL" >> .env + echo "REQUIRED_PASSWORD_ENTROPY=$REQUIRED_PASSWORD_ENTROPY" >> .env + echo "SELLER_WHITELIST_URL=$SELLER_WHITELIST_URL" >> .env echo "SHOW_FAUCET=$SHOW_FAUCET" >> .env - echo "NODE_ENV=production" >> .env - echo "ETH_NODE_ADDRESS_HTTP=$ETH_NODE_ADDRESS_HTTP" >> .env + echo "SYMBOL_ETH=$SYMBOL_ETH" >> .env + echo "SYMBOL_LMR=$SYMBOL_LMR" >> .env + echo "SYMBOL=$SYMBOL" >> .env + echo "TITAN_LIGHTNING_DASHBOARD=$TITAN_LIGHTNING_DASHBOARD" >> .env echo "TITAN_LIGHTNING_POOL=$TITAN_LIGHTNING_POOL" >> .env - echo "PORT_CHECKER_URL=$PORT_CHECKER_URL" >> .env - echo "PORT_CHECK_ERROR_LINK=$PORT_CHECK_ERROR_LINK" >> .env shell: bash - name: Set env from gitlab run: | diff --git a/.github/workflows/auto-release-stg.yml b/.github/workflows/auto-release-stg.yml index a49be120..95d3f109 100644 --- a/.github/workflows/auto-release-stg.yml +++ b/.github/workflows/auto-release-stg.yml @@ -35,26 +35,26 @@ jobs: asset_name: lumerin_v${{github.ref_name}}_arm.dmg runs-on: ${{ matrix.os }} environment: ${{ matrix.environment }} - env: - CHAIN_ID: ${{ secrets.CHAIN_ID }} - EXPLORER_URL: ${{ secrets.EXPLORER_URL }} - DISPLAY_NAME: ${{ secrets.DISPLAY_NAME }} - SYMBOL: ${{ secrets.SYMBOL }} - SYMBOL_ETH: ${{ secrets.SYMBOL_ETH }} - SYMBOL_LMR: ${{ secrets.SYMBOL_LMR }} - PROXY_ROUTER_URL: ${{ secrets.PROXY_ROUTER_URL }} - COIN_DEFAULT_GAS_LIMIT: ${{ secrets.COIN_DEFAULT_GAS_LIMIT }} - MAX_GAS_PRICE: ${{ secrets.MAX_GAS_PRICE }} - LMR_DEFAULT_GAS_LIMIT: ${{ secrets.LMR_DEFAULT_GAS_LIMIT }} - DEFAULT_GAS_PRICE: ${{ secrets.DEFAULT_GAS_PRICE }} - REQUIRED_PASSWORD_ENTROPY: ${{ secrets.REQUIRED_PASSWORD_ENTROPY }} - RECAPTCHA_SITE_KEY: ${{ secrets.RECAPTCHA_SITE_KEY }} - FAUCET_URL: ${{ secrets.FAUCET_URL }} - SHOW_FAUCET: ${{ secrets.SHOW_FAUCET }} + env: # keep alphabetical order + CHAIN_ID: ${{ vars.CHAIN_ID }} + COIN_DEFAULT_GAS_LIMIT: ${{ vars.COIN_DEFAULT_GAS_LIMIT }} + DEFAULT_GAS_PRICE: ${{ vars.DEFAULT_GAS_PRICE }} + DISPLAY_NAME: ${{ vars.DISPLAY_NAME }} ETH_NODE_ADDRESS_HTTP: ${{ vars.ETH_NODE_ADDRESS_HTTP }} - TITAN_LIGHTNING_POOL: ${{ secrets.TITAN_LIGHTNING_POOL }} - PORT_CHECKER_URL: ${{ vars.PORT_CHECKER_URL }} + EXPLORER_API_URLS: ${{ vars.EXPLORER_API_URLS }} + EXPLORER_URL: ${{ vars.EXPLORER_URL }} + FAUCET_URL: ${{ vars.FAUCET_URL }} + LMR_DEFAULT_GAS_LIMIT: ${{ vars.LMR_DEFAULT_GAS_LIMIT }} + MAX_GAS_PRICE: ${{ vars.MAX_GAS_PRICE }} PORT_CHECK_ERROR_LINK: ${{ vars.PORT_CHECK_ERROR_LINK }} + PORT_CHECKER_URL: ${{ vars.PORT_CHECKER_URL }} + RECAPTCHA_SITE_KEY: ${{ secrets.RECAPTCHA_SITE_KEY }} + SELLER_WHITELIST_URL: ${{ vars.SELLER_WHITELIST_URL }} + SHOW_FAUCET: ${{ vars.SHOW_FAUCET }} + SYMBOL_ETH: ${{ vars.SYMBOL_ETH }} + SYMBOL_LMR: ${{ vars.SYMBOL_LMR }} + TITAN_LIGHTNING_DASHBOARD: ${{ vars.TITAN_LIGHTNING_DASHBOARD }} + TITAN_LIGHTNING_POOL: ${{ vars.TITAN_LIGHTNING_POOL }} steps: - name: Checkout uses: actions/checkout@v3 @@ -67,27 +67,32 @@ jobs: if: matrix.target == 'macos-arm' run: npm run postinstallMacDist - name: Set env from github + # keep alphabetical order run: | echo "CHAIN_ID=$CHAIN_ID" >> .env - echo "EXPLORER_URL=$EXPLORER_URL" >> .env - echo "DISPLAY_NAME=$DISPLAY_NAME" >> .env - echo "SYMBOL=$SYMBOL" >> .env - echo "SYMBOL_ETH=$SYMBOL_ETH" >> .env - echo "SYMBOL_LMR=$SYMBOL_LMR" >> .env - echo "PROXY_ROUTER_URL=$PROXY_ROUTER_URL" >> .env echo "COIN_DEFAULT_GAS_LIMIT=$COIN_DEFAULT_GAS_LIMIT" >> .env - echo "MAX_GAS_PRICE=$MAX_GAS_PRICE" >> .env - echo "LMR_DEFAULT_GAS_LIMIT=$LMR_DEFAULT_GAS_LIMIT" >> .env echo "DEFAULT_GAS_PRICE=$DEFAULT_GAS_PRICE" >> .env - echo "REQUIRED_PASSWORD_ENTROPY=$REQUIRED_PASSWORD_ENTROPY" >> .env - echo "RECAPTCHA_SITE_KEY=$RECAPTCHA_SITE_KEY" >> .env + echo "DEV_TOOLS=true" >> .env + echo "DISPLAY_NAME=$DISPLAY_NAME" >> .env + echo "ETH_NODE_ADDRESS_HTTP=$ETH_NODE_ADDRESS_HTTP" >> .env + echo "EXPLORER_API_URLS=$EXPLORER_API_URLS" >> .env + echo "EXPLORER_URL=$EXPLORER_URL" >> .env echo "FAUCET_URL=$FAUCET_URL" >> .env - echo "SHOW_FAUCET=$SHOW_FAUCET" >> .env + echo "LMR_DEFAULT_GAS_LIMIT=$LMR_DEFAULT_GAS_LIMIT" >> .env + echo "MAX_GAS_PRICE=$MAX_GAS_PRICE" >> .env echo "NODE_ENV=production" >> .env - echo "ETH_NODE_ADDRESS_HTTP=$ETH_NODE_ADDRESS_HTTP" >> .env - echo "TITAN_LIGHTNING_POOL=$TITAN_LIGHTNING_POOL" >> .env - echo "PORT_CHECKER_URL=$PORT_CHECKER_URL" >> .env echo "PORT_CHECK_ERROR_LINK=$PORT_CHECK_ERROR_LINK" >> .env + echo "PORT_CHECKER_URL=$PORT_CHECKER_URL" >> .env + echo "PROXY_ROUTER_URL=$PROXY_ROUTER_URL" >> .env + echo "RECAPTCHA_SITE_KEY=$RECAPTCHA_SITE_KEY" >> .env + echo "REQUIRED_PASSWORD_ENTROPY=$REQUIRED_PASSWORD_ENTROPY" >> .env + echo "SELLER_WHITELIST_URL=$SELLER_WHITELIST_URL" >> .env + echo "SHOW_FAUCET=$SHOW_FAUCET" >> .env + echo "SYMBOL_ETH=$SYMBOL_ETH" >> .env + echo "SYMBOL_LMR=$SYMBOL_LMR" >> .env + echo "SYMBOL=$SYMBOL" >> .env + echo "TITAN_LIGHTNING_DASHBOARD=$TITAN_LIGHTNING_DASHBOARD" >> .env + echo "TITAN_LIGHTNING_POOL=$TITAN_LIGHTNING_POOL" >> .env shell: bash - name: Set env from gitlab run: | diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml index 7be55fb0..4cf3debc 100644 --- a/.github/workflows/auto-tag.yml +++ b/.github/workflows/auto-tag.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Auto Tag - uses: butlerlogic/action-autotag@stable + uses: butlerlogic/action-autotag@1.1.2 if: github.ref != 'refs/heads/main' env: GITHUB_TOKEN: "${{ secrets.ACCESS_TOKEN }}" diff --git a/package.json b/package.json index e0dc078b..386915cb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lumerin-wallet-desktop", - "version": "1.2.27", + "version": "1.2.47", "engines": { "node": ">=14" }, @@ -43,7 +43,7 @@ }, "dependencies": { "@electron/remote": "2.0.9", - "@lumerin/wallet-core": "git+ssh://git@github.com:Lumerin-protocol/WalletCore.git#1.0.74", + "@lumerin/wallet-core": "git+ssh://git@github.com:Lumerin-protocol/WalletCore.git#1.0.84", "@reach/menu-button": "0.17.0", "@tabler/icons": "1.119.0", "axios": "0.27.2", @@ -63,6 +63,8 @@ "electron-settings": "4.0.2", "electron-updater": "6.1.4", "electron-window-state": "5.0.3", + "highcharts": "11.1.0", + "highcharts-react-official": "3.2.1", "json-stringify-safe": "5.0.1", "keytar": "7.9.0", "lodash": "4.17.21", @@ -90,6 +92,7 @@ "react-select": "5.7.2", "react-tabs": "4.3.0", "react-timer-hook": "3.0.5", + "react-tabs": "4.3.0", "react-virtualized": "9.20.1", "redux": "4.2.0", "redux-actions": "2.3.0", diff --git a/public/config/index.js b/public/config/index.js index 2162aa2a..e54b34f6 100644 --- a/public/config/index.js +++ b/public/config/index.js @@ -1,6 +1,6 @@ const { parseJSONArray } = require('./utils') -let httpApiUrls +let httpApiUrls, explorerApiURLs try { httpApiUrls = parseJSONArray(process.env.ETH_NODE_ADDRESS_HTTP) @@ -8,19 +8,26 @@ try { throw new Error(`Invalid ETH_NODE_ADDRESS_HTTP: ${err?.message}`); } +try { + explorerApiURLs = parseJSONArray(process.env.EXPLORER_API_URLS) +} catch (err) { + throw new Error(`Invalid EXPLORER_API_URLS: ${err?.message}`); +} + const chain = { displayName: process.env.DISPLAY_NAME, chainId: process.env.CHAIN_ID, - symbol: process.env.SYMBOL_LMR || process.env.SYMBOL || 'LMR', + symbol: process.env.SYMBOL_LMR || 'LMR', symbolEth: process.env.SYMBOL_ETH || 'ETH', lmrTokenAddress: process.env.LUMERIN_TOKEN_ADDRESS, cloneFactoryAddress: process.env.CLONE_FACTORY_ADDRESS, - faucetAddress: process.env.FAUCET_ADDRESS || '0xFE64cAE7Ca5166c8bb0e014e2D402f8d22764f24', proxyRouterUrl: process.env.PROXY_ROUTER_URL, explorerUrl: process.env.EXPLORER_URL, + explorerApiURLs: explorerApiURLs, + wsApiUrl: process.env.ETH_NODE_ADDRESS, httpApiUrls: httpApiUrls, ipLookupUrl: process.env.IP_LOOKUP_URL, @@ -37,15 +44,17 @@ const chain = { portCheckErrorLink: process.env.PORT_CHECK_ERROR_LINK || 'https://gitbook.lumerin.io/lumerin-hashpower-marketplace/buyer/2.-network-changes-for-receiving-hashrate', localProxyRouterUrl: `http://localhost:${process.env - .SPROXY_WEB_DEFAULT_PORT || 8081}`, + .PROXY_WEB_DEFAULT_PORT || 8081}`, faucetUrl: process.env.FAUCET_URL, showFaucet: process.env.SHOW_FAUCET === "true", titanLightningPool: process.env.TITAN_LIGHTNING_POOL, + titanLightningDashboard: process.env.TITAN_LIGHTNING_DASHBOARD || "https://lightning.titan.io", defaultSellerCurrency: process.env.DEFAULT_SELLER_CURRENCY || 'BTC', bypassAuth: process.env.BYPASS_AUTH === "true", + sellerWhitelistUrl: process.env.SELLER_WHITELIST_URL || 'https://forms.gle/wEcAgppfK2p9YZ3g7' }; module.exports = { @@ -60,6 +69,5 @@ module.exports = { statePersistanceDebounce: 2000, trackingId: process.env.TRACKING_ID, web3Timeout: 120000, - blocksUpdateMs: 10_000, recaptchaSiteKey: process.env.RECAPTCHA_SITE_KEY, }; diff --git a/public/errorHandler.js b/public/errorHandler.js index 4d3657d8..cfb0a6b8 100644 --- a/public/errorHandler.js +++ b/public/errorHandler.js @@ -12,7 +12,7 @@ let options = { const handleError = (error) => { try { - options.logger(error); + options.logger("handle error: ", error?.message, error?.stack, error); } catch (loggerError) { // eslint-disable-line unicorn/catch-error-name dialog.showErrorBox('The `logger` option function in electron-unhandled threw an error', loggerError.stack); return; diff --git a/public/main/client/contractsHashrateSyncer.js b/public/main/client/contractsHashrateSyncer.js new file mode 100644 index 00000000..643f0261 --- /dev/null +++ b/public/main/client/contractsHashrateSyncer.js @@ -0,0 +1,41 @@ + +const { create: createAxios } = require('axios') +const { getDb } = require('./database'); +const logger = require("../../logger"); + +const startMonitoringHashrate = (url, period) => { + const interval = setInterval(async () => { + try { + const items = (await createAxios({ baseURL: url })('/contracts')).data; + persistData(items) + } + catch(e) { + logger.debug(e.message, 'Failed to poll hashrate'); + persistData(); + } + }, period) + return interval; +} + +const persistData = (data) => { + + const db = getDb(); + const collection = db.collection('hashrate'); + + if(!data) { + return; + } + + data.forEach((item) => { + const id = item.ID; + const currentHashrate = item.ResourceEstimatesActual['ema--5m']; + collection.insert( + { + id, + hashrate: currentHashrate, + timestamp: new Date().getTime() + }); + }) +} + +module.exports = { startMonitoringHashrate }; \ No newline at end of file diff --git a/public/main/client/database.js b/public/main/client/database.js index de0fbbe6..0967add5 100644 --- a/public/main/client/database.js +++ b/public/main/client/database.js @@ -19,6 +19,7 @@ const promisifyMethods = methods => const promisifyCollection = promisifyMethods([ 'find', + 'insert', 'findOne', 'remove', 'update' diff --git a/public/main/client/handlers/no-core.js b/public/main/client/handlers/no-core.js index df3849d4..5cde67b2 100644 --- a/public/main/client/handlers/no-core.js +++ b/public/main/client/handlers/no-core.js @@ -6,10 +6,10 @@ const logger = require("../../../logger"); const storage = require("../storage"); const auth = require("../auth"); const wallet = require("../wallet"); -const { - setProxyRouterConfig, - getProxyRouterConfig, - getDefaultCurrencySetting, +const { + setProxyRouterConfig, + getProxyRouterConfig, + getDefaultCurrencySetting, setDefaultCurrencySetting, getKey, setKey @@ -27,11 +27,11 @@ function clearCache() { const persistState = (data) => storage.persistState(data).then(() => true); function changePassword({ oldPassword, newPassword }) { - return validatePassword(oldPassword).then(function(isValid) { + return validatePassword(oldPassword).then(function (isValid) { if (!isValid) { return isValid; } - return auth.setPassword(newPassword).then(function() { + return auth.setPassword(newPassword).then(function () { const seed = wallet.getSeed(oldPassword); wallet.setSeed(seed, newPassword); @@ -48,7 +48,7 @@ const getProxyRouterSettings = async () => { }; const handleClientSideError = (data) => { - logger.error(data.message, data.stack); + logger.error("client-side error", data.message, data.stack); } const getDefaultCurrency = async () => getDefaultCurrencySetting(); @@ -57,6 +57,28 @@ const setDefaultCurrency = async (curr) => setDefaultCurrencySetting(curr); const getCustomEnvs = async () => getKey('customEnvs'); const setCustomEnvs = async (value) => setKey('customEnvs', value); +/** + * + * @param {string} contractId + * @param {Date} fromDate + * @returns + */ +const getContractHashrate = async ({contractId, fromDate}) => { + const collection = await dbManager.getDb().collection('hashrate').findAsync({ id: contractId }); + // Uncomment to get a random data + // const data = [] + // const now = new Date().getTime(); + // for (let i = 10; i >= 0; i--) { + // const timestamp = now - i * 1000 * 60 * 5; + // const hashrate = Math.round(Math.random() * 50000) + 70000; + // data.push({ timestamp, hashrate }) + // } + // return data; + return collection + .filter(x => x.timestamp > fromDate.getTime()) + .sort((a, b) => a.timestamp - b.timestamp); +} + const restartWallet = () => restart(1); module.exports = { @@ -72,4 +94,5 @@ module.exports = { getCustomEnvs, setCustomEnvs, restartWallet, + getContractHashrate }; diff --git a/public/main/client/handlers/single-core.js b/public/main/client/handlers/single-core.js index a8dc6221..6764159a 100644 --- a/public/main/client/handlers/single-core.js +++ b/public/main/client/handlers/single-core.js @@ -13,7 +13,6 @@ const { cleanupDb, getProxyRouterConfig, } = require("../settings"); -const restart = require("../electron-restart"); const withAuth = (fn) => (data, { api }) => { if (typeof data.walletId !== "string") { @@ -31,7 +30,7 @@ const withAuth = (fn) => (data, { api }) => { .then((privateKey) => fn(privateKey, data)); }; -const createContract = async function(data, { api }) { +const createContract = async function (data, { api }) { data.walletId = wallet.getAddress().address; data.password = await auth.getSessionPassword(); @@ -50,7 +49,7 @@ const createContract = async function(data, { api }) { )(data, { api }); }; -const purchaseContract = async function(data, { api }) { +const purchaseContract = async function (data, { api }) { data.walletId = wallet.getAddress().address; data.minerPassword = data.password; data.password = await auth.getSessionPassword(); @@ -67,7 +66,7 @@ const purchaseContract = async function(data, { api }) { )(data, { api }); }; -const editContract = async function(data, { api }) { +const editContract = async function (data, { api }) { data.walletId = wallet.getAddress().address; data.password = await auth.getSessionPassword(); @@ -88,7 +87,7 @@ const editContract = async function(data, { api }) { }; -const claimFaucet = async function(data, { api }) { +const claimFaucet = async function (data, { api }) { data.walletId = wallet.getAddress().address; data.password = await auth.getSessionPassword(); @@ -104,7 +103,7 @@ const claimFaucet = async function(data, { api }) { )(data, { api }); }; -const cancelContract = async function(data, { api }) { +const cancelContract = async function (data, { api }) { data.walletId = wallet.getAddress().address; data.password = await auth.getSessionPassword(); @@ -121,7 +120,7 @@ const cancelContract = async function(data, { api }) { )(data, { api }); }; -const setContractDeleteStatus = async function(data, { api }) { +const setContractDeleteStatus = async function (data, { api }) { data.walletId = wallet.getAddress().address; data.password = await auth.getSessionPassword(); @@ -157,7 +156,7 @@ const restartProxyRouter = async (data, { emitter, api }) => { await api["proxy-router"] .kill(config.chain.proxyPort) - .catch(logger.error); + .catch(err => logger.error("proxy router err", err)); emitter.emit("open-proxy-router", { password }); }; @@ -190,10 +189,10 @@ const onboardingCompleted = (data, core) => { ) ) .then(() => true) - .catch((err) => ({error: new WalletError("Onboarding unable to be completed: ", err)})); + .catch((err) => ({ error: new WalletError("Onboarding unable to be completed: ", err) })); }; -const recoverFromMnemonic = function(data, core) { +const recoverFromMnemonic = function (data, core) { if (!auth.isValidPassword(data.password)) { return null; } @@ -217,24 +216,24 @@ function onLoginSubmit({ password }, core) { ? new Promise((r) => r(true)) : auth.isValidPassword(password); - return checkPassword.then(function(isValid) { + return checkPassword.then(function (isValid) { if (!isValid) { return { error: new WalletError("Invalid password") }; } openWallet(core, password); return isValid; - }).catch(logger.error); + }).catch(err => logger.error("onLoginSubmit err", err)); } function refreshAllSockets({ url }, { api, emitter }) { emitter.emit("sockets-scan-started", {}); return api.sockets .getConnections() - .then(function() { + .then(function () { emitter.emit("sockets-scan-finished", { success: true }); return {}; }) - .catch(function(error) { + .catch(function (error) { logger.warn("Could not sync sockets/connections", error.stack); emitter.emit("sockets-scan-finished", { error: error.message, @@ -251,11 +250,11 @@ function refreshAllTransactions({ address }, { api, emitter }) { emitter.emit("transactions-scan-started", {}); return api.explorer .refreshAllTransactions(address) - .then(function() { + .then(function () { emitter.emit("transactions-scan-finished", { success: true }); return {}; }) - .catch(function(error) { + .catch(function (error) { logger.warn("Could not sync transactions/events", error.stack); emitter.emit("transactions-scan-finished", { error: error.message, @@ -268,11 +267,11 @@ function refreshAllTransactions({ address }, { api, emitter }) { }); } -const getMarketplaceFee = async function(data, { api }) { - return api.contracts.getMarketplaceFee(data); +const getMarketplaceFee = async function (data, { api }) { + return api.contracts.getMarketplaceFee(data); }; -function refreshAllContracts({}, { api }) { +function refreshAllContracts({ }, { api }) { const walletId = wallet.getAddress().address; return api.contracts.refreshContracts(null, walletId); } @@ -338,7 +337,7 @@ const getAddressAndPrivateKey = async (data, { api }) => { const refreshProxyRouterConnection = async (data, { api }) => api["proxy-router"].refreshConnectionsStream(data); -const getLocalIp = async ({}, { api }) => api["proxy-router"].getLocalIp(); +const getLocalIp = async ({ }, { api }) => api["proxy-router"].getLocalIp(); const isProxyPortPublic = async (data, { api }) => api["proxy-router"].isProxyPortPublic(data); @@ -346,10 +345,6 @@ const logout = async (data) => { return cleanupDb(); }; -const restartWallet = async (data) => { - return restart(); -}; - const getPoolAddress = async (data) => { const config = getProxyRouterConfig(); return config.buyerDefaultPool || config.defaultPool; @@ -364,7 +359,7 @@ const revealSecretPhrase = async (password) => { if (!isValid) { return { error: new WalletError("Invalid password") }; } - + const entropy = wallet.getEntropy(password); const mnemonic = keys.entropyToMnemonic(entropy); return mnemonic; @@ -399,7 +394,6 @@ module.exports = { getAddressAndPrivateKey, refreshProxyRouterConnection, logout, - restartWallet, getLocalIp, getPoolAddress, restartProxyRouter, diff --git a/public/main/client/index.js b/public/main/client/index.js index 8fa36dd5..9b220ec8 100644 --- a/public/main/client/index.js +++ b/public/main/client/index.js @@ -8,6 +8,7 @@ const logger = require("../../logger"); const subscriptions = require("./subscriptions"); const settings = require("./settings"); const storage = require("./storage"); +const { startMonitoringHashrate } = require("./contractsHashrateSyncer"); const { getAddressAndPrivateKey, @@ -15,11 +16,15 @@ const { } = require("./handlers/single-core"); const { runProxyRouter, isProxyRouterHealthy } = require("./proxyRouter"); - +let interval; function startCore({ chain, core, config: coreConfig }, webContent) { logger.verbose(`Starting core ${chain}`); const { emitter, events, api } = core.start(coreConfig); const proxyRouterApi = api["proxy-router"]; + if(interval) + clearInterval(interval); + + interval = startMonitoringHashrate(coreConfig.localProxyRouterUrl, 5 * 60 * 1000); // emitter.setMaxListeners(30); emitter.setMaxListeners(50); @@ -31,6 +36,7 @@ function startCore({ chain, core, config: coreConfig }, webContent) { "contracts-scan-started", "contracts-scan-finished", "contracts-updated", + 'contract-updated', ); function send(eventName, data) { @@ -41,12 +47,12 @@ function startCore({ chain, core, config: coreConfig }, webContent) { const payload = Object.assign({}, data, { chain }); webContent.sender.send(eventName, payload); } catch (err) { - logger.error(err); + logger.error("send error", err); } } events.forEach((event) => - emitter.on(event, function(data) { + emitter.on(event, function (data) { send(event, data); }) ); @@ -54,47 +60,45 @@ function startCore({ chain, core, config: coreConfig }, webContent) { function syncTransactions({ address }, page = 1, pageSize = 15) { return storage .getSyncBlock(chain) - .then(function(from) { + .then(function (from) { send("transactions-scan-started", {}); return api.explorer .syncTransactions( 0, - (number) => storage.setSyncBlock(number, chain), + 'latest', page, pageSize, address ) - .then(function() { + .then(function () { send("transactions-scan-finished", { success: true }); - emitter.on("coin-block", function({ number }) { - storage.setSyncBlock(number, chain).catch(function(err) { + emitter.on("coin-block", function ({ number }) { + storage.setSyncBlock(number, chain).catch(function (err) { logger.warn("Could not save new synced block", err); }); }); }); }) - .catch(function(err) { + .catch(function (err) { logger.warn("Could not sync transactions/events", err.stack); send("transactions-scan-finished", { error: err.message, success: false, }); - emitter.once("coin-block", () => - syncTransactions({ address }, page, pageSize) - ); + emitter.once("coin-block", () => syncTransactions({ address }, page, pageSize)); }); } emitter.on("open-wallet", (props) => { syncTransactions(props); api.contracts.startWatching({}); - //api.explorer.startWatching({}); + api.explorer.startWatching({ walletAddress: props.address }); }); - emitter.on("wallet-error", function(err) { + emitter.on("wallet-error", function (err) { logger.warn( err.inner ? `${err.message} - ${err.inner.message}` : err.message ); @@ -118,7 +122,7 @@ function startCore({ chain, core, config: coreConfig }, webContent) { { password }, { api } ); - + send("proxy-router-type-changed", { isLocal: true, }); @@ -163,37 +167,28 @@ function stopCore({ core, chain }) { } function createClient(config) { - ipcMain.on("log.error", function(_, args) { - logger.error(args.message); + ipcMain.on("log.error", function (_, args) { + logger.error("ipcMain error ", args.message); }); settings.presetDefaults(); - const customEnvs = settings.getKey("customEnvs"); - - if (customEnvs?.wsNode) { - config.chain.wsApiUrl = customEnvs.wsNode; - } - if (customEnvs?.httpNode) { - config.chain.httpApiUrls.unshift(customEnvs.httpNode); - } - let core = { chain: config.chain.chainId, core: createCore(), config: Object.assign({}, config.chain, config), }; - ipcMain.on("ui-ready", function(webContent, args) { + ipcMain.on("ui-ready", function (webContent, args) { const onboardingComplete = !!settings.getPasswordHash(); storage .getState() - .catch(function(err) { + .catch(function (err) { logger.warn("Failed to get state", err.message); return {}; }) - .then(function(persistedState) { + .then(function (persistedState) { const payload = Object.assign({}, args, { data: { onboardingComplete, @@ -204,17 +199,17 @@ function createClient(config) { webContent.sender.send("ui-ready", payload); // logger.verbose(`<-- ui-ready ${stringify(payload)}`); }) - .catch(function(err) { + .catch(function (err) { logger.error("Could not send ui-ready message back", err.message); }) - .then(function() { + .then(function () { const { emitter, events, api } = startCore(core, webContent); core.emitter = emitter; core.events = events; core.api = api; subscriptions.subscribe(core); }) - .catch(function(err) { + .catch(function (err) { console.log("panic"); console.log(err); console.log("Unknown chain =", err.message); @@ -222,7 +217,7 @@ function createClient(config) { }); }); - ipcMain.on("ui-unload", function() { + ipcMain.on("ui-unload", function () { stopCore(core); subscriptions.unsubscribe(core); }); diff --git a/public/main/client/proxyRouter.js b/public/main/client/proxyRouter.js index 40179e47..2542b17d 100644 --- a/public/main/client/proxyRouter.js +++ b/public/main/client/proxyRouter.js @@ -32,7 +32,7 @@ const isProxyRouterHealthy = async (api, url) => { const healthCheck = await api["proxy-router"].healthCheck(url); return healthCheck?.data?.status === "healthy"; } catch (err) { - logger.error(err); + logger.error("proxy-router error", err); return false; } }; @@ -51,18 +51,19 @@ const runProxyRouter = (config) => { `--contract-address=${config.cloneFactoryAddress}`, `--eth-node-address=${config.wsApiUrl}`, - "--miner-vetting-duration=5m", "--miner-share-timeout=10m", + // "--miner-vetting-duration=5m", "--hashrate-error-threshold=0.05", "--hashrate-cycle-duration=5m", - "--hashrate-share-timeout=7m", - "--hashrate-validation-start-timeout=15m", + + "--hashrate-share-timeout=120m", "--log-level-app=info", "--log-level-scheduler=info", "--log-level-proxy=info", "--log-level-connection=info", + `--log-folder-path=${app.getPath("logs")}/`, `--wallet-private-key=${config.privateKey}`, `--proxy-address=0.0.0.0:${config.proxyPort}`, diff --git a/public/main/client/settings/index.js b/public/main/client/settings/index.js index 71d45958..e0e23e62 100644 --- a/public/main/client/settings/index.js +++ b/public/main/client/settings/index.js @@ -38,7 +38,7 @@ const getProxyRouterConfig = () => { } return data; } catch (e) { - console.error(e); + console.error("error getting proxyrouter config", e); cleanupDb(); } }; @@ -86,7 +86,7 @@ function cleanupDb() { // Overwrite old settings and clear db if settings file version changed upgradeSettings(defaultSettings, currentSettings); const db = getDb(); - db.dropDatabase().catch(function(err) { + db.dropDatabase().catch(function (err) { logger.error("Possible database corruption", err.message); }); restart(1); @@ -98,7 +98,7 @@ const setDefaultCurrencySetting = (currency) => setKey("selectedCurrency", curre const getAppVersion = () => getKey("app.version"); -const setAppVersion = (value) => setKey("app.version", value); +const setAppVersion = (value) => setKey("app.version", value); module.exports = { getPasswordHash, diff --git a/public/main/client/subscriptions/no-core.js b/public/main/client/subscriptions/no-core.js index fa5bb579..dabafb30 100644 --- a/public/main/client/subscriptions/no-core.js +++ b/public/main/client/subscriptions/no-core.js @@ -13,13 +13,13 @@ const listeners = { 'reveal-secret-phrase': handlers.revealSecretPhrase, 'has-stored-secret-phrase': handlers.hasStoredSecretPhrase, "logout": handlers.logout, - "restart": handlers.restartWallet, "save-proxy-router-settings": handlers.saveProxyRouterSettings, "get-proxy-router-settings": handlers.getProxyRouterSettings, "get-default-currency-settings": handlers.getDefaultCurrency, "set-default-currency-settings": handlers.setDefaultCurrency, "get-custom-env-values": handlers.getCustomEnvs, "set-custom-env-values": handlers.setCustomEnvs, + "get-contract-hashrate": handlers.getContractHashrate }; // Subscribe to messages where no core has to react diff --git a/src/client/index.js b/src/client/index.js index 4ed01336..cac2d9ea 100644 --- a/src/client/index.js +++ b/src/client/index.js @@ -146,7 +146,6 @@ const createClient = function(createStore) { 'get-lmr-transfer-gas-limit' ), logout: utils.forwardToMainProcess('logout'), - restartWallet: utils.forwardToMainProcess('restart'), getLocalIp: utils.forwardToMainProcess('get-local-ip'), isProxyPortPublic: utils.forwardToMainProcess('is-proxy-port-public'), getPoolAddress: utils.forwardToMainProcess('get-pool-address'), @@ -172,7 +171,8 @@ const createClient = function(createStore) { getMarketplaceFee: utils.forwardToMainProcess('get-marketplace-fee'), claimFaucet: utils.forwardToMainProcess('claim-faucet', 750000), getCustomEnvValues: utils.forwardToMainProcess('get-custom-env-values'), - setCustomEnvValues: utils.forwardToMainProcess('set-custom-env-values') + setCustomEnvValues: utils.forwardToMainProcess('set-custom-env-values'), + getContractHashrate: utils.forwardToMainProcess('get-contract-hashrate') }; const api = { diff --git a/src/components/common/Root.js b/src/components/common/Root.js index 21d2cc5b..74d254a4 100644 --- a/src/components/common/Root.js +++ b/src/components/common/Root.js @@ -63,7 +63,7 @@ class Root extends React.Component { ) // eslint-disable-next-line no-console .catch(e => { - console.error(e.message); + console.error('root component error', e.message); this.context.toast( 'error', 'Failed to startup wallet. Please wait a few minutes and try again' diff --git a/src/components/contracts/BuyerHub.js b/src/components/contracts/BuyerHub.js index 396b4af3..d0f08dbd 100644 --- a/src/components/contracts/BuyerHub.js +++ b/src/components/contracts/BuyerHub.js @@ -8,6 +8,7 @@ import ContractsList from './contracts-list/ContractsList'; import { ContractsRowContainer } from './contracts-list/ContractsRow.styles'; import HistoryModal from './modals/HistoryModal/HistoryModal'; +import HashrateModal from './modals/HashrateModal/HashrateModal'; import { IconHistory } from '@tabler/icons'; import styled from 'styled-components'; @@ -46,6 +47,7 @@ function BuyerHub({ const contractsToShow = contracts.filter( x => x.buyer === address && x.seller !== address ); + const tabs = [ { value: 'id', name: 'Contract', ratio: 3 }, { value: 'timestamp', name: 'Started', ratio: 3 }, @@ -74,18 +76,24 @@ function BuyerHub({ const rowRenderer = (contractsList, ratio) => ({ key, index, style }) => ( { + setShowHashrateModal(true); + setContactToShowHashrate(id); + }} /> ); const [isHistoryModalOpen, setIsHistoryModalOpen] = useState(false); + const [showHashrateModal, setShowHashrateModal] = useState(false); + const [contactToShowHashrate, setContactToShowHashrate] = useState(); const contractsWithHistory = contracts.filter(c => c.history?.length); const showHistory = contractsWithHistory.length; @@ -129,6 +137,15 @@ function BuyerHub({ setIsHistoryModalOpen(false); }} /> + + { + setShowHashrateModal(false); + setContactToShowHashrate(null); + }} + /> ); } diff --git a/src/components/contracts/Marketplace.js b/src/components/contracts/Marketplace.js index 01124a4b..31fa006c 100644 --- a/src/components/contracts/Marketplace.js +++ b/src/components/contracts/Marketplace.js @@ -132,6 +132,7 @@ function Marketplace({ const rowRenderer = (contractsList, ratio) => ({ key, index, style }) => ( { setContractToPurchase(data); diff --git a/src/components/contracts/SellerHub.js b/src/components/contracts/SellerHub.js index b9577e0b..1412a90a 100644 --- a/src/components/contracts/SellerHub.js +++ b/src/components/contracts/SellerHub.js @@ -14,6 +14,7 @@ import { lmrDecimals } from '../../utils/coinValue'; import { formatBtcPerTh } from './utils'; import ArchiveModal from './modals/ArchiveModal/ArchiveModal'; import { IconArchive } from '@tabler/icons'; +import SellerWhitelistModal from './modals/SellerWhitelistModal/SellerWhitelistModal'; const Container = styled.div` background-color: ${p => p.theme.colors.light}; @@ -80,10 +81,12 @@ function SellerHub({ allowSendTransaction, networkDifficulty, selectedCurrency, + formUrl, ...props }) { const [isModalActive, setIsModalActive] = useState(false); const [isArchiveModalActive, setIsArchiveModalActive] = useState(false); + const [showSellerWhitelistForm, setShowSellerWhitelistForm] = useState(false); const [isEditModalActive, setIsEditModalActive] = useState(false); const [editContractData, setEditContractData] = useState({}); const context = useContext(ToastsContext); @@ -240,8 +243,12 @@ function SellerHub({ setShowSuccess(true); }) .catch(error => { - context.toast('error', error.message || error); setIsModalActive(false); + if (error.message == 'seller is not whitelisted') { + setShowSellerWhitelistForm(true); + return; + } + context.toast('error', error.message || error); }) .finally(() => { removeTempContract(tempContractId, contract); @@ -282,7 +289,9 @@ function SellerHub({ c => c.seller === address && !c.isDead ); - const deadContracts = contracts.filter(c => c.seller === address && c.isDead); + const deadContracts = contracts + .filter(c => c.seller === address && c.isDead) + .sort((a, b) => b.balance - a.balance); const rentedContracts = contractsToShow?.filter(x => Number(x.state) === 1) ?? []; @@ -350,9 +359,18 @@ function SellerHub({ setIsArchiveModalActive(false); }} restore={handleDeleteContractStateChange} + address={address} showSuccess={false} /> + { + setShowSellerWhitelistForm(false); + }} + /> + - {formatTimestamp(contract.timestamp, timer, contract.state)} +
+ {formatTimestamp(contract.timestamp, timer, contract.state)} + { + onGetHashrate(contract.id); + }} + /> +
{contract.inProgress ? ( diff --git a/src/components/contracts/contracts-list/ContractsList.js b/src/components/contracts/contracts-list/ContractsList.js index 24c0d7c5..4d1e544f 100644 --- a/src/components/contracts/contracts-list/ContractsList.js +++ b/src/components/contracts/contracts-list/ContractsList.js @@ -165,6 +165,7 @@ function ContractsList({ }) => ( { return ( diff --git a/src/components/contracts/modals/ArchiveModal/ArchiveModal.js b/src/components/contracts/modals/ArchiveModal/ArchiveModal.js index 84407c26..bf6656c9 100644 --- a/src/components/contracts/modals/ArchiveModal/ArchiveModal.js +++ b/src/components/contracts/modals/ArchiveModal/ArchiveModal.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useContext } from 'react'; import { List as RVList, AutoSizer } from 'react-virtualized'; import { Modal, @@ -10,10 +10,12 @@ import { } from '../CreateContractModal.styles'; import ArchiveRow from './ArchiveRow'; import { withClient } from '../../../../store/hocs/clientContext'; +import { CLOSEOUT_TYPE } from '../../../../enums'; +import { ToastsContext } from '../../../../components/toasts'; function ArchiveModal(props) { - const { isActive, close, deletedContracts, client } = props; - + const { isActive, close, address, deletedContracts, client } = props; + const context = useContext(ToastsContext); const handleClose = e => { close(e); }; @@ -23,6 +25,22 @@ function ArchiveModal(props) { return <>; } + const handleClaim = contractId => { + client.lockSendTransaction(); + return client + .cancelContract({ + contractId: contractId, + walletAddress: address, + closeOutType: CLOSEOUT_TYPE.Claim + }) + .catch(e => { + context.toast('error', `Failed to claim funds: ${e.message}`); + }) + .finally(() => { + client.unlockSendTransaction(); + }); + }; + const handleRestore = contract => { client.lockSendTransaction(); return client @@ -37,16 +55,25 @@ function ArchiveModal(props) { }; const rowRenderer = deletedContracts => ({ key, index, style }) => ( - +
+ +
); return ( - + {CloseModal(handleClose)} Archived contracts diff --git a/src/components/contracts/modals/ArchiveModal/ArchiveRow.js b/src/components/contracts/modals/ArchiveModal/ArchiveRow.js index e267b7b3..4bdbb138 100644 --- a/src/components/contracts/modals/ArchiveModal/ArchiveRow.js +++ b/src/components/contracts/modals/ArchiveModal/ArchiveRow.js @@ -6,11 +6,12 @@ import styled from 'styled-components'; import { formatDuration, formatSpeed, formatPrice } from '../../utils'; import withContractsRowState from '../../../../store/hocs/withContractsRowState'; import Spinner from '../../../common/Spinner'; +import { fromTokenBaseUnitsToLMR } from '../../../../utils/coinValue'; const RowContainer = styled.div` padding: 1.2rem 0; display: grid; - grid-template-columns: 1fr 4fr 1fr 1fr; + grid-template-columns: 1fr 1fr 1fr 1fr 0.5fr 1fr; text-align: center; box-shadow: 0 -1px 0 0 ${p => p.theme.colors.lightShade} inset; color: ${p => p.theme.colors.primary} @@ -48,7 +49,7 @@ const FlexCenter = styled.div` `; function ArchiveRow(props) { - const { explorerUrl, contract, handleRestore, symbol } = props; + const { explorerUrl, contract, handleClaim, handleRestore, symbol } = props; const [isProcessing, setIsProcessing] = useState(false); @@ -65,9 +66,13 @@ function ArchiveRow(props) { {abbreviateAddress(contract.id, 4)} - - {formatPrice(contract.price, symbol)} | - {formatDuration(contract.length)} | + + {formatPrice(contract.price, symbol)} + + + {formatDuration(contract.length)} + + {formatSpeed(contract.speed)} @@ -84,15 +89,30 @@ function ArchiveRow(props) { {contract?.stats?.failCount || 0} - + {isProcessing ? ( ) : ( - + <> + {contract.balance !== '0' && ( +
handleClaim(contract.id)} + > + Claim +
+ )} + + )}
diff --git a/src/components/contracts/modals/CreateContractModal.js b/src/components/contracts/modals/CreateContractModal.js index 7675d6c7..967b8c5b 100644 --- a/src/components/contracts/modals/CreateContractModal.js +++ b/src/components/contracts/modals/CreateContractModal.js @@ -131,6 +131,7 @@ function CreateContractModal(props) { if (!isActive) { return <>; } + const timeField = register('time', { required: true, min: 24, diff --git a/src/components/contracts/modals/CreateContractModal.styles.js b/src/components/contracts/modals/CreateContractModal.styles.js index eff56a66..ab0ba6bd 100644 --- a/src/components/contracts/modals/CreateContractModal.styles.js +++ b/src/components/contracts/modals/CreateContractModal.styles.js @@ -39,12 +39,12 @@ export const Body = styled.div` position: fixed; z-index: 20; background-color: ${p => p.theme.colors.light}; - width: 45%; + width: ${p => p.width || '45%'}; height: ${p => p.height || 'fit-content'}; border-radius: 15px; padding: 3rem 5%; - max-width: 600px; - max-height: 800px; + max-width: ${p => p.maxWidth || '600px'}; + max-height: ${p => p.maxHeight || '800px'}; @media (min-height: 700px) { padding: 5rem; diff --git a/src/components/contracts/modals/HashrateModal/HashrateModal.js b/src/components/contracts/modals/HashrateModal/HashrateModal.js new file mode 100644 index 00000000..f7336f7f --- /dev/null +++ b/src/components/contracts/modals/HashrateModal/HashrateModal.js @@ -0,0 +1,113 @@ +//@ts-check +import React, { useState } from 'react'; +import Highcharts from 'highcharts'; +import HighchartsReact from 'highcharts-react-official'; +import { + Modal, + Body, + TitleWrapper, + Title, + CloseModal +} from '../CreateContractModal.styles'; +import { withClient } from '../../../../store/hocs/clientContext'; +import { renderChart } from './chartRenderer'; +import { useInterval } from '../../../hooks/useInterval'; +import { roundTime } from './utils'; + +const UpdateIntervalMs = 10 * 1000; // how often data will be checked for updates +const TimeResolution = 5 * 60 * 1000; // how granular will be the chart data +const MaxDuration = 24 * 60 * 60 * 1000; // how far back in time will be the chart data + +function HashrateModal({ isActive, close, contractId, client }) { + const [chart, setChart] = useState([]); + + const handleClose = e => close(e); + const handlePropagation = e => e.stopPropagation(); + + useInterval( + async () => { + if (!contractId) return; + + const now = new Date(); + const fromDate = new Date(now.getTime() - MaxDuration); + + const storedHashrate = await client.getContractHashrate({ + contractId, + fromDate + }); + const chartData = mapInputDataToDataPoints(storedHashrate, fromDate, now); + + //@ts-ignore + setChart(renderChart(chartData)); + }, + UpdateIntervalMs, + true, + [contractId] + ); + + if (!isActive) { + return <>; + } + + return ( + + + {CloseModal(handleClose)} + + Dashboard + +
+
Recent Hashrate (last 24 hours)
+
+
+ + +
+ ); +} + +/** + * Maps input hashrate data to the chart data points filling the gaps with zero hashrate + * @param {{hashrate: number, timestamp: number}[]} storedHashrate + * @param {Date} fromDate + * @param {Date} toDate + * @returns {[number, number][]} + */ +function mapInputDataToDataPoints(storedHashrate, fromDate, toDate) { + /** @type {[number, number][]} */ + const chartData = []; + let sourceIndex = 0; + const fromDateRounded = roundTime(fromDate, TimeResolution).getTime(); + const toDateRounded = roundTime(toDate, TimeResolution).getTime(); + + for (let t = fromDateRounded; t < toDateRounded; t += TimeResolution) { + if (sourceIndex < storedHashrate.length) { + const source = storedHashrate[sourceIndex]; + const sourceRounded = roundTime( + new Date(source.timestamp), + TimeResolution + ); + + if (sourceRounded.getTime() === t) { + chartData.push([t, source.hashrate]); + sourceIndex++; + continue; + } + } + + chartData.push([t, 0]); + } + + return chartData; +} + +export default withClient(HashrateModal); diff --git a/src/components/contracts/modals/HashrateModal/chartRenderer.js b/src/components/contracts/modals/HashrateModal/chartRenderer.js new file mode 100644 index 00000000..e3e7217b --- /dev/null +++ b/src/components/contracts/modals/HashrateModal/chartRenderer.js @@ -0,0 +1,85 @@ +import Highcharts from 'highcharts'; + +/** + * @param {[number, number][]} data + */ +export const renderChart = data => { + const chart = { + chart: { + renderTo: 'container', + type: 'spline' + }, + title: { + text: '', + align: 'left' + }, + + legend: { + // layout: 'vertical', + align: 'right', + verticalAlign: 'top', + symbolRadius: 0, + labelFormatter: function() { + if (this.name === 'GH/s') { + return 'GH/s'; + } + return ''; + } + // itemMarginTop: 10, + // itemMarginBottom: 10 + }, + + xAxis: { + type: 'datetime', + tickInterval: 1000 * 3600, // tick every hour + labels: { + formatter: function() { + return Highcharts.dateFormat('%H %M', this.value); + } + } + }, + yAxis: { + min: 0 + }, + tooltip: { + formatter: function() { + return `${Highcharts.dateFormat( + '%m/%d/%Y %H %M', + this.x + )}
Hashrate (5min): ${this.y} GH/s`; + } + }, + plotOptions: { + series: { + name: 'GH/s', + pointInterval: 1000 * 60 * 5 // data every 5 minutes SET 5 + }, + spline: { + lineWidth: 2, + states: { + hover: { + lineWidth: 3 + } + }, + marker: { + enabled: false, + radius: 2, + states: { + hover: { + enabled: true, + symbol: 'circle', + radius: 2, + lineWidth: 1 + } + } + } + } + }, + series: [ + { + data: data + } + ] + }; + return chart; +}; diff --git a/src/components/contracts/modals/HashrateModal/utils.js b/src/components/contracts/modals/HashrateModal/utils.js new file mode 100644 index 00000000..e31c8d8a --- /dev/null +++ b/src/components/contracts/modals/HashrateModal/utils.js @@ -0,0 +1,26 @@ +/** + * Performs rounding of the time to the nearest granularityMs + * @param {Date} input + * @param {number} granularityMs + * @returns {Date} result + */ +export function roundTime(input, granularityMs) { + // used to avoid rounding errors due to possible extra seconds in the leap years + const relativeBase = getStartOfTheDay( + new Date(input.getTime() - granularityMs) + ); + const relativeTime = input.getTime() - relativeBase.getTime(); + const remainder = relativeTime % granularityMs; + if (remainder / granularityMs < 0.5) { + return new Date(input.getTime() - remainder); + } + return new Date(input.getTime() - remainder + granularityMs); +} + +/** + * @param {Date} date + * @returns {Date} + */ +export function getStartOfTheDay(date) { + return new Date(date.getFullYear(), date.getMonth(), date.getDate()); +} diff --git a/src/components/contracts/modals/PurchaseModal/PurchaseContractModal.js b/src/components/contracts/modals/PurchaseModal/PurchaseContractModal.js index 36c3b0d2..b5e8cbb1 100644 --- a/src/components/contracts/modals/PurchaseModal/PurchaseContractModal.js +++ b/src/components/contracts/modals/PurchaseModal/PurchaseContractModal.js @@ -40,7 +40,7 @@ function PurchaseContractModal(props) { } = useForm({ mode: 'onChange' }); useEffect(() => { - setValue('address', `stratum+tcp://${props.ip}:${props.buyerPort}`); + setValue('address', `${props.ip}:${props.buyerPort}`); trigger('address'); setValue('worker', contract?.id); @@ -48,7 +48,7 @@ function PurchaseContractModal(props) { }, [contract]); useEffect(() => { - setValue('address', `stratum+tcp://${props.ip}:${props.buyerPort}`); + setValue('address', `${props.ip}:${props.buyerPort}`); trigger('address'); }, [isActive]); diff --git a/src/components/contracts/modals/PurchaseModal/PurchaseFormModalPage.js b/src/components/contracts/modals/PurchaseModal/PurchaseFormModalPage.js index 940c83c7..ad158e09 100644 --- a/src/components/contracts/modals/PurchaseModal/PurchaseFormModalPage.js +++ b/src/components/contracts/modals/PurchaseModal/PurchaseFormModalPage.js @@ -50,10 +50,11 @@ export const PurchaseFormModalPage = ({ const [isEditPool, setIsEditPool] = useState(false); const validateAddress = address => { - if (!address.includes('stratum+tcp://')) return false; - const credsPart = address.replace('stratum+tcp://', ''); + const regexP = /^[a-zA-Z0-9.-]+:\d+$/; + if (!regexP.test(address)) return false; + const regexPortNumber = /:\d+/; - const portMatch = credsPart.match(regexPortNumber); + const portMatch = address.match(regexPortNumber); if (!portMatch) return false; const port = portMatch[0].replace(':', ''); @@ -62,6 +63,8 @@ export const PurchaseFormModalPage = ({ return true; }; + const poolParts = pool ? pool.replace('stratum+tcp://', '').split(':@') : []; + const handleClose = e => { e.preventDefault(); close(); @@ -122,16 +125,14 @@ export const PurchaseFormModalPage = ({ required: true, validate: validateAddress })} - placeholder={'stratum+tcp://IP_ADDRESS:PORT'} + placeholder={'HOST_IP:PORT'} type="text" name="address" key="address" id="address" /> {formState?.errors?.address?.type === 'validate' && ( - - Address should match stratum+tcp://IP_ADDRESS:PORT - + Address should match HOST_IP:PORT )} @@ -155,9 +156,19 @@ export const PurchaseFormModalPage = ({ Forwarding to (mining pool) - - {pool || 'Validation node default pool address'} - +
+ Pool Address + + {decodeURIComponent( + poolParts[1] || 'Validation node default pool address' + )} + +
+ Account + + {decodeURIComponent(poolParts[0] || '')} + +
onEditPool()}>Edit
diff --git a/src/components/contracts/modals/PurchaseModal/PurchasePreviewModalPage.js b/src/components/contracts/modals/PurchaseModal/PurchasePreviewModalPage.js index 48f3346f..03dcff99 100644 --- a/src/components/contracts/modals/PurchaseModal/PurchasePreviewModalPage.js +++ b/src/components/contracts/modals/PurchaseModal/PurchasePreviewModalPage.js @@ -65,6 +65,7 @@ export const PurchasePreviewModalPage = ({ const [loading, setLoading] = useState(true); const [ip, port] = inputs.address.replace('stratum+tcp://', '').split(':'); + const poolParts = pool ? pool.replace('stratum+tcp://', '').split(':@') : []; useEffect(() => { isProxyPortPublic({ @@ -146,7 +147,7 @@ export const PurchasePreviewModalPage = ({ {calculateAddress(inputs.address, contract.id)} - {isPortPublic !== null && !isPortPublic ? ( + {!isPortPublic ? ( <> window.openLink(portCheckErrorLink)} @@ -189,10 +190,17 @@ export const PurchasePreviewModalPage = ({ forwarding to (mining pool) -
+
Pool Address - {pool || 'Validation node default pool address'} + {decodeURIComponent( + poolParts[1] || 'Validation node default pool address' + )} + +
+ Account + + {decodeURIComponent(poolParts[0] || '')}
diff --git a/src/components/contracts/modals/SellerWhitelistModal/SellerWhitelistModal.js b/src/components/contracts/modals/SellerWhitelistModal/SellerWhitelistModal.js new file mode 100644 index 00000000..d55d6073 --- /dev/null +++ b/src/components/contracts/modals/SellerWhitelistModal/SellerWhitelistModal.js @@ -0,0 +1,59 @@ +import React, { useEffect, useState } from 'react'; +import { List as RVList, AutoSizer } from 'react-virtualized'; +import { + Modal, + Body, + TitleWrapper, + Title, + Subtitle, + CloseModal, + RightBtn, + Row +} from '../CreateContractModal.styles'; +import { withClient } from '../../../../store/hocs/clientContext'; + +function SellerWhitelistModal(props) { + const { isActive, close, formUrl } = props; + + const handleClose = e => { + close(e); + }; + const handlePropagation = e => e.stopPropagation(); + + if (!isActive) { + return <>; + } + + return ( + + + {CloseModal(handleClose)} + + You are not whitelisted as Seller + +

+ Lumerin is hand-selecting the first few hashrate sellers for mainnet + in order to ensure high quality contracts in this initial launch phase + of the marketplace. +

+

+ If you are interested in becoming a seller please fill out the form +

+ + + { + window.open(formUrl, '_blank'); + close(); + }} + > + Open Form + + + +
+ ); +} + +export default withClient(SellerWhitelistModal); diff --git a/src/components/hooks/useInterval.js b/src/components/hooks/useInterval.js new file mode 100644 index 00000000..492972c8 --- /dev/null +++ b/src/components/hooks/useInterval.js @@ -0,0 +1,26 @@ +import { useEffect, useRef } from 'react'; + +/** + * SetInterval for react components + * @param {Function} callback + * @param {number} delay + * @param {boolean} immediate whether to call callback immediately + * @param {React.DependencyList | undefined} deps dependency list, like in useEffect + * @returns {React.MutableRefObject} + */ +export function useInterval(callback, delay, immediate = false, deps = []) { + const intervalRef = useRef(null); + const savedCallback = useRef(callback); + useEffect(() => { + savedCallback.current = callback; + }, [callback, ...deps]); + useEffect(() => { + const tick = () => savedCallback.current(); + if (immediate) { + tick(); + } + intervalRef.current = window.setInterval(tick, delay); + return () => window.clearInterval(intervalRef.current); + }, [delay, ...deps]); + return intervalRef; +} diff --git a/src/components/onboarding/ProxyRouterConfigStep.js b/src/components/onboarding/ProxyRouterConfigStep.js index 8fe4d232..e63e23a3 100644 --- a/src/components/onboarding/ProxyRouterConfigStep.js +++ b/src/components/onboarding/ProxyRouterConfigStep.js @@ -59,8 +59,8 @@ const ProxyRouterConfigStep = props => { onChange={props.onInputChange} noFocus error={props.errors.proxyDefaultPool} - placeholder="stratum+tcp://{pool btc mining url}:{port}" - label="Pool BTC Mining Url" + placeholder="example: btc.global.luxor.tech:8888" + label="Pool BTC Mining Host & Port" value={props.proxyDefaultPool} type="text" id="proxyDefaultPool" @@ -70,7 +70,7 @@ const ProxyRouterConfigStep = props => { + You are running Wallet wihout Proxy-Router. Reset wallet to setup + validator node. + + ); +} + +function TitanLightningProxyPanel(props) { + const { titanLightningDashboard, sellerPoolParts } = props; + return ( + <> + +
+ Titan Lightning Address: {sellerPoolParts?.account}{' '} +
+

{ + window.open(titanLightningDashboard, '_blank'); + }} + > + Dashboard for Lightning users +

+
+ + ); +} + +function ProxyConfigView(props) { + const { isTitanLightning, sellerPoolParts, proxyRouterEditClick } = props; + return ( + <> + {isTitanLightning ? ( + + ) : ( + +
+ Proxy Default Pool: {sellerPoolParts?.pool}{' '} +
+
+ Proxy Default Account: {sellerPoolParts?.account}{' '} +
+
+ )} + Edit + + ); +} + +function ProxyConfigEdit(props) { + const { + isTitanLightning, + sellerPoolParts, + setSellerPoolParts, + errors, + setErrors + } = props; + + const onChangePoolAddess = address => { + const result = validatePoolAddress(address, {}); + + setErrors({ + ...errors, + proxyDefaultPool: result.proxyDefaultPool || null + }); + + setSellerPoolParts({ + ...sellerPoolParts, + pool: address, + isTitanLightning + }); + }; + + return ( + <> + {isTitanLightning ? ( + + Titan Lightning Address: + + setSellerPoolParts({ + ...sellerPoolParts, + account: e.value + }) + } + value={sellerPoolParts?.account} + /> + + ) : ( + <> + + Proxy Default Pool Host & Port:{' '} + onChangePoolAddess(e.value)} + value={sellerPoolParts?.pool} + /> + {errors?.proxyDefaultPool && ( + {errors?.proxyDefaultPool} + )} + + + Proxy Default Account: + + setSellerPoolParts({ + ...sellerPoolParts, + account: e.value + }) + } + value={sellerPoolParts?.account} + /> + + + )} + + ); +} + +export function ProxyConfigPanel(props) { + const [errors, setErrors] = useState({}); + + return !props.isLocalProxyRouter ? ( + + ) : ( + <> + + Proxy-Router Configuration + {props.proxyRouterSettings.isFetching ? ( + + ) : props.proxyRouterSettings.proxyRouterEditMode ? ( + <> +
+ Use Titan Pool for Lightning Payouts + { + props.toggleIsLightning(); + }} + checked={props.isTitanLightning} + type="checkbox" + id="isTitanLightning" + /> +
+ +
+ { + props.setProxyRouterSettings({ + ...props.proxyRouterSettings, + isTitanLightning: props.isTitanLightning, + sellerDefaultPool: generatePoolUrl( + props.sellerPoolParts.account, + !props.isTitanLightning + ? props.sellerPoolParts.pool + : props.titanLightningPool + ) + }); + props.onActiveModalClick('confirm-proxy-restart'); + }} + > + Save + + + ) : ( + + )} + + +
+ + Restart Proxy Router + Restart the connected Proxy Router. + {props.isRestarting ? ( + + ) : ( + + props.onActiveModalClick('confirm-proxy-direct-restart') + } + > + Restart + + )} + + + + ); +} diff --git a/src/components/tools/RevealSecretPhraseModal.js b/src/components/tools/RevealSecretPhraseModal.js index 21301271..198725cf 100644 --- a/src/components/tools/RevealSecretPhraseModal.js +++ b/src/components/tools/RevealSecretPhraseModal.js @@ -1,8 +1,8 @@ import PropTypes from 'prop-types'; import styled from 'styled-components'; -import React, { useEffect } from 'react'; +import React, { useState } from 'react'; -import { Modal, BaseBtn } from '../common'; +import { Modal } from '../common'; import { Container, Message, @@ -10,8 +10,7 @@ import { DismissBtn, ConfirmBtn } from './ConfirmModal.styles'; -import { Input } from './Tools'; -import { useState } from 'react'; +import { Input } from './common'; const Mnemonic = styled.div` padding: 10px 0; diff --git a/src/components/tools/Tools.js b/src/components/tools/Tools.js index 775ea7df..0e5efe83 100644 --- a/src/components/tools/Tools.js +++ b/src/components/tools/Tools.js @@ -3,10 +3,8 @@ import withToolsState from '../../store/hocs/withToolsState'; import PropTypes from 'prop-types'; import styled from 'styled-components'; import React, { useState, useContext, useEffect } from 'react'; -import axios from 'axios'; import ConfirmModal from './ConfirmModal'; -import TestModal from './TestModal'; import WalletStatus from './WalletStatus'; import { ConfirmationWizard, TextInput, Flex, BaseBtn, Sp } from '../common'; import Spinner from '../common/Spinner'; @@ -16,9 +14,11 @@ import ConfirmProxyConfigModal from './ConfirmProxyConfigModal'; import RevealSecretPhraseModal from './RevealSecretPhraseModal'; import { Message } from './ConfirmModal.styles'; import ExportPrivateKeyModal from './ExportPrivateKeyModal'; -import { generatePoolUrl } from '../../utils'; +import { ProxyConfigPanel } from './ProxyConfigPanel'; import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; +import { StyledBtn, Subtitle, StyledParagraph, Input } from './common'; + import 'react-tabs/style/react-tabs.css'; import './styles.css'; @@ -75,48 +75,10 @@ const ValidationMsg = styled.div` opacity: 0.75; `; -const StyledBtn = styled(BaseBtn)` - width: 40%; - height: 40px; - font-size: 1.5rem; - border-radius: 5px; - padding: 0 0.6rem; - background-color: ${p => p.theme.colors.primary}; - color: ${p => p.theme.colors.light}; - - @media (min-width: 1040px) { - width: 35%; - height: 40px; - margin-left: 0; - margin-top: 1.6rem; - } -`; - -const Subtitle = styled.h3` - color: ${p => p.theme.colors.dark}; -`; - -const StyledParagraph = styled.p` - color: ${p => p.theme.colors.dark}; - - span { - font-weight: bold; - } -`; - const WalletInfo = styled.h4` color: ${p => p.theme.colors.dark}; `; -export const Input = styled(TextInput)` - outline: 0; - border: 0px; - background: #eaf7fc; - border-radius: 15px; - padding: 1.2rem 1.2rem; - margin-top: 0.25rem; -`; - const getPoolAndAccount = url => { if (!url) return {}; const addressParts = url.replace('stratum+tcp://', '').split(':'); @@ -147,7 +109,8 @@ const Tools = props => { getCustomEnvs, setCustomEnvs, config, - restartWallet + restartWallet, + titanLightningDashboard } = props; const RenderForm = goToReview => { @@ -170,7 +133,6 @@ const Tools = props => { isFetching: true }); const [sellerPoolParts, setSellerPoolParts] = useState(null); - const [buyerPoolParts, setBuyerPoolParts] = useState(null); const [isTitanLightning, setTitanLightning] = useState(false); const [httpNodeInput, setHttpNodeInput] = useState( @@ -192,7 +154,6 @@ const Tools = props => { getProxyRouterSettings() .then(data => { setSellerPoolParts(getPoolAndAccount(data.sellerDefaultPool)); - setBuyerPoolParts(getPoolAndAccount(data.sellerDefaultPool)); setProxyRouterSettings({ ...data, @@ -272,6 +233,16 @@ const Tools = props => { }); }; + const toggleIsLightning = () => { + setTitanLightning(!isTitanLightning); + setSellerPoolParts({ + ...sellerPoolParts, + pool: '', + account: '', + isTitanLightning: !isTitanLightning + }); + }; + const onRestartClick = async () => { onCloseModal(); restartNode(true); @@ -485,137 +456,25 @@ const Tools = props => { - {props.isLocalProxyRouter ? ( - - - Proxy-Router Configuration - {proxyRouterSettings.isFetching ? ( - - ) : !proxyRouterSettings.proxyRouterEditMode ? ( - <> - - {!isTitanLightning ? ( -
- Proxy Default Pool:{' '} - {sellerPoolParts?.pool}{' '} -
- ) : ( - <> - )} - {!isTitanLightning ? ( -
- Proxy Default Account:{' '} - {sellerPoolParts?.account}{' '} -
- ) : ( -
- Titan Lightning Address:{' '} - {sellerPoolParts?.account}{' '} -
- )} -
- Edit - - ) : ( - <> - {!isTitanLightning ? ( - - Proxy Default Pool:{' '} - - setSellerPoolParts({ - ...sellerPoolParts, - pool: e.value, - isTitanLightning - }) - } - value={sellerPoolParts?.pool} - /> - - ) : ( - <> - )} - - {!isTitanLightning - ? 'Proxy Default Account: ' - : 'Titan Lightning Address: '} - - setSellerPoolParts({ - ...sellerPoolParts, - account: e.value - }) - } - value={sellerPoolParts?.account} - /> - -
- { - setProxyRouterSettings({ - ...proxyRouterSettings, - isTitanLightning, - sellerDefaultPool: generatePoolUrl( - sellerPoolParts.account, - !isTitanLightning - ? sellerPoolParts.pool - : props.titanLightningPool - ) - }); - onActiveModalClick('confirm-proxy-restart'); - }} - > - Save - - - )} - - - -
- - Restart Proxy Router - - Restart the connected Proxy Router. - - {isRestarting ? ( - - ) : ( - - onActiveModalClick('confirm-proxy-direct-restart') - } - > - Restart - - )} - - -
- ) : ( - - - You are running Wallet wihout Proxy-Router. Reset wallet to - setup validator node. - - - )} + + + HTTP ETH Node: diff --git a/src/components/tools/common.js b/src/components/tools/common.js new file mode 100644 index 00000000..25b0af54 --- /dev/null +++ b/src/components/tools/common.js @@ -0,0 +1,57 @@ +import styled from 'styled-components'; + +import 'react-tabs/style/react-tabs.css'; +import './styles.css'; +import { BaseBtn, TextInput } from '../common'; + +export const Sublabel = styled.label` + line-height: 1.4rem; + font-size: 1.1rem; + font-weight: 400; + opacity: 0.65; + cursor: default; + padding: 5px 0 0 5px; +`; + +export const ErrorLabel = styled(Sublabel)` + padding: 5px 0 0 5px; + color: red; +`; + +export const StyledBtn = styled(BaseBtn)` + width: 40%; + height: 40px; + font-size: 1.5rem; + border-radius: 5px; + padding: 0 0.6rem; + background-color: ${p => p.theme.colors.primary}; + color: ${p => p.theme.colors.light}; + + @media (min-width: 1040px) { + width: 35%; + height: 40px; + margin-left: 0; + margin-top: 1.6rem; + } +`; + +export const Subtitle = styled.h3` + color: ${p => p.theme.colors.dark}; +`; + +export const StyledParagraph = styled.p` + color: ${p => p.theme.colors.dark}; + + span { + font-weight: bold; + } +`; + +export const Input = styled(TextInput)` + outline: 0; + border: 0px; + background: #eaf7fc; + border-radius: 15px; + padding: 1.2rem 1.2rem; + margin-top: 0.25rem; +`; diff --git a/src/store/hocs/withContractsState.js b/src/store/hocs/withContractsState.js index 5e3a05ec..0953b33e 100644 --- a/src/store/hocs/withContractsState.js +++ b/src/store/hocs/withContractsState.js @@ -32,6 +32,9 @@ const withContractsState = WrappedComponent => { contractsRefresh = (force = false) => { const now = parseInt(Date.now() / 1000, 10); const timeout = 15; // seconds + if (this.props.syncStatus === 'syncing') { + return; + } if ( this.props.contractsLastUpdatedAt && now - this.props.contractsLastUpdatedAt < timeout && @@ -84,7 +87,8 @@ const withContractsState = WrappedComponent => { lmrCoinPrice: selectors.getRate(state), ethCoinPrice: selectors.getRateEth(state), btcCoinPrice: selectors.getRateBtc(state), - selectedCurrency: selectors.getSellerSelectedCurrency(state) + selectedCurrency: selectors.getSellerSelectedCurrency(state), + formUrl: selectors.getSellerWhitelistForm(state) }); const mapDispatchToProps = dispatch => ({ diff --git a/src/store/hocs/withToolsState.js b/src/store/hocs/withToolsState.js index daff2c49..61afee33 100644 --- a/src/store/hocs/withToolsState.js +++ b/src/store/hocs/withToolsState.js @@ -149,6 +149,7 @@ const withToolsState = WrappedComponent => { selectedCurrency: selectors.getSellerSelectedCurrency(state), isLocalProxyRouter: selectors.getIsLocalProxyRouter(state), titanLightningPool: state.config.chain.titanLightningPool, + titanLightningDashboard: state.config.chain.titanLightningDashboard, config: state.config }); diff --git a/src/store/reducers/contracts.js b/src/store/reducers/contracts.js index e4e04966..e795296f 100644 --- a/src/store/reducers/contracts.js +++ b/src/store/reducers/contracts.js @@ -49,9 +49,7 @@ const reducer = handleActions( return { ...state, - actives: { ...state.actives, ...idContractMap }, - lastUpdated: parseInt(Date.now() / 1000, 10), - syncStatus: 'up-to-date' + actives: { ...state.actives, ...idContractMap } }; }, diff --git a/src/store/selectors/config.js b/src/store/selectors/config.js index 2d8f6fb1..542d9b9c 100644 --- a/src/store/selectors/config.js +++ b/src/store/selectors/config.js @@ -29,3 +29,6 @@ export const getSellerSelectedCurrency = state => state.config.sellerCurrency; export const getSellerDefaultCurrency = state => state.config.chain.defaultSellerCurrency; + +export const getSellerWhitelistForm = state => + state.config.chain.sellerWhitelistUrl; diff --git a/src/store/validators.js b/src/store/validators.js index 1a477e0a..3afe3884 100644 --- a/src/store/validators.js +++ b/src/store/validators.js @@ -137,7 +137,7 @@ export function validateUseMinimum(useMinimum, estimate, errors = {}) { } export const validatePoolAddress = (address, errors = {}) => { - const defaultPoolFormat = 'stratum+tcp://{host}:{port}'; + const defaultPoolFormat = '{host}:{port}'; const expectedFormat = `Expected format: ${defaultPoolFormat}`; if (!address) { @@ -145,7 +145,7 @@ export const validatePoolAddress = (address, errors = {}) => { return errors; } - const pattern = /^stratum\+tcp:\/\/([\w.-]+):(\d+)$/; + const pattern = /^[a-zA-Z0-9.-]+:\d+$/; const result = pattern.test(address); if (!result) { diff --git a/src/subscriptions.js b/src/subscriptions.js index 3b09e672..1286ed2d 100644 --- a/src/subscriptions.js +++ b/src/subscriptions.js @@ -11,6 +11,7 @@ export const subscribeToMainProcessMessages = function(store) { 'transactions-scan-started', 'contracts-scan-finished', 'contracts-updated', + 'contract-updated', 'contracts-scan-started', 'wallet-state-changed', 'coin-price-updated', From f102336ee1c6e6a722c411f5a16af187c2065d83 Mon Sep 17 00:00:00 2001 From: Aleksandr Kukharenko Date: Wed, 20 Dec 2023 15:19:20 +0200 Subject: [PATCH 6/7] refactor: code review fixes --- public/main/client/handlers/single-core.js | 8 +++----- src/components/contracts/BuyerHub.js | 10 ++++++---- src/store/reducers/wallet.js | 1 - src/store/utils/createTransactionParser.js | 8 ++++---- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/public/main/client/handlers/single-core.js b/public/main/client/handlers/single-core.js index 6764159a..b51bce86 100644 --- a/public/main/client/handlers/single-core.js +++ b/public/main/client/handlers/single-core.js @@ -271,13 +271,11 @@ const getMarketplaceFee = async function (data, { api }) { return api.contracts.getMarketplaceFee(data); }; -function refreshAllContracts({ }, { api }) { - const walletId = wallet.getAddress().address; - return api.contracts.refreshContracts(null, walletId); +function refreshAllContracts({}, { api }) { + return api.contracts.refreshContracts(); } function startWatchingContracts({}, { api }) { - const walletId = wallet.getAddress().address; return api.contracts.startWatching(); } @@ -370,7 +368,7 @@ function getPastTransactions({ address, page, pageSize }, { api }) { } module.exports = { - // refreshAllSockets, + refreshAllSockets, refreshAllContracts, startWatchingContracts, purchaseContract, diff --git a/src/components/contracts/BuyerHub.js b/src/components/contracts/BuyerHub.js index d0f08dbd..9f4fbe76 100644 --- a/src/components/contracts/BuyerHub.js +++ b/src/components/contracts/BuyerHub.js @@ -95,8 +95,8 @@ function BuyerHub({ const [showHashrateModal, setShowHashrateModal] = useState(false); const [contactToShowHashrate, setContactToShowHashrate] = useState(); - const contractsWithHistory = contracts.filter(c => c.history?.length); - const showHistory = contractsWithHistory.length; + const contractsWithHistory = contracts.filter(c => !!c.history?.length); + const hasContractsWithHistory = contractsWithHistory.length > 0; const onHistoryOpen = () => setIsHistoryModalOpen(true); return ( @@ -106,10 +106,12 @@ function BuyerHub({ address={address} copyToClipboard={copyToClipboard} > - + History diff --git a/src/store/reducers/wallet.js b/src/store/reducers/wallet.js index 81883814..59f106bd 100644 --- a/src/store/reducers/wallet.js +++ b/src/store/reducers/wallet.js @@ -98,7 +98,6 @@ const reducer = handleActions( }, 'transactions-next-page': (state, { payload }) => { - console.log(payload); return { ...state, hasNextPage: payload.hasNextPage, diff --git a/src/store/utils/createTransactionParser.js b/src/store/utils/createTransactionParser.js index 6f8e1cc3..8f84aefe 100644 --- a/src/store/utils/createTransactionParser.js +++ b/src/store/utils/createTransactionParser.js @@ -7,13 +7,13 @@ import { } from '../../utils/coinValue'; function isSendTransaction(transaction, myAddress) { - const from = transaction.from; - return from.toLowerCase() === myAddress.toLowerCase(); + const from = transaction?.from; + return from?.toLowerCase() === myAddress?.toLowerCase(); } function isReceiveTransaction(transaction, myAddress) { - const to = transaction.to; - return to?.toLowerCase() === myAddress.toLowerCase(); + const to = transaction?.to; + return to?.toLowerCase() === myAddress?.toLowerCase(); } function getTxType(rawTx, tokenData, myAddress) { From d445d279cf304e76272d4f8476ab70c12b1d606c Mon Sep 17 00:00:00 2001 From: Aleksandr Kukharenko Date: Wed, 27 Dec 2023 13:58:14 +0200 Subject: [PATCH 7/7] restore BuyerHub history feature --- public/main/client/handlers/single-core.js | 5 ++ public/main/client/index.js | 2 + .../main/client/subscriptions/single-core.js | 3 +- src/client/index.js | 3 +- src/components/contracts/BuyerHub.js | 9 ++-- .../modals/HistoryModal/HistoryModal.js | 50 ++++++++++++++++--- 6 files changed, 57 insertions(+), 15 deletions(-) diff --git a/public/main/client/handlers/single-core.js b/public/main/client/handlers/single-core.js index b51bce86..774ece3b 100644 --- a/public/main/client/handlers/single-core.js +++ b/public/main/client/handlers/single-core.js @@ -339,6 +339,10 @@ const getLocalIp = async ({ }, { api }) => api["proxy-router"].getLocalIp(); const isProxyPortPublic = async (data, { api }) => api["proxy-router"].isProxyPortPublic(data); +const getContractHistory = async (data, { api }) => { + return api.contracts.getContractHistory(data); +} + const logout = async (data) => { return cleanupDb(); }; @@ -404,4 +408,5 @@ module.exports = { getMarketplaceFee, isProxyPortPublic, stopProxyRouter, + getContractHistory, }; diff --git a/public/main/client/index.js b/public/main/client/index.js index 9b220ec8..7138bd6c 100644 --- a/public/main/client/index.js +++ b/public/main/client/index.js @@ -16,7 +16,9 @@ const { } = require("./handlers/single-core"); const { runProxyRouter, isProxyRouterHealthy } = require("./proxyRouter"); + let interval; + function startCore({ chain, core, config: coreConfig }, webContent) { logger.verbose(`Starting core ${chain}`); const { emitter, events, api } = core.start(coreConfig); diff --git a/public/main/client/subscriptions/single-core.js b/public/main/client/subscriptions/single-core.js index 63362713..eb1dddaa 100644 --- a/public/main/client/subscriptions/single-core.js +++ b/public/main/client/subscriptions/single-core.js @@ -32,7 +32,8 @@ const listeners = { "stop-proxy-router": handlers.stopProxyRouter, "claim-faucet": handlers.claimFaucet, 'get-private-key': handlers.getAddressAndPrivateKey, - "get-marketplace-fee": handlers.getMarketplaceFee + "get-marketplace-fee": handlers.getMarketplaceFee, + "get-contract-history": handlers.getContractHistory, }; let coreListeners = {}; diff --git a/src/client/index.js b/src/client/index.js index cac2d9ea..ed6c2457 100644 --- a/src/client/index.js +++ b/src/client/index.js @@ -172,7 +172,8 @@ const createClient = function(createStore) { claimFaucet: utils.forwardToMainProcess('claim-faucet', 750000), getCustomEnvValues: utils.forwardToMainProcess('get-custom-env-values'), setCustomEnvValues: utils.forwardToMainProcess('set-custom-env-values'), - getContractHashrate: utils.forwardToMainProcess('get-contract-hashrate') + getContractHashrate: utils.forwardToMainProcess('get-contract-hashrate'), + getContractHistory: utils.forwardToMainProcess('get-contract-history') }; const api = { diff --git a/src/components/contracts/BuyerHub.js b/src/components/contracts/BuyerHub.js index 9f4fbe76..781c9d5f 100644 --- a/src/components/contracts/BuyerHub.js +++ b/src/components/contracts/BuyerHub.js @@ -41,8 +41,7 @@ function BuyerHub({ address, client, contractsRefresh, - allowSendTransaction, - ...props + allowSendTransaction }) { const contractsToShow = contracts.filter( x => x.buyer === address && x.seller !== address @@ -95,8 +94,7 @@ function BuyerHub({ const [showHashrateModal, setShowHashrateModal] = useState(false); const [contactToShowHashrate, setContactToShowHashrate] = useState(); - const contractsWithHistory = contracts.filter(c => !!c.history?.length); - const hasContractsWithHistory = contractsWithHistory.length > 0; + const hasContractsWithHistory = true; const onHistoryOpen = () => setIsHistoryModalOpen(true); return ( @@ -134,7 +132,8 @@ function BuyerHub({ { setIsHistoryModalOpen(false); }} diff --git a/src/components/contracts/modals/HistoryModal/HistoryModal.js b/src/components/contracts/modals/HistoryModal/HistoryModal.js index a631c76a..4b69795c 100644 --- a/src/components/contracts/modals/HistoryModal/HistoryModal.js +++ b/src/components/contracts/modals/HistoryModal/HistoryModal.js @@ -10,18 +10,49 @@ import { } from '../CreateContractModal.styles'; import HistoryRow from './HistoryRow'; import { withClient } from '../../../../store/hocs/clientContext'; -import { lmrDecimals } from '../../../../utils/coinValue'; +import Spinner from '../../../common/Spinner'; function HistroyModal(props) { - const { isActive, close, historyContracts, client } = props; + const { isActive, close, client, contracts, address } = props; + + const [historyContracts, setHistory] = useState({}); + const [isLoading, setLoading] = useState(false); + useEffect(() => { + if (!isActive) { + return; + } + if (contracts.length) { + setLoading(true); + } + let loaded = 0; + for (let i = 0; i < contracts.length; i += 1) { + const c = contracts[i]; + client + .getContractHistory({ contractAddr: c.id, walletAddress: address }) + .then(history => { + if (history.length > 0) { + setHistory(prev => ({ + ...prev, + [c.id]: history + })); + } + }) + .catch(err => {}) + .finally(() => { + loaded += 1; + if (loaded === contracts.length) { + setLoading(false); + } + }); + } + }, [isActive]); const handleClose = e => { close(e); }; const handlePropagation = e => e.stopPropagation(); - const history = historyContracts - .map(hc => hc.history) + const history = Object.values(historyContracts) .flat() .map(h => { return { @@ -48,10 +79,7 @@ function HistroyModal(props) { } const rowRenderer = historyContracts => ({ key, index, style }) => ( - + ); return ( @@ -61,6 +89,12 @@ function HistroyModal(props) { Purchase history + {isLoading && !history.length && ( + + Loading... + + )} + {!isLoading && !history.length && No history found} {({ width, height }) => (