import { Injectable } from '@angular/core' ; import { GasRefundApiService } from '@core/services/backend/gas-refund-api/gas-refund-api.service' ; import { BehaviorSubject , from , Observable } from 'rxjs' ; import { Promotion } from '@features/my-trades/models/promotion' ; import { AuthService } from '@core/services/auth/auth.service' ; import { filter , map , switchMap } from 'rxjs/operators' ; import { soliditySha3 } from 'web3-utils' ; import BigNumber from 'bignumber.js' ; import { MerkleTree } from 'merkletreejs' ; import { RootData } from '@features/my-trades/models/root-data' ; import { EthLikeWeb3PrivateService } from '@core/services/blockchain/blockchain-adapters/eth-like/web3-private/eth-like-web3-private.service' ; import { PublicBlockchainAdapterService } from '@core/services/blockchain/blockchain-adapters/public-blockchain-adapter.service' ; import { REFUND_ABI } from '@features/my-trades/constants/refund-abi' ; import { UnknownError } from '@core/errors/models/unknown.error' ; import { TransactionReceipt } from 'web3-eth' ; import { UseTestingModeService } from '@core/services/use-testing-mode/use-testing-mode.service' ; import { REFUND_ADDRESS , REFUND_ADDRESS_TESTNET } from '@features/my-trades/constants/refund-address' ; import { WalletConnectorService } from 'src/app/core/services/blockchain/wallets/wallet-connector-service/wallet-connector.service' ; import { mapToVoid } from '@shared/utils/utils' ; import { EthLikeWeb3Public } from 'src/app/core/services/blockchain/blockchain-adapters/eth-like/web3-public/eth-like-web3-public' ; import { BlockchainsInfo } from '@core/services/blockchain/blockchain-info' ; import CustomError from '@core/errors/models/custom-error' ; @ Injectable ( ) export class GasRefundService { public readonly userPromotions$ : Observable < Promotion [ ] > ; private refundBlockchain = REFUND_ADDRESS . blockchain ; private readonly refundContractAbi = REFUND_ABI ; private refundContractAddress = REFUND_ADDRESS . address ; private readonly _userPromotions$ = new BehaviorSubject < Promotion [ ] > ( [ ] ) ; public get userPromotions ( ) : Promotion [ ] { return this . _userPromotions$ . getValue ( ) ; } constructor ( private readonly gasRefundApiService , private readonly authService , private readonly web3Private , private readonly publicBlockchainAdapterService , private readonly walletConnectorService , private readonly testingModeService ) { this . userPromotions$ = this . _userPromotions$ . asObservable ( ) ; this . setUpdatePromotionsSubscription ( ) ; } param arg private static merkleKeccak256 ( arg ) { const first = `0x${ ( < Buffer > arg ) . toString ( 'hex' ) . slice ( 0 , 64 ) }` ; const second = `0x${ ( < Buffer > arg ) . toString ( 'hex' ) . slice ( 64 ) }` ; let res ; if ( new BigNumber ( first ) . lt ( second ) ) { res = soliditySha3 ( first , second ) ; } else { res = soliditySha3 ( second , first ) ; } return res . slice ( 2 ) ; } /**
     * Subscribes to user changes and updates promotions when it emits.
     */ private setUpdatePromotionsSubscription ( ) { this . authService . getCurrentUser ( ) . pipe ( filter ( user => ! ! user ?. address ) ) . subscribe ( ( ) => this . updateUserPromotions ( ) ) ; this . setupTestingMode ( ) ; } /**
     * Changes contract address after enabling the testing mode.
     */ private setupTestingMode ( ) { this . testingModeService . isTestingMode . subscribe ( isTestingMode => { if ( isTestingMode ) { this . refundBlockchain = REFUND_ADDRESS_TESTNET . blockchain ; this . refundContractAddress = REFUND_ADDRESS_TESTNET . address ; } } ) ; } /**
     * Fetches actual user promotions list, updates promotions storage, then emits void and completes stream.
     */ public updateUserPromotions ( ) { const userPromotions$ = this . gasRefundApiService . getUserPromotions ( ) ; const comparator = ( a , b ) => a . refundDate . valueOf ( ) - b . refundDate . valueOf ( ) ; userPromotions$ . pipe ( map ( promotions => [ ... promotions ] . sort ( comparator ) ) ) . subscribe ( promotions => this . _userPromotions$ . next ( promotions ) ) ; return userPromotions$ . pipe ( mapToVoid ( ) ) ; } param promotionId param onTransactionHash return public refund ( promotionId , onTransactionHash ? ) { const address = this . authService . userAddress ; return from ( this . checkChain ( ) ) . pipe ( filter ( success => success ) , switchMap ( ( ) => this . gasRefundApiService . getPromotionMerkleData ( promotionId ) ) , switchMap ( ( { leaves , rootIndex , amount } ) => { const leaf = soliditySha3 ( address , amount . toFixed ( 0 ) ) ; const tree = new MerkleTree ( leaves , GasRefundService . merkleKeccak256 ) ; const root = `0x${ tree . getRoot ( ) . toString ( 'hex' ) }` ; const proof = tree . getHexProof ( leaf ) ; return from ( this . sendRefund ( proof , amount , { root , rootIndex } , onTransactionHash ) ) . pipe ( map ( receipt => ( { txHash : receipt . transactionHash , leaf } ) ) ) ; } ) , switchMap ( ( { txHash , leaf } ) => { return this . gasRefundApiService . markPromotionAsUsed ( txHash , leaf ) . pipe ( map ( ( ) => txHash ) ) ; } ) ) ; } return private checkChain ( ) { if ( this . walletConnectorService . networkName !== this . refundBlockchain ) { return this . walletConnectorService . switchChain ( this . refundBlockchain ) ; } return Promise . resolve ( true ) ; } param proof param amount param rootData param onTransactionHash return private async sendRefund ( proof , amount , rootData , onTransactionHash ? ) { const address = this . authService . userAddress ; if ( BlockchainsInfo . getBlockchainType ( this . refundBlockchain ) !== 'ethLike' ) { throw new CustomError ( 'Wrong blockchain error' ) ; } const blockchainAdapter = this . publicBlockchainAdapterService [ this . refundBlockchain ] as EthLikeWeb3Public ; const hexRootFromContract = await blockchainAdapter . callContractMethod ( this . refundContractAddress , this . refundContractAbi , 'merkleRoots' , { methodArguments : [ rootData . rootIndex ] } ) ; if ( hexRootFromContract !== rootData . root ) { throw new UnknownError ( `Roots are not equal: expected ${ rootData . root } but got ${ hexRootFromContract }` ) ; } return this . web3Private . tryExecuteContractMethod ( this . refundContractAddress , this . refundContractAbi , 'getTokensByMerkleProof' , [ proof , address , amount . toFixed ( 0 ) , rootData . rootIndex ] , { onTransactionHash } ) ; } }