@toruslabs/ethereum-controllers 4.1.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/dist/ethereumControllers.cjs.js +6153 -0
- package/dist/ethereumControllers.cjs.js.map +1 -0
- package/dist/ethereumControllers.esm.js +5570 -0
- package/dist/ethereumControllers.esm.js.map +1 -0
- package/dist/ethereumControllers.umd.min.js +3 -0
- package/dist/ethereumControllers.umd.min.js.LICENSE.txt +38 -0
- package/dist/ethereumControllers.umd.min.js.map +1 -0
- package/dist/types/Account/AccountTrackerController.d.ts +35 -0
- package/dist/types/Block/PollingBlockTracker.d.ts +14 -0
- package/dist/types/Currency/CurrencyController.d.ts +30 -0
- package/dist/types/Gas/GasFeeController.d.ts +64 -0
- package/dist/types/Gas/IGasFeeController.d.ts +49 -0
- package/dist/types/Gas/gasUtil.d.ts +21 -0
- package/dist/types/Keyring/KeyringController.d.ts +20 -0
- package/dist/types/Message/AbstractMessageController.d.ts +36 -0
- package/dist/types/Message/DecryptMessageController.d.ts +20 -0
- package/dist/types/Message/EncryptionPublicKeyController.d.ts +20 -0
- package/dist/types/Message/MessageController.d.ts +20 -0
- package/dist/types/Message/PersonalMessageController.d.ts +20 -0
- package/dist/types/Message/TypedMessageController.d.ts +21 -0
- package/dist/types/Message/utils.d.ts +10 -0
- package/dist/types/Network/NetworkController.d.ts +40 -0
- package/dist/types/Network/createEthereumMiddleware.d.ts +66 -0
- package/dist/types/Network/createJsonRpcClient.d.ts +9 -0
- package/dist/types/Nfts/INftsController.d.ts +10 -0
- package/dist/types/Nfts/NftHandler.d.ts +35 -0
- package/dist/types/Nfts/NftsController.d.ts +40 -0
- package/dist/types/Preferences/PreferencesController.d.ts +53 -0
- package/dist/types/Tokens/ITokensController.d.ts +10 -0
- package/dist/types/Tokens/TokenHandler.d.ts +20 -0
- package/dist/types/Tokens/TokenRatesController.d.ts +42 -0
- package/dist/types/Tokens/TokensController.d.ts +42 -0
- package/dist/types/Transaction/NonceTracker.d.ts +37 -0
- package/dist/types/Transaction/PendingTransactionTracker.d.ts +32 -0
- package/dist/types/Transaction/TransactionController.d.ts +67 -0
- package/dist/types/Transaction/TransactionGasUtil.d.ts +21 -0
- package/dist/types/Transaction/TransactionStateHistoryHelper.d.ts +16 -0
- package/dist/types/Transaction/TransactionStateManager.d.ts +30 -0
- package/dist/types/Transaction/TransactionUtils.d.ts +70 -0
- package/dist/types/index.d.ts +43 -0
- package/dist/types/utils/abiDecoder.d.ts +17 -0
- package/dist/types/utils/abis.d.ts +84 -0
- package/dist/types/utils/constants.d.ts +81 -0
- package/dist/types/utils/contractAddresses.d.ts +1 -0
- package/dist/types/utils/conversionUtils.d.ts +42 -0
- package/dist/types/utils/helpers.d.ts +24 -0
- package/dist/types/utils/interfaces.d.ts +384 -0
- package/package.json +71 -0
- package/src/Account/AccountTrackerController.ts +157 -0
- package/src/Block/PollingBlockTracker.ts +89 -0
- package/src/Currency/CurrencyController.ts +117 -0
- package/src/Gas/GasFeeController.ts +254 -0
- package/src/Gas/IGasFeeController.ts +56 -0
- package/src/Gas/gasUtil.ts +163 -0
- package/src/Keyring/KeyringController.ts +118 -0
- package/src/Message/AbstractMessageController.ts +136 -0
- package/src/Message/DecryptMessageController.ts +81 -0
- package/src/Message/EncryptionPublicKeyController.ts +83 -0
- package/src/Message/MessageController.ts +74 -0
- package/src/Message/PersonalMessageController.ts +74 -0
- package/src/Message/TypedMessageController.ts +112 -0
- package/src/Message/utils.ts +107 -0
- package/src/Network/NetworkController.ts +184 -0
- package/src/Network/createEthereumMiddleware.ts +307 -0
- package/src/Network/createJsonRpcClient.ts +59 -0
- package/src/Nfts/INftsController.ts +13 -0
- package/src/Nfts/NftHandler.ts +191 -0
- package/src/Nfts/NftsController.ts +230 -0
- package/src/Preferences/PreferencesController.ts +409 -0
- package/src/Tokens/ITokensController.ts +13 -0
- package/src/Tokens/TokenHandler.ts +60 -0
- package/src/Tokens/TokenRatesController.ts +134 -0
- package/src/Tokens/TokensController.ts +278 -0
- package/src/Transaction/NonceTracker.ts +152 -0
- package/src/Transaction/PendingTransactionTracker.ts +235 -0
- package/src/Transaction/TransactionController.ts +558 -0
- package/src/Transaction/TransactionGasUtil.ts +74 -0
- package/src/Transaction/TransactionStateHistoryHelper.ts +41 -0
- package/src/Transaction/TransactionStateManager.ts +315 -0
- package/src/Transaction/TransactionUtils.ts +333 -0
- package/src/index.ts +45 -0
- package/src/utils/abiDecoder.ts +195 -0
- package/src/utils/abis.ts +677 -0
- package/src/utils/constants.ts +379 -0
- package/src/utils/contractAddresses.ts +21 -0
- package/src/utils/conversionUtils.ts +269 -0
- package/src/utils/helpers.ts +177 -0
- package/src/utils/interfaces.ts +454 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { AccountTrackerConfig, AccountTrackerState, BaseController, IAccountTrackerController, PreferencesState } from "@toruslabs/base-controllers";
|
|
2
|
+
import { SafeEventEmitterProvider } from "@toruslabs/openlogin-jrpc";
|
|
3
|
+
import { Mutex } from "async-mutex";
|
|
4
|
+
import { BrowserProvider, Contract, toQuantity } from "ethers";
|
|
5
|
+
import log from "loglevel";
|
|
6
|
+
|
|
7
|
+
import PollingBlockTracker from "../Block/PollingBlockTracker";
|
|
8
|
+
import NetworkController from "../Network/NetworkController";
|
|
9
|
+
import { singleBalanceCheckerAbi } from "../utils/abis";
|
|
10
|
+
import { SINGLE_CALL_BALANCES_ADDRESSES } from "../utils/contractAddresses";
|
|
11
|
+
import { EthereumBlock, ExtendedAddressPreferences } from "../utils/interfaces";
|
|
12
|
+
|
|
13
|
+
interface AccountTrackerControllerOptions {
|
|
14
|
+
config: AccountTrackerConfig<EthereumBlock>;
|
|
15
|
+
state: Partial<AccountTrackerState>;
|
|
16
|
+
provider: SafeEventEmitterProvider;
|
|
17
|
+
blockTracker?: PollingBlockTracker;
|
|
18
|
+
getIdentities: () => PreferencesState<ExtendedAddressPreferences>["identities"];
|
|
19
|
+
onPreferencesStateChange: (listener: (preferencesState: PreferencesState<ExtendedAddressPreferences>) => void) => void;
|
|
20
|
+
getCurrentChainId: NetworkController["getNetworkIdentifier"];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Tracks accounts based on blocks.
|
|
27
|
+
* If block tracker provides latest block, we query accounts from it.
|
|
28
|
+
* Preferences state changes also retrigger accounts update.
|
|
29
|
+
* Network state changes also retrigger accounts update.
|
|
30
|
+
*/
|
|
31
|
+
class AccountTrackerController
|
|
32
|
+
extends BaseController<AccountTrackerConfig<EthereumBlock>, AccountTrackerState>
|
|
33
|
+
implements IAccountTrackerController<AccountTrackerConfig<EthereumBlock>, AccountTrackerState>
|
|
34
|
+
{
|
|
35
|
+
private provider!: SafeEventEmitterProvider;
|
|
36
|
+
|
|
37
|
+
private blockTracker!: PollingBlockTracker;
|
|
38
|
+
|
|
39
|
+
private mutex = new Mutex();
|
|
40
|
+
|
|
41
|
+
private ethersProvider!: BrowserProvider;
|
|
42
|
+
|
|
43
|
+
private getIdentities!: AccountTrackerControllerOptions["getIdentities"];
|
|
44
|
+
|
|
45
|
+
private getCurrentChainId: NetworkController["getNetworkIdentifier"];
|
|
46
|
+
|
|
47
|
+
constructor({
|
|
48
|
+
config,
|
|
49
|
+
state,
|
|
50
|
+
provider,
|
|
51
|
+
blockTracker,
|
|
52
|
+
getIdentities,
|
|
53
|
+
onPreferencesStateChange,
|
|
54
|
+
getCurrentChainId,
|
|
55
|
+
}: AccountTrackerControllerOptions) {
|
|
56
|
+
super({ config, state });
|
|
57
|
+
this.defaultState = {
|
|
58
|
+
accounts: {},
|
|
59
|
+
};
|
|
60
|
+
this.defaultConfig = {
|
|
61
|
+
_currentBlock: null,
|
|
62
|
+
};
|
|
63
|
+
this.initialize();
|
|
64
|
+
this.provider = provider;
|
|
65
|
+
this.blockTracker = blockTracker;
|
|
66
|
+
this.ethersProvider = new BrowserProvider(this.provider, "any");
|
|
67
|
+
|
|
68
|
+
// Initiate block tracker internal tracking.
|
|
69
|
+
this.blockTracker.on("latest", (block: EthereumBlock) => {
|
|
70
|
+
this.configure({ _currentBlock: block });
|
|
71
|
+
this.refresh();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
this.getIdentities = getIdentities;
|
|
75
|
+
this.getCurrentChainId = getCurrentChainId;
|
|
76
|
+
|
|
77
|
+
onPreferencesStateChange(() => {
|
|
78
|
+
log.info("onPreferencesStateChange called");
|
|
79
|
+
const refreshNeeded = this.syncAccounts();
|
|
80
|
+
if (refreshNeeded) this.refresh();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
syncAccounts(): boolean {
|
|
85
|
+
const { accounts } = this.state;
|
|
86
|
+
const addresses = Object.keys(this.getIdentities());
|
|
87
|
+
const existing = Object.keys(accounts);
|
|
88
|
+
const newAddresses = addresses.filter((address) => existing.indexOf(address) === -1);
|
|
89
|
+
const oldAddresses = existing.filter((address) => addresses.indexOf(address) === -1);
|
|
90
|
+
let isUpdated = false;
|
|
91
|
+
newAddresses.forEach((address) => {
|
|
92
|
+
isUpdated = true;
|
|
93
|
+
accounts[address] = { balance: "0x0" };
|
|
94
|
+
});
|
|
95
|
+
oldAddresses.forEach((address) => {
|
|
96
|
+
isUpdated = true;
|
|
97
|
+
delete accounts[address];
|
|
98
|
+
});
|
|
99
|
+
this.update({ accounts: { ...accounts } });
|
|
100
|
+
return isUpdated;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async refresh(): Promise<void> {
|
|
104
|
+
const releaseLock = await this.mutex.acquire();
|
|
105
|
+
try {
|
|
106
|
+
const currentBlock = this.config._currentBlock;
|
|
107
|
+
if (!currentBlock) return;
|
|
108
|
+
this._updateAccounts();
|
|
109
|
+
} catch (error) {
|
|
110
|
+
} finally {
|
|
111
|
+
releaseLock();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private async _updateAccounts(): Promise<void> {
|
|
116
|
+
const { accounts } = this.state;
|
|
117
|
+
const addresses = Object.keys(accounts);
|
|
118
|
+
const chainId = this.getCurrentChainId();
|
|
119
|
+
if (chainId === "loading") return;
|
|
120
|
+
|
|
121
|
+
if (addresses.length > 0) {
|
|
122
|
+
if (SINGLE_CALL_BALANCES_ADDRESSES[chainId]) {
|
|
123
|
+
await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESSES[chainId]);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
log.info("falling back to ethQuery.getBalance");
|
|
127
|
+
await Promise.all(addresses.map((x) => this._updateAccount(x)));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private async _updateAccount(address: string): Promise<void> {
|
|
132
|
+
const balance = await this.provider.request<[string, string], string>({ method: "eth_getBalance", params: [address, "latest"] });
|
|
133
|
+
const { accounts } = this.state;
|
|
134
|
+
if (!accounts[address]) return;
|
|
135
|
+
accounts[address] = { balance: toQuantity(balance) };
|
|
136
|
+
this.update({ accounts });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private async _updateAccountsViaBalanceChecker(addresses: string[], deployedContractAddress: string) {
|
|
140
|
+
const ethContract = new Contract(deployedContractAddress, singleBalanceCheckerAbi, this.ethersProvider);
|
|
141
|
+
try {
|
|
142
|
+
const result: string[] = await ethContract.balances(addresses, [ZERO_ADDRESS]);
|
|
143
|
+
const { accounts } = this.state;
|
|
144
|
+
addresses.forEach((address, index) => {
|
|
145
|
+
const balance = toQuantity(result[index]);
|
|
146
|
+
if (!accounts[address]) return;
|
|
147
|
+
accounts[address] = { balance };
|
|
148
|
+
});
|
|
149
|
+
return this.update({ accounts });
|
|
150
|
+
} catch (error) {
|
|
151
|
+
log.warn("Torus - Account Tracker single call balance fetch failed", error);
|
|
152
|
+
return Promise.all(addresses.map((x) => this._updateAccount(x)));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export default AccountTrackerController;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { BaseBlockTracker, PollingBlockTrackerConfig, timeout } from "@toruslabs/base-controllers";
|
|
2
|
+
import { BlockParams } from "ethers";
|
|
3
|
+
import log from "loglevel";
|
|
4
|
+
|
|
5
|
+
import { idleTimeTracker } from "../utils/helpers";
|
|
6
|
+
import { EthereumBlock, PollingBlockTrackerState } from "../utils/interfaces";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_POLLING_INTERVAL = 20;
|
|
9
|
+
const DEFAULT_RETRY_TIMEOUT = 2;
|
|
10
|
+
const SEC = 1000;
|
|
11
|
+
|
|
12
|
+
class PollingBlockTracker extends BaseBlockTracker<EthereumBlock, PollingBlockTrackerConfig, PollingBlockTrackerState> {
|
|
13
|
+
constructor({ config, state = {} }: { config: Partial<PollingBlockTrackerConfig>; state: Partial<PollingBlockTrackerState> }) {
|
|
14
|
+
if (!config.provider) {
|
|
15
|
+
throw new Error("PollingBlockTracker - no provider specified.");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
super({ config, state });
|
|
19
|
+
|
|
20
|
+
const pollingInterval = config.pollingInterval || DEFAULT_POLLING_INTERVAL;
|
|
21
|
+
|
|
22
|
+
const retryTimeout = config.retryTimeout || DEFAULT_RETRY_TIMEOUT;
|
|
23
|
+
|
|
24
|
+
// merge default + provided config.
|
|
25
|
+
this.defaultConfig = {
|
|
26
|
+
provider: config.provider,
|
|
27
|
+
pollingInterval: pollingInterval * SEC,
|
|
28
|
+
retryTimeout: retryTimeout * SEC,
|
|
29
|
+
setSkipCacheFlag: config.setSkipCacheFlag || false,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
this.initialize();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async checkForLatestBlock(): Promise<EthereumBlock> {
|
|
36
|
+
await this._updateLatestBlock();
|
|
37
|
+
return this.getLatestBlock();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// overrides the BaseBlockTracker._start method.
|
|
41
|
+
protected _start(): void {
|
|
42
|
+
this._synchronize().catch((err) => this.emit("error", err));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private async _synchronize(): Promise<void> {
|
|
46
|
+
while (this.state._isRunning) {
|
|
47
|
+
if (idleTimeTracker.checkIfIdle()) return;
|
|
48
|
+
try {
|
|
49
|
+
await this._updateLatestBlock();
|
|
50
|
+
await timeout(this.config.pollingInterval);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
const newErr = new Error(`PollingBlockTracker - encountered an error while attempting to update latest block:\n${(err as Error).stack}`);
|
|
53
|
+
try {
|
|
54
|
+
this.emit("error", newErr);
|
|
55
|
+
} catch (emitErr) {
|
|
56
|
+
log.error(newErr);
|
|
57
|
+
}
|
|
58
|
+
await timeout(this.config.retryTimeout);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private async _updateLatestBlock(): Promise<void> {
|
|
64
|
+
// fetch + set latest block
|
|
65
|
+
const latestBlock = await this._fetchLatestBlock();
|
|
66
|
+
this._newPotentialLatest(latestBlock);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private async _fetchLatestBlock(): Promise<EthereumBlock> {
|
|
70
|
+
try {
|
|
71
|
+
const block = await this.config.provider.request<[string, boolean], { [K in keyof BlockParams]: string }>({
|
|
72
|
+
method: "eth_getBlockByNumber",
|
|
73
|
+
params: ["latest", false],
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
blockHash: block.hash,
|
|
77
|
+
idempotencyKey: block.number,
|
|
78
|
+
timestamp: block.timestamp,
|
|
79
|
+
baseFeePerGas: block.baseFeePerGas,
|
|
80
|
+
gasLimit: block.gasLimit,
|
|
81
|
+
};
|
|
82
|
+
} catch (error) {
|
|
83
|
+
log.error("Polling Block Tracker: ", error);
|
|
84
|
+
throw new Error(`PollingBlockTracker - encountered error fetching block:\n${(error as Error).message}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default PollingBlockTracker;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { BaseCurrencyController, BaseCurrencyControllerConfig, BaseCurrencyControllerState } from "@toruslabs/base-controllers";
|
|
2
|
+
import { get } from "@toruslabs/http-helpers";
|
|
3
|
+
import log from "loglevel";
|
|
4
|
+
|
|
5
|
+
import { idleTimeTracker } from "../utils/helpers";
|
|
6
|
+
import { EthereumNetworkState } from "../utils/interfaces";
|
|
7
|
+
|
|
8
|
+
export interface IEthereumCurrencyControllerState extends BaseCurrencyControllerState {
|
|
9
|
+
commonDenomination: string;
|
|
10
|
+
commonDenominatorPrice: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default class CurrencyController extends BaseCurrencyController<BaseCurrencyControllerConfig, IEthereumCurrencyControllerState> {
|
|
14
|
+
private conversionInterval: number;
|
|
15
|
+
|
|
16
|
+
constructor({
|
|
17
|
+
config,
|
|
18
|
+
state,
|
|
19
|
+
onNetworkChanged,
|
|
20
|
+
}: {
|
|
21
|
+
config: Partial<BaseCurrencyControllerConfig>;
|
|
22
|
+
state: Partial<IEthereumCurrencyControllerState>;
|
|
23
|
+
onNetworkChanged: (listener: (networkState: EthereumNetworkState) => void) => void;
|
|
24
|
+
}) {
|
|
25
|
+
super({ config, state });
|
|
26
|
+
this.defaultState = {
|
|
27
|
+
...this.defaultState,
|
|
28
|
+
commonDenomination: "USD",
|
|
29
|
+
commonDenominatorPrice: 0,
|
|
30
|
+
};
|
|
31
|
+
this.initialize();
|
|
32
|
+
onNetworkChanged((networkState) => {
|
|
33
|
+
// to be called as (listener) => this.networkController.on('networkDidChange', listener);
|
|
34
|
+
if (networkState.providerConfig.ticker.toUpperCase() !== this.state.nativeCurrency.toUpperCase()) {
|
|
35
|
+
this.setNativeCurrency(networkState.providerConfig.ticker);
|
|
36
|
+
this.updateConversionRate();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public setCommonDenomination(commonDenomination: string): void {
|
|
42
|
+
this.update({ commonDenomination });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public getCommonDenomination(): string {
|
|
46
|
+
return this.state.commonDenomination;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public setCommonDenominatorPrice(commonDenominatorPrice: number): void {
|
|
50
|
+
this.update({ commonDenominatorPrice });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public getCommonDenominatorPrice(): number {
|
|
54
|
+
return this.state.commonDenominatorPrice;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Creates a new poll, using setInterval, to periodically call updateConversionRate. The id of the interval is
|
|
59
|
+
* stored at the controller's conversionInterval property. If it is called and such an id already exists, the
|
|
60
|
+
* previous interval is clear and a new one is created.
|
|
61
|
+
*/
|
|
62
|
+
public scheduleConversionInterval(): void {
|
|
63
|
+
if (this.conversionInterval) {
|
|
64
|
+
window.clearInterval(this.conversionInterval);
|
|
65
|
+
}
|
|
66
|
+
this.conversionInterval = window.setInterval(() => {
|
|
67
|
+
if (!idleTimeTracker.checkIfIdle()) {
|
|
68
|
+
this.updateConversionRate();
|
|
69
|
+
}
|
|
70
|
+
}, this.config.pollInterval);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Updates the conversionRate and conversionDate properties associated with the currentCurrency. Updated info is
|
|
75
|
+
* fetched from an external API
|
|
76
|
+
*/
|
|
77
|
+
public async updateConversionRate() {
|
|
78
|
+
const currentCurrency = this.getCurrentCurrency();
|
|
79
|
+
const nativeCurrency = this.getNativeCurrency();
|
|
80
|
+
const commonDenomination = this.getCommonDenomination();
|
|
81
|
+
const conversionRate = await this.retrieveConversionRate(nativeCurrency, currentCurrency, commonDenomination);
|
|
82
|
+
|
|
83
|
+
const currentCurrencyRate = Number.parseFloat(conversionRate[currentCurrency.toUpperCase()]);
|
|
84
|
+
const commonDenominationRate = Number.parseFloat(conversionRate[commonDenomination.toUpperCase()]);
|
|
85
|
+
// set conversion rate
|
|
86
|
+
if (currentCurrencyRate || commonDenominationRate) {
|
|
87
|
+
// ETC
|
|
88
|
+
this.setConversionRate(currentCurrencyRate);
|
|
89
|
+
this.setConversionDate(Math.floor(Date.now() / 1000).toString());
|
|
90
|
+
if (currentCurrency.toUpperCase() === commonDenomination.toUpperCase()) {
|
|
91
|
+
this.setCommonDenominatorPrice(currentCurrencyRate);
|
|
92
|
+
} else {
|
|
93
|
+
this.setCommonDenominatorPrice(commonDenominationRate);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
this.setConversionRate(0);
|
|
97
|
+
this.setConversionDate("N/A");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private async retrieveConversionRate(fromCurrency: string, toCurrency: string, commonDenomination: string): Promise<Record<string, string>> {
|
|
102
|
+
try {
|
|
103
|
+
// query cryptocompare
|
|
104
|
+
let apiUrl = `${this.config.api}/currency?fsym=${fromCurrency.toUpperCase()}&tsyms=${toCurrency.toUpperCase()}`;
|
|
105
|
+
if (commonDenomination && commonDenomination.toUpperCase() !== toCurrency.toUpperCase()) {
|
|
106
|
+
apiUrl += `,${commonDenomination.toUpperCase()}`;
|
|
107
|
+
}
|
|
108
|
+
const parsedResponse = await get<Record<string, string>>(apiUrl);
|
|
109
|
+
|
|
110
|
+
return parsedResponse;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
log.error(error, `CurrencyController - updateCommonDenominatorPrice: Failed to query rate for currency: ${fromCurrency}/ ${toCurrency}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { [toCurrency.toUpperCase()]: "0", [commonDenomination.toUpperCase()]: "0" };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { addHexPrefix, isHexString } from "@ethereumjs/util";
|
|
2
|
+
import { BaseController } from "@toruslabs/base-controllers";
|
|
3
|
+
import { SafeEventEmitterProvider } from "@toruslabs/openlogin-jrpc";
|
|
4
|
+
import { cloneDeep } from "lodash";
|
|
5
|
+
import log from "loglevel";
|
|
6
|
+
|
|
7
|
+
import NetworkController from "../Network/NetworkController";
|
|
8
|
+
import { GAS_ESTIMATE_TYPES } from "../utils/constants";
|
|
9
|
+
import { idleTimeTracker } from "../utils/helpers";
|
|
10
|
+
import { EthereumNetworkState } from "../utils/interfaces";
|
|
11
|
+
import {
|
|
12
|
+
calculateTimeEstimate,
|
|
13
|
+
fetchEthGasPriceEstimate as defaultFetchEthGasPriceEstimate,
|
|
14
|
+
fetchGasEstimates as defaultFetchGasEstimates,
|
|
15
|
+
fetchGasEstimatesViaEthFeeHistory as defaultFetchGasEstimatesViaEthFeeHistory,
|
|
16
|
+
fetchLegacyGasPriceEstimates as defaultFetchLegacyGasPriceEstimates,
|
|
17
|
+
} from "./gasUtil";
|
|
18
|
+
import { EthereumGasConfig, EthereumGasFeeEstimates, EthereumGasState, EthereumLegacyGasFeeEstimates, GasFeeTimeBounds } from "./IGasFeeController";
|
|
19
|
+
const GAS_FEE_API = "https://mock-gas-server.herokuapp.com/";
|
|
20
|
+
const LEGACY_GAS_PRICES_API_URL = "https://api.metaswap.codefi.network/gasPrices";
|
|
21
|
+
|
|
22
|
+
interface IGasFeeControllerOptions {
|
|
23
|
+
config?: Partial<EthereumGasConfig>;
|
|
24
|
+
state?: Partial<EthereumGasState>;
|
|
25
|
+
getNetworkIdentifier: NetworkController["getNetworkIdentifier"];
|
|
26
|
+
getProvider: NetworkController["getProvider"];
|
|
27
|
+
getCurrentNetworkEIP1559Compatibility: NetworkController["getEIP1559Compatibility"];
|
|
28
|
+
getCurrentAccountEIP1559Compatibility: (address: string) => boolean;
|
|
29
|
+
getCurrentNetworkLegacyGasAPICompatibility: () => boolean;
|
|
30
|
+
fetchGasEstimates?: (url: string) => Promise<EthereumGasFeeEstimates>;
|
|
31
|
+
fetchEthGasPriceEstimate?: (provider: SafeEventEmitterProvider) => Promise<{ gasPrice: string }>;
|
|
32
|
+
fetchLegacyGasPriceEstimates?: (url: string) => Promise<EthereumLegacyGasFeeEstimates>;
|
|
33
|
+
fetchGasEstimatesViaEthFeeHistory?: (provider: SafeEventEmitterProvider) => Promise<EthereumGasFeeEstimates>;
|
|
34
|
+
onNetworkStateChange: (listener: (networkState: EthereumNetworkState) => void) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns gas prices in dec gwei
|
|
39
|
+
*/
|
|
40
|
+
export default class GasFeeController extends BaseController<EthereumGasConfig, EthereumGasState> {
|
|
41
|
+
name = "GasFeeController";
|
|
42
|
+
|
|
43
|
+
private intervalId: number;
|
|
44
|
+
|
|
45
|
+
private provider: SafeEventEmitterProvider;
|
|
46
|
+
|
|
47
|
+
private currentChainId: string;
|
|
48
|
+
|
|
49
|
+
private getNetworkIdentifier: NetworkController["getNetworkIdentifier"];
|
|
50
|
+
|
|
51
|
+
private getProvider: NetworkController["getProvider"];
|
|
52
|
+
|
|
53
|
+
private fetchGasEstimates: (url: string) => Promise<EthereumGasFeeEstimates>;
|
|
54
|
+
|
|
55
|
+
private fetchGasEstimatesViaEthFeeHistory: (provider: SafeEventEmitterProvider) => Promise<EthereumGasFeeEstimates>;
|
|
56
|
+
|
|
57
|
+
private fetchEthGasPriceEstimate: (provider: SafeEventEmitterProvider) => Promise<{ gasPrice: string }>;
|
|
58
|
+
|
|
59
|
+
private fetchLegacyGasPriceEstimates: (url: string) => Promise<EthereumLegacyGasFeeEstimates>;
|
|
60
|
+
|
|
61
|
+
private getCurrentNetworkEIP1559Compatibility: NetworkController["getEIP1559Compatibility"];
|
|
62
|
+
|
|
63
|
+
private getCurrentAccountEIP1559Compatibility: (address?: string) => boolean;
|
|
64
|
+
|
|
65
|
+
private getCurrentNetworkLegacyGasAPICompatibility: () => boolean;
|
|
66
|
+
|
|
67
|
+
constructor({
|
|
68
|
+
config,
|
|
69
|
+
state,
|
|
70
|
+
getNetworkIdentifier,
|
|
71
|
+
getProvider,
|
|
72
|
+
fetchGasEstimates = defaultFetchGasEstimates,
|
|
73
|
+
fetchEthGasPriceEstimate = defaultFetchEthGasPriceEstimate,
|
|
74
|
+
fetchLegacyGasPriceEstimates = defaultFetchLegacyGasPriceEstimates,
|
|
75
|
+
fetchGasEstimatesViaEthFeeHistory = defaultFetchGasEstimatesViaEthFeeHistory,
|
|
76
|
+
getCurrentNetworkLegacyGasAPICompatibility,
|
|
77
|
+
getCurrentNetworkEIP1559Compatibility,
|
|
78
|
+
getCurrentAccountEIP1559Compatibility,
|
|
79
|
+
onNetworkStateChange,
|
|
80
|
+
}: IGasFeeControllerOptions) {
|
|
81
|
+
super({ config, state });
|
|
82
|
+
this.getNetworkIdentifier = getNetworkIdentifier;
|
|
83
|
+
this.getProvider = getProvider;
|
|
84
|
+
this.fetchGasEstimates = fetchGasEstimates;
|
|
85
|
+
this.fetchEthGasPriceEstimate = fetchEthGasPriceEstimate;
|
|
86
|
+
this.fetchLegacyGasPriceEstimates = fetchLegacyGasPriceEstimates;
|
|
87
|
+
this.getCurrentNetworkEIP1559Compatibility = getCurrentNetworkEIP1559Compatibility;
|
|
88
|
+
this.getCurrentNetworkLegacyGasAPICompatibility = getCurrentNetworkLegacyGasAPICompatibility;
|
|
89
|
+
this.getCurrentAccountEIP1559Compatibility = getCurrentAccountEIP1559Compatibility;
|
|
90
|
+
this.fetchGasEstimatesViaEthFeeHistory = fetchGasEstimatesViaEthFeeHistory;
|
|
91
|
+
|
|
92
|
+
this.defaultConfig = {
|
|
93
|
+
interval: 30_000,
|
|
94
|
+
legacyAPIEndpoint: LEGACY_GAS_PRICES_API_URL,
|
|
95
|
+
EIP1559APIEndpoint: GAS_FEE_API,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
this.defaultState = {
|
|
99
|
+
gasFeeEstimates: {} as EthereumGasFeeEstimates,
|
|
100
|
+
estimatedGasFeeTimeBounds: {},
|
|
101
|
+
gasEstimateType: GAS_ESTIMATE_TYPES.NONE,
|
|
102
|
+
};
|
|
103
|
+
// Initialize.
|
|
104
|
+
this.currentChainId = this.getNetworkIdentifier();
|
|
105
|
+
this.provider = this.getProvider();
|
|
106
|
+
this.initialize();
|
|
107
|
+
onNetworkStateChange(() => {
|
|
108
|
+
this.onNetworkStateChange();
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async onNetworkStateChange() {
|
|
113
|
+
this.provider = this.getProvider();
|
|
114
|
+
const newChainId = this.getNetworkIdentifier();
|
|
115
|
+
if (this.currentChainId !== newChainId) {
|
|
116
|
+
this.currentChainId = newChainId;
|
|
117
|
+
await this.resetPolling();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async resetPolling() {
|
|
122
|
+
this.stopPolling();
|
|
123
|
+
await this.getGasFeeEstimatesAndStartPolling();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async fetchGasFeeEstimates() {
|
|
127
|
+
return this._fetchGasFeeEstimateData();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async getGasFeeEstimatesAndStartPolling() {
|
|
131
|
+
await this._fetchGasFeeEstimateData();
|
|
132
|
+
this._startPolling();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
disconnectPoller() {
|
|
136
|
+
this.stopPolling();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Prepare to discard this controller.
|
|
141
|
+
*
|
|
142
|
+
* This stops any active polling.
|
|
143
|
+
*/
|
|
144
|
+
destroy() {
|
|
145
|
+
this.stopPolling();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
stopPolling() {
|
|
149
|
+
if (this.intervalId) {
|
|
150
|
+
clearInterval(this.intervalId);
|
|
151
|
+
}
|
|
152
|
+
this.resetState();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Gets and sets gasFeeEstimates in state
|
|
157
|
+
*
|
|
158
|
+
* @returns GasFeeEstimates
|
|
159
|
+
*/
|
|
160
|
+
private async _fetchGasFeeEstimateData(): Promise<EthereumGasState> {
|
|
161
|
+
let isEIP1559Compatible: boolean;
|
|
162
|
+
const isLegacyGasAPICompatible = this.getCurrentNetworkLegacyGasAPICompatibility();
|
|
163
|
+
|
|
164
|
+
const chainId = this.getNetworkIdentifier();
|
|
165
|
+
if (chainId === "loading") return;
|
|
166
|
+
let chainIdInt: number;
|
|
167
|
+
if (typeof chainId === "string" && isHexString(addHexPrefix(chainId))) {
|
|
168
|
+
chainIdInt = Number.parseInt(chainId, 16);
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
isEIP1559Compatible = await this.getEIP1559Compatibility();
|
|
172
|
+
log.info("eip1559 compatible", isEIP1559Compatible);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
log.warn(error);
|
|
175
|
+
isEIP1559Compatible = false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let newState = cloneDeep(this.defaultState);
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
if (isEIP1559Compatible) {
|
|
182
|
+
let estimates: EthereumGasFeeEstimates;
|
|
183
|
+
try {
|
|
184
|
+
estimates = await this.fetchGasEstimates(this.config.EIP1559APIEndpoint.replace("<chain_id>", `${chainIdInt}`));
|
|
185
|
+
} catch (error) {
|
|
186
|
+
estimates = await this.fetchGasEstimatesViaEthFeeHistory(this.provider);
|
|
187
|
+
}
|
|
188
|
+
const { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas } = estimates.medium;
|
|
189
|
+
const estimatedGasFeeTimeBounds = this.getTimeEstimate(suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas);
|
|
190
|
+
newState = {
|
|
191
|
+
gasFeeEstimates: estimates,
|
|
192
|
+
estimatedGasFeeTimeBounds,
|
|
193
|
+
gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,
|
|
194
|
+
};
|
|
195
|
+
} else if (isLegacyGasAPICompatible) {
|
|
196
|
+
const estimates = await this.fetchLegacyGasPriceEstimates(this.config.legacyAPIEndpoint.replace("<chain_id>", `${chainIdInt}`));
|
|
197
|
+
newState = {
|
|
198
|
+
gasFeeEstimates: estimates,
|
|
199
|
+
estimatedGasFeeTimeBounds: {},
|
|
200
|
+
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
|
|
201
|
+
};
|
|
202
|
+
} else {
|
|
203
|
+
throw new Error("Main gas fee/price estimation failed. Use fallback");
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
try {
|
|
207
|
+
const estimates = await this.fetchEthGasPriceEstimate(this.provider);
|
|
208
|
+
newState = {
|
|
209
|
+
gasFeeEstimates: estimates,
|
|
210
|
+
estimatedGasFeeTimeBounds: {},
|
|
211
|
+
gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE,
|
|
212
|
+
};
|
|
213
|
+
} catch (error) {
|
|
214
|
+
throw new Error(`Gas fee/price estimation failed. Message: ${(error as Error).message}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
this.update(newState);
|
|
218
|
+
|
|
219
|
+
return newState;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private async _startPolling() {
|
|
223
|
+
this._poll();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private async _poll() {
|
|
227
|
+
if (this.intervalId) {
|
|
228
|
+
window.clearInterval(this.intervalId);
|
|
229
|
+
}
|
|
230
|
+
this.intervalId = window.setInterval(async () => {
|
|
231
|
+
if (!idleTimeTracker.checkIfIdle()) {
|
|
232
|
+
await this._fetchGasFeeEstimateData();
|
|
233
|
+
}
|
|
234
|
+
}, this.config.interval);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private resetState() {
|
|
238
|
+
this.update(cloneDeep(this.defaultState));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private async getEIP1559Compatibility(): Promise<boolean> {
|
|
242
|
+
const currentNetworkIsEIP1559Compatible = await this.getCurrentNetworkEIP1559Compatibility();
|
|
243
|
+
const currentAccountIsEIP1559Compatible = this.getCurrentAccountEIP1559Compatibility?.() ?? true;
|
|
244
|
+
|
|
245
|
+
return currentNetworkIsEIP1559Compatible && currentAccountIsEIP1559Compatible;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private getTimeEstimate(maxPriorityFeePerGas: string, maxFeePerGas: string): GasFeeTimeBounds {
|
|
249
|
+
if (!this.state.gasFeeEstimates || this.state.gasEstimateType !== GAS_ESTIMATE_TYPES.FEE_MARKET) {
|
|
250
|
+
return {};
|
|
251
|
+
}
|
|
252
|
+
return calculateTimeEstimate(maxPriorityFeePerGas, maxFeePerGas, this.state.gasFeeEstimates as EthereumGasFeeEstimates);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { BaseConfig, BaseState } from "@toruslabs/base-controllers";
|
|
2
|
+
|
|
3
|
+
import { GAS_ESTIMATE_TYPES } from "../utils/constants";
|
|
4
|
+
|
|
5
|
+
export type GasFeeTimeBounds = {
|
|
6
|
+
lowerTimeBound?: null | number;
|
|
7
|
+
upperTimeBound?: string | number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type GasFeeEstimates = {
|
|
11
|
+
minWaitTimeEstimate: number;
|
|
12
|
+
maxWaitTimeEstimate: number;
|
|
13
|
+
suggestedMaxPriorityFeePerGas: string;
|
|
14
|
+
suggestedMaxFeePerGas: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type EthereumGasFeeEstimates = {
|
|
18
|
+
estimatedBaseFee: string;
|
|
19
|
+
low: GasFeeEstimates;
|
|
20
|
+
medium: GasFeeEstimates;
|
|
21
|
+
high: GasFeeEstimates;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type EthereumLegacyGasFeeEstimates = {
|
|
25
|
+
low: string;
|
|
26
|
+
medium: string;
|
|
27
|
+
high: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export interface GasEstimatesAPIResponse extends EthereumGasFeeEstimates {
|
|
31
|
+
estimatedBaseFee: string;
|
|
32
|
+
networkCongestion: number;
|
|
33
|
+
latestPriorityFeeRange: string[];
|
|
34
|
+
historicalPriorityFeeRange: string[];
|
|
35
|
+
historicalBaseFeeRange: string[];
|
|
36
|
+
priorityFeeTrend: "up" | "down" | "level";
|
|
37
|
+
baseFeeTrend: "up" | "down" | "level";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface GasEstimatesLegacyAPIResponse {
|
|
41
|
+
SafeGasPrice: string;
|
|
42
|
+
ProposeGasPrice: string;
|
|
43
|
+
FastGasPrice: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface EthereumGasConfig extends BaseConfig {
|
|
47
|
+
interval: number;
|
|
48
|
+
legacyAPIEndpoint: string;
|
|
49
|
+
EIP1559APIEndpoint: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface EthereumGasState extends BaseState {
|
|
53
|
+
gasFeeEstimates: EthereumGasFeeEstimates | EthereumLegacyGasFeeEstimates | { gasPrice?: string };
|
|
54
|
+
estimatedGasFeeTimeBounds: GasFeeTimeBounds;
|
|
55
|
+
gasEstimateType: (typeof GAS_ESTIMATE_TYPES)[keyof typeof GAS_ESTIMATE_TYPES];
|
|
56
|
+
}
|