Skip to content

Commit

Permalink
Merge pull request #1263 from tallycash/kraft-erc
Browse files Browse the repository at this point in the history
Kraft-erc: Add ERC20 transfers
  • Loading branch information
Gergő Nagy committed Mar 30, 2022
2 parents a802255 + 2c70542 commit 53d8f6b
Show file tree
Hide file tree
Showing 13 changed files with 201 additions and 104 deletions.
4 changes: 1 addition & 3 deletions .env.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ BLOCKNATIVE_API_KEY="f60816ff-da02-463f-87a6-67a09c6d53fa"
READ_REDUX_CACHE=true
WRITE_REDUX_CACHE=true
HIDE_EARN_PAGE=true
HIDE_SEND_BUTTON=false
HIDE_ADD_SEED=false
HIDE_SWAP=false
HIDE_IMPORT_DERIVATION_PATH=true
HIDE_CREATE_PHRASE=false
HIDE_IMPORT_LEDGER=false
Expand All @@ -15,4 +13,4 @@ ETHEREUM_NETWORK=mainnet
PERSIST_UI_LOCATION=true
USE_MAINNET_FORK=false
MAINNET_FORK_URL="http://127.0.0.1:8545"
MAINNET_FORK_CHAIN_ID=1337
MAINNET_FORK_CHAIN_ID=1337
2 changes: 0 additions & 2 deletions background/features/features.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
export const HIDE_ADD_SEED = process.env.HIDE_ADD_SEED === "true"
export const HIDE_SEND_BUTTON = process.env.HIDE_SEND_BUTTON === "true"
export const HIDE_EARN_PAGE = process.env.HIDE_EARN_PAGE === "true"
export const HIDE_SWAP = process.env.HIDE_SWAP === "true"
export const HIDE_IMPORT_DERIVATION_PATH =
process.env.HIDE_IMPORT_DERIVATION_PATH === "true"
export const HIDE_CREATE_PHRASE = process.env.HIDE_CREATE_PHRASE === "true"
Expand Down
81 changes: 80 additions & 1 deletion background/redux-slices/assets.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import { createSelector, createSlice } from "@reduxjs/toolkit"
import { AnyAsset, PricePoint } from "../assets"
import { ethers } from "ethers"
import {
AnyAsset,
AnyAssetAmount,
isSmartContractFungibleAsset,
PricePoint,
} from "../assets"
import { AddressOnNetwork } from "../accounts"
import { findClosestAssetIndex } from "../lib/asset-similarity"
import { normalizeEVMAddress } from "../lib/utils"
import { createBackgroundAsyncThunk } from "./utils"
import { isNetworkBaseAsset } from "./utils/asset-utils"
import { getProvider } from "./utils/contract-utils"
import { sameNetwork } from "../networks"
import { ERC20_INTERFACE } from "../lib/erc20"
import logger from "../lib/logger"

