Ropsten 테스트넷에서 접속하여서 swap 페이지를 조사를 해봤다.
처음 화면은 이렇게 나온다.
유니스왑 코인이 없을 때 하면 다음과 같은 화면이 나온다.
스왑할 이더를 선택하면 이러한 화면이 나온다.
그리고 그 후에 이렇게 나온다.
여기서 스왑을 선택하면 다음과 같이 나온다.
데이터는 다음과 같았다.
0x414bf389000000000000000000000000c778417e063141139fce010982780140aa0cd5ab0000000000000000000000001f9840a85d5af5bf1d1762f925bdaddc4201f9840000000000000000000000000000000000000000000000000000000000000bb80000000000000000000000009e4bd927441f2e7d0e13a263e6e25488557d667100000000000000000000000000000000000000000000000000000000618692b50000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000001ffa4f22d47ff3a0000000000000000000000000000000000000000000000000000000000000000
이렇게 됨을 알 수 있다.
<ButtonError
onClick={() => {
if (isExpertMode) {
handleSwap()
} else {
setSwapState({
tradeToConfirm: trade,
attemptingTxn: false,
swapErrorMessage: undefined,
showConfirm: true,
txHash: undefined,
})
}
}}
width="100%"
id="swap-button"
disabled={
!isValid ||
(approvalState !== ApprovalState.APPROVED && signatureState !== UseERC20PermitState.SIGNED) ||
priceImpactTooHigh
}
error={isValid && priceImpactSeverity > 2}
>
<Text fontSize={16} fontWeight={500}>
{priceImpactTooHigh ? (
<Trans>High Price Impact</Trans>
) : priceImpactSeverity > 2 ? (
<Trans>Swap Anyway</Trans>
) : (
<Trans>Swap</Trans>
)}
</Text>
</ButtonError>
</AutoColumn>
</AutoRow>
) : (
<ButtonError
onClick={() => {
if (isExpertMode) {
handleSwap()
} else {
setSwapState({
tradeToConfirm: trade,
attemptingTxn: false,
swapErrorMessage: undefined,
showConfirm: true,
txHash: undefined,
})
}
}}
id="swap-button"
disabled={!isValid || priceImpactTooHigh || !!swapCallbackError}
error={isValid && priceImpactSeverity > 2 && !swapCallbackError}
>
<Text fontSize={20} fontWeight={500}>
{swapInputError ? (
swapInputError
) : priceImpactTooHigh ? (
<Trans>Price Impact Too High</Trans>
) : priceImpactSeverity > 2 ? (
<Trans>Swap Anyway</Trans>
) : (
<Trans>Swap</Trans>
)}
</Text>
</ButtonError>
두가지로 나누어진다. showApproveFlow
의 여부에 따라 갈라진다. 그런데 disabled내용과 fontsize만 달라질 뿐 다른 것은 크게 달라지지 않았다. 즉, 스마트 컨트랙트에는 직접적이 영향을 미치지 않는다는 것을 알 수 있었다.
다만, ExpretMode의 상태에 따라 실행되는 함수가 다름을 알 수 있다. isExpertMode가 true이면 handleSwap()
이 false이면 setSwapState가 실행됨을 알 수 있다.
isExpretMode:true=>handleSwap()
const handleSwap = useCallback(() => {
if (!swapCallback) {
return
}
if (priceImpact && !confirmPriceImpactWithoutFee(priceImpact)) {
return
}
setSwapState({ attemptingTxn: true, tradeToConfirm, showConfirm, swapErrorMessage: undefined, txHash: undefined })
swapCallback()
.then((hash) => {
setSwapState({ attemptingTxn: false, tradeToConfirm, showConfirm, swapErrorMessage: undefined, txHash: hash })
ReactGA.event({
category: 'Swap',
action:
recipient === null
? 'Swap w/o Send'
: (recipientAddress ?? recipient) === account
? 'Swap w/o Send + recipient'
: 'Swap w/ Send',
label: [
trade?.inputAmount?.currency?.symbol,
trade?.outputAmount?.currency?.symbol,
getTradeVersion(trade),
'MH',
].join('/'),
})
})
.catch((error) => {
setSwapState({
attemptingTxn: false,
tradeToConfirm,
showConfirm,
swapErrorMessage: error.message,
txHash: undefined,
})
})
}, [swapCallback, priceImpact, tradeToConfirm, showConfirm, recipient, recipientAddress, account, trade])
useCallback은 예전에 사용했던 함수를 재사용할 때 함수를 다시 정의하지 않고 호출하는 react의 내장 함수이다. deps에 props로 받아온 함수를 넣어 주어야 한다.
if (!swapCallback) {
return
}
이 부분은 SwapCallback의 상태가 false이면 빠져나가겠다는 내용이다.
if (priceImpact && !confirmPriceImpactWithoutFee(priceImpact)) {
return
}
위의 내용은 priceImpact가 true이거나 존재하고,
confirmPriceImpactWithoutFee(priceImpact)가 false일 때 빠져나오겠다는 내용이다. 즉,
!swapcallback==true
이면 탈락!
priceImpact==true
이고 !confirmPriceImpactWithoutFee(priceImpact)
가 true이면 탈락!
setSwapState({ attemptingTxn: true, tradeToConfirm, showConfirm, swapErrorMessage: undefined, txHash: undefined })
swapCallback()
.then((hash) => {
setSwapState({ attemptingTxn: false, tradeToConfirm, showConfirm, swapErrorMessage: undefined, txHash: hash })
위의 함수가 실행됨을 알 수 있다. 여기서도 setsWapState가 쓰임을 알 수 있다.
reactGA는 구글 어낼리틱스 내용이니 생략하도록 하겠다.
.catch((error) => {
setSwapState({
attemptingTxn: false,
tradeToConfirm,
showConfirm,
swapErrorMessage: error.message,
txHash: undefined,
})
})
에러늬 내용을 담고 setSwapState의 내용을 위와 같이 채움을 알 수 있다. swapErrorMessage
를띄워줘나보다.
const [{ showConfirm, tradeToConfirm, swapErrorMessage, attemptingTxn, txHash }, setSwapState] = useState<{
showConfirm: boolean
tradeToConfirm: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | undefined
attemptingTxn: boolean
swapErrorMessage: string | undefined
txHash: string | undefined
}>({
showConfirm: false,
tradeToConfirm: undefined,
attemptingTxn: false,
swapErrorMessage: undefined,
txHash: undefined,
})
여기서setSwapState는 Dispatch임을 알 수 있다.
SetStateAction을 변화시키는 것 같다.
이건 const [state,setState]=useState('initialstate')
랑 같은 코드이다.
그러니까 swapstate는 showConfirm, tradeToConfirm, swapErrorMessage, atttemptingTxn,txHash가 있는 것이다.
여기서 tradeToConfirm으로 V2Trade를 사용할 것인지 V3Trade를 사용할 것인지 결정할 수 있음을 알 수 있다.
이건 그냥 상태를 바꾸는 거같다.
일단 진짜로 되는 걸 알아보자.
코드의 주석으로 보니 진짜로 swap을 실행하는 것임을 알 수 있다. 그러면 trade,allowedSlippage,recipient, signatureData를 인자값으로 받아 useSwapCallback을 실행시킨다.
hooks의 useSWapCallback에서 가져옴을 알 수 있다.
447번째 줄에도 나온다.
사이트 화면에서는
이부분이 바로 ConfirmSwapModal이다. 그 안의 trade를 보니 다음과 같은 항목이 나온다.
도저히 뭔지 모르겠어서 마우스를 올려놓으니 다음과 같이 나온다.
여기에 있어서 이걸 찾아보았다.
export function useDerivedSwapInfo(toggledVersion: Version | undefined): {
currencies: { [field in Field]?: Currency | null }
currencyBalances: { [field in Field]?: CurrencyAmount<Currency> }
parsedAmount: CurrencyAmount<Currency> | undefined
inputError?: ReactNode
v2Trade: V2Trade<Currency, Currency, TradeType> | undefined
v3Trade: {
trade: V3Trade<Currency, Currency, TradeType> | null
state: V3TradeState
}
bestTrade: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | undefined
allowedSlippage: Percent
} {
const { account } = useActiveWeb3React()
const {
independentField,
typedValue,
[Field.INPUT]: { currencyId: inputCurrencyId },
[Field.OUTPUT]: { currencyId: outputCurrencyId },
recipient,
} = useSwapState()
const inputCurrency = useCurrency(inputCurrencyId)
const outputCurrency = useCurrency(outputCurrencyId)
const recipientLookup = useENS(recipient ?? undefined)
const to: string | null = (recipient === null ? account : recipientLookup.address) ?? null
const relevantTokenBalances = useCurrencyBalances(account ?? undefined, [
inputCurrency ?? undefined,
outputCurrency ?? undefined,
])
const isExactIn: boolean = independentField === Field.INPUT
const parsedAmount = useMemo(
() => tryParseAmount(typedValue, (isExactIn ? inputCurrency : outputCurrency) ?? undefined),
[inputCurrency, isExactIn, outputCurrency, typedValue]
)
// get v2 and v3 quotes
// skip if other version is toggled
const v2Trade = useBestV2Trade(
isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT,
toggledVersion !== Version.v3 ? parsedAmount : undefined,
(isExactIn ? outputCurrency : inputCurrency) ?? undefined
)
const v3Trade = useBestV3Trade(
isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT,
toggledVersion !== Version.v2 ? parsedAmount : undefined,
(isExactIn ? outputCurrency : inputCurrency) ?? undefined
)
const isV2TradeBetter = useMemo(() => {
try {
// avoids comparing trades when V3Trade is not in a ready state.
return toggledVersion === Version.v2 ||
[V3TradeState.VALID, V3TradeState.SYNCING, V3TradeState.NO_ROUTE_FOUND].includes(v3Trade.state)
? isTradeBetter(v3Trade.trade, v2Trade, TWO_PERCENT)
: undefined
} catch (e) {
// v3 trade may be debouncing or fetching and have different
// inputs/ouputs than v2
return undefined
}
}, [toggledVersion, v2Trade, v3Trade.state, v3Trade.trade])
const bestTrade = isV2TradeBetter == undefined ? undefined : isV2TradeBetter ? v2Trade : v3Trade.trade
const currencyBalances = {
[Field.INPUT]: relevantTokenBalances[0],
[Field.OUTPUT]: relevantTokenBalances[1],
}
const currencies: { [field in Field]?: Currency | null } = {
[Field.INPUT]: inputCurrency,
[Field.OUTPUT]: outputCurrency,
}
let inputError: ReactNode | undefined
if (!account) {
inputError = <Trans>Connect Wallet</Trans>
}
if (!parsedAmount) {
inputError = inputError ?? <Trans>Enter an amount</Trans>
}
if (!currencies[Field.INPUT] || !currencies[Field.OUTPUT]) {
inputError = inputError ?? <Trans>Select a token</Trans>
}
const formattedTo = isAddress(to)
if (!to || !formattedTo) {
inputError = inputError ?? <Trans>Enter a recipient</Trans>
} else {
if (BAD_RECIPIENT_ADDRESSES[formattedTo] || (v2Trade && involvesAddress(v2Trade, formattedTo))) {
inputError = inputError ?? <Trans>Invalid recipient</Trans>
}
}
const allowedSlippage = useSwapSlippageTolerance(bestTrade ?? undefined)
// compare input balance to max input based on version
const [balanceIn, amountIn] = [currencyBalances[Field.INPUT], bestTrade?.maximumAmountIn(allowedSlippage)]
if (balanceIn && amountIn && balanceIn.lessThan(amountIn)) {
inputError = <Trans>Insufficient {amountIn.currency.symbol} balance</Trans>
}
return {
currencies,
currencyBalances,
parsedAmount,
inputError,
v2Trade: v2Trade ?? undefined,
v3Trade,
bestTrade: bestTrade ?? undefined,
allowedSlippage,
}
}
export function useSwapCallback(
trade: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | undefined, // trade to execute, required
allowedSlippage: Percent, // in bips
recipientAddressOrName: string | null, // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender
signatureData: SignatureData | undefined | null
): { state: SwapCallbackState; callback: null | (() => Promise<string>); error: ReactNode | null } {
const { account, chainId, library } = useActiveWeb3React()
const swapCalls = useSwapCallArguments(trade, allowedSlippage, recipientAddressOrName, signatureData)
const addTransaction = useTransactionAdder()
const { address: recipientAddress } = useENS(recipientAddressOrName)
const recipient = recipientAddressOrName === null ? account : recipientAddress
return useMemo(() => {
if (!trade || !library || !account || !chainId) {
return { state: SwapCallbackState.INVALID, callback: null, error: <Trans>Missing dependencies</Trans> }
}
if (!recipient) {
if (recipientAddressOrName !== null) {
return { state: SwapCallbackState.INVALID, callback: null, error: <Trans>Invalid recipient</Trans> }
} else {
return { state: SwapCallbackState.LOADING, callback: null, error: null }
}
}
return {
state: SwapCallbackState.VALID,
callback: async function onSwap(): Promise<string> {
const estimatedCalls: SwapCallEstimate[] = await Promise.all(
swapCalls.map((call) => {
const { address, calldata, value } = call
const tx =
!value || isZero(value)
? { from: account, to: address, data: calldata }
: {
from: account,
to: address,
data: calldata,
value,
}
return library
.estimateGas(tx)
.then((gasEstimate) => {
return {
call,
gasEstimate,
}
})
.catch((gasError) => {
console.debug('Gas estimate failed, trying eth_call to extract error', call)
return library
.call(tx)
.then((result) => {
console.debug('Unexpected successful call after failed estimate gas', call, gasError, result)
return { call, error: <Trans>Unexpected issue with estimating the gas. Please try again.</Trans> }
})
.catch((callError) => {
console.debug('Call threw error', call, callError)
return { call, error: swapErrorToUserReadableMessage(callError) }
})
})
})
)
// a successful estimation is a bignumber gas estimate and the next call is also a bignumber gas estimate
let bestCallOption: SuccessfulCall | SwapCallEstimate | undefined = estimatedCalls.find(
(el, ix, list): el is SuccessfulCall =>
'gasEstimate' in el && (ix === list.length - 1 || 'gasEstimate' in list[ix + 1])
)
// check if any calls errored with a recognizable error
if (!bestCallOption) {
const errorCalls = estimatedCalls.filter((call): call is FailedCall => 'error' in call)
if (errorCalls.length > 0) throw errorCalls[errorCalls.length - 1].error
const firstNoErrorCall = estimatedCalls.find<SwapCallEstimate>(
(call): call is SwapCallEstimate => !('error' in call)
)
if (!firstNoErrorCall) throw new Error(t`Unexpected error. Could not estimate gas for the swap.`)
bestCallOption = firstNoErrorCall
}
const {
call: { address, calldata, value },
} = bestCallOption
return library
.getSigner()
.sendTransaction({
from: account,
to: address,
data: calldata,
// let the wallet try if we can't estimate the gas
...('gasEstimate' in bestCallOption
? { gasLimit: calculateGasMargin(chainId, bestCallOption.gasEstimate) }
: {}),
...(value && !isZero(value) ? { value } : {}),
})
.then((response) => {
addTransaction(
response,
trade.tradeType === TradeType.EXACT_INPUT
? {
type: TransactionType.SWAP,
tradeType: TradeType.EXACT_INPUT,
inputCurrencyId: currencyId(trade.inputAmount.currency),
inputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
expectedOutputCurrencyAmountRaw: trade.outputAmount.quotient.toString(),
outputCurrencyId: currencyId(trade.outputAmount.currency),
minimumOutputCurrencyAmountRaw: trade.minimumAmountOut(allowedSlippage).quotient.toString(),
}
: {
type: TransactionType.SWAP,
tradeType: TradeType.EXACT_OUTPUT,
inputCurrencyId: currencyId(trade.inputAmount.currency),
maximumInputCurrencyAmountRaw: trade.maximumAmountIn(allowedSlippage).quotient.toString(),
outputCurrencyId: currencyId(trade.outputAmount.currency),
outputCurrencyAmountRaw: trade.outputAmount.quotient.toString(),
expectedInputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
}
)
return response.hash
})
.catch((error) => {
// if the user rejected the tx, pass this along
if (error?.code === 4001) {
throw new Error(t`Transaction rejected.`)
} else {
// otherwise, the error was unexpected and we need to convey that
console.error(`Swap failed`, error, address, calldata, value)
throw new Error(t`Swap failed: ${swapErrorToUserReadableMessage(error)}`)
}
})
},
error: null,
}
}, [trade, library, account, chainId, recipient, recipientAddressOrName, swapCalls, addTransaction, allowedSlippage])
}
거래를 만들기 위한 스왑 콜을 리턴하는 함수.
/**
* Returns the swap calls that can be used to make the trade
* @param trade trade to execute
* @param allowedSlippage user allowed slippage
* @param recipientAddressOrName the ENS name or address of the recipient of the swap output
* @param signatureData the signature data of the permit of the input token amount, if available
*/
function useSwapCallArguments(
trade: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | undefined, // trade to execute, required
allowedSlippage: Percent, // in bips
recipientAddressOrName: string | null, // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender
signatureData: SignatureData | null | undefined
): SwapCall[] {
const { account, chainId, library } = useActiveWeb3React()
const { address: recipientAddress } = useENS(recipientAddressOrName)
const recipient = recipientAddressOrName === null ? account : recipientAddress
const deadline = useTransactionDeadline()
const routerContract = useV2RouterContract()
const argentWalletContract = useArgentWalletContract()
return useMemo(() => {
if (!trade || !recipient || !library || !account || !chainId || !deadline) return []
if (trade instanceof V2Trade) {
if (!routerContract) return []
const swapMethods = []
swapMethods.push(
Router.swapCallParameters(trade, {
feeOnTransfer: false,
allowedSlippage,
recipient,
deadline: deadline.toNumber(),
})
)
if (trade.tradeType === TradeType.EXACT_INPUT) {
swapMethods.push(
Router.swapCallParameters(trade, {
feeOnTransfer: true,
allowedSlippage,
recipient,
deadline: deadline.toNumber(),
})
)
}
return swapMethods.map(({ methodName, args, value }) => {
if (argentWalletContract && trade.inputAmount.currency.isToken) {
return {
address: argentWalletContract.address,
calldata: argentWalletContract.interface.encodeFunctionData('wc_multiCall', [
[
approveAmountCalldata(trade.maximumAmountIn(allowedSlippage), routerContract.address),
{
to: routerContract.address,
value,
data: routerContract.interface.encodeFunctionData(methodName, args),
},
],
]),
value: '0x0',
}
} else {
return {
address: routerContract.address,
calldata: routerContract.interface.encodeFunctionData(methodName, args),
value,
}
}
})
} else {
// trade is V3Trade
const swapRouterAddress = chainId ? SWAP_ROUTER_ADDRESSES[chainId] : undefined
if (!swapRouterAddress) return []
const { value, calldata } = SwapRouter.swapCallParameters(trade, {
recipient,
slippageTolerance: allowedSlippage,
deadline: deadline.toString(),
...(signatureData
? {
inputTokenPermit:
'allowed' in signatureData
? {
expiry: signatureData.deadline,
nonce: signatureData.nonce,
s: signatureData.s,
r: signatureData.r,
v: signatureData.v as any,
}
: {
deadline: signatureData.deadline,
amount: signatureData.amount,
s: signatureData.s,
r: signatureData.r,
v: signatureData.v as any,
},
}
: {}),
})
if (argentWalletContract && trade.inputAmount.currency.isToken) {
return [
{
address: argentWalletContract.address,
calldata: argentWalletContract.interface.encodeFunctionData('wc_multiCall', [
[
approveAmountCalldata(trade.maximumAmountIn(allowedSlippage), swapRouterAddress),
{
to: swapRouterAddress,
value,
data: calldata,
},
],
]),
value: '0x0',
},
]
}
return [
{
address: swapRouterAddress,
calldata,
value,
},
]
}
}, [
account,
allowedSlippage,
argentWalletContract,
chainId,
deadline,
library,
recipient,
routerContract,
signatureData,
trade,
])
}
여기가 진짜로 실질적인 스마트 컨트랙트가 이루어지는 함수인 것 같다.
function useSwapCallArguments(
trade: V2Trade<Currency, Currency, TradeType> | V3Trade<Currency, Currency, TradeType> | undefined, // trade to execute, required
allowedSlippage: Percent, // in bips
recipientAddressOrName: string | null, // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender
signatureData: SignatureData | null | undefined
): SwapCall[] {
const { account, chainId, library } = useActiveWeb3React()
const { address: recipientAddress } = useENS(recipientAddressOrName)
const recipient = recipientAddressOrName === null ? account : recipientAddress
const deadline = useTransactionDeadline()
const routerContract = useV2RouterContract()
const argentWalletContract = useArgentWalletContract()
return useMemo(() => {
if (!trade || !recipient || !library || !account || !chainId || !deadline) return []
if (trade instanceof V2Trade) {
if (!routerContract) return []
const swapMethods = []
swapMethods.push(
Router.swapCallParameters(trade, {
feeOnTransfer: false,
allowedSlippage,
recipient,
deadline: deadline.toNumber(),
})
)
if (trade.tradeType === TradeType.EXACT_INPUT) {
swapMethods.push(
Router.swapCallParameters(trade, {
feeOnTransfer: true,
allowedSlippage,
recipient,
deadline: deadline.toNumber(),
})
)
}
return swapMethods.map(({ methodName, args, value }) => {
if (argentWalletContract && trade.inputAmount.currency.isToken) {
return {
address: argentWalletContract.address,
calldata: argentWalletContract.interface.encodeFunctionData('wc_multiCall', [
[
approveAmountCalldata(trade.maximumAmountIn(allowedSlippage), routerContract.address),
{
to: routerContract.address,
value,
data: routerContract.interface.encodeFunctionData(methodName, args),
},
],
]),
value: '0x0',
}
} else {
return {
address: routerContract.address,
calldata: routerContract.interface.encodeFunctionData(methodName, args),
value,
}
}
})
} else {
// trade is V3Trade
const swapRouterAddress = chainId ? SWAP_ROUTER_ADDRESSES[chainId] : undefined
if (!swapRouterAddress) return []
const { value, calldata } = SwapRouter.swapCallParameters(trade, {
recipient,
slippageTolerance: allowedSlippage,
deadline: deadline.toString(),
...(signatureData
? {
inputTokenPermit:
'allowed' in signatureData
? {
expiry: signatureData.deadline,
nonce: signatureData.nonce,
s: signatureData.s,
r: signatureData.r,
v: signatureData.v as any,
}
: {
deadline: signatureData.deadline,
amount: signatureData.amount,
s: signatureData.s,
r: signatureData.r,
v: signatureData.v as any,
},
}
: {}),
})
if (argentWalletContract && trade.inputAmount.currency.isToken) {
return [
{
address: argentWalletContract.address,
calldata: argentWalletContract.interface.encodeFunctionData('wc_multiCall', [
[
approveAmountCalldata(trade.maximumAmountIn(allowedSlippage), swapRouterAddress),
{
to: swapRouterAddress,
value,
data: calldata,
},
],
]),
value: '0x0',
},
]
}
return [
{
address: swapRouterAddress,
calldata,
value,
},
]
}
}, [
account,
allowedSlippage,
argentWalletContract,
chainId,
deadline,
library,
recipient,
routerContract,
signatureData,
trade,
])
}
export function useActiveWeb3React() {
const context = useWeb3React<Web3Provider>()
const contextNetwork = useWeb3React<Web3Provider>(NetworkContextName)
return context.active ? context : contextNetwork
}
위의 useSwapCallArguments에서 const { account, chainId, library } = useActiveWeb3React()
였음을 기어갛자.
그러면Web3Provider을 알아보아야한다.
export class Web3Provider extends JsonRpcProvider {
readonly provider: ExternalProvider;
readonly jsonRpcFetchFunc: JsonRpcFetchFunc;
constructor(provider: ExternalProvider | JsonRpcFetchFunc, network?: Networkish) {
logger.checkNew(new.target, Web3Provider);
if (provider == null) {
logger.throwArgumentError("missing provider", "provider", provider);
}
let path: string = null;
let jsonRpcFetchFunc: JsonRpcFetchFunc = null;
let subprovider: ExternalProvider = null;
if (typeof(provider) === "function") {
path = "unknown:";
jsonRpcFetchFunc = provider;
} else {
path = provider.host || provider.path || "";
if (!path && provider.isMetaMask) {
path = "metamask";
}
subprovider = provider;
if (provider.request) {
if (path === "") { path = "eip-1193:"; }
jsonRpcFetchFunc = buildEip1193Fetcher(provider);
} else if (provider.sendAsync) {
jsonRpcFetchFunc = buildWeb3LegacyFetcher(provider, provider.sendAsync.bind(provider));
} else if (provider.send) {
jsonRpcFetchFunc = buildWeb3LegacyFetcher(provider, provider.send.bind(provider));
} else {
logger.throwArgumentError("unsupported provider", "provider", provider);
}
if (!path) { path = "unknown:"; }
}
super(path, network);
defineReadOnly(this, "jsonRpcFetchFunc", jsonRpcFetchFunc);
defineReadOnly(this, "provider", subprovider);
}
send(method: string, params: Array<any>): Promise<any> {
return this.jsonRpcFetchFunc(method, params);
}
}
import React from 'react';
import { Web3ReactContextInterface } from './types';
export declare const PRIMARY_KEY = "primary";
interface Web3ReactProviderArguments {
getLibrary: (provider?: any, connector?: Required<Web3ReactContextInterface>['connector']) => any;
children: any;
}
export declare function createWeb3ReactRoot(key: string): (args: Web3ReactProviderArguments) => JSX.Element;
export declare const Web3ReactProvider: (args: Web3ReactProviderArguments) => JSX.Element;
export declare function getWeb3ReactContext<T = any>(key?: string): React.Context<Web3ReactContextInterface<T>>;
export declare function useWeb3React<T = any>(key?: string): Web3ReactContextInterface<T>;
export {};
export const NetworkContextName = 'NETWORK'
export class JsonRpcProvider extends BaseProvider {
readonly connection: ConnectionInfo;
_pendingFilter: Promise<number>;
_nextId: number;
// During any given event loop, the results for a given call will
// all be the same, so we can dedup the calls to save requests and
// bandwidth. @TODO: Try out generalizing this against send?
_eventLoopCache: Record<string, Promise<any>>;
get _cache(): Record<string, Promise<any>> {
if (this._eventLoopCache == null) {
this._eventLoopCache = { };
}
return this._eventLoopCache;
}
constructor(url?: ConnectionInfo | string, network?: Networkish) {
logger.checkNew(new.target, JsonRpcProvider);
let networkOrReady: Networkish | Promise<Network> = network;
// The network is unknown, query the JSON-RPC for it
if (networkOrReady == null) {
networkOrReady = new Promise((resolve, reject) => {
setTimeout(() => {
this.detectNetwork().then((network) => {
resolve(network);
}, (error) => {
reject(error);
});
}, 0);
});
}
super(networkOrReady);
// Default URL
if (!url) { url = getStatic<() => string>(this.constructor, "defaultUrl")(); }
if (typeof(url) === "string") {
defineReadOnly(this, "connection",Object.freeze({
url: url
}));
} else {
defineReadOnly(this, "connection", Object.freeze(shallowCopy(url)));
}
this._nextId = 42;
}
static defaultUrl(): string {
return "http:/\/localhost:8545";
}
detectNetwork(): Promise<Network> {
if (!this._cache["detectNetwork"]) {
this._cache["detectNetwork"] = this._uncachedDetectNetwork();
// Clear this cache at the beginning of the next event loop
setTimeout(() => {
this._cache["detectNetwork"] = null;
}, 0);
}
return this._cache["detectNetwork"];
}
async _uncachedDetectNetwork(): Promise<Network> {
await timer(0);
let chainId = null;
try {
chainId = await this.send("eth_chainId", [ ]);
} catch (error) {
try {
chainId = await this.send("net_version", [ ]);
} catch (error) { }
}
if (chainId != null) {
const getNetwork = getStatic<(network: Networkish) => Network>(this.constructor, "getNetwork");
try {
return getNetwork(BigNumber.from(chainId).toNumber());
} catch (error) {
return logger.throwError("could not detect network", Logger.errors.NETWORK_ERROR, {
chainId: chainId,
event: "invalidNetwork",
serverError: error
});
}
}
return logger.throwError("could not detect network", Logger.errors.NETWORK_ERROR, {
event: "noNetwork"
});
}
getSigner(addressOrIndex?: string | number): JsonRpcSigner {
return new JsonRpcSigner(_constructorGuard, this, addressOrIndex);
}
getUncheckedSigner(addressOrIndex?: string | number): UncheckedJsonRpcSigner {
return this.getSigner(addressOrIndex).connectUnchecked();
}
listAccounts(): Promise<Array<string>> {
return this.send("eth_accounts", []).then((accounts: Array<string>) => {
return accounts.map((a) => this.formatter.address(a));
});
}
send(method: string, params: Array<any>): Promise<any> {
const request = {
method: method,
params: params,
id: (this._nextId++),
jsonrpc: "2.0"
};
this.emit("debug", {
action: "request",
request: deepCopy(request),
provider: this
});
// We can expand this in the future to any call, but for now these
// are the biggest wins and do not require any serializing parameters.
const cache = ([ "eth_chainId", "eth_blockNumber" ].indexOf(method) >= 0);
if (cache && this._cache[method]) {
return this._cache[method];
}
const result = fetchJson(this.connection, JSON.stringify(request), getResult).then((result) => {
this.emit("debug", {
action: "response",
request: request,
response: result,
provider: this
});
return result;
}, (error) => {
this.emit("debug", {
action: "response",
error: error,
request: request,
provider: this
});
throw error;
});
// Cache the fetch, but clear it on the next event loop
if (cache) {
this._cache[method] = result;
setTimeout(() => {
this._cache[method] = null;
}, 0);
}
return result;
}
prepareRequest(method: string, params: any): [ string, Array<any> ] {
switch (method) {
case "getBlockNumber":
return [ "eth_blockNumber", [] ];
case "getGasPrice":
return [ "eth_gasPrice", [] ];
case "getBalance":
return [ "eth_getBalance", [ getLowerCase(params.address), params.blockTag ] ];
case "getTransactionCount":
return [ "eth_getTransactionCount", [ getLowerCase(params.address), params.blockTag ] ];
case "getCode":
return [ "eth_getCode", [ getLowerCase(params.address), params.blockTag ] ];
case "getStorageAt":
return [ "eth_getStorageAt", [ getLowerCase(params.address), params.position, params.blockTag ] ];
case "sendTransaction":
return [ "eth_sendRawTransaction", [ params.signedTransaction ] ]
case "getBlock":
if (params.blockTag) {
return [ "eth_getBlockByNumber", [ params.blockTag, !!params.includeTransactions ] ];
} else if (params.blockHash) {
return [ "eth_getBlockByHash", [ params.blockHash, !!params.includeTransactions ] ];
}
return null;
case "getTransaction":
return [ "eth_getTransactionByHash", [ params.transactionHash ] ];
case "getTransactionReceipt":
return [ "eth_getTransactionReceipt", [ params.transactionHash ] ];
case "call": {
const hexlifyTransaction = getStatic<(t: TransactionRequest, a?: { [key: string]: boolean }) => { [key: string]: string }>(this.constructor, "hexlifyTransaction");
return [ "eth_call", [ hexlifyTransaction(params.transaction, { from: true }), params.blockTag ] ];
}
case "estimateGas": {
const hexlifyTransaction = getStatic<(t: TransactionRequest, a?: { [key: string]: boolean }) => { [key: string]: string }>(this.constructor, "hexlifyTransaction");
return [ "eth_estimateGas", [ hexlifyTransaction(params.transaction, { from: true }) ] ];
}
case "getLogs":
if (params.filter && params.filter.address != null) {
params.filter.address = getLowerCase(params.filter.address);
}
return [ "eth_getLogs", [ params.filter ] ];
default:
break;
}
return null;
}
async perform(method: string, params: any): Promise<any> {
// Legacy networks do not like the type field being passed along (which
// is fair), so we delete type if it is 0 and a non-EIP-1559 network
if (method === "call" || method === "estimateGas") {
const tx = params.transaction;
if (tx && tx.type != null && BigNumber.from(tx.type).isZero()) {
// If there are no EIP-1559 properties, it might be non-EIP-a559
if (tx.maxFeePerGas == null && tx.maxPriorityFeePerGas == null) {
const feeData = await this.getFeeData();
if (feeData.maxFeePerGas == null && feeData.maxPriorityFeePerGas == null) {
// Network doesn't know about EIP-1559 (and hence type)
params = shallowCopy(params);
params.transaction = shallowCopy(tx);
delete params.transaction.type;
}
}
}
}
const args = this.prepareRequest(method, params);
if (args == null) {
logger.throwError(method + " not implemented", Logger.errors.NOT_IMPLEMENTED, { operation: method });
}
try {
return await this.send(args[0], args[1])
} catch (error) {
return checkError(method, error, params);
}
}
_startEvent(event: Event): void {
if (event.tag === "pending") { this._startPending(); }
super._startEvent(event);
}
_startPending(): void {
if (this._pendingFilter != null) { return; }
const self = this;
const pendingFilter: Promise<number> = this.send("eth_newPendingTransactionFilter", []);
this._pendingFilter = pendingFilter;
pendingFilter.then(function(filterId) {
function poll() {
self.send("eth_getFilterChanges", [ filterId ]).then(function(hashes: Array<string>) {
if (self._pendingFilter != pendingFilter) { return null; }
let seq = Promise.resolve();
hashes.forEach(function(hash) {
// @TODO: This should be garbage collected at some point... How? When?
self._emitted["t:" + hash.toLowerCase()] = "pending";
seq = seq.then(function() {
return self.getTransaction(hash).then(function(tx) {
self.emit("pending", tx);
return null;
});
});
});
return seq.then(function() {
return timer(1000);
});
}).then(function() {
if (self._pendingFilter != pendingFilter) {
self.send("eth_uninstallFilter", [ filterId ]);
return;
}
setTimeout(function() { poll(); }, 0);
return null;
}).catch((error: Error) => { });
}
poll();
return filterId;
}).catch((error: Error) => { });
}
_stopEvent(event: Event): void {
if (event.tag === "pending" && this.listenerCount("pending") === 0) {
this._pendingFilter = null;
}
super._stopEvent(event);
}
// Convert an ethers.js transaction into a JSON-RPC transaction
// - gasLimit => gas
// - All values hexlified
// - All numeric values zero-striped
// - All addresses are lowercased
// NOTE: This allows a TransactionRequest, but all values should be resolved
// before this is called
// @TODO: This will likely be removed in future versions and prepareRequest
// will be the preferred method for this.
static hexlifyTransaction(transaction: TransactionRequest, allowExtra?: { [key: string]: boolean }): { [key: string]: string | AccessList } {
// Check only allowed properties are given
const allowed = shallowCopy(allowedTransactionKeys);
if (allowExtra) {
for (const key in allowExtra) {
if (allowExtra[key]) { allowed[key] = true; }
}
}
checkProperties(transaction, allowed);
const result: { [key: string]: string | AccessList } = {};
// Some nodes (INFURA ropsten; INFURA mainnet is fine) do not like leading zeros.
["gasLimit", "gasPrice", "type", "maxFeePerGas", "maxPriorityFeePerGas", "nonce", "value"].forEach(function(key) {
if ((<any>transaction)[key] == null) { return; }
const value = hexValue((<any>transaction)[key]);
if (key === "gasLimit") { key = "gas"; }
result[key] = value;
});
["from", "to", "data"].forEach(function(key) {
if ((<any>transaction)[key] == null) { return; }
result[key] = hexlify((<any>transaction)[key]);
});
if ((<any>transaction).accessList) {
result["accessList"] = accessListify((<any>transaction).accessList);
}
return result;
}
}
export function useExpertModeManager(): [boolean, () => void] {
const dispatch = useAppDispatch()
const expertMode = useIsExpertMode()
const toggleSetExpertMode = useCallback(() => {
dispatch(updateUserExpertMode({ userExpertMode: !expertMode }))
}, [expertMode, dispatch])
return [expertMode, toggleSetExpertMode]
}