@exodus/ethereum-api 2.24.3 → 2.24.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "2.24.3",
3
+ "version": "2.24.5",
4
4
  "description": "Ethereum Api",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -16,7 +16,7 @@
16
16
  "dependencies": {
17
17
  "@exodus/asset-lib": "^3.5.4",
18
18
  "@exodus/crypto": "^1.0.0-rc.0",
19
- "@exodus/ethereum-lib": "^2.21.2",
19
+ "@exodus/ethereum-lib": "^2.21.4",
20
20
  "@exodus/ethereumjs-util": "^7.1.0-exodus.6",
21
21
  "@exodus/fetch": "^1.2.1",
22
22
  "@exodus/simple-retry": "^0.0.6",
@@ -34,5 +34,5 @@
34
34
  "devDependencies": {
35
35
  "@exodus/models": "^8.7.2"
36
36
  },
37
- "gitHead": "0c9784a5eba8cec14a1907a77d5343c2eefa4f64"
37
+ "gitHead": "2ad238fc652010aa759fcee58c6bbad707ec940d"
38
38
  }
@@ -1,5 +1,7 @@
1
1
  /* @flow */
2
2
  import { getServer } from '@exodus/ethereum-api'
3
+ import { isRpcBalanceAsset, getAssetAddresses } from '@exodus/ethereum-lib'
4
+
3
5
  import {
4
6
  type PendingTransactionsDictionary,
5
7
  checkPendingTransactions,
@@ -7,9 +9,7 @@ import {
7
9
  getDeriveDataNeededForTick,
8
10
  getDeriveTransactionsToCheck,
9
11
  getHistoryFromServer,
10
- isRpcBalanceAsset,
11
- getAssetAddresses,
12
- } from '@exodus/ethereum-lib'
12
+ } from './monitor-utils'
13
13
 
