From de458dece91c2d535a30b71044cf65034c080082 Mon Sep 17 00:00:00 2001 From: Jonathan Downing Date: Thu, 24 Aug 2023 14:11:55 -0500 Subject: [PATCH 1/3] Add private key export support and provider caching --- background/main.ts | 9 ++ background/redux-slices/keyrings.ts | 8 ++ .../chain/serial-fallback-provider.ts | 14 ++- background/services/keyring/index.ts | 7 ++ manifest/manifest.json | 2 +- ui/_locales/en/messages.json | 1 + .../AccountItem/AccountItemOptionsMenu.tsx | 92 ++++++++++++++++++- 7 files changed, 126 insertions(+), 7 deletions(-) diff --git a/background/main.ts b/background/main.ts index 16a1476d..8203b60a 100644 --- a/background/main.ts +++ b/background/main.ts @@ -189,6 +189,8 @@ import { import { isBuiltInNetworkBaseAsset } from "./redux-slices/utils/asset-utils" import { getPricePoint, getTokenPrices } from "./lib/prices" import localStorageShim from "./utils/local-storage-shim" +import { JsonRpcProvider, WebSocketProvider } from "@ethersproject/providers" +import { JsonRpcProvider as QuaisJsonRpcProvider, WebSocketProvider as QuaisWebSocketProvider } from "@quais/providers" // This sanitizer runs on store and action data before serializing for remote // redux devtools. The goal is to end up with an object that is directly @@ -298,6 +300,8 @@ export default class Main extends BaseService { public SelectedShard: string + public UrlToProvider: Map + balanceChecker: NodeJS.Timer static create: ServiceCreatorFunction = async () => { @@ -527,6 +531,7 @@ export default class Main extends BaseService { }) this.initializeRedux() + this.UrlToProvider = new Map() globalThis.main = this } @@ -1772,6 +1777,10 @@ export default class Main extends BaseService { return this.keyringService.unlock(password) } + async exportPrivKey(address: string): Promise { + return this.keyringService.exportPrivKey(address) + } + async getActivityDetails(txHash: string): Promise { const addressNetwork = this.store.getState().ui.selectedAccount const transaction = await this.chainService.getTransaction( diff --git a/background/redux-slices/keyrings.ts b/background/redux-slices/keyrings.ts index 4a64c128..aaf52586 100644 --- a/background/redux-slices/keyrings.ts +++ b/background/redux-slices/keyrings.ts @@ -39,6 +39,7 @@ export type Events = { generateNewKeyring: string | undefined deriveAddress: DeriveAddressData importKeyring: ImportKeyring + exportPrivKey: string } export const emitter = new Emittery() @@ -171,3 +172,10 @@ export const createPassword = createBackgroundAsyncThunk( await emitter.emit("createPassword", password) } ) + +export const exportPrivKey = createBackgroundAsyncThunk( + "keyrings/exportPrivKey", + async (address: string) => { + return { key: await main.exportPrivKey(address) } + } +) diff --git a/background/services/chain/serial-fallback-provider.ts b/background/services/chain/serial-fallback-provider.ts index 8b57034a..6da729bd 100644 --- a/background/services/chain/serial-fallback-provider.ts +++ b/background/services/chain/serial-fallback-provider.ts @@ -25,7 +25,6 @@ import { ALCHEMY_KEY, transactionFromAlchemyWebsocketTransaction, } from "../../lib/alchemy" -import { error } from "ajv/dist/vocabularies/applicator/dependencies" // Back off by this amount as a base, exponentiated by attempts and jittered. const BASE_BACKOFF_MS = 400 @@ -1031,11 +1030,18 @@ export function makeSerialFallbackProvider( type: "Quai" as const, creator: () => { const url = new URL(rpcUrl) + if(globalThis.main.UrlToProvider.has(rpcUrl)){ + return globalThis.main.UrlToProvider.get(rpcUrl) as WebSocketProvider | JsonRpcProvider | QuaisJsonRpcProvider | QuaisWebSocketProvider + } else { + console.log("Provider not found in map, creating new provider...") + } + let provider: WebSocketProvider | JsonRpcProvider | QuaisJsonRpcProvider | QuaisWebSocketProvider if (/^wss?/.test(url.protocol)) { - return new QuaisWebSocketProvider(rpcUrl) + provider = new QuaisWebSocketProvider(rpcUrl) } - - return new QuaisJsonRpcProvider(rpcUrl) + provider = new QuaisJsonRpcProvider(rpcUrl) + globalThis.main.UrlToProvider.set(rpcUrl, provider) + return provider }, shard: ShardFromRpcUrl(rpcUrl), rpcUrl: rpcUrl, diff --git a/background/services/keyring/index.ts b/background/services/keyring/index.ts index 6a9910eb..478258ca 100644 --- a/background/services/keyring/index.ts +++ b/background/services/keyring/index.ts @@ -358,6 +358,13 @@ export default class KeyringService extends BaseService { return newKeyring.id } + async exportPrivKey(address: string): Promise { + this.requireUnlocked() + let keyring = await this.#findKeyring(address) + const privKey = keyring.exportPrivateKey(address, "I solemnly swear that I am treating this private key material with great care.") + return privKey??"Not found" + } + /** * Return the source of a given address' keyring if it exists. If an * address does not have a keyring associated with it - returns null. diff --git a/manifest/manifest.json b/manifest/manifest.json index 575bfe3a..25d4796c 100644 --- a/manifest/manifest.json +++ b/manifest/manifest.json @@ -39,7 +39,7 @@ "default_title": "Pelagus", "default_popup": "popup.html" }, - "permissions": ["alarms", "storage", "unlimitedStorage", "activeTab", "runtime"], + "permissions": ["alarms", "storage", "unlimitedStorage", "activeTab"], "background": { "service_worker": "background.js" } diff --git a/ui/_locales/en/messages.json b/ui/_locales/en/messages.json index f61362a8..8df17b2c 100644 --- a/ui/_locales/en/messages.json +++ b/ui/_locales/en/messages.json @@ -13,6 +13,7 @@ "lastAccountWarningBody": "Are you sure you want to proceed?", "removeAddress": "Remove address", "copyAddress": "Copy address", + "exportAccount": "Export private key", "removeConfirm": "Yes, I want to remove it" }, "notificationPanel": { diff --git a/ui/components/AccountItem/AccountItemOptionsMenu.tsx b/ui/components/AccountItem/AccountItemOptionsMenu.tsx index cca60c31..f082e6b5 100644 --- a/ui/components/AccountItem/AccountItemOptionsMenu.tsx +++ b/ui/components/AccountItem/AccountItemOptionsMenu.tsx @@ -2,11 +2,14 @@ import { AccountTotal } from "@pelagus/pelagus-background/redux-slices/selectors import { setSnackbarMessage } from "@pelagus/pelagus-background/redux-slices/ui" import React, { ReactElement, useCallback, useState } from "react" import { useTranslation } from "react-i18next" -import { useBackgroundDispatch } from "../../hooks" +import { useAreKeyringsUnlocked, useBackgroundDispatch } from "../../hooks" import SharedDropdown from "../Shared/SharedDropDown" import SharedSlideUpMenu from "../Shared/SharedSlideUpMenu" import AccountItemEditName from "./AccountItemEditName" import AccountItemRemovalConfirm from "./AccountItemRemovalConfirm" +import { useHistory } from "react-router-dom" +import { exportPrivKey } from "@pelagus/pelagus-background/redux-slices/keyrings" +import AccountitemOptionLabel from "./AccountItemOptionLabel" type AccountItemOptionsMenuProps = { accountTotal: AccountTotal @@ -19,9 +22,13 @@ export default function AccountItemOptionsMenu({ keyPrefix: "accounts.accountItem", }) const dispatch = useBackgroundDispatch() + const history = useHistory() + const areKeyringsUnlocked = useAreKeyringsUnlocked(false) const { address, network } = accountTotal const [showAddressRemoveConfirm, setShowAddressRemoveConfirm] = useState(false) + const [key, setKey] = useState("") + const [showExportPrivateKey, setShowExportPrivateKey] = useState(false) const [showEditName, setShowEditName] = useState(false) const copyAddress = useCallback(() => { @@ -29,6 +36,11 @@ export default function AccountItemOptionsMenu({ dispatch(setSnackbarMessage("Address copied to clipboard")) }, [address, dispatch]) + const copyKey = useCallback(() => { + navigator.clipboard.writeText(key) + dispatch(setSnackbarMessage("Key copied to clipboard")) + }, [key, dispatch]) + return (
+ >
e.stopPropagation()} @@ -72,6 +84,36 @@ export default function AccountItemOptionsMenu({ />
+ { + e?.stopPropagation() + setKey("") + setShowExportPrivateKey(false) + }} + > +
  • +
    +
    Private Key
    + {key} +
    +
  • + +
    (
    From 93acc44d94d7bd8a15d8b7b1c048c1242ee7e0e1 Mon Sep 17 00:00:00 2001 From: Jonathan Downing Date: Fri, 25 Aug 2023 12:44:47 -0500 Subject: [PATCH 2/3] Bugfix: Wallet now works with web3 dapps, also reduced frequency of tx lookups --- background/lib/erc20.ts | 3 ++- background/main.ts | 4 ++-- background/redux-slices/utils/contract-utils.ts | 15 ++++++++++++--- background/services/chain/asset-data-helper.ts | 3 +++ background/services/chain/index.ts | 16 ++++++++-------- .../services/chain/serial-fallback-provider.ts | 1 + background/services/chain/utils/index.ts | 2 +- background/services/enrichment/transactions.ts | 16 +++++++++++++++- background/services/indexing/index.ts | 4 ++-- .../services/internal-ethereum-provider/index.ts | 16 +++++++++------- background/services/name/resolvers/ens.ts | 5 +++-- background/services/name/resolvers/rns.ts | 1 + background/services/name/resolvers/uns.ts | 1 + 13 files changed, 60 insertions(+), 27 deletions(-) diff --git a/background/lib/erc20.ts b/background/lib/erc20.ts index 4e757205..f853e56b 100644 --- a/background/lib/erc20.ts +++ b/background/lib/erc20.ts @@ -20,6 +20,7 @@ import { import logger from "./logger" import SerialFallbackProvider from "../services/chain/serial-fallback-provider" import { ShardToMulticall, getShardFromAddress } from "../constants" +import { get } from "lodash" export const ERC20_FUNCTIONS = { allowance: FunctionFragment.from( @@ -202,7 +203,7 @@ export const getTokenBalances = async ( CHAIN_SPECIFIC_MULTICALL_CONTRACT_ADDRESSES[network.chainID] || MULTICALL_CONTRACT_ADDRESS if (network.isQuai) { - multicallAddress = ShardToMulticall(globalThis.main.SelectedShard, network) + multicallAddress = ShardToMulticall(getShardFromAddress(address), network) } const contract = new quais.Contract( diff --git a/background/main.ts b/background/main.ts index 8203b60a..5d23eba3 100644 --- a/background/main.ts +++ b/background/main.ts @@ -965,9 +965,9 @@ export default class Main extends BaseService { const signedTransactionResult = await this.signingService.signTransaction(request, accountSigner) await this.store.dispatch(transactionSigned(signedTransactionResult)) - transactionConstructionSliceEmitter.emit("signedTransactionResult", + setTimeout(() => transactionConstructionSliceEmitter.emit("signedTransactionResult", signedTransactionResult - ) + ), 1000) // could check broadcastOnSign here and broadcast if false but this is a hacky solution (could result in tx broadcasted twice) this.analyticsService.sendAnalyticsEvent( AnalyticsEvent.TRANSACTION_SIGNED, { diff --git a/background/redux-slices/utils/contract-utils.ts b/background/redux-slices/utils/contract-utils.ts index 4df250d8..1bb05144 100644 --- a/background/redux-slices/utils/contract-utils.ts +++ b/background/redux-slices/utils/contract-utils.ts @@ -2,6 +2,7 @@ import TallyWindowProvider from "@tallyho/window-provider" import { Contract, ethers, ContractInterface } from "ethers" import Emittery from "emittery" import TallyWeb3Provider from "../../tally-provider" +import { SECOND } from "../../constants" type InternalProviderPortEvents = { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -53,8 +54,16 @@ export const getSignerAddress = async (): Promise => { return signerAddress } +var latestBlockNumber = 0 +var latestAsk = 0 export const getCurrentTimestamp = async (): Promise => { - const provider = getProvider() - const { timestamp } = await provider.getBlock(provider.getBlockNumber()) - return timestamp + if(latestAsk + 10 * SECOND < Date.now()) { + const provider = getProvider() + const { timestamp } = await provider.getBlock(provider.getBlockNumber()) + latestBlockNumber = timestamp + latestAsk = Date.now() + return timestamp + } else { + return latestBlockNumber + } } diff --git a/background/services/chain/asset-data-helper.ts b/background/services/chain/asset-data-helper.ts index ee2cb0ce..83cbfc33 100644 --- a/background/services/chain/asset-data-helper.ts +++ b/background/services/chain/asset-data-helper.ts @@ -68,9 +68,12 @@ export default class AssetDataHelper { addressOnNetwork: AddressOnNetwork, smartContractAddresses?: HexString[] ): Promise { + let prevShard = globalThis.main.GetShard() + globalThis.main.SetShard(getShardFromAddress(addressOnNetwork.address)) const provider = this.providerTracker.providerForNetwork( addressOnNetwork.network ) + globalThis.main.SetShard(prevShard) if (typeof provider === "undefined") { return [] } diff --git a/background/services/chain/index.ts b/background/services/chain/index.ts index 4b9f4f38..91fdef38 100644 --- a/background/services/chain/index.ts +++ b/background/services/chain/index.ts @@ -1,9 +1,9 @@ import { TransactionReceipt, TransactionResponse, -} from "@ethersproject/providers" -import { BigNumber, ethers, utils } from "ethers" -import { Logger, UnsignedTransaction } from "ethers/lib/utils" +} from "@quais/providers" +import { BigNumber, quais, utils } from "quais" +import { Logger, UnsignedTransaction } from "quais/lib/utils" import logger from "../../lib/logger" import getBlockPrices from "../../lib/gas" import { HexString, NormalizedEVMAddress, UNIXTime } from "../../types" @@ -97,9 +97,9 @@ const NETWORK_POLLING_TIMEOUT = MINUTE * 2.05 // mempool) reasons. const TRANSACTION_CHECK_LIFETIME_MS = 10 * HOUR -const GAS_POLLS_PER_PERIOD = 4 // 4 times per minute +const GAS_POLLS_PER_PERIOD = 1 // 1 time per 5 minutes -const GAS_POLLING_PERIOD = 1 // 1 minute +const GAS_POLLING_PERIOD = 5 // 5 minutes // Maximum number of transactions with priority. // Transactions that will be retrieved before others for one account. @@ -254,7 +254,7 @@ export default class ChainService extends BaseService { super({ queuedTransactions: { schedule: { - delayInMinutes: 0.5, + delayInMinutes: 1, periodInMinutes: 1, }, handler: () => { @@ -633,7 +633,7 @@ export default class ChainService extends BaseService { maxFeePerGas: maxFeePerGas ?? 0n, maxPriorityFeePerGas: maxPriorityFeePerGas ?? 0n, input: input ?? null, - type: 2 as const, + type: network.isQuai ? 0 as const : 2 as const, network, chainID: network.chainID, nonce, @@ -1174,7 +1174,7 @@ export default class ChainService extends BaseService { const provider = await this.providerForNetworkOrThrow(network) - const GasOracle = new ethers.Contract( + const GasOracle = new quais.Contract( OPTIMISM_GAS_ORACLE_ADDRESS, OPTIMISM_GAS_ORACLE_ABI, provider diff --git a/background/services/chain/serial-fallback-provider.ts b/background/services/chain/serial-fallback-provider.ts index 6da729bd..4e7997cd 100644 --- a/background/services/chain/serial-fallback-provider.ts +++ b/background/services/chain/serial-fallback-provider.ts @@ -361,6 +361,7 @@ export default class SerialFallbackProvider extends QuaisJsonRpcProvider { ) ) { if (this.shouldSendMessageOnNextProvider(messageId)) { + console.trace() // If there is another provider to try - try to send the message on that provider if (this.currentProviderIndex + 1 < this.providerCreators.length) { return await this.attemptToSendMessageOnNewProvider(messageId) diff --git a/background/services/chain/utils/index.ts b/background/services/chain/utils/index.ts index b2ce21ba..08b7ec1f 100644 --- a/background/services/chain/utils/index.ts +++ b/background/services/chain/utils/index.ts @@ -7,7 +7,7 @@ import { import { Transaction as EthersTransaction, UnsignedTransaction, -} from "@ethersproject/transactions" +} from "@quais/transactions" import { AnyEVMTransaction, diff --git a/background/services/enrichment/transactions.ts b/background/services/enrichment/transactions.ts index 939ad70d..c50fb1b7 100644 --- a/background/services/enrichment/transactions.ts +++ b/background/services/enrichment/transactions.ts @@ -28,7 +28,7 @@ import { getERC20LogsForAddresses, } from "./utils" import { enrichAddressOnNetwork } from "./addresses" -import { OPTIMISM } from "../../constants" +import { OPTIMISM, SECOND } from "../../constants" import { parseLogsForWrappedDepositsAndWithdrawals } from "../../lib/wrappedAsset" import { ERC20TransferLog, @@ -183,6 +183,8 @@ export async function annotationsFromLogs( * Resolve an annotation for a partial transaction request, or a pending * or mined transaction. */ +var latestWorkedAsk = 0 +var numAsks = 0 export default async function resolveTransactionAnnotation( chainService: ChainService, indexingService: IndexingService, @@ -198,6 +200,7 @@ export default async function resolveTransactionAnnotation( }), desiredDecimals: number ): Promise { + const assets = await indexingService.getCachedAssets(network) // By default, annotate all requests as contract interactions, unless they @@ -216,6 +219,17 @@ export default async function resolveTransactionAnnotation( )?.metadata?.logoURL, } + if(numAsks > 10 && latestWorkedAsk + 5 * SECOND > Date.now()) { + // Requesting too often + console.log("Requesting tx annotations too often, skipping") + return txAnnotation + } else if (numAsks > 10 && latestWorkedAsk + 5 * SECOND < Date.now()) { + // Reset + numAsks = 0 + } + numAsks++ + latestWorkedAsk = Date.now() + let block: AnyEVMBlock | undefined const { diff --git a/background/services/indexing/index.ts b/background/services/indexing/index.ts index d5ac5ef9..49725e87 100644 --- a/background/services/indexing/index.ts +++ b/background/services/indexing/index.ts @@ -55,7 +55,7 @@ import { const FAST_TOKEN_REFRESH_BLOCK_RANGE = 10 // The number of ms to coalesce tokens whose balances are known to have changed // before balance-checking them. -const ACCELERATED_TOKEN_REFRESH_TIMEOUT = 300 +const ACCELERATED_TOKEN_REFRESH_TIMEOUT = 3000 interface Events extends ServiceLifecycleEvents { accountsWithBalances: { @@ -517,7 +517,7 @@ export default class IndexingService extends BaseService { const balances = await this.chainService.assetData.getTokenBalances( addressNetwork, - smartContractAssets?.map(({ contractAddress }) => contractAddress) + smartContractAssets?.map(({ contractAddress }) => getShardFromAddress(contractAddress) == getShardFromAddress(addressNetwork.address) ? contractAddress : "") ) const listedAssetByAddress = (smartContractAssets ?? []).reduce<{ diff --git a/background/services/internal-ethereum-provider/index.ts b/background/services/internal-ethereum-provider/index.ts index 576ea366..66ef674a 100644 --- a/background/services/internal-ethereum-provider/index.ts +++ b/background/services/internal-ethereum-provider/index.ts @@ -1,5 +1,5 @@ -import { TransactionRequest as EthersTransactionRequest } from "@ethersproject/abstract-provider" -import { serialize as serializeEthersTransaction } from "@ethersproject/transactions" +import { TransactionRequest as EthersTransactionRequest } from "@quais/abstract-provider" +import { serialize as serializeEthersTransaction } from "@quais/transactions" import { EIP1193Error, @@ -384,15 +384,15 @@ export default class InternalEthereumProviderService extends BaseService async getCurrentOrDefaultNetworkForOrigin( origin: string ): Promise { - /*const currentNetwork = await this.db.getCurrentNetworkForOrigin(origin) + const currentNetwork = await this.db.getCurrentNetworkForOrigin(origin) if (!currentNetwork) { // If this is a new dapp or the dapp has not implemented wallet_switchEthereumChain // use the default network. const defaultNetwork = await this.getCurrentInternalNetwork() return defaultNetwork } - return currentNetwork*/ - return QUAI_NETWORK + return currentNetwork + //return QUAI_NETWORK } async removePrefererencesForChain(chainId: string): Promise { @@ -415,9 +415,11 @@ export default class InternalEthereumProviderService extends BaseService throw new Error("Transactions must have a from address for signing.") } - const currentNetwork = await this.getCurrentOrDefaultNetworkForOrigin( + /*let currentNetwork = await this.getCurrentOrDefaultNetworkForOrigin( origin - ) + )*/ + const currentNetwork = globalThis.main.store.getState().ui.selectedAccount.network + const isRootstock = currentNetwork.chainID === ROOTSTOCK.chainID diff --git a/background/services/name/resolvers/ens.ts b/background/services/name/resolvers/ens.ts index 46a19b68..bd7cc8a4 100644 --- a/background/services/name/resolvers/ens.ts +++ b/background/services/name/resolvers/ens.ts @@ -100,6 +100,7 @@ export default function ensResolverFor( address, network, }: AddressOnNetwork): Promise { + return undefined const name = await chainService // Hard-coded to ETHEREUM to support ENS names on ETH L2's. .providerForNetwork(QUAI_NETWORK) @@ -109,10 +110,10 @@ export default function ensResolverFor( return undefined } - return { + /*return { name, network, - } + }*/ }, } } diff --git a/background/services/name/resolvers/rns.ts b/background/services/name/resolvers/rns.ts index 85979c5d..b7b56b66 100644 --- a/background/services/name/resolvers/rns.ts +++ b/background/services/name/resolvers/rns.ts @@ -84,6 +84,7 @@ export default function rnsResolver(): NameResolver<"RNS"> { address, network, }: AddressOnNetwork): Promise { + return undefined const reverseRecordHash = utils.namehash( `${stripHexPrefix(address)}.addr.reverse` ) diff --git a/background/services/name/resolvers/uns.ts b/background/services/name/resolvers/uns.ts index a51465bf..d7b91933 100644 --- a/background/services/name/resolvers/uns.ts +++ b/background/services/name/resolvers/uns.ts @@ -206,6 +206,7 @@ export default function unsResolver(): NameResolver<"UNS"> { address, network, }: AddressOnNetwork): Promise { + return undefined // Get all the records associated with the particular ETH address const data = (await reverseLookupAddress(address))?.data // Since for a given address you can have multiple UNS records, we just pick the first one From 90f27681595084651d9808d41d3b1da09aca23c2 Mon Sep 17 00:00:00 2001 From: Jonathan Downing Date: Fri, 1 Sep 2023 16:58:04 -0500 Subject: [PATCH 3/3] Feature: Send page has advanced tab with nonce, gas limit, max fee, miner tip --- background/redux-slices/assets.ts | 34 ++++++++++-- ui/pages/Send.tsx | 92 ++++++++++++++++++++++++++++++- 2 files changed, 120 insertions(+), 6 deletions(-) diff --git a/background/redux-slices/assets.ts b/background/redux-slices/assets.ts index 22283627..9420d427 100644 --- a/background/redux-slices/assets.ts +++ b/background/redux-slices/assets.ts @@ -30,6 +30,7 @@ import type { RootState } from "." import { hexlify, stripZeros, toUtf8Bytes, accessListify, hexConcat, RLP } from "quais/lib/utils" import { emitter as transactionConstructionSliceEmitter } from "./transaction-construction" import { AccountSigner } from "../services/signing" +import { normalizeEVMAddress } from "../lib/utils"; export type AssetWithRecentPrices = T & { @@ -209,6 +210,25 @@ export const removeAssetData = createBackgroundAsyncThunk( } ) +export const getAccountNonceAndGasPrice = createBackgroundAsyncThunk( + "assets/getAccountNonceAndGasPrice", + async (details: { + network: EVMNetwork, + address: string, + } + ): Promise<{ nonce: number; maxFeePerGas: string, maxPriorityFeePerGas: string }> => { + const prevShard = globalThis.main.GetShard() + globalThis.main.SetShard(getShardFromAddress(details.address)) + const provider = globalThis.main.chainService.providerForNetworkOrThrow(details.network) + const normalizedAddress = normalizeEVMAddress(details.address) + const nonce = await provider.getTransactionCount(normalizedAddress) + const feeData = await provider.getFeeData() + globalThis.main.SetShard(prevShard) + return { nonce, maxFeePerGas: feeData.maxFeePerGas ? feeData.maxFeePerGas.toString() : '0', maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ? feeData.maxPriorityFeePerGas.toString() : '0' } + } +) + + /** * Executes an asset transfer between two addresses, for a set amount. Supports * an optional fixed gas limit. @@ -224,6 +244,8 @@ export const transferAsset = createBackgroundAsyncThunk( assetAmount: AnyAssetAmount gasLimit?: bigint nonce?: number + maxPriorityFeePerGas?: bigint + maxFeePerGas?: bigint accountSigner: AccountSigner }) => { var { @@ -232,6 +254,8 @@ export const transferAsset = createBackgroundAsyncThunk( assetAmount, gasLimit, nonce, + maxPriorityFeePerGas, + maxFeePerGas, accountSigner, } = transferDetails; @@ -280,7 +304,7 @@ export const transferAsset = createBackgroundAsyncThunk( } } - let tx = genQuaiRawTransaction(fromNetwork, fromAddress, toAddress, assetAmount, nonce, fromNetwork.chainID, data) + let tx = genQuaiRawTransaction(fromNetwork, fromAddress, toAddress, assetAmount, nonce, fromNetwork.chainID, data, gasLimit??BigInt(200000), maxFeePerGas??BigInt(2000000000), maxPriorityFeePerGas??BigInt(1000000000)) signData({transaction: tx, accountSigner: accountSigner}) return } @@ -330,7 +354,7 @@ export const transferAsset = createBackgroundAsyncThunk( } ) -function genQuaiRawTransaction (network: EVMNetwork, fromAddress: string, toAddress: string, assetAmount: AnyAssetAmount, nonce: number, chainId: string, data: string): EIP1559TransactionRequest { +function genQuaiRawTransaction (network: EVMNetwork, fromAddress: string, toAddress: string, assetAmount: AnyAssetAmount, nonce: number, chainId: string, data: string, gasLimit: bigint, maxFeePerGas: bigint, maxPriorityFeePerGas: bigint): EIP1559TransactionRequest { let isExternal = false let type = 0 @@ -352,9 +376,9 @@ function genQuaiRawTransaction (network: EVMNetwork, fromAddress: string, toAddr from: fromAddress, value: assetAmount.amount, nonce: nonce, - gasLimit: BigInt(200000), - maxFeePerGas: BigInt(2000000000), - maxPriorityFeePerGas: BigInt(1000000000), + gasLimit: gasLimit??BigInt(200000), + maxFeePerGas: maxFeePerGas, + maxPriorityFeePerGas: maxPriorityFeePerGas, type: type as KnownTxTypes, chainID: chainId, input: data, diff --git a/ui/pages/Send.tsx b/ui/pages/Send.tsx index 836c2dd8..1f5b4eea 100644 --- a/ui/pages/Send.tsx +++ b/ui/pages/Send.tsx @@ -24,6 +24,7 @@ import { parseToFixedPointNumber, } from "@pelagus/pelagus-background/lib/fixed-point" import { + getAccountNonceAndGasPrice, selectAssetPricePoint, transferAsset, } from "@pelagus/pelagus-background/redux-slices/assets" @@ -51,6 +52,7 @@ import { import SharedLoadingSpinner from "../components/Shared/SharedLoadingSpinner" import ReadOnlyNotice from "../components/Shared/ReadOnlyNotice" import SharedIcon from "../components/Shared/SharedIcon" +import { BigNumber } from "ethers" export default function Send(): ReactElement { const { t } = useTranslation() @@ -69,6 +71,13 @@ export default function Send(): ReactElement { const [assetType, setAssetType] = useState<"token" | "nft">("token") const [selectedNFT, setSelectedNFT] = useState(null) + const [nonce, setNonce] = useState(0) + const [maxFeePerGas, setMaxFeePerGas] = useState(BigNumber.from(0)) + const [maxPriorityFeePerGas, setMaxPriorityFeePerGas] = useState(BigNumber.from(0)) + const [gasLimit, setGasLimit] = useState(100000) + + const [advancedVisible, setAdvancedVisible] = useState(false); + const handleAssetSelect = (asset: FungibleAsset) => { setSelectedAsset(asset) setAssetType("token") @@ -82,10 +91,17 @@ export default function Send(): ReactElement { } else { setSelectedAsset(currentAccount.network.baseAsset) } + + dispatch(getAccountNonceAndGasPrice(currentAccount)).then( ({nonce, maxFeePerGas, maxPriorityFeePerGas}) => { + setNonce(nonce) + setMaxFeePerGas(BigNumber.from(maxFeePerGas)) + setMaxPriorityFeePerGas(BigNumber.from(maxPriorityFeePerGas)) + }) + // This disable is here because we don't necessarily have equality-by-reference // due to how we persist the ui redux slice with webext-redux. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentAccount.network.baseAsset.symbol]) + }, [currentAccount.network.baseAsset.symbol, currentAccount]) const [destinationAddress, setDestinationAddress] = useState< string | undefined @@ -166,6 +182,10 @@ export default function Send(): ReactElement { }, assetAmount, accountSigner: currentAccountSigner, + gasLimit: BigInt(gasLimit), + nonce: nonce, + maxFeePerGas: maxFeePerGas.toBigInt(), + maxPriorityFeePerGas: maxPriorityFeePerGas.toBigInt(), }) ) } finally { @@ -259,6 +279,59 @@ export default function Send(): ReactElement { resolved_address: resolvedNameToAddress, })} /> + + {advancedVisible && ( +
    + + setNonce(parseInt(event.target.value))} + className={classNames({ + error: addressErrorMessage !== undefined, + resolved_address: resolvedNameToAddress, + })} + /> + + setGasLimit(parseInt(event.target.value))} + className={classNames({ + error: addressErrorMessage !== undefined, + resolved_address: resolvedNameToAddress, + })} + /> + + setMaxFeePerGas(BigNumber.from(event.target.value))} + className={classNames({ + error: addressErrorMessage !== undefined, + resolved_address: resolvedNameToAddress, + })} + /> + + setMaxPriorityFeePerGas(BigNumber.from(event.target.value))} + className={classNames({ + error: addressErrorMessage !== undefined, + resolved_address: resolvedNameToAddress, + })} + /> +
    + )} {addressIsValidating ? (

    @@ -386,6 +459,23 @@ export default function Send(): ReactElement { transition: padding-bottom 0.2s; } + + input#send_address_alt { + box-sizing: border-box; + height: 40px; + width: 100%; + + font-size: 22px; + font-weight: 500; + line-height: 72px; + color: #fff; + + border-radius: 4px; + background-color: var(--green-95); + padding: 0px 16px; + + transition: padding-bottom 0.2s; + } input#send_address::placeholder { color: var(--green-40); }