@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.
|
|
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.
|
|
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": "
|
|
32
|
+
"gitHead": "e82aa2dcf0a729e17ec2c7e304625fc6ae9d66a4"
|
|
32
33
|
}
|
package/src/eth-like-util.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
6
|
-
const asset = assets.ethereum
|
|
7
|
-
const currency = asset.currency
|
|
6
|
+
const ethDecimals = assets.ethereum.units.ETH
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
const ethHexToInt = (hexValue) => parseInt(hexValue, '16')
|
|
10
9
|
|
|
11
|
-
|
|
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
|
-
|
|
18
|
+
return decimals
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return ethDecimals
|
|
22
|
+
}
|
|
15
23
|
|
|
16
|
-
export async function
|
|
17
|
-
|
|
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
|
|
42
|
-
const delta = balanceChange
|
|
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
|
-
|
|
45
|
-
symbol:
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
willSend.push({
|
|
57
|
+
symbol: asset.symbol,
|
|
58
|
+
balance: delta.slice(1),
|
|
59
|
+
assetType: asset.type,
|
|
60
|
+
decimal,
|
|
61
|
+
})
|
|
48
62
|
} else {
|
|
49
|
-
|
|
50
|
-
symbol:
|
|
51
|
-
|
|
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 (!
|
|
58
|
-
|
|
73
|
+
if (!willSend.length) {
|
|
74
|
+
willSend.push({
|
|
59
75
|
symbol: 'ETH',
|
|
60
|
-
|
|
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
|
|
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
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
.
|
|
226
|
-
|
|
227
|
-
|
|
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
|
}
|