From 12601ad4ef809e20c09a85c67b8371bc976adcf8 Mon Sep 17 00:00:00 2001 From: Matt Holtzman Date: Fri, 18 Feb 2022 17:59:29 -0500 Subject: [PATCH] create erc-20 contract class --- main/abi/erc20.ts | 88 -------------------------------- main/accounts/Account/index.ts | 60 ++++++++++++---------- main/accounts/types.ts | 2 +- main/contracts/erc20.ts | 73 ++++++++++++++++++++++++++ main/{abi => contracts}/index.ts | 0 package-lock.json | 2 +- package.json | 2 +- 7 files changed, 108 insertions(+), 119 deletions(-) delete mode 100644 main/abi/erc20.ts create mode 100644 main/contracts/erc20.ts rename main/{abi => contracts}/index.ts (100%) diff --git a/main/abi/erc20.ts b/main/abi/erc20.ts deleted file mode 100644 index c217db7422..0000000000 --- a/main/abi/erc20.ts +++ /dev/null @@ -1,88 +0,0 @@ -import log from 'electron-log' - -import { decodeCallData as decodeContractData, DecodedCallData } from '.' -import { Interface } from '@ethersproject/abi' -import erc20 from '../externalData/balances/erc-20-abi' -import { Provider } from '../provider' - -const erc20Abi = JSON.stringify(erc20) -const erc20Interface = new Interface(erc20) - -function decodeCallData (contractAddress: Address, calldata: string) { - const decodedCall = decodeContractData(calldata, erc20Abi) - - if (decodedCall) { - return { - contractAddress: contractAddress.toLowerCase(), - contractName: 'ERC-20', - source: 'erc-20 contract', - ...decodedCall - } - } -} - -function isApproval (data: DecodedCallData) { - return ( - data.method === 'approve' && - data.args.length === 2 && - (data.args[0].name || '').toLowerCase().endsWith('spender') && data.args[0].type === 'address' && - (data.args[1].name || '').toLowerCase().endsWith('value') && data.args[1].type === 'uint256' - ) -} - -async function callContractFunction (provider: Provider, contractAddress: Address, functionName: string) { - const calldata = erc20Interface.encodeFunctionData(functionName) - - return new Promise((resolve, reject) => { - setTimeout(() => reject({ message: `request to contract ${contractAddress} timed out`}), 2500) - - provider.send({ - id: 1, - jsonrpc: '2.0', - _origin: 'frame.eth', - method: 'eth_call', - params: [ - { - to: contractAddress, - data: calldata, - value: '0x0' - }, 'latest' - ] - }, res => { - if (!res.result) return reject(res.error || { message: 'unknown error' }) - - resolve(erc20Interface.decodeFunctionResult(functionName, res.result)) - }) - }) -} - -async function getTokenData (provider: Provider, contractAddress: Address) { - const contractFnCall = callContractFunction.bind(null, provider, contractAddress) - - const calls = await Promise.all([ - contractFnCall('decimals').then(res => parseInt(res as string, 16) || 0), - contractFnCall('name').then(res => ((res as string[] || [])[0] || '').trim()), - contractFnCall('symbol').then(res => ((res as string[] || [])[0] || '').trim()) - ]).catch(err => { - log.warn(err.message) - - return [0, '', ''] - }) - - return { - decimals: calls[0], - name: calls[1], - symbol: calls[2] - } -} - -function encodeFunctionData (fn: string, params: any[]) { - return erc20Interface.encodeFunctionData(fn, params) -} - -export default { - encodeFunctionData, - decodeCallData, - getTokenData, - isApproval -} diff --git a/main/accounts/Account/index.ts b/main/accounts/Account/index.ts index 7261ff8ea6..aac7454115 100644 --- a/main/accounts/Account/index.ts +++ b/main/accounts/Account/index.ts @@ -3,8 +3,8 @@ import { isValidAddress, addHexPrefix } from 'ethereumjs-util' import { Version } from 'eth-sig-util' import { AccessRequest, AccountRequest, Accounts, RequestMode, TransactionRequest } from '..' -import { decodeContractCall } from '../../abi' -import erc20 from '../../abi/erc20' +import { decodeContractCall } from '../../contracts' +import erc20 from '../../contracts/erc20' import nebulaApi from '../../nebula' import signers from '../../signers' import windows from '../../windows' @@ -16,6 +16,7 @@ import { getType as getSignerType, Type as SignerType } from '../../signers/Sign import provider from '../../provider' import { ApprovalType } from '../../../resources/constants' +import Erc20Contract from '../../contracts/erc20' const nebula = nebulaApi('accounts') @@ -238,34 +239,37 @@ class FrameAccount { } private async checkForErc20Approve (req: TransactionRequest, calldata: string) { - const decodedData = erc20.decodeCallData(req.data.to || '', calldata) - - if (decodedData && erc20.isApproval(decodedData)) { - const spender = decodedData.args[0].value - const amount = decodedData.args[1].value - const { decimals, name, symbol } = await erc20.getTokenData(provider, decodedData.contractAddress) - - this.addRequiredApproval( - req, - ApprovalType.TokenSpendApproval, - { - decimals, - name, - symbol, - amount, - contract: decodedData.contractAddress - }, - data => { - req.data.data = erc20.encodeFunctionData('approve', [spender, data.amount]) - - if (req.decodedData) { - req.decodedData.args[1].value = data.amount - } + const contractAddress = req.data.to + if (!contractAddress) return + + const contract = new Erc20Contract(contractAddress, provider) + const decodedData = contract.decodeCallData(calldata) + + if (decodedData && Erc20Contract.isApproval(decodedData)) { + const spender = decodedData.args[0].toLowerCase() + const amount = decodedData.args[1].toNumber() + const { decimals, name, symbol } = await contract.getTokenData() + + this.addRequiredApproval( + req, + ApprovalType.TokenSpendApproval, + { + decimals, + name, + symbol, + amount, + contract: contractAddress + }, + data => { + req.data.data = contract.encodeCallData('approve', [spender, data.amount]) + + if (req.decodedData) { + req.decodedData.args[1].value = data.amount } - ) + } + ) - req.decodedData = decodedData - this.update() + this.update() } } diff --git a/main/accounts/types.ts b/main/accounts/types.ts index f1038624c6..d988436226 100644 --- a/main/accounts/types.ts +++ b/main/accounts/types.ts @@ -1,5 +1,5 @@ import type { Version } from 'eth-sig-util' -import type { DecodedCallData } from '../abi' +import type { DecodedCallData } from '../contracts' import type { Chain } from '../chains' import type { TransactionData } from '../transaction' diff --git a/main/contracts/erc20.ts b/main/contracts/erc20.ts new file mode 100644 index 0000000000..a070f14509 --- /dev/null +++ b/main/contracts/erc20.ts @@ -0,0 +1,73 @@ +import { TransactionDescription } from '@ethersproject/abi' +import { Contract } from '@ethersproject/contracts' +import { Web3Provider } from '@ethersproject/providers' +import erc20Abi from '../externalData/balances/erc-20-abi' +import type { Provider } from '../provider' + +function createWeb3ProviderWrapper (provider: Provider) { + const wrappedSend = (request: { method: string, params?: any[] }, cb: (error: any, response: any) => void) => { + provider.sendAsync({ + method: request.method, + params: request.params || [], + id: 1, + jsonrpc: '2.0', + _origin: 'frame.eth' }, cb) + } + + return { + sendAsync: wrappedSend, + send: wrappedSend + } +} + +export default class Erc20Contract { + private contract: Contract + + constructor (address: Address, provider: Provider) { + const web3Provider = new Web3Provider(createWeb3ProviderWrapper(provider)) + this.contract = new Contract(address, erc20Abi, web3Provider) + } + + static isApproval (data: TransactionDescription) { + return ( + data.name === 'approve' && + data.functionFragment.inputs.length === 2 && + (data.functionFragment.inputs[0].name || '').toLowerCase().endsWith('spender') && data.functionFragment.inputs[0].type === 'address' && + (data.functionFragment.inputs[1].name || '').toLowerCase().endsWith('value') && data.functionFragment.inputs[1].type === 'uint256' + ) + } + + decodeCallData (calldata: string) { + try { + return this.contract.interface.parseTransaction({ data: calldata }) + } catch (e) { + // call does not match ERC-20 interface + } + } + + encodeCallData (fn: string, params: any[]) { + return this.contract.interface.encodeFunctionData(fn, params) + } + + async getTokenData () { + try { + const calls = await Promise.all([ + this.contract.decimals(), + this.contract.name(), + this.contract.symbol() + ]) + + return { + decimals: calls[0], + name: calls[1], + symbol: calls[2] + } + } catch (e) { + return { + decimals: 0, + name: '', + symbol: '' + } + } + } +} diff --git a/main/abi/index.ts b/main/contracts/index.ts similarity index 100% rename from main/abi/index.ts rename to main/contracts/index.ts diff --git a/package-lock.json b/package-lock.json index 0496ce4ad6..864cddeb72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@aragon/wrapper": "5.5.1", "@ethereumjs/common": "2.6.0", "@ethereumjs/tx": "3.4.0", - "@ethersproject/abi": "5.5.0", + "@ethersproject/contracts": "5.5.0", "@githubprimer/octicons-react": "8.5.0", "@ledgerhq/hw-app-eth": "6.23.1", "@ledgerhq/hw-transport-node-hid-noevents": "6.20.0", diff --git a/package.json b/package.json index 4cd0f046a9..f833642243 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "@aragon/wrapper": "5.5.1", "@ethereumjs/common": "2.6.0", "@ethereumjs/tx": "3.4.0", - "@ethersproject/abi": "5.5.0", + "@ethersproject/contracts": "5.5.0", "@githubprimer/octicons-react": "8.5.0", "@ledgerhq/hw-app-eth": "6.23.1", "@ledgerhq/hw-transport-node-hid-noevents": "6.20.0",