diff --git a/lib/controller/transaction/index.js b/lib/controller/transaction/index.js index 750467172c..d065532f1e 100644 --- a/lib/controller/transaction/index.js +++ b/lib/controller/transaction/index.js @@ -1,8 +1,8 @@ // const { getMessage } = require('./messageConstructor') const { networkMap } = require('../../networks') const Sentry = require('@sentry/node') -const { publishUserTransactionAdded } = require('../../subscriptions') -const reducers = require('../../reducers/cosmosV0-reducers') // TODO the whole transaction service only works for cosmos rn +const { publishUserTransactionAddedV2 } = require('../../subscriptions') +const reducers = require('../../reducers/cosmosV2-reducers') // TODO the whole transaction service only works for cosmos rn const { prestore, storePrestored, @@ -271,31 +271,40 @@ async function pollTransactionSuccess( // publishUserTransactionAdded is also done in the block subscription // but also here as a fallback // TODO the client might now update twice as it receives the success twice, could be fine though - const transaction = reducers.transactionReducer(res, reducers) + const transactions = reducers.transactionReducerV2(res, reducers) // store in db storePrestored(hash) // we need to call - publishUserTransactionAdded(networkId, senderAddress, transaction) + transactions.forEach(transaction => + publishUserTransactionAddedV2(networkId, senderAddress, transaction) + ) } catch (error) { console.error('TX failed:', hash, error) - let transaction + + let transactions if (res.tx) { - transaction = reducers.transactionReducer(res, reducers) + transactions = reducers.transactionReducerV2(res, reducers) } else { - // on timeout we don't get a transaction back - transaction = { - type: '', - hash, - height: -1, - group: '', - timestamp: '', - signature: '', - value: '', - success: false, - log: error.message - } + // on timeout we don't get a transactionV2 back + transactions = [ + { + type: '', + hash, + key: '', + height: -1, + details: {}, + timestamp: '', + memo: '', + success: false, + log: error.message + } + ] } - publishUserTransactionAdded(networkId, senderAddress, transaction) + + transactions.forEach(transaction => + publishUserTransactionAddedV2(networkId, senderAddress, transaction) + ) + Sentry.withScope(scope => { scope.setExtra('api_url', url) scope.setExtra('hash', hash) diff --git a/lib/reducers/cosmosV0-reducers.js b/lib/reducers/cosmosV0-reducers.js index 238a10ca3d..01c32993eb 100644 --- a/lib/reducers/cosmosV0-reducers.js +++ b/lib/reducers/cosmosV0-reducers.js @@ -1,13 +1,14 @@ -const { uniqWith, sortBy, reverse } = require('lodash') +const { uniqWith, sortBy, reverse, flatten } = require('lodash') const { cosmosMessageType } = require('../message-types') -const { - cosmosWhitelistedMessageTypes, - lunieMessageTypes -} = require('../../lib/message-types') +const { cosmosWhitelistedMessageTypes } = require('../../lib/message-types') const BigNumber = require('bignumber.js') -const _ = require('lodash') const Sentry = require('@sentry/node') +/** + * Modify the following reducers with care as they are used for ./cosmosV2-reducer.js as well + * [proposalBeginTime, proposalEndTime, getDeposit, tallyReducer, atoms, getValidatorStatus, coinReducer] + */ + function proposalBeginTime(proposal) { switch (proposal.proposal_status.toLowerCase()) { case 'depositperiod': @@ -223,7 +224,11 @@ function validatorReducer(networkId, signedBlocksWindow, validator) { : undefined, uptimePercentage: 1 - - Number(validator.signing_info.missed_blocks_counter) / + Number( + validator.signing_info + ? validator.signing_info.missed_blocks_counter + : 0 + ) / Number(signedBlocksWindow), tokens: atoms(validator.tokens), commissionUpdateTime: validator.commission.update_time, @@ -381,7 +386,7 @@ async function overviewReducer( ) { stakingDenom = denomLookup(stakingDenom) - const totalRewards = _.flatten(rewards) + const totalRewards = flatten(rewards) // this filter is here for multidenom networks. If there is the field denoms inside rewards, we filter // only the staking denom rewards for 'totalRewards' .filter(reward => @@ -512,89 +517,6 @@ function extractInvolvedAddresses(transaction) { return involvedAddresses } -function transactionReducerV2(transaction, reducers, stakingDenom) { - try { - // TODO check if this is anywhere not an array - let fees - if (Array.isArray(transaction.tx.value.fee.amount)) { - fees = transaction.tx.value.fee.amount.map(coinReducer) - } else { - fees = [coinReducer(transaction.tx.value.fee.amount)] - } - // We do display only the transactions we support in Lunie - const filteredMessages = transaction.tx.value.msg.filter( - ({ type }) => getMessageType(type) !== 'Unknown' - ) - const { claimMessages, otherMessages } = filteredMessages.reduce( - ({ claimMessages, otherMessages }, message) => { - // we need to aggregate all withdraws as we display them together in one transaction - if (getMessageType(message.type) === lunieMessageTypes.CLAIM_REWARDS) { - claimMessages.push(message) - } else { - otherMessages.push(message) - } - return { claimMessages, otherMessages } - }, - { claimMessages: [], otherMessages: [] } - ) - - // we need to aggregate claim rewards messages in one single one to avoid transaction repetition - const claimMessage = - claimMessages.length > 0 - ? claimRewardsMessagesAggregator(claimMessages) - : undefined - const allMessages = claimMessage - ? otherMessages.concat(claimMessage) // add aggregated claim message - : otherMessages - const returnedMessages = allMessages.map(({ value, type }, index) => ({ - type: getMessageType(type), - hash: transaction.txhash, - key: `${transaction.txhash}_${index}`, - height: transaction.height, - details: transactionDetailsReducer( - getMessageType(type), - value, - reducers, - transaction, - stakingDenom - ), - timestamp: transaction.timestamp, - memo: transaction.tx.value.memo, - fees, - success: - transaction.logs && transaction.logs[index] - ? transaction.logs[index].success || false - : false, - log: - transaction.logs && transaction.logs[index] - ? transaction.logs[index].log - ? transaction.logs[index].log || transaction.logs[0] // failing txs show the first logs - : transaction.logs[0].log || '' - : JSON.parse(transaction.raw_log).message, - involvedAddresses: _.uniq(reducers.extractInvolvedAddresses(transaction)) - })) - return returnedMessages - } catch (error) { - Sentry.withScope(function(scope) { - scope.setExtra('transaction', transaction) - Sentry.captureException(error) - }) - return [] // must return something differ from undefined - } -} - -function transactionsReducerV2(txs, reducers, stakingDenom) { - const duplicateFreeTxs = uniqWith(txs, (a, b) => a.txhash === b.txhash) - const sortedTxs = sortBy(duplicateFreeTxs, ['timestamp']) - const reversedTxs = reverse(sortedTxs) - // here we filter out all transactions related to validators - return reversedTxs.reduce((collection, transaction) => { - return collection.concat( - transactionReducerV2(transaction, reducers, stakingDenom) - ) - }, []) -} - // to be able to catch all validators from a multi-claim reward tx, we need to capture // more than just the first value message. function txMultiClaimRewardReducer(txMessages) { @@ -651,177 +573,6 @@ function transactionReducer(transaction, reducers) { } } -// map Cosmos SDK message types to Lunie message types -function getMessageType(type) { - // different networks use different prefixes for the transaction types like cosmos/MsgSend vs core/MsgSend in Terra - const transactionTypeSuffix = type.split('/')[1] - switch (transactionTypeSuffix) { - case 'MsgSend': - return lunieMessageTypes.SEND - case 'MsgDelegate': - return lunieMessageTypes.STAKE - case 'MsgBeginRedelegate': - return lunieMessageTypes.RESTAKE - case 'MsgUndelegate': - return lunieMessageTypes.UNSTAKE - case 'MsgWithdrawDelegationReward': - return lunieMessageTypes.CLAIM_REWARDS - case 'MsgSubmitProposal': - return lunieMessageTypes.SUBMIT_PROPOSAL - case 'MsgVote': - return lunieMessageTypes.VOTE - case 'MsgDeposit': - return lunieMessageTypes.DEPOSIT - default: - return lunieMessageTypes.UNKNOWN - } -} - -// function to map cosmos messages to our details format -function transactionDetailsReducer( - type, - message, - reducers, - transaction, - stakingDenom -) { - let details - switch (type) { - case lunieMessageTypes.SEND: - details = sendDetailsReducer(message, reducers) - break - case lunieMessageTypes.STAKE: - details = stakeDetailsReducer(message, reducers) - break - case lunieMessageTypes.RESTAKE: - details = restakeDetailsReducer(message, reducers) - break - case lunieMessageTypes.UNSTAKE: - details = unstakeDetailsReducer(message, reducers) - break - case lunieMessageTypes.CLAIM_REWARDS: - details = claimRewardsDetailsReducer( - message, - reducers, - transaction, - stakingDenom - ) - break - case lunieMessageTypes.SUBMIT_PROPOSAL: - details = submitProposalDetailsReducer(message, reducers) - break - case lunieMessageTypes.VOTE: - details = voteProposalDetailsReducer(message, reducers) - break - case lunieMessageTypes.DEPOSIT: - details = depositDetailsReducer(message, reducers) - break - default: - details = {} - } - - return { - type, - ...details - } -} - -function claimRewardsMessagesAggregator(claimMessages) { - // reduce all withdraw messages to one one collecting the validators from all the messages - const onlyValidatorsAddressesArray = claimMessages.map( - msg => msg.value.validator_address - ) - return { - type: `type/MsgWithdrawDelegationReward`, - value: { - validators: onlyValidatorsAddressesArray - } - } -} - -function sendDetailsReducer(message, reducers) { - return { - from: [message.from_address], - to: [message.to_address], - amount: reducers.coinReducer(message.amount[0]) - } -} - -function stakeDetailsReducer(message, reducers) { - return { - to: [message.validator_address], - amount: reducers.coinReducer(message.amount) - } -} - -function restakeDetailsReducer(message, reducers) { - return { - from: [message.validator_src_address], - to: [message.validator_dst_address], - amount: reducers.coinReducer(message.amount) - } -} - -function unstakeDetailsReducer(message, reducers) { - return { - from: [message.validator_address], - amount: reducers.coinReducer(message.amount) - } -} - -function claimRewardsDetailsReducer( - message, - reducers, - transaction, - stakingDenom -) { - return { - from: message.validators, - amounts: claimRewardsAmountReducer(transaction, reducers, stakingDenom) - } -} - -function claimRewardsAmountReducer(transaction, reducers, stakingDenom) { - if (!transaction.success) { - return [ - { - amoun: 0, - denom: stakingDenom - } - ] - } - return reducers.rewardCoinReducer( - transaction.events - .find(event => event.type === `transfer`) - .attributes.find(attribute => attribute.key === `amount`).value, - stakingDenom - ) -} - -function submitProposalDetailsReducer(message, reducers) { - return { - proposalType: message.content.type, - proposalTitle: message.content.value.title, - proposalDescription: message.content.value.description, - initialDeposit: reducers.coinReducer(message.initial_deposit[0]) - } -} - -function voteProposalDetailsReducer(message) { - return { - proposalId: message.proposal_id, - voteOption: message.option - } -} - -// TO TEST! -function depositDetailsReducer(message, reducers) { - return { - proposalId: message.proposal_id, - amount: reducers.coinReducer(message.amount) - } -} - module.exports = { proposalReducer, governanceParameterReducer, @@ -841,8 +592,6 @@ module.exports = { calculateTokens, undelegationEndTimeReducer, formatTransactionsReducer, - transactionsReducerV2, - transactionReducerV2, atoms, proposalBeginTime, diff --git a/lib/reducers/cosmosV2-reducers.js b/lib/reducers/cosmosV2-reducers.js index 67f5dd6977..86cf620751 100644 --- a/lib/reducers/cosmosV2-reducers.js +++ b/lib/reducers/cosmosV2-reducers.js @@ -1,3 +1,6 @@ +const { reverse, sortBy, uniq, uniqWith } = require('lodash') +const Sentry = require('@sentry/node') +const { lunieMessageTypes } = require('../../lib/message-types') const cosmosV0Reducers = require('./cosmosV0-reducers') const { proposalBeginTime, @@ -5,9 +8,181 @@ const { getDeposit, tallyReducer, atoms, - getValidatorStatus + getValidatorStatus, + coinReducer } = cosmosV0Reducers +// map Cosmos SDK message types to Lunie message types +function getMessageType(type) { + // different networks use different prefixes for the transaction types like cosmos/MsgSend vs core/MsgSend in Terra + const transactionTypeSuffix = type.split('/')[1] + switch (transactionTypeSuffix) { + case 'MsgSend': + return lunieMessageTypes.SEND + case 'MsgDelegate': + return lunieMessageTypes.STAKE + case 'MsgBeginRedelegate': + return lunieMessageTypes.RESTAKE + case 'MsgUndelegate': + return lunieMessageTypes.UNSTAKE + case 'MsgWithdrawDelegationReward': + return lunieMessageTypes.CLAIM_REWARDS + case 'MsgSubmitProposal': + return lunieMessageTypes.SUBMIT_PROPOSAL + case 'MsgVote': + return lunieMessageTypes.VOTE + case 'MsgDeposit': + return lunieMessageTypes.DEPOSIT + default: + return lunieMessageTypes.UNKNOWN + } +} + +function sendDetailsReducer(message, reducers) { + return { + from: [message.from_address], + to: [message.to_address], + amount: reducers.coinReducer(message.amount[0]) + } +} + +function stakeDetailsReducer(message, reducers) { + return { + to: [message.validator_address], + amount: reducers.coinReducer(message.amount) + } +} + +function restakeDetailsReducer(message, reducers) { + return { + from: [message.validator_src_address], + to: [message.validator_dst_address], + amount: reducers.coinReducer(message.amount) + } +} + +function unstakeDetailsReducer(message, reducers) { + return { + from: [message.validator_address], + amount: reducers.coinReducer(message.amount) + } +} + +function claimRewardsDetailsReducer( + message, + reducers, + transaction, + stakingDenom +) { + return { + from: message.validators, + amounts: claimRewardsAmountReducer(transaction, reducers, stakingDenom) + } +} + +function claimRewardsAmountReducer(transaction, reducers, stakingDenom) { + if (!transaction.success) { + return [ + { + amoun: 0, + denom: stakingDenom + } + ] + } + return reducers.rewardCoinReducer( + transaction.events + .find(event => event.type === `transfer`) + .attributes.find(attribute => attribute.key === `amount`).value, + stakingDenom + ) +} + +function submitProposalDetailsReducer(message, reducers) { + return { + proposalType: message.content.type, + proposalTitle: message.content.value.title, + proposalDescription: message.content.value.description, + initialDeposit: reducers.coinReducer(message.initial_deposit[0]) + } +} + +function voteProposalDetailsReducer(message) { + return { + proposalId: message.proposal_id, + voteOption: message.option + } +} + +// TO TEST! +function depositDetailsReducer(message, reducers) { + return { + proposalId: message.proposal_id, + amount: reducers.coinReducer(message.amount) + } +} + +// function to map cosmos messages to our details format +function transactionDetailsReducer( + type, + message, + reducers, + transaction, + stakingDenom +) { + let details + switch (type) { + case lunieMessageTypes.SEND: + details = sendDetailsReducer(message, reducers) + break + case lunieMessageTypes.STAKE: + details = stakeDetailsReducer(message, reducers) + break + case lunieMessageTypes.RESTAKE: + details = restakeDetailsReducer(message, reducers) + break + case lunieMessageTypes.UNSTAKE: + details = unstakeDetailsReducer(message, reducers) + break + case lunieMessageTypes.CLAIM_REWARDS: + details = claimRewardsDetailsReducer( + message, + reducers, + transaction, + stakingDenom + ) + break + case lunieMessageTypes.SUBMIT_PROPOSAL: + details = submitProposalDetailsReducer(message, reducers) + break + case lunieMessageTypes.VOTE: + details = voteProposalDetailsReducer(message, reducers) + break + case lunieMessageTypes.DEPOSIT: + details = depositDetailsReducer(message, reducers) + break + default: + details = {} + } + + return { + type, + ...details + } +} + +function claimRewardsMessagesAggregator(claimMessages) { + // reduce all withdraw messages to one one collecting the validators from all the messages + const onlyValidatorsAddressesArray = claimMessages.map( + msg => msg.value.validator_address + ) + return { + type: `type/MsgWithdrawDelegationReward`, + value: { + validators: onlyValidatorsAddressesArray + } + } +} + function proposalReducer( networkId, proposal, @@ -31,6 +206,89 @@ function proposalReducer( } } +function transactionReducerV2(transaction, reducers, stakingDenom) { + try { + // TODO check if this is anywhere not an array + let fees + if (Array.isArray(transaction.tx.value.fee.amount)) { + fees = transaction.tx.value.fee.amount.map(coinReducer) + } else { + fees = [coinReducer(transaction.tx.value.fee.amount)] + } + // We do display only the transactions we support in Lunie + const filteredMessages = transaction.tx.value.msg.filter( + ({ type }) => getMessageType(type) !== 'Unknown' + ) + const { claimMessages, otherMessages } = filteredMessages.reduce( + ({ claimMessages, otherMessages }, message) => { + // we need to aggregate all withdraws as we display them together in one transaction + if (getMessageType(message.type) === lunieMessageTypes.CLAIM_REWARDS) { + claimMessages.push(message) + } else { + otherMessages.push(message) + } + return { claimMessages, otherMessages } + }, + { claimMessages: [], otherMessages: [] } + ) + + // we need to aggregate claim rewards messages in one single one to avoid transaction repetition + const claimMessage = + claimMessages.length > 0 + ? claimRewardsMessagesAggregator(claimMessages) + : undefined + const allMessages = claimMessage + ? otherMessages.concat(claimMessage) // add aggregated claim message + : otherMessages + const returnedMessages = allMessages.map(({ value, type }, index) => ({ + type: getMessageType(type), + hash: transaction.txhash, + key: `${transaction.txhash}_${index}`, + height: transaction.height, + details: transactionDetailsReducer( + getMessageType(type), + value, + reducers, + transaction, + stakingDenom + ), + timestamp: transaction.timestamp, + memo: transaction.tx.value.memo, + fees, + success: + transaction.logs && transaction.logs[index] + ? transaction.logs[index].success || false + : false, + log: + transaction.logs && transaction.logs[index] + ? transaction.logs[index].log + ? transaction.logs[index].log || transaction.logs[0] // failing txs show the first logs + : transaction.logs[0].log || '' + : JSON.parse(transaction.raw_log).message, + involvedAddresses: uniq(reducers.extractInvolvedAddresses(transaction)) + })) + return returnedMessages + } catch (error) { + Sentry.withScope(function(scope) { + scope.setExtra('transaction', transaction) + Sentry.captureException(error) + }) + return [] // must return something differ from undefined + } +} + +function transactionsReducerV2(txs, reducers, stakingDenom) { + const duplicateFreeTxs = uniqWith(txs, (a, b) => a.txhash === b.txhash) + const sortedTxs = sortBy(duplicateFreeTxs, ['timestamp']) + const reversedTxs = reverse(sortedTxs) + // here we filter out all transactions related to validators + return reversedTxs.reduce((collection, transaction) => { + return collection.concat( + transactionReducerV2(transaction, reducers, stakingDenom) + ) + }, []) +} + function delegationReducer(delegation, validator) { return { validatorAddress: delegation.validator_address, @@ -127,10 +385,14 @@ function undelegationEndTimeReducer(transaction) { } module.exports = { - ...cosmosV0Reducers, + transactionsReducerV2, + transactionReducerV2, proposalReducer, delegationReducer, validatorReducer, undelegationEndTimeReducer, - extractInvolvedAddresses + extractInvolvedAddresses, + + // CosmosV0 Reducers + ...cosmosV0Reducers }