@exodus/ethereum-api 2.15.1 → 2.16.0

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.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "2.15.1",
3
+ "version": "2.16.0",
4
4
  "description": "Ethereum Api",
5
5
  "main": "src/index.js",
6
6
  "author": "Exodus Movement, Inc.",
@@ -12,9 +12,10 @@
12
12
  "dependencies": {
13
13
  "@exodus/asset-lib": "^3.5.4",
14
14
  "@exodus/crypto": "^1.0.0-rc.0",
15
- "@exodus/ethereum-lib": "^2.14.1",
15
+ "@exodus/ethereum-lib": "^2.15.0",
16
16
  "@exodus/ethereumjs-util": "^7.1.0-exodus.6",
17
17
  "@exodus/simple-retry": "^0.0.6",
18
+ "@exodus/solidity-contract": "^1.0.1",
18
19
  "fetchival": "0.3.3",
19
20
  "make-concurrent": "4.0.0",
20
21
  "minimalistic-assert": "^1.0.1",
@@ -28,5 +29,5 @@
28
29
  "@exodus/assets-base": "^8.0.136",
29
30
  "@exodus/models": "^8.7.2"
30
31
  },
31
- "gitHead": "eb59c41504a3c004bafbab789bf5f50a9f40d020"
32
+ "gitHead": "e82aa2dcf0a729e17ec2c7e304625fc6ae9d66a4"
32
33
  }
@@ -1,6 +1,8 @@
1
- import { normalizeTxId, isEthereumLikeAsset, isEthereumLikeToken } from '@exodus/ethereum-lib'
1
+ import { normalizeTxId, isEthereumLikeAsset, isEthereumLikeToken, ABI } from '@exodus/ethereum-lib'
2
2
  import { eth, serverMap, getServer } from './exodus-eth-server'
3
3
  import { memoizeLruCache } from '@exodus/asset-lib'
4
+ import assets from '@exodus/assets'
5
+ import SolidityContract from '@exodus/solidity-contract'
4
6
 
5
7
  // Mobile only.
6
8
  // Behavior is buggy, because the default server used is ethereum.
@@ -74,3 +76,72 @@ export const getIsForwarderContract = memoizeLruCache(
74
76
  ({ asset, address }) => `${asset.name}:${address}`,
75
77
  { max: 100 }
76
78
  )
79
+
80
+ const ERC20 = new SolidityContract(ABI.erc20)
81
+ const ERC20BytesParams = new SolidityContract(ABI.erc20BytesParams)
82
+ const DEFAULT_PARAM_NAMES = ['decimals', 'name', 'symbol']
83
+ const erc20ParamsCache = {}
84
+
85
+ export const getERC20Params = async ({
86
+ assetName,
87
+ address,
88
+ paramNames = DEFAULT_PARAM_NAMES,
89
+ } = {}) => {
90
+ const asset = assets[assetName]
91
+ if (!asset) {
92
+ throw new Error(`${assetName} not found`)
93
+ }
94
+ if (!address) {
95
+ throw new Error(`Token address should be provided, got: ${address}`)
96
+ }
97
+
98
+ const cacheKey = `${address}:${paramNames}`
99
+ if (erc20ParamsCache[cacheKey]) {
100
+ return erc20ParamsCache[cacheKey]
101
+ }
102
+
103
+ const server = getServer(asset)
104
+
105
+ const paramValues = await Promise.all(
106
+ paramNames.map(async (method) => {
107
+ let callResponse
108
+ try {
109
+ callResponse = await server.ethCall({ to: address, data: ERC20[method].methodId })
110
+ } catch (err) {
111
+ if (err.message === 'execution reverted') {
112
+ throw Error(
113
+ `Can't find parameters for contract with address ${address}. Are you sure it is a valid ERC20 contract?`
114
+ )
115
+ }
116
+
117
+ throw Error(err.message)
118
+ }
119
+
120
+ if (method === 'decimals') return parseInt(callResponse)
121
+
122
+ try {
123
+ return ERC20.decodeOutput({ method, data: callResponse })[0]
124
+ } catch (err) {
125
+ // sometimes ERC20s violate the standard and use 'bytes32' type instead of 'string'
126
+ if (err.message.includes('overflow') && callResponse) {
127
+ const hex = ERC20BytesParams.decodeOutput({ method, data: callResponse })[0]
128
+ const rawName = Buffer.from(hex.split('0x')[1], 'hex').toString()
129
+
130
+ // trims 'Maker\x00\x00\x00...' to 'Maker'
131
+ return rawName.slice(0, rawName.indexOf('\x00'))
132
+ }
133
+ }
134
+ })
135
+ )
136
+
137
+ const response = paramNames.reduce(
138
+ (accumulatedObj, paramName, index) => ({
139
+ ...accumulatedObj,
140
+ [paramName]: paramValues[index],
141
+ }),
142
+ {}
143
+ )
144
+ erc20ParamsCache[cacheKey] = response
145
+
146
+ return response
147
+ }
@@ -4,7 +4,7 @@ import fetchival from 'fetchival'
4
4
  import ms from 'ms'
5
5
  import createWebSocket from './ws'
6
6
  import { retry } from '@exodus/simple-retry'
7
- import { simpleErc20 } from '@exodus/solidity-contract'
7
+ import SolidityContract from '@exodus/solidity-contract'
8
8
  import { bufferToHex } from '@exodus/ethereumjs-util'
