Skip to content

Commit

Permalink
Merge pull request #542 from Adamant-im/feat/update-web3-eth
Browse files Browse the repository at this point in the history
Upgrade `web3-eth` from v1 to v4
  • Loading branch information
bludnic authored Oct 27, 2023
2 parents 36a1267 + d7bc161 commit 51a44f9
Show file tree
Hide file tree
Showing 8 changed files with 639 additions and 1,507 deletions.
1,622 changes: 304 additions & 1,318 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 @@ -32,7 +32,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 @@ -87,8 +86,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 @@ -134,7 +136,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)

return '(' + tupleTypes.join(',') + ')'
}

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 { getRandomNodeUrl } from '@/config/utils'
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 = getRandomNodeUrl('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 = getRandomNodeUrl('eth')
contract.setProvider(endpoint)

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

0 comments on commit 51a44f9

Please sign in to comment.