Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade web3-eth from v1 to v4 #542

Merged
merged 13 commits into from
Oct 27, 2023
Merged
1,624 changes: 305 additions & 1,319 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
"@stablelib/utf8": "^1.0.1",
"@zxing/browser": "^0.1.4",
"@zxing/library": "^0.20.0",
"abi-decoder": "^2.4.0",
"assert": "^2.1.0",
"axios": "^1.5.0",
"b64-to-blob": "^1.2.19",
Expand Down Expand Up @@ -83,8 +82,11 @@
"vuetify": "^3.3.17",
"vuex": "^4.1.0",
"vuex-persist": "^3.1.3",
"web3-eth": "^1.9.0",
"web3-utils": "^1.9.0"
"web3-eth": "^4.2.0",
"web3-eth-abi": "^4.1.3",
"web3-eth-accounts": "^4.0.6",
"web3-eth-contract": "^4.1.0",
"web3-utils": "^4.0.6"
},
"devDependencies": {
"@electron/notarize": "^2.1.0",
Expand Down Expand Up @@ -127,7 +129,8 @@
"vue-cli-plugin-vuetify": "~2.5.8",
"vue-eslint-parser": "^9.3.1",
"vue-template-compiler": "^2.7.14",
"vue-tsc": "^1.8.13"
"vue-tsc": "^1.8.13",
"web3-types": "^1.3.0"
},
"main": "dist-electron/main.js",
"keywords": [
Expand Down
55 changes: 55 additions & 0 deletions src/lib/__tests__/eth-utils.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// @vitest-environment node
// Some crypto libs throw errors when using `jsdom` environment

import { describe, it, expect } from 'vitest'
import Web3Eth from 'web3-eth'

import { toEther, toWei, getAccountFromPassphrase } from '@/lib/eth-utils'

describe('eth-utils', () => {
describe('toEther', () => {
it('should convert Wei amount to Ether from a number', () => {
expect(toEther(1)).toBe('0.000000000000000001')
})

it('should convert Wei amount to Ether from a numeric string', () => {
expect(toEther('1')).toBe('0.000000000000000001')
})
})

describe('toWei', () => {
it('should convert Ether value into Wei from a number', () => {
expect(toWei(1)).toBe('1000000000000000000')
})

it('should convert Ether value into Wei from a numeric string', () => {
expect(toWei('1')).toBe('1000000000000000000')
})

it('should convert Gwei value into Wei', () => {
expect(toWei(1, 'gwei')).toBe('1000000000')
})
})

describe('getAccountFromPassphrase', () => {
const passphrase = 'joy mouse injury soft decade bid rough about alarm wreck season sting'
const api = new Web3Eth('https://clown.adamant.im')

it('should generate account from passphrase with "web3Account"', () => {
expect(getAccountFromPassphrase(passphrase, api)).toMatchObject({
web3Account: {
address: '0x045d7e948087D9C6D88D58e41587A610400869B6',
privateKey: '0x344854fa2184c252bdcc09daf8fe7fbcc960aed8f4da68de793f9fbc50b5a686'
},
address: '0x045d7e948087D9C6D88D58e41587A610400869B6',
privateKey: '0x344854fa2184c252bdcc09daf8fe7fbcc960aed8f4da68de793f9fbc50b5a686'
})
})

it('should generate account from passphrase without "web3Account"', () => {
expect(getAccountFromPassphrase(passphrase)).toEqual({
privateKey: '0x344854fa2184c252bdcc09daf8fe7fbcc960aed8f4da68de793f9fbc50b5a686'
})
})
})
})
136 changes: 136 additions & 0 deletions src/lib/abi/abi-decoder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/**
* The code is based on https://github.com/Consensys/abi-decoder
*/
import type { Components, JsonEventInterface, JsonFunctionInterface } from 'web3-types'
import { sha3 } from 'web3-utils'
import { decodeParameters } from 'web3-eth-abi'
import BigNumber from 'bignumber.js'

type Method = {
name: string
params: MethodsParams[]
}

type MethodsParams = {
name: string
type: string
value: string | string[]
}

function componentType(input: Components): string {
if (input.type === 'tuple') {
const tupleTypes = input.components!.map(componentType)
martiliones marked this conversation as resolved.
Show resolved Hide resolved

return '(' + tupleTypes.join(',') + ')'
bludnic marked this conversation as resolved.
Show resolved Hide resolved
}

return input.type
}

/**
* Returns `true` if the input type is one of:
* uint, uint8, uint16, uint32, uint64, uint128, uint256
*/
function isUint(input: Components) {
return input.type.startsWith('uint')
}

/**
* Returns `true` if the input type is one of:
* int, int8, int16, int32, int64, int128, int256
*/
function isInt(input: Components) {
return input.type.startsWith('int')
}

/**
* Returns `true` if the input is an ETH address
*/
function isAddress(input: Components) {
return input.type === 'address'
}

export class AbiDecoder {
readonly schema: Array<JsonFunctionInterface | JsonEventInterface>
readonly methods: Record<string, JsonFunctionInterface | JsonEventInterface>

constructor(schema: Array<JsonFunctionInterface | JsonEventInterface>) {
this.schema = schema
this.methods = this.parseMethods()
}

decodeMethod(data: string) {
const methodId = data.slice(2, 10)
const abiItem = this.methods[methodId]
if (abiItem) {
const decodedParams = decodeParameters(abiItem.inputs, data.slice(10))

const retData: Method = {
name: abiItem.name,
params: []
}

for (let i = 0; i < decodedParams.__length__; i++) {
const param = decodedParams[i] as string | string[]
let parsedParam = param

const input = abiItem.inputs[i]

if (isInt(input) || isUint(input)) {
const isArray = Array.isArray(param)

if (isArray) {
parsedParam = param.map((number) => new BigNumber(number).toString())
} else {
parsedParam = new BigNumber(param).toString()
}
}

// Addresses returned by web3 are randomly cased, so we need to standardize and lowercase all
if (isAddress(input)) {
const isArray = Array.isArray(param)

if (isArray) {
parsedParam = param.map((address) => address.toLowerCase())
} else {
parsedParam = param.toLowerCase()
}
}

retData.params.push({
name: input.name,
value: parsedParam,
type: input.type
})
}

return retData
}
}

methodName(data: string): string | null {
const methodId = data.slice(2, 10)
const method = this.methods[methodId]

return method ? method.name : null
}

private parseMethods(): Record<string, JsonFunctionInterface | JsonEventInterface> {
const methods: Record<string, JsonFunctionInterface | JsonEventInterface> = {}

for (const abi of this.schema) {
if (!abi.name) {
continue
}

const inputTypes = abi.inputs.map(componentType)
const signature = sha3(abi.name + '(' + inputTypes.join(',') + ')') as string

const methodId = abi.type === 'event' ? signature.slice(2) : signature.slice(2, 10) // event | function

methods[methodId] = abi
}

return methods
}
}
40 changes: 3 additions & 37 deletions src/lib/eth-utils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import hdkey from 'hdkey'
import web3Utils from 'web3-utils'
import * as web3Utils from 'web3-utils'
import { privateKeyToAccount } from 'web3-eth-accounts'
import BigNumber from 'bignumber.js'
import cache from '@/store/cache.js'

Expand Down Expand Up @@ -33,7 +34,7 @@ export function getAccountFromPassphrase(passphrase, api) {
hdkey.fromMasterSeed(seed).derive(HD_KEY_PATH)._privateKey
)
// web3Account is for user wallet; We don't need it, when exporting a private key
const web3Account = api ? api.accounts.privateKeyToAccount(privateKey) : undefined
const web3Account = api ? privateKeyToAccount(privateKey) : undefined

return {
web3Account,
Expand Down Expand Up @@ -105,38 +106,3 @@ export function toFraction(amount, decimals, separator = '.') {

return whole + (fraction ? separator + fraction : '')
}

export class BatchQueue {
constructor(createBatchRequest) {
this._createBatchRequest = createBatchRequest
this._queue = []
this._timer = null
}

enqueue(key, supplier) {
if (typeof supplier !== 'function') return
if (this._queue.some((x) => x.key === key)) return

const requests = supplier()
this._queue.push({ key, requests: Array.isArray(requests) ? requests : [requests] })
}

start() {
this.stop()
this._timer = setInterval(() => this._execute(), 2000)
}

stop() {
clearInterval(this._timer)
}

_execute() {
const requests = this._queue.splice(0, 20)
if (!requests.length) return

const batch = this._createBatchRequest()
requests.forEach((x) => x.requests.forEach((r) => batch.add(r)))

batch.execute()
}
}
24 changes: 15 additions & 9 deletions src/store/modules/erc20/erc20-actions.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import abiDecoder from 'abi-decoder'

import * as ethUtils from '../../../lib/eth-utils'
import { FetchStatus, INCREASE_FEE_MULTIPLIER } from '@/lib/constants'
import EthContract from 'web3-eth-contract'
import Erc20 from './erc20.abi.json'
import createActions from '../eth-base/eth-base-actions'
import getEndpointUrl from '@/lib/getEndpointUrl'
import { AbiDecoder } from '@/lib/abi/abi-decoder'

/** Timestamp of the most recent status update */
let lastStatusUpdate = 0
/** Status update interval is 25 sec: ERC20 balance */
const STATUS_INTERVAL = 25000

// Setup decoder
abiDecoder.addABI(Erc20)
const abiDecoder = new AbiDecoder(Erc20)

const initTransaction = (api, context, ethAddress, amount, increaseFee) => {
const contract = new api.Contract(Erc20, context.state.contractAddress)
const contract = new EthContract(Erc20, context.state.contractAddress)

const transaction = {
from: context.state.address,
Expand Down Expand Up @@ -52,17 +53,17 @@ const parseTransaction = (context, tx) => {
// Why comparing to eth.actions, there is no fee and status?
hash: tx.hash,
senderId: tx.from,
blockNumber: tx.blockNumber,
blockNumber: Number(tx.blockNumber),
amount,
recipientId,
gasPrice: +(tx.gasPrice || tx.effectiveGasPrice)
gasPrice: Number(tx.gasPrice || tx.effectiveGasPrice)
}
}

return null
}

const createSpecificActions = (api, queue) => ({
const createSpecificActions = (api) => ({
updateBalance: {
root: true,
async handler({ state, commit }, payload = {}) {
Expand All @@ -71,7 +72,9 @@ const createSpecificActions = (api, queue) => ({
}

try {
const contract = new api.Contract(Erc20, state.contractAddress)
const contract = new EthContract(Erc20, state.contractAddress)
const endpoint = getEndpointUrl('ETH')
contract.setProvider(endpoint)
const rawBalance = await contract.methods.balanceOf(state.address).call()
const balance = Number(ethUtils.toFraction(rawBalance, state.decimals))

Expand All @@ -88,7 +91,10 @@ const createSpecificActions = (api, queue) => ({
updateStatus(context) {
if (!context.state.address) return

const contract = new api.Contract(Erc20, context.state.contractAddress)
const contract = new EthContract(Erc20, context.state.contractAddress)
const endpoint = getEndpointUrl('ETH')
contract.setProvider(endpoint)

contract.methods
.balanceOf(context.state.address)
.call()
Expand Down
Loading