import { CivilErrors , delay , hashPersonalMessage } from "@joincivil/utils" ; import Debug from "debug" ; import { bufferToHex , fromRpcSig , fromUtf8 , toBuffer } from "ethereumjs-util" ; import { ReplaySubject , Observable } from "rxjs" ; import { ContractOptions , BigNumber , bigNumberify , DecodedTransactionReceipt , EthAddress , EthSignedMessage , TxHash , } from "@joincivil/typescript-types" ; import { AbiDecoder } from "./abidecoder" ; import { requireAccount } from "./helpers" ; import Web3 from "web3" ; import Contract from "web3-eth-contract" ; import { Block } from "web3-eth" ; import { Transaction , TransactionConfig , Log , TransactionReceipt , provider as Provider } from "web3-core" ; import { AbiItem } from "web3-utils" ; const debug = Debug ( "civil:ethapi" ) ; export const DEFAULT_HTTP_NODE = "http://127.0.0.1:8545" ; const POLL_MILLISECONDS = 5000 ; export class EthApi { private web3 ! : Web3 ; private abiDecoder : AbiDecoder ; private accountObservable : ReplaySubject < EthAddress | undefined > ; private networkObservable : ReplaySubject < number > ; constructor ( provider , abis ) { this . currentProvider = provider ; this . abiDecoder = new AbiDecoder ( abis ) ; this . accountObservable = new ReplaySubject < EthAddress | undefined > ( 1 ) ; this . web3 . eth . getAccounts ( ) . then ( accounts => { const account = accounts && accounts . length > 0 ? accounts [ 0 ] : undefined ; this . web3 . eth . defaultAccount = account ! ; if ( account ) { this . accountObservable . next ( account ) ; } } ) . catch ( err => { console . error ( "error getting accounts" , err ) ; } ) ; if ( provider . on ) { provider . on ( "accountsChanged" , accounts => { const account = accounts && accounts . length > 0 ? accounts [ 0 ] : undefined ; this . web3 . eth . defaultAccount = account ! ; if ( account ) { this . accountObservable . next ( account ) ; } } ) ; } this . networkObservable = new ReplaySubject < number > ( 1 ) ; this . web3 . eth . net . getId ( ) . then ( network => { this . networkObservable . next ( network ) ; } ) . catch ( err => { console . error ( "error getting network" , err ) ; } ) ; if ( provider . on ) { provider . on ( "networkChanged" , network => { this . networkObservable . next ( network ) ; } ) ; } } public get currentProvider ( ) : Provider { return this . web3 . currentProvider ; } public set currentProvider ( provider ) { console . log ( "provider change" ) ; this . web3 = new Web3 ( provider ) ; } public get accountStream ( ) : Observable < EthAddress | undefined > { return this . accountObservable ; } public get networkStream ( ) : Observable < number > { return this . networkObservable ; } public async network ( ) { const network = await this . networkObservable . first ( ) . toPromise ( ) ; return network ; } public async getAccount ( ) { return this . web3 . eth . defaultAccount ; } public async getGasPriceString ( ) { return this . web3 . eth . getGasPrice ( ) ; } public async getGasPrice ( ) { return this . getGasPriceString ( ) . then ( gp => new BigNumber ( gp ) ) ; } public async getBlock ( blockNumber ) { return this . web3 . eth . getBlock ( blockNumber ) ; } public async getLatestBlockNumber ( ) { return this . web3 . eth . getBlockNumber ( ) ; } public async getConfirmations ( startBlock ) { const block = await this . getLatestBlockNumber ( ) ; return block - startBlock ; } public async getCode ( address ) { return this . web3 . eth . getCode ( address ) ; } public async sendTransaction ( txData ) { return new Promise ( ( resolve , reject ) => { this . web3 . eth . sendTransaction ( txData ) . once ( "transactionHash" , hash => { resolve ( hash ) ; } ) . catch ( reject ) ; } ) ; } public async getTransaction ( txHash ) { return this . web3 . eth . getTransaction ( txHash ) ; } public async signMessage ( message , account ? ) { const messageHex = fromUtf8 ( message ) ; const signerAccount = account || ( await requireAccount ( this ) . toPromise ( ) ) ; const signature = await this . web3 . eth . personal . sign ( messageHex , signerAccount ) ; const rsv = fromRpcSig ( signature ) ; return { ... hashPersonalMessage ( message ) , signature , message , r : bufferToHex ( rsv . r ) , s : bufferToHex ( rsv . s ) , v : bufferToHex ( toBuffer ( rsv . v ) ) , signer : signerAccount , } ; } param address public toChecksumAddress ( address ) { return this . web3 . utils . toChecksumAddress ( address ) ; } param numberOrHexString public toBigNumber ( amount ) { if ( typeof amount === "number" ) { return bigNumberify ( amount ) ; } else if ( amount instanceof BigNumber ) { return amount ; } else if ( typeof amount === "string" && amount . startsWith ( "<sub>" ) ) { console . log ( "found a weird bignumber:" , amount ) ; return bigNumberify ( 0 ) ; } return bigNumberify ( amount ) ; } public toWei ( amount ) { const wei = this . web3 . utils . toWei ( amount . toString ( ) ) ; return this . toBigNumber ( wei ) ; } public async accountBalace ( account ) { const balance = await this . web3 . eth . getBalance ( account ) ; return parseFloat ( this . web3 . utils . fromWei ( balance , "ether" ) ) ; } param txHash param blockConfirmations public async awaitReceipt < R extends DecodedTransactionReceipt | TransactionReceipt = TransactionReceipt > ( txHash , blockConfirmations = 1 ) { while ( true ) { const receipt = await this . getReceipt < R > ( txHash ) ; if ( ! receipt ) { await delay ( POLL_MILLISECONDS ) ; continue ; } this . checkForEvmException ( receipt ) ; await this . awaitConfirmations ( receipt . blockNumber , blockConfirmations ) ; return receipt ; } } public async awaitConfirmations ( startblock , confirmations ) { while ( true ) { const confirmationsSoFar = await this . getConfirmations ( startblock ) ; if ( confirmationsSoFar >= confirmations ) { return ; } else { await delay ( POLL_MILLISECONDS ) ; } } } param txHash returns public async getReceipt < R extends DecodedTransactionReceipt | TransactionReceipt = TransactionReceipt > ( txHash ) { const receipt = await this . web3 . eth . getTransactionReceipt ( txHash ) ; if ( receipt ) { return this . receiptToDecodedReceipt < R > ( receipt ) ; } return null ; } public getContractClass ( abi , address ? , options ? ) { const contract = new this . web3 . eth . Contract ( abi , address , options ) ; if ( ! contract . options . address ) { contract . options . address = address ! ; } return contract ; } public async estimateGas ( options ) { return this . web3 . eth . estimateGas ( options ) ; } private receiptToDecodedReceipt < R extends DecodedTransactionReceipt < any > | TransactionReceipt > ( receipt ) { receipt . logs = receipt . logs ! . map ( ( log ) => this . abiDecoder . tryToDecodeLogOrNoop ( log ) ) ; return ( receipt as any ) as R ; } private checkForEvmException ( receipt ) { if ( receipt . status === null ) { debug ( "Warning: Pre-Bizantium block, not supported" ) ; throw new Error ( CivilErrors . EvmException ) ; } if ( receipt . status === false ) { throw new Error ( CivilErrors . EvmException ) ; } } }