type SingleAssetState = AnyAsset & {
prices: PricePoint[]
Expand Down Expand Up @@ -145,6 +158,72 @@ const selectPairedAssetSymbol = (
pairedAssetSymbol: string
) => pairedAssetSymbol

/**
* Executes an asset transfer between two addresses, for a set amount. Supports
* an optional fixed gas limit.
*
* If the from address is not a writeable address in the wallet, this signature
* will not be possible.
*/
export const transferAsset = createBackgroundAsyncThunk(
"assets/transferAsset",
async ({
fromAddressNetwork: { address: fromAddress, network: fromNetwork },
toAddressNetwork: { address: toAddress, network: toNetwork },
assetAmount,
gasLimit,
}: {
fromAddressNetwork: AddressOnNetwork
toAddressNetwork: AddressOnNetwork
assetAmount: AnyAssetAmount
gasLimit: bigint | undefined
}) => {
if (!sameNetwork(fromNetwork, toNetwork)) {
throw new Error("Only same-network transfers are supported for now.")
}

const provider = getProvider()
const signer = provider.getSigner()

if (isNetworkBaseAsset(assetAmount.asset, fromNetwork)) {
logger.debug(
`Sending ${assetAmount.amount} ${assetAmount.asset.symbol} from ` +
`${fromAddress} to ${toAddress} as a base asset transfer.`
)
await signer.sendTransaction({
from: fromAddress,
to: toAddress,
value: assetAmount.amount,
gasLimit,
})
} else if (isSmartContractFungibleAsset(assetAmount.asset)) {
logger.debug(
`Sending ${assetAmount.amount} ${assetAmount.asset.symbol} from ` +
`${fromAddress} to ${toAddress} as an ERC20 transfer.`
)
const token = new ethers.Contract(
assetAmount.asset.contractAddress,
ERC20_INTERFACE,
signer
)

const transactionDetails = await token.populateTransaction.transfer(
toAddress,
assetAmount.amount
)

await signer.sendUncheckedTransaction({
...transactionDetails,
gasLimit: gasLimit ?? transactionDetails.gasLimit,
})
} else {
throw new Error(
"Only base and fungible smart contract asset transfers are supported for now."
)
}
}
)

/**
* Selects a particular asset price point given the asset symbol and the paired
* asset symbol used to price it.
Expand Down
1 change: 0 additions & 1 deletion background/redux-slices/transaction-construction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
REGULAR,
} from "../constants/network-fees"
import { USE_MAINNET_FORK } from "../features/features"
import logger from "../lib/logger"

import {
BlockEstimate,
Expand Down
24 changes: 24 additions & 0 deletions background/redux-slices/utils/asset-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import {
PricePoint,
FungibleAsset,
UnitPricePoint,
AnyAsset,
} from "../../assets"
import { fromFixedPointNumber } from "../../lib/fixed-point"
import { AnyNetwork } from "../../networks"

/**
* Adds user-specific amounts based on preferences. This is the combination of
Expand All @@ -33,6 +35,28 @@ export type AssetDecimalAmount = {
localizedDecimalAmount: string
}

/**
* Given an asset and a network, determines whether the given asset is the base
* asset for the given network. Used to special-case transactions that should
* work differently for base assets vs, for example, smart contract assets.
*
* @param asset The asset that could be a base asset for a network.
* @param network The network whose base asset `asset` should be checked against.
*
* @return True if the passed asset is the base asset for the passed network.
*/
export function isNetworkBaseAsset(
asset: AnyAsset,
network: AnyNetwork
): boolean {
return (
!("homeNetwork" in asset) &&
"family" in network &&
network.family === "EVM" &&
asset.symbol === network.baseAsset.symbol
)
}

/**
* Given an asset symbol, price as a JavaScript number, and a number of desired
* decimals during formatting, format the price in a localized way as a
Expand Down
5 changes: 4 additions & 1 deletion ui/components/Shared/SharedButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interface Props {
isDisabled?: boolean
linkTo?: History.LocationDescriptor<unknown>
showLoadingOnClick: boolean
isLoading: boolean
isFormSubmit: boolean
}

Expand All @@ -41,6 +42,7 @@ export default function SharedButton(props: Props): ReactElement {
iconPosition,
linkTo,
showLoadingOnClick,
isLoading,
isFormSubmit,
} = props

Expand Down Expand Up @@ -68,7 +70,7 @@ export default function SharedButton(props: Props): ReactElement {
}
}

const isShowingLoadingSpinner = isClicked && showLoadingOnClick
const isShowingLoadingSpinner = isLoading || (isClicked && showLoadingOnClick)

return (
<button
Expand Down Expand Up @@ -317,5 +319,6 @@ SharedButton.defaultProps = {
iconPosition: "right",
linkTo: null,
showLoadingOnClick: false,
isLoading: false,
isFormSubmit: false,
}
4 changes: 0 additions & 4 deletions ui/components/Swap/SwapQuote.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ import {
} from "@tallyho/tally-background/redux-slices/0x-swap"
import { useHistory } from "react-router-dom"
import { FungibleAsset } from "@tallyho/tally-background/assets"
import {
clearTransactionState,
TransactionConstructionStatus,
} from "@tallyho/tally-background/redux-slices/transaction-construction"
import SharedButton from "../Shared/SharedButton"
import SharedActivityHeader from "../Shared/SharedActivityHeader"
import SwapQuoteAssetCard from "./SwapQuoteAssetCard"
Expand Down
3 changes: 1 addition & 2 deletions ui/components/Wallet/WalletAccountBalanceControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import classNames from "classnames"
import { useDispatch } from "react-redux"
import { refreshBackgroundPage } from "@tallyho/tally-background/redux-slices/ui"
import { selectCurrentAccountSigningMethod } from "@tallyho/tally-background/redux-slices/selectors"
import { HIDE_SEND_BUTTON } from "@tallyho/tally-background/features/features"
import { useBackgroundSelector, useLocalStorage } from "../../hooks"
import SharedButton from "../Shared/SharedButton"
import SharedSlideUpMenu from "../Shared/SharedSlideUpMenu"
Expand Down Expand Up @@ -157,7 +156,7 @@ export default function WalletAccountBalanceControl(
{!shouldIndicateLoading && <BalanceReloader />}
</span>
</span>
{currentAccountSigningMethod && !HIDE_SEND_BUTTON ? (
{currentAccountSigningMethod ? (
<>
{hasSavedSeed ? (
<div className="send_receive_button_wrap">
Expand Down
24 changes: 8 additions & 16 deletions ui/components/Wallet/WalletAssetListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@ export default function WalletAssetListItem(props: Props): ReactElement {
<Link
to={{
pathname: "/singleAsset",
state: {
symbol: assetAmount.asset.symbol,
contractAddress,
},
state: assetAmount.asset,
}}
>
<div className="wallet_asset_list_item">
Expand Down Expand Up @@ -60,18 +57,13 @@ export default function WalletAssetListItem(props: Props): ReactElement {
</div>
</div>
<div className="right">
{!contractAddress && (
<Link
to={{
pathname: "/send",
state: {
symbol: assetAmount.asset.symbol,
contractAddress,
},
}}
className="asset_list_item_icon asset_list_item_icon_send_asset"
/>
)}
<Link
to={{
pathname: "/send",
state: assetAmount.asset,
}}
className="asset_list_item_icon asset_list_item_icon_send_asset"
/>
<Link
to={{
pathname: "/swap",
Expand Down
65 changes: 47 additions & 18 deletions ui/pages/Send.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import React, { ReactElement, useState } from "react"
import React, { ReactElement, useCallback, useState } from "react"
import { isAddress } from "@ethersproject/address"
import {
selectCurrentAccount,
selectCurrentAccountBalances,
selectMainCurrencySymbol,
} from "@tallyho/tally-background/redux-slices/selectors"
import {
broadcastOnSign,
NetworkFeeSettings,
selectEstimatedFeesPerGas,
setFeeType,
updateTransactionOptions,
} from "@tallyho/tally-background/redux-slices/transaction-construction"
import { utils } from "ethers"
import {
FungibleAsset,
isFungibleAssetAmount,
Expand All @@ -22,9 +19,13 @@ import {
convertFixedPointNumber,
parseToFixedPointNumber,
} from "@tallyho/tally-background/lib/fixed-point"
import { selectAssetPricePoint } from "@tallyho/tally-background/redux-slices/assets"
import {
selectAssetPricePoint,
transferAsset,
} from "@tallyho/tally-background/redux-slices/assets"
import { CompleteAssetAmount } from "@tallyho/tally-background/redux-slices/accounts"
import { enrichAssetAmountWithMainCurrencyValues } from "@tallyho/tally-background/redux-slices/utils/asset-utils"
import { useHistory, useLocation } from "react-router-dom"
import NetworkSettingsChooser from "../components/NetworkFees/NetworkSettingsChooser"
import SharedAssetInput from "../components/Shared/SharedAssetInput"
import SharedBackButton from "../components/Shared/SharedBackButton"
Expand All @@ -34,17 +35,23 @@ import SharedSlideUpMenu from "../components/Shared/SharedSlideUpMenu"
import FeeSettingsButton from "../components/NetworkFees/FeeSettingsButton"

export default function Send(): ReactElement {
const [selectedAsset, setSelectedAsset] = useState<FungibleAsset>(ETH)
const location = useLocation<FungibleAsset>()
const [selectedAsset, setSelectedAsset] = useState<FungibleAsset>(
location.state ?? ETH
)
const [destinationAddress, setDestinationAddress] = useState("")
const [amount, setAmount] = useState("")
const [gasLimit, setGasLimit] = useState<bigint | undefined>(undefined)
const [isSendingTransactionRequest, setIsSendingTransactionRequest] =
useState(false)
const [hasError, setHasError] = useState(false)
const [networkSettingsModalOpen, setNetworkSettingsModalOpen] =
useState(false)

const estimatedFeesPerGas = useBackgroundSelector(selectEstimatedFeesPerGas)
const history = useHistory()

const dispatch = useBackgroundDispatch()
const estimatedFeesPerGas = useBackgroundSelector(selectEstimatedFeesPerGas)
const currentAccount = useBackgroundSelector(selectCurrentAccount)
const balanceData = useBackgroundSelector(selectCurrentAccountBalances)
const mainCurrencySymbol = useBackgroundSelector(selectMainCurrencySymbol)
Expand Down Expand Up @@ -86,17 +93,38 @@ export default function Send(): ReactElement {

const assetAmount = assetAmountFromForm()

const sendTransactionRequest = async () => {
dispatch(broadcastOnSign(true))
const transaction = {
from: currentAccount.address,
to: destinationAddress,
// eslint-disable-next-line no-underscore-dangle
value: BigInt(utils.parseEther(amount?.toString())._hex),
gasLimit,
const sendTransactionRequest = useCallback(async () => {
if (assetAmount === undefined) {
return
}
return dispatch(updateTransactionOptions(transaction))
}

try {
setIsSendingTransactionRequest(true)

await dispatch(
transferAsset({
fromAddressNetwork: currentAccount,
toAddressNetwork: {
address: destinationAddress,
network: currentAccount.network,
},
assetAmount,
gasLimit,
})
)
} finally {
setIsSendingTransactionRequest(false)
}

history.push("/singleAsset", assetAmount.asset)
}, [
assetAmount,
currentAccount,
destinationAddress,
dispatch,
gasLimit,
history,
])

const networkSettingsSaved = (networkSetting: NetworkFeeSettings) => {
setGasLimit(networkSetting.gasLimit)
Expand Down Expand Up @@ -130,7 +158,6 @@ export default function Send(): ReactElement {
}}
selectedAsset={selectedAsset}
amount={amount}
disableDropdown
/>
<div className="value">
${assetAmount?.localizedMainCurrencyAmount ?? "-"}
Expand Down Expand Up @@ -174,6 +201,8 @@ export default function Send(): ReactElement {
hasError
}
onClick={sendTransactionRequest}
isFormSubmit
isLoading={isSendingTransactionRequest}
>
Send
</SharedButton>
Expand Down
Loading

0 comments on commit 53d8f6b

Please sign in to comment.