@exodus/ethereum-api 2.15.1 → 2.17.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.
- package/package/package.json +33 -0
- package/package/src/eth-like-util.js +147 -0
- package/package/src/etherscan/account.js +49 -0
- package/package/src/etherscan/index.js +48 -0
- package/package/src/etherscan/logs.js +16 -0
- package/package/src/etherscan/proxy.js +47 -0
- package/package/src/etherscan/request.js +25 -0
- package/package/src/etherscan/ws.js +88 -0
- package/package/src/exodus-eth-server/api.js +242 -0
- package/package/src/exodus-eth-server/index.js +36 -0
- package/package/src/exodus-eth-server/ws.js +108 -0
- package/package/src/fee-monitor/avalanchec.js +12 -0
- package/package/src/fee-monitor/bsc.js +12 -0
- package/package/src/fee-monitor/ethereum.js +13 -0
- package/package/src/fee-monitor/ethereumclassic.js +12 -0
- package/package/src/fee-monitor/fantom.js +12 -0
- package/package/src/fee-monitor/harmony.js +12 -0
- package/package/src/fee-monitor/index.js +7 -0
- package/package/src/fee-monitor/polygon.js +12 -0
- package/package/src/gas-estimation.js +103 -0
- package/package/src/get-balances.js +38 -0
- package/package/src/index.js +11 -0
- package/package/src/simulate-tx/fetch-tx-preview.js +21 -0
- package/package/src/simulate-tx/index.js +2 -0
- package/package/src/simulate-tx/simulate-eth-tx.js +86 -0
- package/package/src/staking/fantom-staking.js +115 -0
- package/package/src/staking/index.js +2 -0
- package/package/src/staking/matic-staking.js +159 -0
- package/package/src/tx-log/__tests__/assets-for-test-helper.js +30 -0
- package/package/src/tx-log/__tests__/bsc-history-return-values-for-test-helper.js +94 -0
- package/package/src/tx-log/__tests__/bsc-monitor.integration.test.js +167 -0
- package/package/src/tx-log/__tests__/bsc-monitor.test.js +143 -0
- package/package/src/tx-log/__tests__/ethereum-history-return-values-for-test-helper.js +357 -0
- package/package/src/tx-log/__tests__/ethereum-history-unknown-token-helper.js +612 -0
- package/package/src/tx-log/__tests__/ethereum-monitor.integration.test.js +163 -0
- package/package/src/tx-log/__tests__/ethereum-monitor.test.js +211 -0
- package/package/src/tx-log/__tests__/monitor-test-helper.js +39 -0
- package/package/src/tx-log/__tests__/steth-monitor.integration.test.js +91 -0
- package/package/src/tx-log/__tests__/uniswap-monitor.integration.test.js +86 -0
- package/package/src/tx-log/__tests__/uniswap-monitor.test.js +158 -0
- package/package/src/tx-log/__tests__/uniswap-return-values-for-test-helper.js +193 -0
- package/package/src/tx-log/ethereum-monitor.js +293 -0
- package/package/src/tx-log/index.js +1 -0
- package/package/src/tx-log/ws-updates.js +75 -0
- package/package/src/websocket/index.android.js +2 -0
- package/package/src/websocket/index.ios.js +2 -0
- package/package/src/websocket/index.js +23 -0
- package/package.json +4 -3
- package/src/eth-like-util.js +72 -1
- package/src/exodus-eth-server/api.js +2 -2
- package/src/simulate-tx/simulate-eth-tx.js +43 -25
- package/src/tx-log/ethereum-monitor.js +9 -11
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { create } from './api'
|
|
2
|
+
|
|
3
|
+
const EXODUS_ETH_SERVER_URL = 'https://geth.a.exodus.io/wallet/v1/'
|
|
4
|
+
const EXODUS_ETC_SERVER_URL = 'https://getc.a.exodus.io/wallet/v1/'
|
|
5
|
+
const EXODUS_BSC_SERVER_URL = 'https://bsc.a.exodus.io/wallet/v1/'
|
|
6
|
+
const EXODUS_POLYGON_SERVER_URL = 'https://polygon.a.exodus.io/wallet/v1/'
|
|
7
|
+
const EXODUS_AVAXC_SERVER_URL = 'https://avax-c.a.exodus.io/wallet/v1/'
|
|
8
|
+
const EXODUS_FTM_SERVER_URL = 'https://fantom.a.exodus.io/wallet/v1/'
|
|
9
|
+
const EXODUS_HARMONY_SERVER_URL = 'https://harmony.a.exodus.io/wallet/v1/'
|
|
10
|
+
// allow self-signed certs
|
|
11
|
+
// process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
|
|
12
|
+
export const eth = create(EXODUS_ETH_SERVER_URL)
|
|
13
|
+
export const etc = create(EXODUS_ETC_SERVER_URL)
|
|
14
|
+
export const bsc = create(EXODUS_BSC_SERVER_URL)
|
|
15
|
+
export const polygon = create(EXODUS_POLYGON_SERVER_URL)
|
|
16
|
+
export const avaxc = create(EXODUS_AVAXC_SERVER_URL)
|
|
17
|
+
export const ftm = create(EXODUS_FTM_SERVER_URL)
|
|
18
|
+
export const harmony = create(EXODUS_HARMONY_SERVER_URL)
|
|
19
|
+
|
|
20
|
+
// exported for in-library use only.
|
|
21
|
+
export const serverMap = {
|
|
22
|
+
ethereum: eth,
|
|
23
|
+
ethereumclassic: etc,
|
|
24
|
+
bsc,
|
|
25
|
+
matic: polygon,
|
|
26
|
+
avalanchec: avaxc,
|
|
27
|
+
fantommainnet: ftm,
|
|
28
|
+
harmonymainnet: harmony,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getServer(asset) {
|
|
32
|
+
const baseAssetName = asset.baseAsset.name
|
|
33
|
+
const server = serverMap[baseAssetName]
|
|
34
|
+
if (!server) throw new Error(`unsupported base asset ${baseAssetName}`)
|
|
35
|
+
return server
|
|
36
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import ms from 'ms'
|
|
2
|
+
import { EventEmitter } from 'events'
|
|
3
|
+
import WebSocket from '../websocket'
|
|
4
|
+
|
|
5
|
+
const RECONNECT_INTERVAL = ms('10s')
|
|
6
|
+
const PING_INTERVAL = ms('10s')
|
|
7
|
+
const MAX_RECONNECT_DELAY = ms('15s')
|
|
8
|
+
|
|
9
|
+
export default function createWebSocket(getURL) {
|
|
10
|
+
const addresses = new Set()
|
|
11
|
+
const events = new EventEmitter()
|
|
12
|
+
let ws
|
|
13
|
+
let pingIntervalId = null
|
|
14
|
+
let opened = false
|
|
15
|
+
let isWSOpened = false
|
|
16
|
+
let openTimeoutId
|
|
17
|
+
|
|
18
|
+
function subscribeAddress(address) {
|
|
19
|
+
const data = JSON.stringify({ type: 'subscribe-address', address })
|
|
20
|
+
ws.send(data)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function subscribeGasPrice() {
|
|
24
|
+
const data = JSON.stringify({ type: 'subscribe-gasprice' })
|
|
25
|
+
ws.send(data)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function clearPing() {
|
|
29
|
+
clearInterval(pingIntervalId)
|
|
30
|
+
pingIntervalId = null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function onMessage(data) {
|
|
34
|
+
data = JSON.parse(data)
|
|
35
|
+
switch (data.type) {
|
|
36
|
+
case 'address':
|
|
37
|
+
events.emit(`address-${data.address}`)
|
|
38
|
+
break
|
|
39
|
+
case 'gasprice':
|
|
40
|
+
events.emit('gasprice', data.gasprice)
|
|
41
|
+
break
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function open() {
|
|
46
|
+
opened = true
|
|
47
|
+
clearTimeout(openTimeoutId)
|
|
48
|
+
if (ws) return
|
|
49
|
+
|
|
50
|
+
ws = new WebSocket(getURL())
|
|
51
|
+
ws.onerror = (e) => {}
|
|
52
|
+
ws.onmessage = (e) => {
|
|
53
|
+
try {
|
|
54
|
+
onMessage(e.data)
|
|
55
|
+
} catch (err) {}
|
|
56
|
+
}
|
|
57
|
+
ws.onopen = () => {
|
|
58
|
+
for (const address of addresses.values()) subscribeAddress(address)
|
|
59
|
+
subscribeGasPrice()
|
|
60
|
+
if (typeof ws.ping === 'function') {
|
|
61
|
+
pingIntervalId = setInterval(() => ws.ping(), PING_INTERVAL)
|
|
62
|
+
} else {
|
|
63
|
+
console.warn('Client side ping not available')
|
|
64
|
+
}
|
|
65
|
+
isWSOpened = true
|
|
66
|
+
events.emit('open')
|
|
67
|
+
}
|
|
68
|
+
ws.onclose = () => {
|
|
69
|
+
ws = null
|
|
70
|
+
isWSOpened = false
|
|
71
|
+
clearPing()
|
|
72
|
+
if (opened) {
|
|
73
|
+
openTimeoutId = setTimeout(open, reconnectDelay())
|
|
74
|
+
}
|
|
75
|
+
events.emit('close')
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function close() {
|
|
80
|
+
opened = false
|
|
81
|
+
isWSOpened = false
|
|
82
|
+
clearTimeout(openTimeoutId)
|
|
83
|
+
if (!ws) return
|
|
84
|
+
ws.onerror = null
|
|
85
|
+
ws.onmessage = null
|
|
86
|
+
ws.onopen = null
|
|
87
|
+
ws.onclose = null
|
|
88
|
+
ws.close()
|
|
89
|
+
ws = null
|
|
90
|
+
clearPing()
|
|
91
|
+
events.emit('close')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function reconnectDelay() {
|
|
95
|
+
const min = RECONNECT_INTERVAL
|
|
96
|
+
const max = MAX_RECONNECT_DELAY
|
|
97
|
+
return Math.floor(Math.random() * (max - min + 1) + min)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function watch(address) {
|
|
101
|
+
if (addresses.has(address)) return
|
|
102
|
+
addresses.add(address)
|
|
103
|
+
|
|
104
|
+
if (ws) subscribeAddress(address)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { events, open, close, watch, isCreated: () => !!ws, isOpened: () => isWSOpened }
|
|
108
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { EthereumLikeFeeMonitor } from '@exodus/ethereum-lib'
|
|
2
|
+
import { avaxc as avalancheServer } from '../exodus-eth-server'
|
|
3
|
+
|
|
4
|
+
export class AvalancheFeeMonitor extends EthereumLikeFeeMonitor {
|
|
5
|
+
constructor({ updateFee }) {
|
|
6
|
+
super({
|
|
7
|
+
updateFee,
|
|
8
|
+
assetName: 'avalanchec',
|
|
9
|
+
getGasPrice: avalancheServer.gasPrice,
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { EthereumLikeFeeMonitor } from '@exodus/ethereum-lib'
|
|
2
|
+
import { bsc } from '../exodus-eth-server'
|
|
3
|
+
|
|
4
|
+
export class BscFeeMonitor extends EthereumLikeFeeMonitor {
|
|
5
|
+
constructor({ updateFee }) {
|
|
6
|
+
super({
|
|
7
|
+
updateFee,
|
|
8
|
+
assetName: 'bsc',
|
|
9
|
+
getGasPrice: bsc.gasPrice,
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { EthereumLikeFeeMonitor } from '@exodus/ethereum-lib'
|
|
2
|
+
import { eth as ethServer } from '../exodus-eth-server'
|
|
3
|
+
|
|
4
|
+
export class EthereumFeeMonitor extends EthereumLikeFeeMonitor {
|
|
5
|
+
constructor({ updateFee, interval }) {
|
|
6
|
+
super({
|
|
7
|
+
updateFee,
|
|
8
|
+
assetName: 'ethereum',
|
|
9
|
+
getGasPrice: ethServer.gasPrice,
|
|
10
|
+
interval,
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { EthereumLikeFeeMonitor } from '@exodus/ethereum-lib'
|
|
2
|
+
import { etc as etcServer } from '../exodus-eth-server'
|
|
3
|
+
|
|
4
|
+
export class EthereumClassicFeeMonitor extends EthereumLikeFeeMonitor {
|
|
5
|
+
constructor({ updateFee }) {
|
|
6
|
+
super({
|
|
7
|
+
updateFee,
|
|
8
|
+
assetName: 'ethereumclassic',
|
|
9
|
+
getGasPrice: etcServer.gasPrice,
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { EthereumLikeFeeMonitor } from '@exodus/ethereum-lib'
|
|
2
|
+
import { ftm as fantomServer } from '../exodus-eth-server'
|
|
3
|
+
|
|
4
|
+
export class FantomFeeMonitor extends EthereumLikeFeeMonitor {
|
|
5
|
+
constructor({ updateFee }) {
|
|
6
|
+
super({
|
|
7
|
+
updateFee,
|
|
8
|
+
assetName: 'fantommainnet',
|
|
9
|
+
getGasPrice: fantomServer.gasPrice,
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { EthereumLikeFeeMonitor } from '@exodus/ethereum-lib'
|
|
2
|
+
import { harmony as harmonyServer } from '../exodus-eth-server'
|
|
3
|
+
|
|
4
|
+
export class HarmonyFeeMonitor extends EthereumLikeFeeMonitor {
|
|
5
|
+
constructor({ updateFee }) {
|
|
6
|
+
super({
|
|
7
|
+
updateFee,
|
|
8
|
+
assetName: 'harmonymainnet',
|
|
9
|
+
getGasPrice: harmonyServer.gasPrice,
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { EthereumLikeFeeMonitor } from '@exodus/ethereum-lib'
|
|
2
|
+
import { polygon as polygonServer } from '../exodus-eth-server'
|
|
3
|
+
|
|
4
|
+
export class PolygonFeeMonitor extends EthereumLikeFeeMonitor {
|
|
5
|
+
constructor({ updateFee }) {
|
|
6
|
+
super({
|
|
7
|
+
updateFee,
|
|
8
|
+
assetName: 'matic',
|
|
9
|
+
getGasPrice: polygonServer.gasPrice,
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import BN from 'bn.js'
|
|
2
|
+
import * as ethUtil from '@exodus/ethereumjs-util'
|
|
3
|
+
import { currency2buffer, isToken } from '@exodus/ethereum-lib'
|
|
4
|
+
import { estimateGas, isContractAddress } from './eth-like-util'
|
|
5
|
+
|
|
6
|
+
const EXTRA_PERCENTAGE = 20
|
|
7
|
+
|
|
8
|
+
// Starting with geth v1.9.14, if gasPrice is set for eth_estimateGas call, the call allowance will
|
|
9
|
+
// be calculated with account's balance divided by gasPrice. If user's balance is too low,
|
|
10
|
+
// the gasEstimation will fail. If gasPrice is set to '0x0', the account's balance is not
|
|
11
|
+
// used to estimate gas.
|
|
12
|
+
export async function estimateGasLimit(
|
|
13
|
+
asset: Object,
|
|
14
|
+
fromAddress: string,
|
|
15
|
+
toAddress: string,
|
|
16
|
+
amount: Buffer | Object,
|
|
17
|
+
data: Buffer | string,
|
|
18
|
+
gasPrice?: string = '0x',
|
|
19
|
+
extraPercentage?: number = EXTRA_PERCENTAGE
|
|
20
|
+
): number {
|
|
21
|
+
const opts = {
|
|
22
|
+
from: fromAddress,
|
|
23
|
+
to: toAddress,
|
|
24
|
+
value: normalizeAmount(amount),
|
|
25
|
+
data: Buffer.isBuffer(data) ? ethUtil.bufferToHex(data) : data,
|
|
26
|
+
gasPrice: normalizeGasPrice(gasPrice),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const estimatedGas = await estimateGas({ asset, ...opts })
|
|
30
|
+
return new BN(estimatedGas.slice(2), 16)
|
|
31
|
+
.imuln(100 + extraPercentage)
|
|
32
|
+
.idivn(100)
|
|
33
|
+
.toNumber()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function fetchGasLimit({
|
|
37
|
+
asset,
|
|
38
|
+
fromAddress,
|
|
39
|
+
toAddress,
|
|
40
|
+
txInput = '0x',
|
|
41
|
+
amount,
|
|
42
|
+
feeData = {},
|
|
43
|
+
bip70,
|
|
44
|
+
isContract,
|
|
45
|
+
throwOnError = true,
|
|
46
|
+
extraPercentage,
|
|
47
|
+
}) {
|
|
48
|
+
if (bip70?.bitpay?.data && bip70?.bitpay?.gasPrice)
|
|
49
|
+
return asset.name === 'ethereum' ? 65000 : 130000 // from on chain stats https://dune.xyz/queries/189123
|
|
50
|
+
|
|
51
|
+
if (!amount) amount = asset.currency.ZERO
|
|
52
|
+
if (!feeData.gasPrice) feeData.gasPrice = asset.baseAsset.currency.ZERO
|
|
53
|
+
|
|
54
|
+
const _isToken = isToken(asset)
|
|
55
|
+
if (_isToken) {
|
|
56
|
+
txInput = ethUtil.bufferToHex(
|
|
57
|
+
asset.contract.transfer.build(toAddress, amount.toBase().toString({ unit: false }))
|
|
58
|
+
)
|
|
59
|
+
amount = asset.baseAsset.currency.ZERO
|
|
60
|
+
toAddress = asset.contract.address
|
|
61
|
+
} else if (!isContract) {
|
|
62
|
+
if (isContract === undefined)
|
|
63
|
+
isContract = await isContractAddress({ asset, address: toAddress })
|
|
64
|
+
if (!isContract) return asset.gasLimit
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const gasPrice = ethUtil.bufferToHex(currency2buffer(feeData.gasPrice))
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
return await estimateGasLimit(
|
|
71
|
+
asset,
|
|
72
|
+
fromAddress,
|
|
73
|
+
toAddress,
|
|
74
|
+
amount,
|
|
75
|
+
txInput,
|
|
76
|
+
gasPrice,
|
|
77
|
+
extraPercentage
|
|
78
|
+
)
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (throwOnError) throw err
|
|
81
|
+
console.log('fetchGasLimit error', err)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return _isToken ? asset.gasLimit : asset.contractGasLimit
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeAmount(amount) {
|
|
88
|
+
if (!Buffer.isBuffer(amount)) {
|
|
89
|
+
amount = currency2buffer(amount)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
amount = ethUtil.bufferToHex(amount)
|
|
93
|
+
while (amount[2] === '0') amount = '0x' + amount.slice(3)
|
|
94
|
+
if (amount === '0x') amount = '0x0'
|
|
95
|
+
|
|
96
|
+
return amount
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function normalizeGasPrice(gasPrice) {
|
|
100
|
+
while (gasPrice[2] === '0') gasPrice = '0x' + gasPrice.slice(3)
|
|
101
|
+
if (gasPrice === '0x') gasPrice = '0x0'
|
|
102
|
+
return gasPrice
|
|
103
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { isRpcBalanceAsset } from '@exodus/ethereum-lib'
|
|
2
|
+
|
|
3
|
+
const fixBalance = ({ txLog, balance }) => {
|
|
4
|
+
for (const tx of txLog) {
|
|
5
|
+
// TODO: pending can only be less than a few minutes old, we can only search the latest txs to improve performance
|
|
6
|
+
if (tx.sent && tx.pending && !tx.error) {
|
|
7
|
+
// coinAmount is negative for sent tx
|
|
8
|
+
balance = balance.sub(tx.coinAmount.abs())
|
|
9
|
+
if (tx.coinAmount.unitType.equals(tx.feeAmount.unitType)) {
|
|
10
|
+
balance = balance.sub(tx.feeAmount)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return balance
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Api method to return the balance based on either account state balances or tx history.
|
|
19
|
+
*
|
|
20
|
+
* @param asset the asset to get the balances
|
|
21
|
+
* @param txLog the txLog when the balance is transaction based
|
|
22
|
+
* @param accountState the account state when the balance is loaded from RPC
|
|
23
|
+
* @returns {{balance}|null} an object with the balance or null if the balance is unknown/zero
|
|
24
|
+
*/
|
|
25
|
+
export const getBalances = ({ asset, txLog, accountState }) => {
|
|
26
|
+
if (isRpcBalanceAsset(asset)) {
|
|
27
|
+
const balance =
|
|
28
|
+
asset.baseAsset.name === asset.name
|
|
29
|
+
? accountState?.balance
|
|
30
|
+
: accountState?.tokenBalances?.[asset.name]
|
|
31
|
+
return balance && !balance.isZero ? { balance: fixBalance({ txLog, balance }) } : null
|
|
32
|
+
}
|
|
33
|
+
return txLog.size
|
|
34
|
+
? {
|
|
35
|
+
balance: txLog.getMutations().slice(-1)[0].balance,
|
|
36
|
+
}
|
|
37
|
+
: null
|
|
38
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Ideally, we should reduce the exports to just api.
|
|
2
|
+
// If the client is importing one module specifically,
|
|
3
|
+
// it's breaking the encapsulation
|
|
4
|
+
export * from './eth-like-util'
|
|
5
|
+
export * from './fee-monitor'
|
|
6
|
+
export * from './gas-estimation'
|
|
7
|
+
export * from './exodus-eth-server'
|
|
8
|
+
export * from './tx-log'
|
|
9
|
+
export * from './get-balances'
|
|
10
|
+
export * from './staking'
|
|
11
|
+
export * from './simulate-tx'
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const ETHEREUM_TX_PREVIEW_API = 'https://simulation.a.exodus.io/simulate'
|
|
2
|
+
|
|
3
|
+
export async function fetchTxPreview(transaction) {
|
|
4
|
+
const response = await fetch(ETHEREUM_TX_PREVIEW_API, {
|
|
5
|
+
method: 'POST',
|
|
6
|
+
headers: {
|
|
7
|
+
'Content-Type': 'application/json',
|
|
8
|
+
},
|
|
9
|
+
body: JSON.stringify({
|
|
10
|
+
network: 'ethereum',
|
|
11
|
+
chain: 'main',
|
|
12
|
+
transactions: [transaction],
|
|
13
|
+
}),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
throw new Error(await response.text())
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return response.json()
|
|
21
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import assets from '@exodus/assets'
|
|
2
|
+
|
|
3
|
+
import { fetchTxPreview } from './fetch-tx-preview'
|
|
4
|
+
import { getERC20Params } from '../eth-like-util'
|
|
5
|
+
|
|
6
|
+
const ethDecimals = assets.ethereum.units.ETH
|
|
7
|
+
|
|
8
|
+
const ethHexToInt = (hexValue) => parseInt(hexValue, '16')
|
|
9
|
+
|
|
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
|
+
})
|
|
17
|
+
|
|
18
|
+
return decimals
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return ethDecimals
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function simulateAndRetrieveSideEffects(transaction) {
|
|
25
|
+
const willSend = []
|
|
26
|
+
const willReceive = []
|
|
27
|
+
|
|
28
|
+
if (!transaction.to) throw new Error(`'to' field is missing in the TX object`)
|
|
29
|
+
|
|
30
|
+
const blocknativeTxObject = {
|
|
31
|
+
to: transaction.to,
|
|
32
|
+
input: transaction.data,
|
|
33
|
+
from: transaction.from,
|
|
34
|
+
value: ethHexToInt(transaction.value),
|
|
35
|
+
gas: ethHexToInt(transaction.gas),
|
|
36
|
+
maxFeePerGas: ethHexToInt(transaction.maxFeePerGas),
|
|
37
|
+
maxPriorityFeePerGas: ethHexToInt(transaction.maxPriorityFeePerGas),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const simulatedTx = await fetchTxPreview(blocknativeTxObject)
|
|
42
|
+
|
|
43
|
+
const [simulatedBalanceChanges] = simulatedTx.netBalanceChanges
|
|
44
|
+
|
|
45
|
+
const [sender] =
|
|
46
|
+
simulatedBalanceChanges &&
|
|
47
|
+
simulatedBalanceChanges.filter(({ address }) => address === transaction.from)
|
|
48
|
+
|
|
49
|
+
if (sender) {
|
|
50
|
+
for (const balanceChange of sender.balanceChanges) {
|
|
51
|
+
const { delta, asset } = balanceChange
|
|
52
|
+
|
|
53
|
+
const decimal = await getDecimals(asset.type, asset.contractAddress)
|
|
54
|
+
|
|
55
|
+
if (delta.startsWith('-')) {
|
|
56
|
+
willSend.push({
|
|
57
|
+
symbol: asset.symbol,
|
|
58
|
+
balance: delta.slice(1),
|
|
59
|
+
assetType: asset.type,
|
|
60
|
+
decimal,
|
|
61
|
+
})
|
|
62
|
+
} else {
|
|
63
|
+
willReceive.push({
|
|
64
|
+
symbol: asset.symbol,
|
|
65
|
+
balance: delta,
|
|
66
|
+
assetType: asset.type,
|
|
67
|
+
decimal,
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!willSend.length) {
|
|
74
|
+
willSend.push({
|
|
75
|
+
symbol: 'ETH',
|
|
76
|
+
balance: ethHexToInt(transaction.value).toString(),
|
|
77
|
+
assetType: 'ether',
|
|
78
|
+
decimal: ethDecimals,
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
} catch (_) {
|
|
82
|
+
throw new Error('Simulation of Ethereum transaction failed.')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { willSend, willReceive }
|
|
86
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createContract } from '@exodus/ethereum-lib'
|
|
2
|
+
|
|
3
|
+
export const SFC_ADDRESS = '0xfc00face00000000000000000000000000000000'
|
|
4
|
+
|
|
5
|
+
export class FantomStaking {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.sfcContract = createContract(SFC_ADDRESS, 'fantomSfc')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
buildTxData = (contract, method, ...args) => {
|
|
11
|
+
return contract[method].build(...args)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get tx data of method to delegate (stake) amount to validator
|
|
16
|
+
*/
|
|
17
|
+
delegate = ({ validatorId }) => {
|
|
18
|
+
this.#validateValidatorId(validatorId)
|
|
19
|
+
return this.buildTxData(this.sfcContract, 'delegate', validatorId)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* undelegate creates a transaction preparing part of the delegations
|
|
24
|
+
* to be withdrawn.
|
|
25
|
+
*
|
|
26
|
+
* Note: The amount must be lower than the total delegation amount for that account. Also,
|
|
27
|
+
* the requestId value has to be unique and previously unused numeric identifier of the new
|
|
28
|
+
* withdrawal. The actual withdraw execution, available after a lock period, will use the same
|
|
29
|
+
* request id to process the prepared withdrawal.
|
|
30
|
+
*/
|
|
31
|
+
undelegate = ({ requestId, validatorId, amount }) => {
|
|
32
|
+
this.#validateRequestId(requestId)
|
|
33
|
+
this.#validateValidatorId(validatorId)
|
|
34
|
+
return this.buildTxData(this.sfcContract, 'undelegate', validatorId, requestId, amount)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* withdraw creates a transaction executing partial withdraw for the given
|
|
39
|
+
* prepared withdraw request.
|
|
40
|
+
*
|
|
41
|
+
* Note: The request id has to exist and has to be prepared for the withdraw to execute
|
|
42
|
+
* correctly.
|
|
43
|
+
*/
|
|
44
|
+
withdraw = ({ requestId, validatorId }) => {
|
|
45
|
+
this.#validateRequestId(requestId)
|
|
46
|
+
this.#validateValidatorId(validatorId)
|
|
47
|
+
return this.buildTxData(this.sfcContract, 'withdraw', validatorId, requestId)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* claimRewards creates a new delegator rewards claiming transaction.
|
|
52
|
+
* The call transfers all the rewards from SFC back to the stake in single transaction.
|
|
53
|
+
*/
|
|
54
|
+
claimRewards = ({ validatorId }) => {
|
|
55
|
+
this.#validateValidatorId(validatorId)
|
|
56
|
+
return this.buildTxData(this.sfcContract, 'claimRewards', validatorId)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* restakeRewards creates a new delegator rewards claiming transaction.
|
|
61
|
+
* The call transfers all the rewards from SFC back to the stake in single transaction.
|
|
62
|
+
*/
|
|
63
|
+
restakeRewards = ({ validatorId }) => {
|
|
64
|
+
this.#validateValidatorId(validatorId)
|
|
65
|
+
return this.buildTxData(this.sfcContract, 'restakeRewards', validatorId)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* lockStake creates a transaction for locking delegation.
|
|
70
|
+
*/
|
|
71
|
+
lockStake = ({ validatorId, amount, durationSeconds }) => {
|
|
72
|
+
this.#validateValidatorId(validatorId)
|
|
73
|
+
this.#validateDuration(durationSeconds)
|
|
74
|
+
return this.buildTxData(this.sfcContract, 'lockStake', validatorId, durationSeconds, amount)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* relockStake creates a transaction for re-locking delegation.
|
|
79
|
+
*/
|
|
80
|
+
relockStake = ({ validatorId, amount, durationSeconds }) => {
|
|
81
|
+
this.#validateValidatorId(validatorId)
|
|
82
|
+
this.#validateDuration(durationSeconds)
|
|
83
|
+
return this.buildTxData(this.sfcContract, 'relockStake', validatorId, durationSeconds, amount)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* unlockStake creates a transaction for unlocking delegation.
|
|
88
|
+
*/
|
|
89
|
+
unlockStake = ({ validatorId, amount }) => {
|
|
90
|
+
this.#validateValidatorId(validatorId)
|
|
91
|
+
return this.buildTxData(this.sfcContract, 'unlockStake', validatorId, amount)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#validateValidatorId = (validatorId) => {
|
|
95
|
+
if (!Number.isInteger(validatorId) || validatorId <= 0) {
|
|
96
|
+
throw new Error('Validator id must be positive unsigned integer value.')
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#validateRequestId = (requestId) => {
|
|
101
|
+
if (!Number.isInteger(requestId) || requestId <= 0) {
|
|
102
|
+
throw new Error('Request id must be a valid numeric identifier.')
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#validateDuration = (duration) => {
|
|
107
|
+
if (!Number.isInteger(duration)) {
|
|
108
|
+
throw new Error('The lock duration must be an integer.')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (duration > 365 * 86400) {
|
|
112
|
+
throw new Error('The lock duration must be at most 365 days.')
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|