import { PinoLogger } from 'nestjs-pino' ; import { Currency , EstimateGasEth , generateAddressFromXPub , generatePrivateKeyFromMnemonic , generateWallet , polygonGetGasPriceInWei , preparePolygonSignedTransaction , preparePolygonSmartContractWriteMethodInvocation , sendPolygonSmartContractReadMethodInvocationTransaction , SignatureId , SmartContractMethodInvocation , SmartContractReadMethodInvocation , TransactionHash , TransferErc20 , } from '@tatumio/tatum' ; import { BroadcastOrStoreKMSTransaction } from '@tatumio/blockchain-connector-common' ; import Web3 from 'web3' ; import { fromWei } from 'web3-utils' ; import axios from 'axios' ; import { PolygonError } from './PolygonError' ; import BigNumber from 'bignumber.js' ; export abstract class PolygonService { public static mapBlock ( block ) { return { difficulty : parseInt ( block . difficulty , 16 ) , extraData : block . extraData , gasLimit : parseInt ( block . gasLimit , 16 ) , gasUsed : parseInt ( block . gasUsed , 16 ) , hash : block . hash , logsBloom : block . logsBloom , miner : block . miner , nonce : block . nonce , number : parseInt ( block . number , 16 ) , parentHash : block . parentHash , sha3Uncles : block . sha3Uncles , size : parseInt ( block . size , 16 ) , stateRoot : block . stateRoot , timestamp : parseInt ( block . timestamp , 16 ) , totalDifficulty : parseInt ( block . totalDifficulty , 16 ) , transactions : block . transactions . map ( PolygonService . mapTransaction ) , uncles : block . uncles , } ; } public static mapTransaction ( tx ) { delete tx . r ; delete tx . s ; delete tx . v ; return { ... tx , blockNumber : parseInt ( tx . blockNumber , 16 ) , gas : parseInt ( tx . gas , 16 ) , gasPrice : parseInt ( tx . gasPrice , 16 ) , nonce : parseInt ( tx . nonce , 16 ) , transactionIndex : parseInt ( tx . transactionIndex , 16 ) , value : new BigNumber ( tx . value ) . toString ( ) , gasUsed : tx . gasUsed !== undefined ? new BigNumber ( tx . gasUsed ) . toString ( ) : undefined , cumulativeGasUsed : tx . cumulativeGasUsed !== undefined ? new BigNumber ( tx . cumulativeGasUsed ) . toString ( ) : undefined , transactionHash : tx . hash , status : tx . status !== undefined ? ! ! parseInt ( tx . status , 16 ) : undefined , logs : tx . logs ?. map ( l => ( { ... l , logIndex : parseInt ( l . logIndex , 16 ) , transactionIndex : parseInt ( l . transactionIndex , 16 ) , blockNumber : parseInt ( l . blockNumber , 16 ) , } ) ) } ; } ; protected constructor ( protected readonly logger ) { } protected abstract isTestnet ( ) ; protected abstract getNodesUrl ( testnet ) ; protected abstract storeKMSTransaction ( txData , currency , signatureId , index ? ) ; protected abstract completeKMSTransaction ( txId , signatureId ) ; public async getFirstNodeUrl ( testnet ) { const nodes = await this . getNodesUrl ( testnet ) ; if ( nodes . length === 0 ) { new PolygonError ( 'Nodes url array must have at least one element.' , 'polygon.nodes.url' ) ; } return nodes [ 0 ] ; } public async getClient ( testnet ) { return new Web3 ( await this . getFirstNodeUrl ( testnet ) ) ; } public async broadcast ( txData , signatureId ? ) { this . logger . info ( `Broadcast tx for MATIC with data '${ txData }'` ) ; let txId ; try { const url = await this . getFirstNodeUrl ( await this . isTestnet ( ) ) ; const { result , error } = ( await axios . post ( url , { jsonrpc : '2.0' , id : 0 , method : 'eth_sendRawTransaction' , params : [ txData ] } , { headers : { 'Content-Type' : 'application/json' } } ) ) . data ; if ( error ) { throw new PolygonError ( `Unable to broadcast transaction due to ${ error . message }.` , 'polygon.broadcast.failed' ) ; } txId = result ; } catch ( e ) { if ( e . constructor . name === PolygonError . name ) { throw e ; } this . logger . error ( e ) ; throw new PolygonError ( `Unable to broadcast transaction due to ${ e . message }.` , 'polygon.broadcast.failed' ) ; } if ( signatureId ) { try { await this . completeKMSTransaction ( txId , signatureId ) ; } catch ( e ) { this . logger . error ( e ) ; return { txId , failed : true } ; } } return { txId } ; } public async getCurrentBlock ( testnet ? ) { const t = testnet === undefined ? await this . isTestnet ( ) : testnet ; return ( await this . getClient ( t ) ) . eth . getBlockNumber ( ) ; } public async getBlock ( hash , testnet ? ) { const t = testnet === undefined ? await this . isTestnet ( ) : testnet ; try { const isHash = typeof hash === 'string' && hash . length >= 64 ; const block = ( await axios . post ( await this . getFirstNodeUrl ( t ) , { jsonrpc : '2.0' , id : 0 , method : isHash ? 'eth_getBlockByHash' : 'eth_getBlockByNumber' , params : [ isHash ? hash : `0x${ new BigNumber ( hash ) . toString ( 16 ) }` , true ] } , { headers : { 'Content-Type' : 'application/json' } } ) ) . data . result ; return PolygonService . mapBlock ( block ) ; } catch ( e ) { this . logger . error ( e ) ; throw e ; } } public async getTransaction ( txId , testnet ? ) { const t = testnet === undefined ? await this . isTestnet ( ) : testnet ; try { const data = ( await axios . post ( await this . getFirstNodeUrl ( t ) , { jsonrpc : '2.0' , id : 0 , method : 'eth_getTransactionByHash' , params : [ txId ] } , { headers : { 'Content-Type' : 'application/json' } } ) ) . data ; if ( ! data ?. result ) { throw new PolygonError ( 'Transaction not found. Possible not exists or is still pending.' , 'tx.not.found' ) ; } const { r , s , v , hash , ... transaction } = data . result ; let receipt = { } ; try { receipt = ( await axios . post ( await this . getFirstNodeUrl ( t ) , { jsonrpc : '2.0' , id : 0 , method : 'eth_getTransactionReceipt' , params : [ txId ] } , { headers : { 'Content-Type' : 'application/json' } } ) ) . data ?. result ; } catch ( _ ) { transaction . transactionHash = hash ; } return PolygonService . mapTransaction ( { ... transaction , ... receipt , hash } ) ; } catch ( e ) { this . logger . error ( e ) ; throw new PolygonError ( 'Transaction not found. Possible not exists or is still pending.' , 'tx.not.found' ) ; } } private async broadcastOrStoreKMSTransaction ( { transactionData , signatureId , index } ) { if ( signatureId ) { return { signatureId : await this . storeKMSTransaction ( transactionData , Currency . MATIC , [ signatureId ] , index ) , } ; } return this . broadcast ( transactionData ) ; } public async web3Method ( body ) { const node = await this . getFirstNodeUrl ( await this . isTestnet ( ) ) ; return ( await axios . post ( node , body , { headers : { 'Content-Type' : 'application/json' } } ) ) . data ; } public async generateWallet ( mnemonic ? ) { return generateWallet ( Currency . MATIC , await this . isTestnet ( ) , mnemonic ) ; } public async generatePrivateKey ( mnemonic , index ) { const key = await generatePrivateKeyFromMnemonic ( Currency . MATIC , await this . isTestnet ( ) , mnemonic , index ) ; return { key } ; } public async generateAddress ( xpub , derivationIndex ) { const address = await generateAddressFromXPub ( Currency . MATIC , await this . isTestnet ( ) , xpub , parseInt ( derivationIndex ) ) ; return { address } ; } public async estimateGas ( body ) { const client = await this . getClient ( await this . isTestnet ( ) ) ; return { gasLimit : await client . eth . estimateGas ( body ) , gasPrice : await polygonGetGasPriceInWei ( ) , } ; } public async getBalance ( address ) { const client = await this . getClient ( await this . isTestnet ( ) ) ; return { balance : fromWei ( await client . eth . getBalance ( address ) , 'ether' ) } ; } public async sendMatic ( transfer ) { const transactionData = await preparePolygonSignedTransaction ( await this . isTestnet ( ) , transfer , await this . getFirstNodeUrl ( await this . isTestnet ( ) ) ) ; return this . broadcastOrStoreKMSTransaction ( { transactionData , signatureId : transfer . signatureId , index : transfer . index } ) ; } public async getTransactionCount ( address ) { const client = await this . getClient ( await this . isTestnet ( ) ) ; return client . eth . getTransactionCount ( address , 'pending' ) ; } public async invokeSmartContractMethod ( smartContractMethodInvocation ) { const node = await this . getFirstNodeUrl ( await this . isTestnet ( ) ) ; if ( smartContractMethodInvocation . methodABI . stateMutability === 'view' ) { return sendPolygonSmartContractReadMethodInvocationTransaction ( await this . isTestnet ( ) , smartContractMethodInvocation , node ) ; } const transactionData = await preparePolygonSmartContractWriteMethodInvocation ( await this . isTestnet ( ) , smartContractMethodInvocation , node ) ; return this . broadcastOrStoreKMSTransaction ( { transactionData , signatureId : ( smartContractMethodInvocation as SmartContractMethodInvocation ) . signatureId , index : ( smartContractMethodInvocation as SmartContractMethodInvocation ) . index } ) ; } }