From d5d6714d69c5b3608106cc9bab57639ce03d0a05 Mon Sep 17 00:00:00 2001 From: 0xDaedalus <0xDaedalus@users.noreply.github.com> Date: Wed, 23 Mar 2022 16:00:57 -0600 Subject: [PATCH 01/30] Display ETH as an option in 'Swap To:' --- ui/pages/Swap.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ui/pages/Swap.tsx b/ui/pages/Swap.tsx index 5845276489..45cac68d75 100644 --- a/ui/pages/Swap.tsx +++ b/ui/pages/Swap.tsx @@ -18,6 +18,7 @@ import { selectCurrentAccountBalances } from "@tallyho/tally-background/redux-sl import { AnyAsset, FungibleAsset, + isFungibleAsset, isSmartContractFungibleAsset, SmartContractFungibleAsset, } from "@tallyho/tally-background/assets" @@ -93,7 +94,11 @@ export default function Swap(): ReactElement { // Some type massaging needed to remind TypeScript how these types fit // together. const knownAssets: AnyAsset[] = state.assets - return knownAssets.filter(isSmartContractFungibleAsset) + return knownAssets.filter( + (asset): asset is SmartContractFungibleAsset | FungibleAsset => + isSmartContractFungibleAsset(asset) || + (isFungibleAsset(asset) && asset.symbol === "ETH") + ) }) const { From 34fe1c5061c23a4487d87cdc2e255316e57882ad Mon Sep 17 00:00:00 2001 From: 0xDaedalus <0xDaedalus@users.noreply.github.com> Date: Wed, 23 Mar 2022 16:03:54 -0600 Subject: [PATCH 02/30] Add note about ETH hard-code --- ui/pages/Swap.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/pages/Swap.tsx b/ui/pages/Swap.tsx index 45cac68d75..4562b50c8c 100644 --- a/ui/pages/Swap.tsx +++ b/ui/pages/Swap.tsx @@ -97,6 +97,8 @@ export default function Swap(): ReactElement { return knownAssets.filter( (asset): asset is SmartContractFungibleAsset | FungibleAsset => isSmartContractFungibleAsset(asset) || + // Explicity add ETH even though it is not an ERC-20 token + // @TODO change as part of multi-network refactor. (isFungibleAsset(asset) && asset.symbol === "ETH") ) }) From 931c6a075314593fc4d8bdf6c1a583b16bc3fd92 Mon Sep 17 00:00:00 2001 From: 0xDaedalus <0xDaedalus@users.noreply.github.com> Date: Thu, 24 Mar 2022 13:26:47 -0500 Subject: [PATCH 03/30] Allow ETH <> WETH swaps. We need to add a special exception to get ETH <> WETH swaps working because of an issue in the 0x api. This is a temporary solution and details are commented in code. --- background/constants/index.ts | 2 ++ background/redux-slices/0x-swap.ts | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/background/constants/index.ts b/background/constants/index.ts index c878dbfaae..6650412175 100644 --- a/background/constants/index.ts +++ b/background/constants/index.ts @@ -26,5 +26,7 @@ export const DAY = 24 * HOUR export const COMMUNITY_MULTISIG_ADDRESS = "0x99b36fDbC582D113aF36A21EBa06BFEAb7b9bE12" +export const MAINNET_WETH_ADDRESS = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + export * from "./currencies" export * from "./networks" diff --git a/background/redux-slices/0x-swap.ts b/background/redux-slices/0x-swap.ts index 52db23d68c..b3ac2749d9 100644 --- a/background/redux-slices/0x-swap.ts +++ b/background/redux-slices/0x-swap.ts @@ -12,7 +12,7 @@ import { } from "../lib/validate" import { getProvider } from "./utils/contract-utils" import { ERC20_ABI } from "../lib/erc20" -import { COMMUNITY_MULTISIG_ADDRESS } from "../constants" +import { COMMUNITY_MULTISIG_ADDRESS, MAINNET_WETH_ADDRESS } from "../constants" interface SwapAssets { sellAsset: SmartContractFungibleAsset | FungibleAsset @@ -47,6 +47,10 @@ export const initialState: SwapState = { inProgressApprovalContract: undefined, } +const is0xWETHWrap = (buyAddress: string, sellAddress: string) => { + return buyAddress === MAINNET_WETH_ADDRESS && buyAddress === sellAddress +} + // The magic string used by the 0x API to signify we're dealing with ETH rather // than an ERC-20 const ZEROX_ETH_SIGNIFIER = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" @@ -249,7 +253,15 @@ export const fetchSwapPrice = createBackgroundAsyncThunk( let needsApproval = false // If we aren't selling ETH, check whether we need an approval to swap // TODO Handle other non-ETH base assets - if (quote.sellTokenAddress !== ZEROX_ETH_SIGNIFIER) { + if ( + quote.sellTokenAddress !== ZEROX_ETH_SIGNIFIER && + // When attempting to wrap ETH into WETH or vice-versa - the 0x api currently erroneously returns the requested quote's + // sellTokenAddress to the the buyTokenAddress (WETH's contract address). We have been told that this will be + // addressed in a future update to the api (https://github.com/0xProject/0x-api/pull/850) but until that PR + // lands we will need to add the below escape hatch to let us slap ETH <> WETH in-app. + // @TODO 5/1 Check if we still need the below code. + !is0xWETHWrap(quote.buyTokenAddress, quote.sellTokenAddress) + ) { const assetContract = new ethers.Contract( quote.sellTokenAddress, ERC20_ABI, From 43933ef9e6ee97a3378d7bbbd172b12a22b24b1d Mon Sep 17 00:00:00 2001 From: 0xDaedalus <0xDaedalus@users.noreply.github.com> Date: Thu, 24 Mar 2022 23:23:05 -0500 Subject: [PATCH 04/30] Add a better method for detecting if token approval is needed. The 0x API docs specifically mentions that allowanceTarget will be AddressZero when wrapping or unwrapping WETH, so we can use that to both determine no-need-for-approval both when swapping ETH for ERC20 and when wrapping ETH to WETH --- background/constants/index.ts | 2 -- background/redux-slices/0x-swap.ts | 20 ++------------------ 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/background/constants/index.ts b/background/constants/index.ts index 6650412175..c878dbfaae 100644 --- a/background/constants/index.ts +++ b/background/constants/index.ts @@ -26,7 +26,5 @@ export const DAY = 24 * HOUR export const COMMUNITY_MULTISIG_ADDRESS = "0x99b36fDbC582D113aF36A21EBa06BFEAb7b9bE12" -export const MAINNET_WETH_ADDRESS = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" - export * from "./currencies" export * from "./networks" diff --git a/background/redux-slices/0x-swap.ts b/background/redux-slices/0x-swap.ts index b3ac2749d9..dd1a4256ca 100644 --- a/background/redux-slices/0x-swap.ts +++ b/background/redux-slices/0x-swap.ts @@ -12,7 +12,7 @@ import { } from "../lib/validate" import { getProvider } from "./utils/contract-utils" import { ERC20_ABI } from "../lib/erc20" -import { COMMUNITY_MULTISIG_ADDRESS, MAINNET_WETH_ADDRESS } from "../constants" +import { COMMUNITY_MULTISIG_ADDRESS } from "../constants" interface SwapAssets { sellAsset: SmartContractFungibleAsset | FungibleAsset @@ -47,14 +47,6 @@ export const initialState: SwapState = { inProgressApprovalContract: undefined, } -const is0xWETHWrap = (buyAddress: string, sellAddress: string) => { - return buyAddress === MAINNET_WETH_ADDRESS && buyAddress === sellAddress -} - -// The magic string used by the 0x API to signify we're dealing with ETH rather -// than an ERC-20 -const ZEROX_ETH_SIGNIFIER = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" - const swapSlice = createSlice({ name: "0x-swap", initialState, @@ -253,15 +245,7 @@ export const fetchSwapPrice = createBackgroundAsyncThunk( let needsApproval = false // If we aren't selling ETH, check whether we need an approval to swap // TODO Handle other non-ETH base assets - if ( - quote.sellTokenAddress !== ZEROX_ETH_SIGNIFIER && - // When attempting to wrap ETH into WETH or vice-versa - the 0x api currently erroneously returns the requested quote's - // sellTokenAddress to the the buyTokenAddress (WETH's contract address). We have been told that this will be - // addressed in a future update to the api (https://github.com/0xProject/0x-api/pull/850) but until that PR - // lands we will need to add the below escape hatch to let us slap ETH <> WETH in-app. - // @TODO 5/1 Check if we still need the below code. - !is0xWETHWrap(quote.buyTokenAddress, quote.sellTokenAddress) - ) { + if (quote.allowanceTarget !== ethers.constants.AddressZero) { const assetContract = new ethers.Contract( quote.sellTokenAddress, ERC20_ABI, From a2eb8a16d1b6ae93289d25076921160434797706 Mon Sep 17 00:00:00 2001 From: alleycat0001 Date: Sun, 27 Mar 2022 08:12:50 -0500 Subject: [PATCH 05/30] #1227, shared input parser prop function --- .../NetworkFees/NetworkSettingsSelect.tsx | 42 +++++++++---------- ui/components/Shared/SharedInput.tsx | 41 ++++++++++++++---- 2 files changed, 55 insertions(+), 28 deletions(-) diff --git a/ui/components/NetworkFees/NetworkSettingsSelect.tsx b/ui/components/NetworkFees/NetworkSettingsSelect.tsx index ba9a38ee1c..a8744a28a1 100644 --- a/ui/components/NetworkFees/NetworkSettingsSelect.tsx +++ b/ui/components/NetworkFees/NetworkSettingsSelect.tsx @@ -12,7 +12,6 @@ import { weiToGwei } from "@tallyho/tally-background/lib/utils" import { ETH } from "@tallyho/tally-background/constants" import { PricePoint } from "@tallyho/tally-background/assets" import { enrichAssetAmountWithMainCurrencyValues } from "@tallyho/tally-background/redux-slices/utils/asset-utils" -import logger from "@tallyho/tally-background/lib/logger" import SharedInput from "../Shared/SharedInput" import { useBackgroundSelector } from "../../hooks" import capitalize from "../../utils/capitalize" @@ -201,25 +200,8 @@ export default function NetworkSettingsSelect({ updateGasOptions() }, [updateGasOptions]) - const setGasLimit = (newGasLimit: string) => { - try { - if (newGasLimit.trim() === "") { - onNetworkSettingsChange({ - ...networkSettings, - gasLimit: undefined, - }) - } else { - const parsedGasLimit = BigInt(newGasLimit) - if (parsedGasLimit >= 0n) { - onNetworkSettingsChange({ - ...networkSettings, - gasLimit: parsedGasLimit, - }) - } - } - } catch (error) { - logger.debug("Failed to parse network settings gas limit", newGasLimit) - } + const setGasLimit = (gasLimit: bigint | undefined) => { + onNetworkSettingsChange({ ...networkSettings, gasLimit }) } return ( @@ -249,11 +231,29 @@ export default function NetworkSettingsSelect({ })}
- id="gasLimit" value={networkSettings.gasLimit?.toString() ?? ""} placeholder={networkSettings.suggestedGasLimit?.toString() ?? ""} onChange={setGasLimit} + parser={(value) => { + try { + if (value.trim() === "") { + return { state: "parsed", parsed: undefined } + } + const parsed = BigInt(value) + if (parsed < 0n) { + return { + state: "error", + message: "Gas Limit must be greater than 0", + } + } + + return { state: "parsed", parsed } + } catch (e) { + return { state: "error", message: "Gas Limit must be a number" } + } + }} label="Gas limit" type="number" focusedLabelBackgroundColor="var(--green-95)" diff --git a/ui/components/Shared/SharedInput.tsx b/ui/components/Shared/SharedInput.tsx index 62514ed624..99bb33233e 100644 --- a/ui/components/Shared/SharedInput.tsx +++ b/ui/components/Shared/SharedInput.tsx @@ -1,23 +1,32 @@ -import React, { ReactElement, useEffect, useRef } from "react" +import React, { + ChangeEventHandler, + ReactElement, + useEffect, + useRef, + useState, +} from "react" import classNames from "classnames" import { useRunOnFirstRender } from "../../hooks" -interface Props { +interface Props { id?: string label: string focusedLabelBackgroundColor: string - defaultValue?: string + defaultValue?: T placeholder?: string type: "password" | "text" | "number" value?: string | number | undefined - onChange?: (value: string) => void + onChange?: (value: T) => void onFocus?: () => void errorMessage?: string autoFocus?: boolean autoSelect?: boolean + parser?: ( + value: string + ) => { state: "error"; message: string } | { state: "parsed"; parsed: T } } -export default function SharedInput(props: Props): ReactElement { +export default function SharedInput(props: Props): ReactElement { const { id, label, @@ -31,9 +40,10 @@ export default function SharedInput(props: Props): ReactElement { errorMessage, autoFocus = false, autoSelect = false, + parser, } = props - const inputRef = useRef(null) + const [parserError, setParserError] = useState(null) useEffect(() => { if (autoFocus) inputRef.current?.focus() @@ -49,6 +59,22 @@ export default function SharedInput(props: Props): ReactElement { } }) + const onInputChange: ChangeEventHandler = ({ + target: { value: inputValue }, + }) => { + if (parser) { + const result = parser(inputValue) + if (result.state === "error") { + setParserError(result.message) + } else { + setParserError(null) + onChange?.(result.parsed) + } + } else { + onChange?.(value as unknown as T) + } + } + return ( <> onChange?.(event.target.value)} + onChange={onInputChange} onFocus={onFocus} className={classNames({ error: errorMessage, @@ -70,6 +96,7 @@ export default function SharedInput(props: Props): ReactElement { /> {errorMessage &&
{errorMessage}
} + {parserError &&
{parserError}
}