diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index dd84ea7824..7df37298c2 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -8,31 +8,31 @@ assignees: '' --- **Describe the bug** -A clear and concise description of what the bug is. + **To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error + + + + + **Expected behavior** -A clear and concise description of what you expected to happen. + **Screenshots** -If applicable, add screenshots to help explain your problem. + **Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] + - OS: + - Browser: + - Version: **Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] + - Device: + - OS: + - Browser: + - Version: **Additional context** -Add any other context about the problem here. + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index bbcbbe7d61..a6f653e0b8 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -8,13 +8,13 @@ assignees: '' --- **Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + **Describe the solution you'd like** -A clear and concise description of what you want to happen. + **Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. + **Additional context** -Add any other context or screenshots about the feature request here. + diff --git a/data/network-capabilities.js b/data/network-capabilities.js index 1d29768056..e874191806 100644 --- a/data/network-capabilities.js +++ b/data/network-capabilities.js @@ -117,6 +117,23 @@ const getNetworkCapabilities = { action_deposit: false, action_vote: false, action_proposal: false + }, + "akash-testnet": { + feature_session: true, + feature_explore: true, + feature_portfolio: true, + feature_validators: true, + feature_proposals: false, + feature_activity: false, + feature_explorer: false, + action_send: false, + action_claim_rewards: false, + action_delegate: false, + action_redelegate: false, + action_undelegate: false, + action_deposit: false, + action_vote: false, + action_proposal: false } } diff --git a/data/network-fees.js b/data/network-fees.js new file mode 100644 index 0000000000..38254617c2 --- /dev/null +++ b/data/network-fees.js @@ -0,0 +1,43 @@ +const { UserInputError } = require('apollo-server') + +const transactionTypesSet = new Set(['SendTx', 'StakeTx', 'UnstakeTx', 'RestakeTx', 'ClaimRewardsTx', 'SubmitProposalTx', 'VoteTx', 'DepositTx', 'UnknownTx']) + +const getNetworkTransactionGasEstimates = (networkId, transactionType) => { + if (transactionType && !transactionTypesSet.has(transactionType)) { + throw new UserInputError(`Unrecognized transaction type. Valid transaction types are 'SendTx', 'StakeTx', 'UnstakeTx', 'RestakeTx', 'ClaimRewardsTx', 'SubmitProposalTx', 'VoteTx', 'DepositTx' and 'UnknownTx'`) + } + const networkGasEstimates = networkGasEstimatesDictionary[networkId] + if (!networkGasEstimates) { + throw new UserInputError(`Unrecognized network. Currently only Cosmos, Terra and e-Money do have a network fees set`) + } + return transactionType && networkGasEstimates[`${transactionType}`] ? networkGasEstimates[`${transactionType}`] : networkGasEstimates.default +} + +const terraGasEstimates = { + default: 300000 +} + +const cosmosGasEstimates = { + default: 550000 +} + +const emoneyGasEstimates = { + default: 200000, + ClaimRewardsTx: 550000 +} + +const akashGasEstimates = { + default: 200000 +} + +const networkGasEstimatesDictionary = { + "cosmos-hub-mainnet": cosmosGasEstimates, + "cosmos-hub-testnet": cosmosGasEstimates, + "terra-mainnet": terraGasEstimates, + "terra-testnet": terraGasEstimates, + "emoney-mainnet": emoneyGasEstimates, + "emoney-testnet": emoneyGasEstimates, + "akash-testnet": akashGasEstimates +} + +module.exports = {getNetworkTransactionGasEstimates} \ No newline at end of file diff --git a/data/networks.js b/data/networks.js index 260803c280..040b9f5c8e 100644 --- a/data/networks.js +++ b/data/networks.js @@ -168,7 +168,28 @@ module.exports = [ enabled: false, icon: 'https://app.lunie.io/img/networks/polkadot-testnet.png', slug: 'kusama' - } + }, + { + id: 'akash-testnet', + title: 'Akash Testnet', + chain_id: 'devnet', + api_url: 'http://95.179.133.80:8080', + rpc_url: 'wss://95.179.133.80:26657/websocket', + bech32_prefix: 'akash', + address_prefix: 'akash', + address_creator: 'cosmos', + ledger_app: 'cosmos', + network_type: 'cosmos', + source_class_name: 'source/akashV0-source', + block_listener_class_name: 'block-listeners/cosmos-node-subscription', + testnet: true, + ...getNetworkCapabilities[`akash-testnet`], + default: false, + stakingDenom: 'STAKE', + enabled: false, + icon: 'https://app.lunie.io/img/networks/akash-testnet.png', + slug: 'akash-testnet' + }, // { // id: 'livepeer-mainnet', // title: 'Livepeer', 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/message-types.js b/lib/message-types.js index 05f16e52e7..0716a7f6a2 100644 --- a/lib/message-types.js +++ b/lib/message-types.js @@ -1,32 +1,3 @@ -const cosmosMessageType = { - SEND: 'cosmos-sdk/MsgSend', - CREATE_VALIDATOR: 'cosmos-sdk/MsgCreateValidator', - EDIT_VALIDATOR: 'cosmos-sdk/MsgEditValidator', - DELEGATE: 'cosmos-sdk/MsgDelegate', - UNDELEGATE: 'cosmos-sdk/MsgUndelegate', - BEGIN_REDELEGATE: 'cosmos-sdk/MsgBeginRedelegate', - UNJAIL: 'cosmos-sdk/MsgUnjail', - SUBMIT_PROPOSAL: 'cosmos-sdk/MsgSubmitProposal', - DEPOSIT: 'cosmos-sdk/MsgDeposit', - VOTE: 'cosmos-sdk/MsgVote', - SET_WITHDRAW_ADDRESS: 'cosmos-sdk/MsgSetWithdrawAddress', - WITHDRAW_DELEGATION_REWARD: 'cosmos-sdk/MsgWithdrawDelegationReward', - WITHDRAW_VALIDATOR_COMMISSION: 'cosmos-sdk/MsgWithdrawValidatorCommission', - MULTI_SEND: 'cosmos-sdk/MsgMultiSend' -} - -const cosmosWhitelistedMessageTypes = new Set([ - `MsgSend`, - `MsgDelegate`, - `MsgBeginRedelegate`, - `MsgUndelegate`, - `MsgVote`, - `MsgDeposit`, - `MsgWithdrawDelegationReward`, - `MsgSubmitProposal`, - `MsgMultiSend` -]) - const lunieMessageTypes = { SEND: `SendTx`, STAKE: `StakeTx`, @@ -40,7 +11,5 @@ const lunieMessageTypes = { } module.exports = { - cosmosMessageType, - cosmosWhitelistedMessageTypes, lunieMessageTypes } diff --git a/lib/reducers/akashV0-reducers.js b/lib/reducers/akashV0-reducers.js new file mode 100644 index 0000000000..272ea87343 --- /dev/null +++ b/lib/reducers/akashV0-reducers.js @@ -0,0 +1,29 @@ +const terraV3Reducers = require('./terraV3-reducers') + +function blockReducer(networkId, block, transactions) { + return { + networkId, + height: block.block.header.height, + chainId: block.block.header.chain_id, + hash: block.block_id.hash, + time: block.block.header.time, + transactions, + proposer_address: block.block.header.proposer_address + } +} + +function delegationReducer(delegation, validator) { + const delegationCoin = terraV3Reducers.coinReducer(delegation.balance) + return { + validatorAddress: delegation.validator_address, + delegatorAddress: delegation.delegator_address, + validator, + amount: delegationCoin.amount + } +} + +module.exports = { + ...terraV3Reducers, + blockReducer, + delegationReducer +} diff --git a/lib/reducers/cosmosV0-reducers.js b/lib/reducers/cosmosV0-reducers.js index 238a10ca3d..93fd9d025f 100644 --- a/lib/reducers/cosmosV0-reducers.js +++ b/lib/reducers/cosmosV0-reducers.js @@ -1,12 +1,10 @@ -const { uniqWith, sortBy, reverse } = require('lodash') -const { cosmosMessageType } = require('../message-types') -const { - cosmosWhitelistedMessageTypes, - lunieMessageTypes -} = require('../../lib/message-types') +const { flatten } = require('lodash') 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()) { @@ -223,7 +221,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 +383,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 => @@ -445,56 +447,6 @@ async function totalStakeFiatValueReducer( ) } -function getGroupByType(transactionType) { - const transactionGroup = { - [cosmosMessageType.SEND]: 'banking', - [cosmosMessageType.MULTI_SEND]: 'banking', - [cosmosMessageType.CREATE_VALIDATOR]: 'staking', - [cosmosMessageType.EDIT_VALIDATOR]: 'staking', - [cosmosMessageType.DELEGATE]: 'staking', - [cosmosMessageType.UNDELEGATE]: 'staking', - [cosmosMessageType.BEGIN_REDELEGATE]: 'staking', - [cosmosMessageType.UNJAIL]: 'staking', - [cosmosMessageType.SUBMIT_PROPOSAL]: 'governance', - [cosmosMessageType.DEPOSIT]: 'governance', - [cosmosMessageType.VOTE]: 'governance', - [cosmosMessageType.SET_WITHDRAW_ADDRESS]: 'distribution', - [cosmosMessageType.WITHDRAW_DELEGATION_REWARD]: 'distribution', - [cosmosMessageType.WITHDRAW_VALIDATOR_COMMISSION]: 'distribution' - } - - return transactionGroup[transactionType] || 'unknown' -} - -function undelegationEndTimeReducer(transaction) { - if (transaction.tags) { - if (transaction.tags.find(tx => tx.key === `end-time`)) { - return transaction.tags.filter(tx => tx.key === `end-time`)[0].value - } - } else { - return null - } -} - -function formatTransactionsReducer(txs, reducers) { - 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 - let filteredMsgs = [] - reversedTxs.forEach(transaction => { - transaction.tx.value.msg.forEach(msg => { - // only push transactions messages supported by Lunie - if (cosmosWhitelistedMessageTypes.has(msg.type.split('/')[1])) { - filteredMsgs.push(msg) - } - }) - transaction.tx.value.msg = filteredMsgs - filteredMsgs = [] - }) - return reversedTxs.map(tx => transactionReducer(tx, reducers)) -} - function extractInvolvedAddresses(transaction) { // If the transaction has failed, it doesn't get tagged if (!Array.isArray(transaction.tags)) return [] @@ -512,316 +464,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) { - const filteredMessages = txMessages.filter( - msg => msg.type.split('/')[1] === `MsgWithdrawDelegationReward` - ) - return filteredMessages.length > 0 ? filteredMessages : null -} - -function transactionReducer(transaction, reducers) { - try { - let fee = coinReducer(false) - if (Array.isArray(transaction.tx.value.fee.amount)) { - fee = coinReducer(transaction.tx.value.fee.amount[0]) - } else { - fee = coinReducer(transaction.tx.value.fee.amount) - } - - const result = { - type: transaction.tx.value.msg[0].type, - group: getGroupByType(transaction.tx.value.msg[0].type), - hash: transaction.txhash, - height: Number(transaction.height), - timestamp: transaction.timestamp, - gasUsed: transaction.gas_used, - gasWanted: transaction.gas_wanted, - success: transaction.logs ? transaction.logs[0].success : false, - log: transaction.logs - ? transaction.logs[0].log - : JSON.parse(transaction.raw_log).message, - memo: transaction.tx.value.memo, - fee, - signature: transaction.tx.value.signatures[0].signature, - value: JSON.stringify(transaction.tx.value.msg[0].value), - raw: transaction, - withdrawValidators: JSON.stringify( - txMultiClaimRewardReducer(transaction.tx.value.msg) - ), - undelegationEndTime: reducers.undelegationEndTimeReducer(transaction) - } - - return result - } catch (err) { - Sentry.withScope(function(scope) { - scope.setExtra('transaction', { - ...transaction, - raw: JSON.stringify(transaction) - }) - Sentry.captureException(err) - }) - return { - raw: transaction - } - } -} - -// 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, @@ -833,16 +475,11 @@ module.exports = { gasPriceReducer, rewardCoinReducer, balanceReducer, - transactionReducer, undelegationReducer, rewardReducer, overviewReducer, accountInfoReducer, calculateTokens, - undelegationEndTimeReducer, - formatTransactionsReducer, - transactionsReducerV2, - transactionReducerV2, atoms, proposalBeginTime, @@ -851,7 +488,6 @@ module.exports = { getTotalVotePercentage, getValidatorStatus, expectedRewardsPerToken, - getGroupByType, denomLookup, extractInvolvedAddresses } diff --git a/lib/reducers/cosmosV2-reducers.js b/lib/reducers/cosmosV2-reducers.js index 67f5dd6977..9e82a34e96 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,184 @@ 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) { + const isTxSucceded = + transaction.success || + transaction.logs.find(({ success }) => success === true) + if (!isTxSucceded) { + return [ + { + amount: 0, + denom: reducers.denomLookup(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 +209,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,7 +388,10 @@ function undelegationEndTimeReducer(transaction) { } module.exports = { + // CosmosV0 Reducers ...cosmosV0Reducers, + transactionsReducerV2, + transactionReducerV2, proposalReducer, delegationReducer, validatorReducer, diff --git a/lib/resolvers.js b/lib/resolvers.js index 37ddb8da12..c01af5a4eb 100644 --- a/lib/resolvers.js +++ b/lib/resolvers.js @@ -9,6 +9,7 @@ const { encodeB32, decodeB32 } = require('./tools') const { UserInputError, withFilter } = require('apollo-server') const { formatBech32Reducer } = require('./reducers/livepeerV0-reducers') const { networkList, networkMap } = require('./networks') +const { getNetworkTransactionGasEstimates } = require('../data/network-fees') const database = require('./database') const config = require('../config.js') const { logOverview } = require('./statistics') @@ -353,13 +354,19 @@ const resolvers = { logOverview(overview, fingerprint) return overview }, - transactions: (_, { networkId, address, pageNumber }, { dataSources }) => - remoteFetch(dataSources, networkId).getTransactions(address, pageNumber), transactionsV2: (_, { networkId, address, pageNumber }, { dataSources }) => remoteFetch(dataSources, networkId).getTransactionsV2( address, pageNumber ), + networkFees: (_, { networkId, transactionType }) => { + return { + gasEstimate: getNetworkTransactionGasEstimates( + networkId, + transactionType + ) + } + }, estimate: () => { try { const gasEstimate = 550000 diff --git a/lib/schema.js b/lib/schema.js index 0fa167f1ef..cab6db7d49 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -310,6 +310,10 @@ const typeDefs = gql` event(networkId: String!, eventType: String!, ressourceId: String): Event } + type NetworkFees { + gasEstimate: Int! + } + type EstimateResult { gasEstimate: Int error: String @@ -363,16 +367,12 @@ const typeDefs = gql` operatorAddress: String fiatCurrency: String ): [Reward] - transactions( - networkId: String! - address: String! - pageNumber: Int - ): [Transaction] transactionsV2( networkId: String! address: String! pageNumber: Int ): [TransactionV2] + networkFees(networkId: String!, transactionType: String): NetworkFees estimate: EstimateResult! } ` diff --git a/lib/source/akashV0-source.js b/lib/source/akashV0-source.js new file mode 100644 index 0000000000..9b8a771fa0 --- /dev/null +++ b/lib/source/akashV0-source.js @@ -0,0 +1,48 @@ +const TerraV3API = require('./terraV3-source') +const CosmosV0API = require('./cosmosV0-source') + +const gasPrices = [ + { + denom: 'stake', + price: '0.01' + }, + { + denom: 'akash', + price: '0.01' + } +] + +class AkashV0API extends TerraV3API { + setReducers() { + this.reducers = require('../reducers/akashV0-reducers') + this.gasPrices = gasPrices + } + + async getBlockByHeightV2(blockHeight) { + let block, transactions + if (blockHeight) { + const response = await Promise.all([ + this.getRetry(`blocks/${blockHeight}`), + this.getTransactionsV2ByHeight(blockHeight) + ]) + block = response[0] + transactions = response[1] + } else { + block = await this.getRetry(`blocks/latest`) + transactions = await this.getTransactionsV2ByHeight( + block.block.header.height + ) + } + return this.reducers.blockReducer(this.networkId, block, transactions) + } + + async getAllValidators(height) { + return CosmosV0API.prototype.getAllValidators.call(this, height) + } + + async getExpectedReturns(validator) { + return CosmosV0API.prototype.getExpectedReturns.call(this, validator) + } +} + +module.exports = AkashV0API diff --git a/lib/source/cosmosV0-source.js b/lib/source/cosmosV0-source.js index eac4571459..8102aa36d6 100644 --- a/lib/source/cosmosV0-source.js +++ b/lib/source/cosmosV0-source.js @@ -511,23 +511,6 @@ class CosmosV0API extends RESTDataSource { ) } - async loadPaginatedTxs(url, page = 1, totalAmount = 0) { - const pagination = `&limit=1000000000&page=${page}` - let allTxs = [] - - const { txs, total_count } = await this.getRetry(`${url}${pagination}`) - allTxs = allTxs.concat(txs) - - // there is a bug in page_number in gaia-13007 so we can't use is - if (allTxs.length + totalAmount < Number(total_count)) { - return allTxs.concat( - await this.loadPaginatedTxs(url, page + 1, totalAmount + allTxs.length) - ) - } - - return allTxs - } - async getTransactions(address) { this.checkAddress(address) @@ -551,6 +534,23 @@ class CosmosV0API extends RESTDataSource { ]).then(transactionGroups => [].concat(...transactionGroups)) return this.reducers.formatTransactionsReducer(txs, this.reducers) } + + async loadPaginatedTxs(url, page = 1, totalAmount = 0) { + const pagination = `&limit=1000000000&page=${page}` + let allTxs = [] + + const { txs, total_count } = await this.getRetry(`${url}${pagination}`) + allTxs = allTxs.concat(txs) + + // there is a bug in page_number in gaia-13007 so we can't use is + if (allTxs.length + totalAmount < Number(total_count)) { + return allTxs.concat( + await this.loadPaginatedTxs(url, page + 1, totalAmount + allTxs.length) + ) + } + + return allTxs + } } module.exports = CosmosV0API