14
14
  import {
15
15
  enableWSUpdates,
@@ -0,0 +1,39 @@
1
+ // @flow
2
+ import getSenderNonceKey from './get-sender-nonce-key'
3
+ import getFeeAmount from './get-fee-amount'
4
+ import type { PendingTransactionsDictionary } from './types'
5
+
6
+ export default function checkPendingTransactions({
7
+ pendingTransactionsGroupedByAddressAndNonce = {},
8
+ pendingTransactionsToCheck = {},
9
+ txlist,
10
+ ourWalletAddress,
11
+ asset,
12
+ }: {
13
+ pendingTransactionsGroupedByAddressAndNonce: PendingTransactionsDictionary,
14
+ pendingTransactionsToCheck: PendingTransactionsDictionary,
15
+ txlist: Array<Object>,
16
+ ourWalletAddress: string,
17
+ asset: Object,
18
+ }): PendingTransactionsDictionary {
19
+ for (const tx of txlist) {
20
+ delete pendingTransactionsToCheck[tx.txId || tx.hash] // Found this transaction, do not mark it as dropped
21
+
22
+ // check if this TX replaces another pending TX
23
+ const replacedTx =
24
+ pendingTransactionsGroupedByAddressAndNonce[getSenderNonceKey(tx, ourWalletAddress)]
25
+ if (replacedTx && tx.hash !== replacedTx.tx.txId) {
26
+ // if a pending TX with lower fees comes from the server, don't add it
27
+ if (
28
+ replacedTx.tx.sent &&
29
+ replacedTx.tx.feeAmount.gt(getFeeAmount(asset, tx)) &&
30
+ tx.confirmations === 0
31
+ ) {
32
+ continue
33
+ }
34
+ replacedTx.replaced = true
35
+ }
36
+ }
37
+
38
+ return { pendingTransactionsGroupedByAddressAndNonce, pendingTransactionsToCheck }
39
+ }
@@ -0,0 +1,32 @@
1
+ import getLogItemsFromServerTx from './get-log-items-from-server-tx'
2
+
3
+ // Iterates through an array of server transactions and formats them into log item
4
+ // objects (in Tx class fields format). It returns an object of arrays of log
5
+ // items, keyed by asset name.
6
+
7
+ export default function getAllLogItemsByAsset({
8
+ allTransactionsFromServer,
9
+ ourWalletAddress,
10
+ asset,
11
+ tokensByAddress,
12
+ assets,
13
+ }) {
14
+ const allAssets = [asset, ...tokensByAddress.values()]
15
+ const logItemsByAsset = Object.fromEntries(allAssets.map((asset) => [asset.name, []]))
16
+
17
+ allTransactionsFromServer.forEach((serverTx) => {
18
+ const logItemsByAssetForServerTx = getLogItemsFromServerTx({
19
+ serverTx,
20
+ ourWalletAddress,
21
+ tokensByAddress,
22
+ asset,
23
+ assets,
24
+ })
25
+
26
+ Object.entries(logItemsByAssetForServerTx).forEach(([assetName, logItem]) => {
27
+ logItemsByAsset[assetName].push(logItem)
28
+ })
29
+ })
30
+
31
+ return logItemsByAsset
32
+ }
@@ -0,0 +1,14 @@
1
+ // A super-selector that returns all the current data needed for a tick of the ETH monitor.
2
+
3
+ export default function getDeriveDataNeededForTick(aci) {
4
+ return async function({ assetName, walletAccount }) {
5
+ const receiveAddress = await aci.getReceiveAddress({ assetName, walletAccount })
6
+ const currentAccountState = await aci.getAccountState({ assetName, walletAccount })
7
+ const minimumConfirmations = await aci.getConfirmationsNumber({ assetName })
8
+ return {
9
+ ourWalletAddress: receiveAddress.toLowerCase(),
10
+ currentAccountState,
11
+ minimumConfirmations,
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,51 @@
1
+ import ms from 'ms'
2
+ import getSenderNonceKey from './get-sender-nonce-key'
3
+
4
+ const UNCONFIRMED_TX_LIMIT = ms('5m')
5
+
6
+ const mapToObject = (map) => Object.fromEntries([...map.entries()]) // only for string keys
7
+
8
+ export default function getDeriveTransactionsToCheck({ getTxLog }) {
9
+ return async ({ assetName: _assetName, walletAccount, tokens, ourWalletAddress }) => {
10
+ const pendingTransactionsToCheck = new Map()
11
+ const pendingTransactionsGroupedByAddressAndNonce = new Map()
12
+ const simulatedTransactions = new Map()
13
+ const now = Date.now()
14
+
15
+ for (const assetName of [_assetName, ...tokens.map(({ name }) => name)]) {
16
+ const txSet = await getTxLog({ assetName, walletAccount })
17
+ for (const tx of txSet) {
18
+ // ERC20 sends have an entry in ETH and one in the ERC20 log so we just want the ETH one
19
+ if (
20
+ !pendingTransactionsToCheck.has(tx.txId) &&
21
+ tx.pending &&
22
+ now - tx.date.getTime() > UNCONFIRMED_TX_LIMIT
23
+ ) {
24
+ pendingTransactionsToCheck.set(tx.txId, { tx, assetName })
25
+ }
26
+ if (tx.meta.simulated) simulatedTransactions.set(tx.txId, tx)
27
+
28
+ const senderNonceKey = getSenderNonceKey(tx, ourWalletAddress)
29
+ if (
30
+ tx.pending &&
31
+ !pendingTransactionsGroupedByAddressAndNonce.has(senderNonceKey) &&
32
+ !tx.dropped
33
+ ) {
34
+ pendingTransactionsGroupedByAddressAndNonce.set(senderNonceKey, {
35
+ tx,
36
+ replaced: false,
37
+ assetName,
38
+ })
39
+ }
40
+ }
41
+ }
42
+
43
+ return {
44
+ pendingTransactionsToCheck: mapToObject(pendingTransactionsToCheck),
45
+ pendingTransactionsGroupedByAddressAndNonce: mapToObject(
46
+ pendingTransactionsGroupedByAddressAndNonce
47
+ ),
48
+ simulatedTransactions: mapToObject(simulatedTransactions),
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,14 @@
1
+ // Returns the amount of ethereum used for a transaction
2
+ // from the server, as a number unit.
3
+
4
+ export default function getFeeAmount(asset, serverTx) {
5
+ const { gas: gasLimit, gasUsed, gasPrice } = serverTx
6
+
7
+ // genesis, coinbase, uncles
8
+ if (!gasPrice) return asset.currency.ZERO
9
+
10
+ return asset.currency
11
+ .baseUnit(gasUsed || gasLimit)
12
+ .mul(gasPrice)
13
+ .toDefault()
14
+ }
@@ -0,0 +1,43 @@
1
+ import isConfirmedServerTx from './is-confirmed-server-tx'
2
+
3
+ // Fetches JSON transaction history from the given Ethereum server
4
+ // object (from @exodus/ethereum-api). Starts fetching from the given
5
+ // block cursor and returns the latest cursor and all transactions
6
+ // fetched in the process. minimumConfirmations determines how many
7
+ // confirmations a transaction must have for the cursor to be moved
8
+ // to that transaction's block.
9
+
10
+ export default async function getHistoryFromServer({
11
+ server,
12
+ ourWalletAddress,
13
+ index,
14
+ minimumConfirmations,
15
+ limit = 1000,
16
+ }) {
17
+ if (limit === 0) return []
18
+ const allTransactionsFromServer = []
19
+
20
+ let isEndOfAddressHistory = false
21
+ let newIndex = 0
22
+ while (true) {
23
+ const transactions = await server.getHistoryV2(ourWalletAddress, {
24
+ index,
25
+ limit, // the server returns the number of TXs that's less or equal to the limit
26
+ })
27
+
28
+ if (transactions.length === 0) isEndOfAddressHistory = true
29
+ for (const transaction of transactions) {
30
+ allTransactionsFromServer.push(transaction)
31
+ index = transaction.addressIndex + 1 // this index is used to fetch a next page of TXs
32
+ if (transaction.confirmations >= minimumConfirmations) newIndex = transaction.addressIndex + 1 // this index is used to return from the function
33
+ if (!isConfirmedServerTx(transaction)) isEndOfAddressHistory = true
34
+ }
35
+
36
+ if (isEndOfAddressHistory) {
37
+ return {
38
+ allTransactionsFromServer,
39
+ index: newIndex,
40
+ }
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,141 @@
1
+ import getValueOfTransfers from './get-value-of-transfers'
2
+ import getTransfersByTokenName from './get-transfers-by-token-name'
3
+ import getNamesOfTokensTransferredByServerTx from './get-names-of-tokens-transferred-by-server-tx'
4
+ import getFeeAmount from './get-fee-amount'
5
+ import isConsideredSentTokenTx from './is-considered-sent-token-tx'
6
+ import isConfirmedServerTx from './is-confirmed-server-tx'
7
+ import lodash from 'lodash'
8
+
9
+ // This function takes a server transaction object fetched from magnifier,
10
+ // and transforms it into Tx models to update the exodus state.
11
+
12
+ export default function getLogItemsFromServerTx({
13
+ serverTx,
14
+ asset,
15
+ ourWalletAddress,
16
+ tokensByAddress,
17
+ assets,
18
+ }) {
19
+ const confirmations = isConfirmedServerTx(serverTx) ? 1 : 0
20
+ const date = parseServerTxDate(serverTx.timestamp) // included even for unconfirmed txs
21
+ const txId = serverTx.hash
22
+ const nonce = parseInt(serverTx.nonce, 16)
23
+ const gasLimit = parseInt(serverTx.gas, 16)
24
+ const error = serverTx.error || (serverTx.status === false ? 'Failed' : null)
25
+ const feeAmount = getFeeAmount(asset, serverTx)
26
+ const ethereumTransfers = [serverTx, ...(serverTx.internal || [])]
27
+ const tokenTransfersByTokenName = getTransfersByTokenName(serverTx.erc20 || [], tokensByAddress)
28
+ const toAddress = tryFindExternalRecipient(ethereumTransfers, ourWalletAddress)
29
+ const ourWalletWasSender = serverTx.from === ourWalletAddress
30
+
31
+ const logItemCommonProperties = {
32
+ confirmations,
33
+ date,
34
+ error,
35
+ feeAmount: ourWalletWasSender ? feeAmount : undefined,
36
+ txId,
37
+ dropped: false,
38
+ }
39
+
40
+ const logItemsForServerTxEntries = []
41
+
42
+ {
43
+ const sendingTransferPresent = ethereumTransfers.some(({ from }) => from === ourWalletAddress)
44
+ const receivingTransferPresent = ethereumTransfers.some(({ to }) => to === ourWalletAddress)
45
+
46
+ if (sendingTransferPresent || receivingTransferPresent) {
47
+ const coinAmount = getValueOfTransfers(ourWalletAddress, asset, ethereumTransfers)
48
+ const selfSend = isSelfSendTx({
49
+ coinAmount,
50
+ ourWalletWasSender,
51
+ sendingTransferPresent,
52
+ receivingTransferPresent,
53
+ })
54
+ logItemsForServerTxEntries.push([
55
+ asset.name,
56
+ {
57
+ ...logItemCommonProperties,
58
+ coinAmount,
59
+ coinName: asset.name,
60
+ data: {
61
+ data: serverTx.data || '0x',
62
+ nonce,
63
+ gasLimit,
64
+ },
65
+ from: ourWalletWasSender ? [] : [serverTx.from],
66
+ to: ourWalletWasSender ? toAddress : undefined,
67
+ selfSend,
68
+ tokens: getNamesOfTokensTransferredByServerTx({
69
+ asset,
70
+ tokensByAddress,
71
+ serverTx,
72
+ ourWalletAddress,
73
+ }),
74
+ },
75
+ ])
76
+ }
77
+ }
78
+ // handle erc20
79
+ Object.entries(tokenTransfersByTokenName).forEach(([tokenName, tokenTransfers]) => {
80
+ const sendingTransferPresent = tokenTransfers.some(({ from }) => from === ourWalletAddress)
81
+ const receivingTransferPresent = tokenTransfers.some(({ to }) => to === ourWalletAddress)
82
+ if (!sendingTransferPresent && !receivingTransferPresent) return
83
+
84
+ const token = assets[tokenName]
85
+ const tokenTransferToAddress = tryFindExternalRecipient(tokenTransfers, ourWalletAddress)
86
+ const coinAmount = getValueOfTransfers(
87
+ ourWalletAddress,
88
+ token,
89
+ lodash.filter(tokenTransfers, { events: confirmations > 0 })
90
+ )
91
+ const tokenFromAddresses = lodash.uniq(
92
+ tokenTransfers.filter(({ to }) => to === ourWalletAddress).map(({ from }) => from)
93
+ )
94
+
95
+ const isConsideredSent = isConsideredSentTokenTx({
96
+ coinAmount,
97
+ ourWalletWasSender,
98
+ sendingTransferPresent,
99
+ receivingTransferPresent,
100
+ })
101
+ const selfSend = isSelfSendTx({
102
+ coinAmount,
103
+ ourWalletWasSender,
104
+ sendingTransferPresent,
105
+ receivingTransferPresent,
106
+ })
107
+
108
+ logItemsForServerTxEntries.push([
109
+ tokenName,
110
+ {
111
+ ...logItemCommonProperties,
112
+ coinAmount,
113
+ coinName: tokenName,
114
+ data: { nonce, gasLimit },
115
+ from: isConsideredSent ? [] : tokenFromAddresses,
116
+ to: isConsideredSent ? tokenTransferToAddress : undefined,
117
+ selfSend,
118
+ },
119
+ ])
120
+ })
121
+
122
+ return Object.fromEntries(logItemsForServerTxEntries)
123
+ }
124
+
125
+ const tryFindExternalRecipient = (transfers, ourWalletAddress) =>
126
+ transfers.find(({ to }) => to !== ourWalletAddress)?.to || ourWalletAddress
127
+
128
+ // If the timestamp is in the future, use the current time.
129
+ const parseServerTxDate = (timestamp) =>
130
+ new Date(Math.min(Date.now(), parseInt(timestamp, 16) * 1000))
131
+
132
+ function isSelfSendTx({
133
+ coinAmount,
134
+ ourWalletWasSender,
135
+ sendingTransferPresent,
136
+ receivingTransferPresent,
137
+ }) {
138
+ return (
139
+ coinAmount.isZero && sendingTransferPresent && receivingTransferPresent && ourWalletWasSender
140
+ )
141
+ }
@@ -0,0 +1,39 @@
1
+ import lodash from 'lodash'
2
+ import getValueOfTransfers from './get-value-of-transfers'
3
+ import { FEE_PAYMENT_PREFIX } from '@exodus/ethereum-lib'
4
+
5
+ // For ETH transactions, we store an array of token names inside
6
+ // the TX model. This array is used to display text in the UI for
7
+ // transactions which pay fees for ERC20 token transfers. This
8
+ // function is used to determine that array, choosing which
9
+ // tokens a transaction ostensibly 'paid fees' for.
10
+
11
+ export default function getNamesOfTokensTransferredByServerTx({
12
+ asset,
13
+ tokensByAddress,
14
+ ourWalletAddress,
15
+ serverTx,
16
+ }) {
17
+ // Treat smart contract ETH transfer transactions as ETH transfers, not token transfers
18
+ if (!getValueOfTransfers(ourWalletAddress, asset, serverTx.internal || []).isZero) {
19
+ return []
20
+ }
21
+
22
+ const tokenAddresses = (serverTx.erc20 || []).map((event) => event.address)
23
+ if (
24
+ tokenAddresses.length === 0 &&
25
+ serverTx.data &&
26
+ serverTx.data !== '0x' &&
27
+ !serverTx.data.startsWith(FEE_PAYMENT_PREFIX)
28
+ ) {
29
+ // We may still be transacting with tokens even though we found no `tokenAddresses`. This includes
30
+ // failed token transactions as well as other transactions sent to a token contract. If we do not
31
+ // recognize the `to` address in the next step, then we will not treat it as a token contract address.
32
+ tokenAddresses.push(serverTx.to)
33
+ }
34
+
35
+ return lodash
36
+ .uniq(tokenAddresses)
37
+ .map((address) => tokensByAddress.get(address)?.name)
38
+ .filter(Boolean)
39
+ }
@@ -0,0 +1,11 @@
1
+ export default function getSenderNonceKey(tx, ourWalletAddress): string {
2
+ if (Array.isArray(tx.from)) {
3
+ // Tx class. we don't define the 'from' address if our wallet was the sender.
4
+ const from = tx.from[0] || ourWalletAddress
5
+ return `${from}:${tx.data.nonce}`
6
+ }
7
+
8
+ // ethereum tx json object from magnifier
9
+ const txnonce = parseInt(tx.nonce, 16)
10
+ return `${tx.from}:${txnonce}`
11
+ }
@@ -0,0 +1,16 @@
1
+ import lodash from 'lodash'
2
+
3
+ // Sorts an array of token transfer events into an object of transfer event arrays,
4
+ // whose keys are the names of the tokens which are being transferred in the corresponding array.
5
+ // Transfers whose token addresses are not present in the tokensByAddress map are ignored.
6
+ //
7
+ // i.e. [{ address: '0x1', from, to, value }, { address: '0x2', from, to, value }]
8
+ // -> { token1: [{ address: '0x1', from, to, value }], token2: [{ address: '0x2', from, to, value }] }
9
+
10
+ export default function getTransfersByTokenName(transfers, tokensByAddress) {
11
+ const transfersByTokenName = lodash.groupBy(
12
+ transfers,
13
+ (transfer) => tokensByAddress.get(transfer.address.toLowerCase())?.name || 'UNKNOWN'
14
+ )
15
+ return lodash.omit(transfersByTokenName, 'UNKNOWN')
16
+ }
@@ -0,0 +1,22 @@
1
+ // Transfer events for ETH or ERC20 tokens have the structure { from, to, value }.
2
+ // This determines the relative amount of a given asset that has been transfered in
3
+ // or out of ourWalletAddress as a number unit, assuming each item in the `transfers`
4
+ // array is a transfer for that asset. If we sent more than we received, the value is negative.
5
+
6
+ export default function getValueOfTransfers(ourWalletAddress, asset, transfers) {
7
+ return transfers
8
+ .reduce((balanceDifference, { from, to, value }) => {
9
+ const transferAmount = asset.currency.baseUnit(value)
10
+
11
+ if (from === ourWalletAddress) {
12
+ balanceDifference = balanceDifference.sub(transferAmount)
13
+ }
14
+
15
+ if (to === ourWalletAddress) {
16
+ balanceDifference = balanceDifference.add(transferAmount)
17
+ }
18
+
19
+ return balanceDifference
20
+ }, asset.currency.ZERO)
21
+ .to(asset.ticker)
22
+ }
@@ -0,0 +1,7 @@
1
+ export { default as getDeriveDataNeededForTick } from './get-derive-data-needed-for-tick'
2
+ export { default as getAllLogItemsByAsset } from './get-all-log-items-by-asset'
3
+ export { default as getHistoryFromServer } from './get-history-from-server'
4
+ export { default as checkPendingTransactions } from './check-pending-transactions'
5
+ export { default as getDeriveTransactionsToCheck } from './get-derive-transactions-to-check'
6
+
7
+ export type { PendingTransactionsDictionary } from './types'
@@ -0,0 +1,6 @@
1
+ // NOTE: serverTx.confirmations actually counts the number of blocks AFTER
2
+ // the TX was confirmed, so we must check for the precense of blockNumber instead.
3
+
4
+ export default function isConfirmedServerTx(serverTx) {
5
+ return serverTx.blockNumber != null
6
+ }
@@ -0,0 +1,25 @@
1
+ // Token transactions are provided with either .from or .to address properties based this logic:
2
+ // - First, try to decide based on whether the token amount transferred is positive or negative.
3
+ // - If the amount is zero, try to decide based on whether the token transfer array
4
+ // contained only send or only receive type events.
5
+ // - Finally, if the value is zero and both receiving and sending transfer events
6
+ // exist, consider the TX as sent if our wallet was the TX initiator.
7
+
8
+ export default function isConsideredSentTokenTx({
9
+ coinAmount,
10
+ ourWalletWasSender,
11
+ sendingTransferPresent,
12
+ receivingTransferPresent,
13
+ }) {
14
+ if (coinAmount.isNegative) return true
15
+ if (coinAmount.isPositive) return false
16
+
17
+ // if the token TX's coinAmount is zero, the TX probably failed, so check
18
+ // whether the failed transfers were solely out of, or solely in to ourWalletAddress
19
+ if (sendingTransferPresent && !receivingTransferPresent) return true
20
+ if (!sendingTransferPresent && receivingTransferPresent) return false
21
+
22
+ // A rare edgecase which could only occur if somehow a token TX
23
+ // transferred out exactly as much as our wallet received.
24
+ return ourWalletWasSender
25
+ }
@@ -0,0 +1,11 @@
1
+ import type { Tx } from '@exodus/models'
2
+
3
+ export type PendingTransactionsDictionary =
4
+ | {
5
+ [string]: {
6
+ tx: Tx,
7
+ replaced?: boolean,
8
+ assetName: string,
9
+ },
10
+ }
11
+ | {}