9
9
  import { randomUUID } from '@exodus/crypto/randomUUID'
10
10
 
@@ -75,7 +75,7 @@ export function create(defaultURL) {
75
75
  },
76
76
 
77
77
  async balanceOf(address, tokenAddress, tag = 'latest') {
78
- const contract = simpleErc20(tokenAddress)
78
+ const contract = SolidityContract.simpleErc20(tokenAddress)
79
79
  const callData = contract.balanceOf.build(address)
80
80
  const data = {
81
81
  data: bufferToHex(callData),
@@ -1,20 +1,29 @@
1
1
  import assets from '@exodus/assets'
2
2
 
3
3
  import { fetchTxPreview } from './fetch-tx-preview'
4
+ import { getERC20Params } from '../eth-like-util'
4
5
 
5
- function weiToEth(wei) {
6
- const asset = assets.ethereum
7
- const currency = asset.currency
6
+ const ethDecimals = assets.ethereum.units.ETH
8
7
 
9
- const valueInEth = currency.baseUnit(wei).toDefaultString()
8
+ const ethHexToInt = (hexValue) => parseInt(hexValue, '16')
10
9
 
11
- return valueInEth
12
- }
10
+ async function getDecimals(type, assetContractAddress = null) {
11
+ if (type === 'erc20' && assetContractAddress) {
12
+ const { decimals } = await getERC20Params({
13
+ address: assetContractAddress,
14
+ assetName: 'ethereum',
15
+ paramNames: ['decimals'],
16
+ })
13
17
 
14
- const ethHexToInt = (hexValue) => parseInt(hexValue, '16')
18
+ return decimals
19
+ }
20
+
21
+ return ethDecimals
22
+ }
15
23
 
16
- export async function simulateTx(transaction) {
17
- let accounts = {}
24
+ export async function simulateAndRetrieveSideEffects(transaction) {
25
+ const willSend = []
26
+ const willReceive = []
18
27
 
19
28
  if (!transaction.to) throw new Error(`'to' field is missing in the TX object`)
20
29
 
@@ -38,31 +47,40 @@ export async function simulateTx(transaction) {
38
47
  simulatedBalanceChanges.filter(({ address }) => address === transaction.from)
39
48
 
40
49
  if (sender) {
41
- sender.balanceChanges.forEach((balanceChange) => {
42
- const delta = balanceChange.delta
50
+ for (const balanceChange of sender.balanceChanges) {
51
+ const { delta, asset } = balanceChange
52
+
53
+ const decimal = await getDecimals(asset.type, asset.contractAddress)
54
+
43
55
  if (delta.startsWith('-')) {
44
- accounts.willSend = {
45
- symbol: balanceChange.asset.symbol,
46
- amount: weiToEth(delta.slice(1)),
47
- }
56
+ willSend.push({
57
+ symbol: asset.symbol,
58
+ balance: delta.slice(1),
59
+ assetType: asset.type,
60
+ decimal,
61
+ })
48
62
  } else {
49
- accounts.willReceive = {
50
- symbol: balanceChange.asset.symbol,
51
- amount: weiToEth(delta),
52
- }
63
+ willReceive.push({
64
+ symbol: asset.symbol,
65
+ balance: delta,
66
+ assetType: asset.type,
67
+ decimal,
68
+ })
53
69
  }
54
- })
70
+ }
55
71
  }
56
72
 
57
- if (!accounts.willSend) {
58
- accounts.willSend = {
73
+ if (!willSend.length) {
74
+ willSend.push({
59
75
  symbol: 'ETH',
60
- amount: weiToEth(transaction.value),
61
- }
76
+ balance: ethHexToInt(transaction.value).toString(),
77
+ assetType: 'ether',
78
+ decimal: ethDecimals,
79
+ })
62
80
  }
63
81
  } catch (_) {
64
82
  throw new Error('Simulation of Ethereum transaction failed.')
65
83
  }
66
84
 
67
- return accounts
85
+ return { willSend, willReceive }
68
86
  }
@@ -217,18 +217,16 @@ export class EthereumMonitor extends BaseMonitor {
217
217
  const { confirmed } = await server.getBalance(ourWalletAddress)
218
218
  newAccountState.balance = asset.currency.baseUnit(confirmed.value)
219
219
  }
220
- const tokenBalances = Object.assign(
221
- {},
222
- ...(await Promise.all(
223
- tokens
224
- .filter((token) => isRpcBalanceAsset(token) && token.contract.address)
225
- .map(async (token) => {
226
- const { confirmed } = await server.balanceOf(ourWalletAddress, token.contract.address)
227
- const value = token.currency.baseUnit(confirmed[token.contract.address] || 0)
228
- return value.isZero ? {} : { [token.name]: value }
229
- })
230
- ))
220
+ const tokenBalancePairs = await Promise.all(
221
+ tokens
222
+ .filter((token) => isRpcBalanceAsset(token) && token.contract.address)
223
+ .map(async (token) => {
224
+ const { confirmed } = await server.balanceOf(ourWalletAddress, token.contract.address)
225
+ const value = token.currency.baseUnit(confirmed[token.contract.address] || 0)
226
+ return value.isZero ? null : [token.name, value]
227
+ })
231
228
  )
229
+ const tokenBalances = Object.fromEntries(tokenBalancePairs.filter((pair) => pair))
232
230
  if (!isEmpty(tokenBalances)) newAccountState.tokenBalances = tokenBalances
233
231
  return newAccountState
234
232
  }