diff --git a/public/main/client/handlers/single-core.js b/public/main/client/handlers/single-core.js index eff9c7da..774ece3b 100644 --- a/public/main/client/handlers/single-core.js +++ b/public/main/client/handlers/single-core.js @@ -271,9 +271,12 @@ 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 }) { + return api.contracts.startWatching(); } function refreshTransaction({ hash, address }, { api }) { @@ -336,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(); }; @@ -361,12 +368,13 @@ 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 = { - // refreshAllSockets, + refreshAllSockets, refreshAllContracts, + startWatchingContracts, purchaseContract, createContract, cancelContract, @@ -400,4 +408,5 @@ module.exports = { getMarketplaceFee, isProxyPortPublic, stopProxyRouter, + getContractHistory, }; diff --git a/public/main/client/index.js b/public/main/client/index.js index ace5f911..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); @@ -35,6 +37,7 @@ function startCore({ chain, core, config: coreConfig }, webContent) { "transactions-scan-finished", "contracts-scan-started", "contracts-scan-finished", + "contracts-updated", 'contract-updated', ); @@ -65,10 +68,10 @@ function startCore({ chain, core, config: coreConfig }, webContent) { return api.explorer .syncTransactions( 0, - address, - (number) => storage.setSyncBlock(number, chain), + 'latest', page, - pageSize + pageSize, + address ) .then(function () { send("transactions-scan-finished", { success: true }); @@ -91,7 +94,11 @@ function startCore({ chain, core, config: coreConfig }, webContent) { }); } - emitter.on("open-wallet", syncTransactions); + emitter.on("open-wallet", (props) => { + syncTransactions(props); + api.contracts.startWatching({}); + api.explorer.startWatching({ walletAddress: props.address }); + }); 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..eb1dddaa 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, @@ -31,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 e0f72db0..ed6c2457 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'), @@ -168,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 902ab781..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 showHistory = contractsWithHistory.length; + const hasContractsWithHistory = true; const onHistoryOpen = () => setIsHistoryModalOpen(true); return ( @@ -106,10 +104,12 @@ function BuyerHub({ address={address} copyToClipboard={copyToClipboard} > - + History @@ -132,7 +132,8 @@ function BuyerHub({ { setIsHistoryModalOpen(false); }} 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/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 }) => ( { 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 && diff --git a/src/store/reducers/contracts.js b/src/store/reducers/contracts.js index 3ceb0d3b..e795296f 100644 --- a/src/store/reducers/contracts.js +++ b/src/store/reducers/contracts.js @@ -44,13 +44,12 @@ const reducer = handleActions( }; }, - 'contract-updated': (state, { payload }) => { + 'contracts-updated': (state, { payload }) => { const idContractMap = keyBy(payload.actives, 'id'); return { ...state, - actives: { ...state.actives, ...idContractMap }, - lastUpdated: parseInt(Date.now() / 1000, 10) + actives: { ...state.actives, ...idContractMap } }; }, diff --git a/src/store/reducers/wallet.js b/src/store/reducers/wallet.js index c0b5499f..59f106bd 100644 --- a/src/store/reducers/wallet.js +++ b/src/store/reducers/wallet.js @@ -26,51 +26,24 @@ export const initialState = { * Should filter transactions without receipt if we received ones */ const mergeTransactions = (stateTxs, 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 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; }; @@ -114,22 +87,23 @@ const reducer = handleActions( } }), - 'token-transactions-changed': (state, { payload }) => ({ - ...state, - token: { - ...state.token, - transactions: mergeTransactions( - state.token.transactions, - payload.transactions - ) - } - }), - - 'transactions-next-page': (state, { payload }) => ({ - ...state, - hasNextPage: payload.hasNextPage, - page: payload.page - }), + 'token-transactions-changed': (state, { payload }) => { + return { + ...state, + token: { + ...state.token, + transactions: mergeTransactions(state.token.transactions, payload) + } + }; + }, + + 'transactions-next-page': (state, { payload }) => { + return { + ...state, + hasNextPage: payload.hasNextPage, + page: payload.page + }; + }, 'token-state-changed': (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..8f84aefe 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; - return from.toLowerCase() === myAddress.toLowerCase(); +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; - return to?.toLowerCase() === myAddress.toLowerCase(); -} - -function isImportRequestTransaction(rawTx) { - return get(rawTx.meta, 'lumerin.importRequest', false); +function isReceiveTransaction(transaction, myAddress) { + const to = transaction?.to; + return to?.toLowerCase() === myAddress?.toLowerCase(); } 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; diff --git a/src/subscriptions.js b/src/subscriptions.js index 8550268a..1286ed2d 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', 'contract-updated', 'contracts-scan-started', 'wallet-state-changed',