Commit 3a34c2ec authored by Moody Salem's avatar Moody Salem

eth swaps kinda working

parent 83b0ef94
...@@ -13,12 +13,14 @@ import { RowBetween, RowFixed } from '../Row' ...@@ -13,12 +13,14 @@ import { RowBetween, RowFixed } from '../Row'
import FormattedPriceImpact from './FormattedPriceImpact' import FormattedPriceImpact from './FormattedPriceImpact'
import SwapRoute from './SwapRoute' import SwapRoute from './SwapRoute'
function TradeSummary({ trade, allowedSlippage }: { trade: V2Trade | V3Trade; allowedSlippage: number }) { function TradeSummary({ trade }: { trade: V2Trade | V3Trade }) {
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
const { priceImpactWithoutFee, realizedLPFee } = computeTradePriceBreakdown(trade) const { priceImpactWithoutFee, realizedLPFee } = computeTradePriceBreakdown(trade)
const isExactIn = trade.tradeType === TradeType.EXACT_INPUT const isExactIn = trade.tradeType === TradeType.EXACT_INPUT
const [allowedSlippage] = useUserSlippageTolerance()
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(trade, allowedSlippage) const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(trade, allowedSlippage)
const price = trade.executionPrice const price = trade.executionPrice
return ( return (
<> <>
<AutoColumn style={{ padding: '8px 16px' }} gap="8px"> <AutoColumn style={{ padding: '8px 16px' }} gap="8px">
...@@ -97,8 +99,6 @@ export interface AdvancedSwapDetailsProps { ...@@ -97,8 +99,6 @@ export interface AdvancedSwapDetailsProps {
export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) { export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) {
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
const [allowedSlippage] = useUserSlippageTolerance()
const showRoute = Boolean( const showRoute = Boolean(
(trade && trade instanceof V2Trade && trade.route.pairs.length > 2) || (trade && trade instanceof V2Trade && trade.route.pairs.length > 2) ||
(trade instanceof V3Trade && trade.route.pools.length > 2) (trade instanceof V3Trade && trade.route.pools.length > 2)
...@@ -108,7 +108,7 @@ export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) { ...@@ -108,7 +108,7 @@ export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) {
<AutoColumn gap="0px"> <AutoColumn gap="0px">
{trade && ( {trade && (
<> <>
<TradeSummary trade={trade} allowedSlippage={allowedSlippage} /> <TradeSummary trade={trade} />
{showRoute && ( {showRoute && (
<> <>
<RowBetween style={{ padding: '4px 16px' }}> <RowBetween style={{ padding: '4px 16px' }}>
......
import { MaxUint256 } from '@ethersproject/constants' import { MaxUint256 } from '@ethersproject/constants'
import { TransactionResponse } from '@ethersproject/providers' import { TransactionResponse } from '@ethersproject/providers'
import { TokenAmount, CurrencyAmount, ETHER, ChainId } from '@uniswap/sdk-core' import { TokenAmount, CurrencyAmount, ETHER, ChainId, Percent } from '@uniswap/sdk-core'
import { Trade as V2Trade } from '@uniswap/v2-sdk' import { Trade as V2Trade } from '@uniswap/v2-sdk'
import { Trade as V3Trade } from '@uniswap/v3-sdk' import { Trade as V3Trade } from '@uniswap/v3-sdk'
import { useCallback, useMemo } from 'react' import { useCallback, useMemo } from 'react'
import { V2_ROUTER_ADDRESS } from '../constants' import { V2_ROUTER_ADDRESS } from '../constants'
import { SWAP_ROUTER_ADDRESSES } from '../constants/v3' import { SWAP_ROUTER_ADDRESSES } from '../constants/v3'
import { Field } from '../state/swap/actions'
import { useTransactionAdder, useHasPendingApproval } from '../state/transactions/hooks' import { useTransactionAdder, useHasPendingApproval } from '../state/transactions/hooks'
import { computeSlippageAdjustedAmounts } from '../utils/prices'
import { calculateGasMargin } from '../utils' import { calculateGasMargin } from '../utils'
import { useTokenContract } from './useContract' import { useTokenContract } from './useContract'
import { useActiveWeb3React } from './index' import { useActiveWeb3React } from './index'
...@@ -101,12 +99,15 @@ export function useApproveCallback( ...@@ -101,12 +99,15 @@ export function useApproveCallback(
} }
// wraps useApproveCallback in the context of a swap // wraps useApproveCallback in the context of a swap
export function useApproveCallbackFromTrade(trade?: V2Trade | V3Trade, allowedSlippage = 0) { export function useApproveCallbackFromTrade(trade: V2Trade | V3Trade | undefined, allowedSlippage: number) {
const { chainId } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
const swapRouterAddress = SWAP_ROUTER_ADDRESSES[chainId as ChainId] const swapRouterAddress = SWAP_ROUTER_ADDRESSES[chainId as ChainId]
const amountToApprove = useMemo( const amountToApprove = useMemo(
() => (trade ? computeSlippageAdjustedAmounts(trade, allowedSlippage)[Field.INPUT] : undefined), () => (trade ? trade.maximumAmountIn(new Percent(allowedSlippage, 10_000)) : undefined),
[trade, allowedSlippage] [trade, allowedSlippage]
) )
return useApproveCallback(amountToApprove, trade instanceof V2Trade ? V2_ROUTER_ADDRESS : swapRouterAddress) return useApproveCallback(
amountToApprove,
trade instanceof V2Trade ? V2_ROUTER_ADDRESS : trade instanceof V3Trade ? swapRouterAddress : undefined
)
} }
import JSBI from 'jsbi' import JSBI from 'jsbi'
import { BigNumber } from '@ethersproject/bignumber' import { BigNumber } from '@ethersproject/bignumber'
import { Contract } from '@ethersproject/contracts' import { Router, Trade as V2Trade } from '@uniswap/v2-sdk'
import { Router, SwapParameters, Trade as V2Trade } from '@uniswap/v2-sdk' import { SwapRouter, Trade as V3Trade } from '@uniswap/v3-sdk'
import { Trade as V3Trade } from '@uniswap/v3-sdk' import { ChainId, Percent, TradeType } from '@uniswap/sdk-core'
import { Percent, TradeType } from '@uniswap/sdk-core'
import { useMemo } from 'react' import { useMemo } from 'react'
import { BIPS_BASE, INITIAL_ALLOWED_SLIPPAGE } from '../constants' import { BIPS_BASE, INITIAL_ALLOWED_SLIPPAGE } from '../constants'
import { SWAP_ROUTER_ADDRESSES } from '../constants/v3'
import { getTradeVersion } from '../utils/getTradeVersion' import { getTradeVersion } from '../utils/getTradeVersion'
import { useTransactionAdder } from '../state/transactions/hooks' import { useTransactionAdder } from '../state/transactions/hooks'
import { calculateGasMargin, isAddress, shortenAddress } from '../utils' import { calculateGasMargin, isAddress, shortenAddress } from '../utils'
...@@ -23,8 +23,9 @@ export enum SwapCallbackState { ...@@ -23,8 +23,9 @@ export enum SwapCallbackState {
} }
interface SwapCall { interface SwapCall {
contract: Contract address: string
parameters: SwapParameters calldata: string
value: string
} }
interface SuccessfulCall { interface SuccessfulCall {
...@@ -55,13 +56,13 @@ function useSwapCallArguments( ...@@ -55,13 +56,13 @@ function useSwapCallArguments(
const { address: recipientAddress } = useENS(recipientAddressOrName) const { address: recipientAddress } = useENS(recipientAddressOrName)
const recipient = recipientAddressOrName === null ? account : recipientAddress const recipient = recipientAddressOrName === null ? account : recipientAddress
const deadline = useTransactionDeadline() const deadline = useTransactionDeadline()
const routerContract = useV2RouterContract() const routerContract = useV2RouterContract()
return useMemo(() => { return useMemo(() => {
if (!trade || !recipient || !library || !account || !chainId || !deadline || !routerContract) return [] if (!trade || !recipient || !library || !account || !chainId || !deadline) return []
if (trade instanceof V2Trade) { if (trade instanceof V2Trade) {
if (!routerContract) return []
const swapMethods = [] const swapMethods = []
swapMethods.push( swapMethods.push(
Router.swapCallParameters(trade, { Router.swapCallParameters(trade, {
...@@ -82,10 +83,28 @@ function useSwapCallArguments( ...@@ -82,10 +83,28 @@ function useSwapCallArguments(
}) })
) )
} }
return swapMethods.map((parameters) => ({ parameters, contract: routerContract })) return swapMethods.map(({ methodName, args, value }) => ({
address: routerContract.address,
calldata: routerContract.interface.encodeFunctionData(methodName, args),
value,
}))
} else {
// trade is V3Trade
const { value, calldata } = SwapRouter.swapCallParameters(trade, {
recipient,
slippageTolerance: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE),
deadline: deadline.toNumber(),
})
const swapRouterAddress = SWAP_ROUTER_ADDRESSES[chainId as ChainId]
if (!swapRouterAddress) return []
return [
{
address: swapRouterAddress,
calldata,
value,
},
]
} }
return []
}, [account, allowedSlippage, chainId, deadline, library, recipient, routerContract, trade]) }, [account, allowedSlippage, chainId, deadline, library, recipient, routerContract, trade])
} }
...@@ -124,13 +143,19 @@ export function useSwapCallback( ...@@ -124,13 +143,19 @@ export function useSwapCallback(
callback: async function onSwap(): Promise<string> { callback: async function onSwap(): Promise<string> {
const estimatedCalls: EstimatedSwapCall[] = await Promise.all( const estimatedCalls: EstimatedSwapCall[] = await Promise.all(
swapCalls.map((call) => { swapCalls.map((call) => {
const { const { address, calldata, value } = call
parameters: { methodName, args, value },
contract,
} = call
const options = !value || isZero(value) ? {} : { value }
return contract.estimateGas[methodName](...args, options) const tx =
!value || isZero(value)
? { to: address, data: calldata }
: {
to: address,
data: calldata,
value,
}
return library
.estimateGas(tx)
.then((gasEstimate) => { .then((gasEstimate) => {
return { return {
call, call,
...@@ -140,7 +165,8 @@ export function useSwapCallback( ...@@ -140,7 +165,8 @@ export function useSwapCallback(
.catch((gasError) => { .catch((gasError) => {
console.debug('Gas estimate failed, trying eth_call to extract error', call) console.debug('Gas estimate failed, trying eth_call to extract error', call)
return contract.callStatic[methodName](...args, options) return library
.call(tx)
.then((result) => { .then((result) => {
console.debug('Unexpected successful call after failed estimate gas', call, gasError, result) console.debug('Unexpected successful call after failed estimate gas', call, gasError, result)
return { call, error: new Error('Unexpected issue with estimating the gas. Please try again.') } return { call, error: new Error('Unexpected issue with estimating the gas. Please try again.') }
...@@ -176,16 +202,18 @@ export function useSwapCallback( ...@@ -176,16 +202,18 @@ export function useSwapCallback(
} }
const { const {
call: { call: { address, calldata, value },
contract,
parameters: { methodName, args, value },
},
gasEstimate, gasEstimate,
} = successfulEstimation } = successfulEstimation
return contract[methodName](...args, { return library
.getSigner()
.sendTransaction({
from: account,
to: address,
data: calldata,
gasLimit: calculateGasMargin(gasEstimate), gasLimit: calculateGasMargin(gasEstimate),
...(value && !isZero(value) ? { value, from: account } : { from: account }), ...(value && !isZero(value) ? { value } : {}),
}) })
.then((response: any) => { .then((response: any) => {
const inputSymbol = trade.inputAmount.currency.symbol const inputSymbol = trade.inputAmount.currency.symbol
...@@ -218,7 +246,7 @@ export function useSwapCallback( ...@@ -218,7 +246,7 @@ export function useSwapCallback(
throw new Error('Transaction rejected.') throw new Error('Transaction rejected.')
} else { } else {
// otherwise, the error was unexpected and we need to convey that // otherwise, the error was unexpected and we need to convey that
console.error(`Swap failed`, error, methodName, args, value) console.error(`Swap failed`, error, address, calldata, value)
throw new Error(`Swap failed: ${error.message}`) throw new Error(`Swap failed: ${error.message}`)
} }
}) })
......
...@@ -183,27 +183,23 @@ export default function Swap({ history }: RouteComponentProps) { ...@@ -183,27 +183,23 @@ export default function Swap({ history }: RouteComponentProps) {
const noRoute = !route const noRoute = !route
// check whether the user has approved the router on the input token // check whether the user has approved the router on the input token
const [approval, approveCallback] = useApproveCallbackFromTrade(trade, allowedSlippage) const [approvalState, approveCallback] = useApproveCallbackFromTrade(trade, allowedSlippage)
// check if user has gone through approval process, used to show two step buttons, reset on token change // check if user has gone through approval process, used to show two step buttons, reset on token change
const [approvalSubmitted, setApprovalSubmitted] = useState<boolean>(false) const [approvalSubmitted, setApprovalSubmitted] = useState<boolean>(false)
// mark when a user has submitted an approval, reset onTokenSelection for input field // mark when a user has submitted an approval, reset onTokenSelection for input field
useEffect(() => { useEffect(() => {
if (approval === ApprovalState.PENDING) { if (approvalState === ApprovalState.PENDING) {
setApprovalSubmitted(true) setApprovalSubmitted(true)
} }
}, [approval, approvalSubmitted]) }, [approvalState, approvalSubmitted])
const maxInputAmount: CurrencyAmount | undefined = maxAmountSpend(currencyBalances[Field.INPUT]) const maxInputAmount: CurrencyAmount | undefined = maxAmountSpend(currencyBalances[Field.INPUT])
const atMaxInputAmount = Boolean(maxInputAmount && parsedAmounts[Field.INPUT]?.equalTo(maxInputAmount)) const atMaxInputAmount = Boolean(maxInputAmount && parsedAmounts[Field.INPUT]?.equalTo(maxInputAmount))
// the callback to execute the swap // the callback to execute the swap
const { callback: swapCallback, error: swapCallbackError } = useSwapCallback( const { callback: swapCallback, error: swapCallbackError } = useSwapCallback(trade, allowedSlippage, recipient)
trade instanceof V2Trade ? trade : undefined,
allowedSlippage,
recipient
)
const { priceImpactWithoutFee } = computeTradePriceBreakdown(trade) const { priceImpactWithoutFee } = computeTradePriceBreakdown(trade)
...@@ -272,9 +268,9 @@ export default function Swap({ history }: RouteComponentProps) { ...@@ -272,9 +268,9 @@ export default function Swap({ history }: RouteComponentProps) {
// never show if price impact is above threshold in non expert mode // never show if price impact is above threshold in non expert mode
const showApproveFlow = const showApproveFlow =
!swapInputError && !swapInputError &&
(approval === ApprovalState.NOT_APPROVED || (approvalState === ApprovalState.NOT_APPROVED ||
approval === ApprovalState.PENDING || approvalState === ApprovalState.PENDING ||
(approvalSubmitted && approval === ApprovalState.APPROVED)) && (approvalSubmitted && approvalState === ApprovalState.APPROVED)) &&
!(priceImpactSeverity > 3 && !isExpertMode) !(priceImpactSeverity > 3 && !isExpertMode)
const handleConfirmDismiss = useCallback(() => { const handleConfirmDismiss = useCallback(() => {
...@@ -432,10 +428,10 @@ export default function Swap({ history }: RouteComponentProps) { ...@@ -432,10 +428,10 @@ export default function Swap({ history }: RouteComponentProps) {
<AutoColumn style={{ width: '100%' }} gap="12px"> <AutoColumn style={{ width: '100%' }} gap="12px">
<ButtonConfirmed <ButtonConfirmed
onClick={approveCallback} onClick={approveCallback}
disabled={approval !== ApprovalState.NOT_APPROVED || approvalSubmitted} disabled={approvalState !== ApprovalState.NOT_APPROVED || approvalSubmitted}
width="100%" width="100%"
altDisabledStyle={approval === ApprovalState.PENDING} // show solid button while waiting altDisabledStyle={approvalState === ApprovalState.PENDING} // show solid button while waiting
confirmed={approval === ApprovalState.APPROVED} confirmed={approvalState === ApprovalState.APPROVED}
> >
<AutoRow justify="space-between"> <AutoRow justify="space-between">
<span style={{ display: 'flex', alignItems: 'center' }}> <span style={{ display: 'flex', alignItems: 'center' }}>
...@@ -447,9 +443,9 @@ export default function Swap({ history }: RouteComponentProps) { ...@@ -447,9 +443,9 @@ export default function Swap({ history }: RouteComponentProps) {
{/* we need to shorted this string on mobile */} {/* we need to shorted this string on mobile */}
{'Allow Uniswap to spend your ' + currencies[Field.INPUT]?.symbol} {'Allow Uniswap to spend your ' + currencies[Field.INPUT]?.symbol}
</span> </span>
{approval === ApprovalState.PENDING ? ( {approvalState === ApprovalState.PENDING ? (
<Loader stroke="white" /> <Loader stroke="white" />
) : approvalSubmitted && approval === ApprovalState.APPROVED ? ( ) : approvalSubmitted && approvalState === ApprovalState.APPROVED ? (
<Unlock size="16" stroke="white" /> <Unlock size="16" stroke="white" />
) : ( ) : (
<Unlock size="16" stroke="white" /> <Unlock size="16" stroke="white" />
...@@ -473,7 +469,9 @@ export default function Swap({ history }: RouteComponentProps) { ...@@ -473,7 +469,9 @@ export default function Swap({ history }: RouteComponentProps) {
width="100%" width="100%"
id="swap-button" id="swap-button"
disabled={ disabled={
!isValid || approval !== ApprovalState.APPROVED || (priceImpactSeverity > 3 && !isExpertMode) !isValid ||
approvalState !== ApprovalState.APPROVED ||
(priceImpactSeverity > 3 && !isExpertMode)
} }
error={isValid && priceImpactSeverity > 2} error={isValid && priceImpactSeverity > 2}
> >
...@@ -484,7 +482,7 @@ export default function Swap({ history }: RouteComponentProps) { ...@@ -484,7 +482,7 @@ export default function Swap({ history }: RouteComponentProps) {
</Text> </Text>
</ButtonError> </ButtonError>
</AutoColumn> </AutoColumn>
{showApproveFlow && <ProgressSteps steps={[approval === ApprovalState.APPROVED]} />} {showApproveFlow && <ProgressSteps steps={[approvalState === ApprovalState.APPROVED]} />}
</AutoRow> </AutoRow>
) : ( ) : (
<ButtonError <ButtonError
......
...@@ -8,9 +8,9 @@ import { ALLOWED_PRICE_IMPACT_HIGH, ALLOWED_PRICE_IMPACT_LOW, ALLOWED_PRICE_IMPA ...@@ -8,9 +8,9 @@ import { ALLOWED_PRICE_IMPACT_HIGH, ALLOWED_PRICE_IMPACT_LOW, ALLOWED_PRICE_IMPA
import { Field } from '../state/swap/actions' import { Field } from '../state/swap/actions'
import { basisPointsToPercent } from './index' import { basisPointsToPercent } from './index'
const BASE_FEE = new Percent(JSBI.BigInt(30), JSBI.BigInt(10000)) const THIRTY_BIPS_FEE = new Percent(JSBI.BigInt(30), JSBI.BigInt(10000))
const ONE_HUNDRED_PERCENT = new Percent(JSBI.BigInt(10000), JSBI.BigInt(10000)) const ONE_HUNDRED_PERCENT = new Percent(JSBI.BigInt(10000), JSBI.BigInt(10000))
const INPUT_FRACTION_AFTER_FEE = ONE_HUNDRED_PERCENT.subtract(BASE_FEE) const INPUT_FRACTION_AFTER_FEE = ONE_HUNDRED_PERCENT.subtract(THIRTY_BIPS_FEE)
// computes price breakdown for the trade // computes price breakdown for the trade
export function computeTradePriceBreakdown( export function computeTradePriceBreakdown(
...@@ -46,9 +46,26 @@ export function computeTradePriceBreakdown( ...@@ -46,9 +46,26 @@ export function computeTradePriceBreakdown(
return { priceImpactWithoutFee: priceImpactWithoutFeePercent, realizedLPFee: realizedLPFeeAmount } return { priceImpactWithoutFee: priceImpactWithoutFeePercent, realizedLPFee: realizedLPFeeAmount }
} else { } else {
const realizedLPFee = !trade
? undefined
: ONE_HUNDRED_PERCENT.subtract(
trade.route.pools.reduce<Fraction>(
(currentFee: Fraction, pool): Fraction =>
currentFee.multiply(ONE_HUNDRED_PERCENT.subtract(new Fraction(pool.fee, 10_000))),
ONE_HUNDRED_PERCENT
)
)
const realizedLPFeeAmount =
realizedLPFee &&
trade &&
(trade.inputAmount instanceof TokenAmount
? new TokenAmount(trade.inputAmount.token, realizedLPFee.multiply(trade.inputAmount.raw).quotient)
: CurrencyAmount.ether(realizedLPFee.multiply(trade.inputAmount.raw).quotient))
return { return {
priceImpactWithoutFee: undefined, // TODO: real price impact
realizedLPFee: undefined, priceImpactWithoutFee: new Percent(0),
realizedLPFee: realizedLPFeeAmount,
} }
} }
} }
...@@ -65,11 +82,20 @@ export function computeSlippageAdjustedAmounts( ...@@ -65,11 +82,20 @@ export function computeSlippageAdjustedAmounts(
} }
} }
const IMPACT_TIERS = [
BLOCKED_PRICE_IMPACT_NON_EXPERT,
ALLOWED_PRICE_IMPACT_HIGH,
ALLOWED_PRICE_IMPACT_MEDIUM,
ALLOWED_PRICE_IMPACT_LOW,
]
export function warningSeverity(priceImpact: Percent | undefined): 0 | 1 | 2 | 3 | 4 { export function warningSeverity(priceImpact: Percent | undefined): 0 | 1 | 2 | 3 | 4 {
if (!priceImpact?.lessThan(BLOCKED_PRICE_IMPACT_NON_EXPERT)) return 4 if (!priceImpact) return 4
if (!priceImpact?.lessThan(ALLOWED_PRICE_IMPACT_HIGH)) return 3 let impact = IMPACT_TIERS.length
if (!priceImpact?.lessThan(ALLOWED_PRICE_IMPACT_MEDIUM)) return 2 for (const impactLevel of IMPACT_TIERS) {
if (!priceImpact?.lessThan(ALLOWED_PRICE_IMPACT_LOW)) return 1 if (priceImpact.lessThan(impactLevel)) return impact as 0 | 1 | 2 | 3 | 4
impact--
}
return 0 return 0
} }
......
...@@ -4158,10 +4158,10 @@ ...@@ -4158,10 +4158,10 @@
"@uniswap/v2-core" "1.0.1" "@uniswap/v2-core" "1.0.1"
"@uniswap/v3-core" "1.0.0-rc.2" "@uniswap/v3-core" "1.0.0-rc.2"
"@uniswap/v3-sdk@^1.0.0-alpha.22": "@uniswap/v3-sdk@^1.0.0-alpha.23":
version "1.0.0-alpha.22" version "1.0.0-alpha.23"
resolved "https://registry.yarnpkg.com/@uniswap/v3-sdk/-/v3-sdk-1.0.0-alpha.22.tgz#74acca3b952fc71a103fca14e79ee696f90096ba" resolved "https://registry.yarnpkg.com/@uniswap/v3-sdk/-/v3-sdk-1.0.0-alpha.23.tgz#0971b38acd46e08d727f17ad8b10b5c21a25b99f"
integrity sha512-HoITh2zpxG6xDh3hEtLPrqYAF3alNkPnpZ5mWlIvoql1W/3c2LKMvbsBowj7RGDSeMVK4OJjyE2m+rX9b/EqNw== integrity sha512-ibVW9EnwymIQAHBCCQCorwA5yLzRfQ4OYwafTkD1fHx2UtrZoHVYLBSshMa4tcN1uMAuiglFOQv/IIJ20ZRgyw==
dependencies: dependencies:
"@ethersproject/abi" "^5.0.12" "@ethersproject/abi" "^5.0.12"
"@ethersproject/solidity" "^5.0.9" "@ethersproject/solidity" "^5.0.9"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment