import Web3 from 'web3' ; import { Method } from 'web3-core-method' ; import BigNumber from 'bignumber.js' ; import { HttpProvider , provider as Provider , Transaction } from 'web3-core' ; import { BlockchainData } from '@shared/models/blockchain/blockchain-data' ; import { BLOCKCHAIN_NAME } from '@shared/models/blockchain/blockchain-name' ; import { BlockchainTokenExtended } from '@shared/models/tokens/blockchain-token-extended' ; import { AbiItem , isAddress , toChecksumAddress } from 'web3-utils' ; import { BlockTransactionString } from 'web3-eth' ; import { NATIVE_TOKEN_ADDRESS } from '@shared/constants/blockchain/native-token-address' ; import { UndefinedError } from '@core/errors/models/undefined.error' ; import { BIG_NUMBER_FORMAT } from '@shared/constants/formats/big-number-format' ; import { from , Observable , of } from 'rxjs' ; import { HEALTHCHECK } from '@core/services/blockchain/constants/healthcheck' ; import { catchError , map , timeout } from 'rxjs/operators' ; import { Web3SupportedBlockchains } from '@core/services/blockchain/blockchain-adapters/public-blockchain-adapter.service' ; import { HttpClient } from '@angular/common/http' ; import { BatchCall } from '@core/services/blockchain/models/batch-call' ; import { RpcResponse } from '@core/services/blockchain/models/rpc-response' ; import { Cacheable } from 'ts-cacheable' ; import { MethodData } from '@shared/models/blockchain/method-data' ; import ERC20_TOKEN_ABI from '@core/services/blockchain/constants/erc-20-abi' ; import MULTICALL_ABI from '@core/services/blockchain/constants/multicall-abi' ; import { Call } from '@core/services/blockchain/models/call' ; import { MULTICALL_ADDRESSES , MULTICALL_ADDRESSES_TESTNET } from '@core/services/blockchain/constants/multicall-addresses' ; import { UseTestingModeService } from '@core/services/use-testing-mode/use-testing-mode.service' ; import { TransactionOptions } from '@shared/models/blockchain/transaction-options' ; import { Web3Public } from '@core/services/blockchain/blockchain-adapters/common/web3-public' ; import { Web3Pure } from '@core/services/blockchain/blockchain-adapters/common/web3-pure' ; import InsufficientFundsError from '@core/errors/models/instant-trade/insufficient-funds-error' ; type AllowanceParams = { /**
     * Address of the smart-contract corresponding to the token.
     */ tokenAddress : string ; /**
     * Wallet address to spend from.
     */ ownerAddress : string ; /**
     * Wallet or contract address, allowed to spend.
     */ spenderAddress : string ; } ; interface MulticallResponse { success : boolean ; returnData : string ; } const supportedTokenFields = [ 'decimals' , 'symbol' , 'name' , 'totalSupply' ] as const ; type TokenField = typeof supportedTokenFields [ number ] ; type TokenFields = Partial < Record < TokenField , string > > ; export class EthLikeWeb3Public extends Web3Public < AllowanceParams , Transaction > { private multicallAddresses : { [ k in BLOCKCHAIN_NAME ] ? : string ; } ; constructor ( private web3 , public blockchain , useTestingModeService , private readonly httpClient ) { super ( ) ; this . multicallAddresses = MULTICALL_ADDRESSES ; useTestingModeService . isTestingMode . subscribe ( isTestingMode => { if ( isTestingMode ) { this . multicallAddresses = MULTICALL_ADDRESSES_TESTNET ; } } ) ; } static get nativeTokenAddress ( ) : string { return NATIVE_TOKEN_ADDRESS ; } static addressToBytes32 ( address ) { if ( address . slice ( 0 , 2 ) !== '0x' || address . length !== 42 ) { console . error ( 'Wrong address format' ) ; throw new UndefinedError ( ) ; } return `0x${ address . slice ( 2 ) . padStart ( 64 , '0' ) }` ; } static toChecksumAddress ( address ) { return toChecksumAddress ( address ) ; } param address public isAddressCorrect ( address ) { return isAddress ( address ) ; } param address public isNativeAddress = ( address ) : boolean => { return address === NATIVE_TOKEN_ADDRESS ; } ; param provider public setProvider ( provider ) { this . web3 . setProvider ( provider ) ; } param timeoutMs return public healthCheck ( timeoutMs = 4000 ) { const healthcheckData = HEALTHCHECK [ this . blockchain . name as Web3SupportedBlockchains ] ; if ( ! healthcheckData ) { return of ( null ) ; } const contract = new this . web3 . eth . Contract ( healthcheckData . contractAbi , healthcheckData . contractAddress ) ; return from ( contract . methods [ healthcheckData . method ] ( ) . call ( ) ) . pipe ( timeout ( timeoutMs ) , map ( result => result === healthcheckData . expected ) , catchError ( ( err ) => { if ( ( err as Error ) ?. name === 'TimeoutError' ) { console . debug ( `${ this . blockchain . label } node healthcheck timeout (${ timeoutMs }ms) has occurred.` ) ; } else { console . debug ( `${ this . blockchain . label } node healthcheck fail: ${ err }` ) ; } return of ( false ) ; } ) ) ; } param address param options param options . inWei return public async getBalance ( address , options = { } ) { const balance = await this . web3 . eth . getBalance ( address ) ; return new BigNumber ( options . inWei ? balance : Web3Pure . fromWei ( balance ) ) ; } param userAddress param tokenAddress public async getTokenOrNativeBalance ( userAddress , tokenAddress ) { let balance ; if ( this . isNativeAddress ( tokenAddress ) ) { balance = await this . web3 . eth . getBalance ( userAddress ) ; } else { balance = await this . getTokenBalance ( userAddress , tokenAddress ) ; } return new BigNumber ( balance ) ; } param tokenAddress param address return public async getTokenBalance ( address , tokenAddress ) { const contract = new this . web3 . eth . Contract ( ERC20_TOKEN_ABI , tokenAddress ) ; const balance = await contract . methods . balanceOf ( address ) . call ( ) ; return new BigNumber ( balance ) ; } /**
     * get latest block
     */ public getBlock ( ) { return this . web3 . eth . getBlock ( 'latest' ) ; } param contractAbi param contractAddress param methodName param methodArguments param fromAddress param value return public async getEstimatedGas ( contractAbi , contractAddress , methodName , methodArguments , fromAddress , value ? ) { const contract = new this . web3 . eth . Contract ( contractAbi , contractAddress ) ; const gasLimit = await contract . methods [ methodName ] ( ... methodArguments ) . estimateGas ( { from : fromAddress , gas : 40000000 , ... ( value && { value } ) } ) ; return new BigNumber ( gasLimit ) ; } return public async getGasPrice ( ) { return this . getGasPrice$ ( ) . toPromise ( ) ; } return public async getGasPriceInETH ( ) { const gasPrice = await this . getGasPrice ( ) ; return new BigNumber ( gasPrice ) . div ( 10 ** 18 ) ; } param gasLimit param etherPrice return public async getGasFee ( gasLimit , etherPrice ) { const gasPrice = await this . getGasPriceInETH ( ) ; return gasPrice . multipliedBy ( gasLimit ) . multipliedBy ( etherPrice ) ; } param params AllowanceParams .
     *  return public async getAllowance ( params ) { const { tokenAddress , ownerAddress , spenderAddress } = params ; const contract = new this . web3 . eth . Contract ( ERC20_TOKEN_ABI , tokenAddress ) ; const allowance = await contract . methods . allowance ( ownerAddress , spenderAddress ) . call ( { from : ownerAddress } ) ; return new BigNumber ( allowance ) ; } param hash param options param options . inWei return public async getTransactionGasFee ( hash , options = { } ) { const transaction = await this . getTransactionByHash ( hash ) ; const receipt = await this . web3 . eth . getTransactionReceipt ( hash ) ; if ( ! transaction || ! receipt ) { return null ; } const gasPrice = new BigNumber ( transaction . gasPrice ) ; const gasLimit = new BigNumber ( receipt . gasUsed ) ; return options . inWei ? gasPrice . multipliedBy ( gasLimit ) : gasPrice . multipliedBy ( gasLimit ) . div ( 10 ** 18 ) ; } param hash param attempt param attemptsLimit param delay public async getTransactionByHash ( hash , attempt ? , attemptsLimit ? , delay ? ) { attempt = attempt || 0 ; const limit = attemptsLimit || 10 ; const timeoutMs = delay || 500 ; if ( attempt >= limit ) { return null ; } const transaction = await this . web3 . eth . getTransaction ( hash ) ; if ( transaction === null ) { return new Promise ( resolve => setTimeout ( ( ) => resolve ( this . getTransactionByHash ( hash , attempt + 1 ) ) , timeoutMs ) ) ; } return transaction ; } param contractAddress param contractAbi param methodName param options param options . from param options . methodArguments return public async callContractMethod < T = string > ( contractAddress , contractAbi , methodName , options = { methodArguments : [ ] } ) { const contract = new this . web3 . eth . Contract ( contractAbi , contractAddress ) ; return contract . methods [ methodName ] ( ... options . methodArguments ) . call ( { ... ( options . from && { from : options . from } ) } ) ; } param tokenAddress return public getTokenSymbol : ( tokenAddress : string ) => Promise < string > = this . getTokenSymbolCachingDecorator ( ) ; private getTokenSymbolCachingDecorator ( ) { const tokensSymbolsCache = { } ; return async ( tokenAddress ) : Promise < string > => { if ( ! tokensSymbolsCache [ tokenAddress ] ) { tokensSymbolsCache [ tokenAddress ] = ( await this . callForTokenInfo ( tokenAddress , [ 'symbol' ] ) ) . symbol ; } return tokensSymbolsCache [ tokenAddress ] ; } ; } param tokenAddress return public getTokenInfo : ( tokenAddress : string ) => Promise < BlockchainTokenExtended > = this . getTokenInfoCachingDecorator ( ) ; private getTokenInfoCachingDecorator ( ) { const tokensCache = { } ; return async ( tokenAddress ) : Promise < BlockchainTokenExtended > => { if ( ! tokensCache [ tokenAddress ] ) { if ( this . isNativeAddress ( tokenAddress ) ) { return { ... this . blockchain . nativeCoin , blockchain : this . blockchain . name } ; } const tokenInfo = ( await this . callForTokenInfo ( tokenAddress ) ) as Record < TokenField , string > ; tokensCache [ tokenAddress ] = { blockchain : this . blockchain . name , address : tokenAddress , ... tokenInfo , decimals : Number ( tokenInfo . decimals ) } ; } return tokensCache [ tokenAddress ] ; } ; } param tokenAddress param tokenFields private async callForTokenInfo ( tokenAddress , tokenFields = [ 'decimals' , 'symbol' , 'name' , 'totalSupply' ] ) { const tokenFieldsPromises = tokenFields . map ( method => this . callContractMethod ( tokenAddress , ERC20_TOKEN_ABI , method ) ) ; const tokenFieldsResults = await Promise . all ( tokenFieldsPromises ) ; return Object . fromEntries ( tokenFieldsResults . map ( ( field , index ) => [ tokenFields [ index ] , field ] ) ) ; } param address param tokensAddresses public async getTokensBalances ( address , tokensAddresses ) { const contract = new this . web3 . eth . Contract ( ERC20_TOKEN_ABI , tokensAddresses [ 0 ] ) ; const indexOfNativeCoin = tokensAddresses . findIndex ( this . isNativeAddress ) ; const promises = [ undefined , undefined ] ; if ( indexOfNativeCoin !== - 1 ) { tokensAddresses . splice ( indexOfNativeCoin , 1 ) ; promises [ 1 ] = this . getBalance ( address , { inWei : true } ) ; } const calls = tokensAddresses . map ( tokenAddress => ( { target : tokenAddress , callData : contract . methods . balanceOf ( address ) . encodeABI ( ) } ) ) ; promises [ 0 ] = this . multicall ( calls ) ; const results = await Promise . all ( promises ) ; const tokensBalances = results [ 0 ] . map ( ( { success , returnData : hexBalance } ) => success ? new BigNumber ( hexBalance ) : new BigNumber ( 0 ) ) ; if ( indexOfNativeCoin !== - 1 ) { tokensBalances . splice ( indexOfNativeCoin , 0 , results [ 1 ] ) ; } return tokensBalances ; } param contractAddress param contractAbi param methodsData public async multicallContractMethods < Output > ( contractAddress , contractAbi , methodsData ) { const contract = new this . web3 . eth . Contract ( contractAbi , contractAddress ) ; const calls = methodsData . map ( ( { methodName , methodArguments } ) => ( { callData : contract . methods [ methodName ] ( ... methodArguments ) . encodeABI ( ) , target : contractAddress } ) ) ; const outputs = await this . multicall ( calls ) ; return outputs . map ( ( output , index ) => { const methodOutputAbi = contractAbi . find ( funcSignature => funcSignature . name === methodsData [ index ] . methodName ) . outputs ; return { success : output . success , output : output . success ? ( this . web3 . eth . abi . decodeParameters ( methodOutputAbi , output . returnData ) as Output ) : null } ; } ) ; } param contractAbi param contractsData public async multicallContractsMethods < Output > ( contractAbi , contractsData ) { const calls = contractsData . map ( ( { contractAddress , methodsData } ) => { const contract = new this . web3 . eth . Contract ( contractAbi , contractAddress ) ; return methodsData . map ( ( { methodName , methodArguments } ) => ( { callData : contract . methods [ methodName ] ( ... methodArguments ) . encodeABI ( ) , target : contractAddress } ) ) ; } ) ; const outputs = await this . multicall ( calls . flat ( ) ) ; let outputIndex = 0 ; return contractsData . map ( contractData => contractData . methodsData . map ( methodData => { const methodOutputAbi = contractAbi . find ( funcSignature => funcSignature . name === methodData . methodName ) . outputs ; const output = outputs [ outputIndex ] ; outputIndex ++ ; return { success : output . success , output : output . success ? ( this . web3 . eth . abi . decodeParameters ( methodOutputAbi , output . returnData ) as Output ) : null } ; } ) ) ; } private async multicall ( calls ) { const contract = new this . web3 . eth . Contract ( MULTICALL_ABI , this . multicallAddresses [ this . blockchain . name ] ) ; return contract . methods . tryAggregate ( false , calls ) . call ( ) ; } param token param amount param userAddress public async checkBalance ( token , amount , userAddress ) { let balance ; if ( this . isNativeAddress ( token . address ) ) { balance = await this . getBalance ( userAddress , { inWei : true } ) ; } else { balance = await this . getTokenBalance ( userAddress , token . address ) ; } const amountAbsolute = Web3Pure . toWei ( amount , token . decimals ) ; if ( balance . lt ( amountAbsolute ) ) { const formattedTokensBalance = Web3Pure . fromWei ( balance , token . decimals ) . toFormat ( BIG_NUMBER_FORMAT ) ; throw new InsufficientFundsError ( token . symbol , formattedTokensBalance , amount . toFormat ( BIG_NUMBER_FORMAT ) ) ; } } param abi param contractAddress param fromAddress param callsData return public async batchEstimatedGas ( abi , contractAddress , fromAddress , callsData ) { try { const contract = new this . web3 . eth . Contract ( abi , contractAddress ) ; const dataList = callsData . map ( callData => contract . methods [ callData . contractMethod ] ( ... callData . params ) . encodeABI ( ) ) ; const rpcCallsData = dataList . map ( ( data , index ) => ( { rpcMethod : 'eth_estimateGas' , params : { from : fromAddress , to : contractAddress , data , ... ( callsData [ index ] . value && { value : `0x${ new BigNumber ( callsData [ index ] . value ) . toString ( 16 ) }` } ) } } ) ) ; const result = await this . rpcBatchRequest < string > ( rpcCallsData ) ; return result . map ( value => value && new BigNumber ( value ) ) ; } catch ( e ) { console . error ( e ) ; return callsData . map ( ( ) => null ) ; } } see https param calls param callsParams return private web3BatchRequest < T extends string | string [ ] > ( calls , callsParams ) { const batch = new this . web3 . BatchRequest ( ) ; const promises = calls . map ( ( call , index ) => new Promise ( ( resolve , reject ) => batch . add ( call . request ( { ... callsParams [ index ] } , ( error , result ) => error ? reject ( error ) : resolve ( result ) ) ) ) ) ; batch . execute ( ) ; return Promise . all ( promises ) ; } see https param rpcCallsData return private async rpcBatchRequest < T extends string | string [ ] > ( rpcCallsData ) { const seed = Date . now ( ) ; const batch = rpcCallsData . map ( ( callData , index ) => ( { id : seed + index , jsonrpc : '2.0' , method : callData . rpcMethod , params : [ { ... callData . params } ] } ) ) ; const response = await this . httpClient . post < RpcResponse < T > [ ] > ( ( < HttpProvider > this . web3 . currentProvider ) . host , batch ) . toPromise ( ) ; if ( Array . isArray ( response ) ) { return response . sort ( ( a , b ) => a . id - b . id ) . map ( item => ( item . error ? null : item . result ) ) ; } return [ response ] ; } return @ Cacheable ( { maxAge : 10000 } ) private getGasPrice$ ( ) { return from ( this . web3 . eth . getGasPrice ( ) ) ; } param contractAddress param contractAbi param methodName param methodArguments param fromAddress param options param options . value param options . gas public async tryExecuteContractMethod ( contractAddress , contractAbi , methodName , methodArguments , fromAddress , options = { } ) { const contract = new this . web3 . eth . Contract ( contractAbi , contractAddress ) ; try { await contract . methods [ methodName ] ( ... methodArguments ) . call ( { from : fromAddress , ... ( options . value && { value : options . value } ) , ... ( options . gas && { gas : options . gas } ) } ) ; return ; } catch ( err ) { console . error ( 'Method execution error:' , err ) ; throw err ; } } }