import { Injectable , OnDestroy } from '@angular/core' ; import { BehaviorSubject , combineLatest , Observable , of } from 'rxjs' ; import { catchError , map , switchMap , take , tap } from 'rxjs/operators' ; import { ApiResponse , ApiService } from '../../../common/api/api.service' ; import { FormToastService } from '../../../common/services/form-toast.service' ; import { BoostContractService } from '../../blockchain/contracts/boost-contract.service' ; import { Web3WalletService } from '../../blockchain/web3-wallet.service' ; import { MindsUser } from '../../../interfaces/entities' ; import * as BN from 'bn.js' ; import toFriendlyCryptoVal from '../../../helpers/friendly-crypto' ; export const MAXIMUM_SINGLE_BOOST_IMPRESSIONS = 5000 ; export const MINIMUM_SINGLE_BOOST_IMPRESSIONS = 500 ; export const MINIMUM_BOOST_OFFER_TOKENS = 1 ; export const DEFAULT_BOOST_RATE = 1000 ; export const DEFAULT_ACTIVE_TAB = 'newsfeed' ; export const DEFAULT_PAYMENT_METHOD = 'offchain' ; export const DEFAULT_IMPRESSIONS = MAXIMUM_SINGLE_BOOST_IMPRESSIONS / 2 ; export const DEFAULT_TOKENS = DEFAULT_IMPRESSIONS / 1000 ; export const DEFAULT_ENTITY = { guid : '' } ; export const DEFAULT_TARGET_USER = null ; export const DEFAULT_BALANCE = 0 ; /**
 * Subject of the boost, a channel or post.
 */ export type BoostSubject = 'channel' | 'post' | 'blog' | '' ; /**
 * Selected tab, newsfeed or offer.
 */ export type BoostTab = 'newsfeed' | 'offer' ; /**
 * Selected payment method, onchain or offchain.
 */ export type BoostPaymentMethod = 'onchain' | 'offchain' ; /**
 * Target user used for offers.
 */ export type TargetUser = MindsUser | null ; /**
 * Boost wallet.
 */ export type BoostWallet = { balance : string ; address : string ; label : string ; ether_balance ? : string ; available ? : string ; } ; /**
 * Entity that is boostable.
 */ export type BoostableEntity = { guid ? : string ; type ? : string ; subtype ? : string ; owner_guid ? : string ; time_created ? : number | string ; } ; /**
 * Balance endpoint response.
 */ export type BalanceResponse = { status ? : string ; addresses ? : BoostWallet [ ] ; balance ? : string ; wireCap ? : string ; boostCap ? : string ; testnetBalance ? : string ; } ; /**
 * Boost prepare endpoint response.
 */ export type PrepareResponse = { status ? : string ; guid ? : string ; checksum ? : string ; } ; /**
 * Boost activity post response.
 */ export type BoostActivityPostResponse = { status ? : string ; } ; /**
 * POST Boost payload.
 */ export type BoostPayload = { bidType ? : string ; checksum ? : string ; guid ? : string ; impressions ? : number ; paymentMethod ? : PayloadPaymentMethod ; } ; /**
 * Payment method for boost payload.
 */ export type PayloadPaymentMethod = { method ? : string ; address ? : string | false ; txHash ? : string | false ; } ; /**
 * Peer boost payload (offers).
 */ export type PeerBoostPayload = { bid : string ; checksum : string ; currency : string ; destination : string ; guid : string ; paymentMethod ? : PayloadPaymentMethod ; } ; /**
 * Post response for peer boosts (boost offers)
 */ export type PeerBoostPostResponse = { status : string ; boost_guid : string ; } ; /**
 * Main service for boost modal. Handles logic involved in boost preparation and submission.
 */ @ Injectable ( { providedIn : 'root' } ) export class BoostModalService implements OnDestroy { public readonly rate$ : BehaviorSubject < number > = new BehaviorSubject < number > ( DEFAULT_BOOST_RATE ) ; /**
     * Active boost tab.
     */ public readonly activeTab$ : BehaviorSubject < BoostTab > = new BehaviorSubject < BoostTab > ( DEFAULT_ACTIVE_TAB ) ; /**
     * Payment method, onchain or offchain.
     */ public readonly paymentMethod$ : BehaviorSubject < BoostPaymentMethod > = new BehaviorSubject < BoostPaymentMethod > ( DEFAULT_PAYMENT_METHOD ) ; /**
     * Target impressions of the boost.
     */ public readonly impressions$ : BehaviorSubject < number > = new BehaviorSubject < number > ( DEFAULT_IMPRESSIONS ) ; /**
     * Value of tokens.
     */ public readonly tokens$ : BehaviorSubject < number > = new BehaviorSubject < number > ( DEFAULT_TOKENS ) ; /**
     * Entity being boosted.
     */ public readonly entity$ : BehaviorSubject < BoostableEntity > = new BehaviorSubject < BoostableEntity > ( DEFAULT_ENTITY ) ; /**
     * Target user for boost offers.
     */ public targetUser$ : BehaviorSubject < TargetUser > = new BehaviorSubject < TargetUser > ( DEFAULT_TARGET_USER ) ; /**
     * Users onchain balance. Populated via fetchBalance.
     */ public readonly onchainBalance$ : BehaviorSubject < number > = new BehaviorSubject < number > ( DEFAULT_BALANCE ) ; /**
     * Users offchain balance. Populated via fetchBalance.
     */ public readonly offchainBalance$ : BehaviorSubject < number > = new BehaviorSubject < number > ( DEFAULT_BALANCE ) ; /**
     * Is the subject a post or a channel
     */ get entityType$ ( ) : Observable < BoostSubject > { return this . entity$ . pipe ( map ( entity => { if ( entity . type === 'object' || entity . subtype === 'blog' ) { return 'blog' ; } if ( entity . type === 'user' ) { return 'channel' ; } return 'post' ; } ) ) ; } /**
     * Disable the ability to boost?
     */ get disabled$ ( ) : Observable < boolean > { return combineLatest ( [ combineLatest ( [ this . activeTab$ , this . paymentMethod$ , this . impressions$ , this . tokens$ , this . targetUser$ , ] ) , combineLatest ( [ this . onchainBalance$ , this . offchainBalance$ , this . rate$ , this . entity$ , ] ) , ] ) . pipe ( map ( ( [ [ activeTab , paymentMethod , impressions , tokens , targetUser ] , [ onchainBalance , offchainBalance , rate , entity ] , ] ) => { if ( this . isScheduled ( entity ) ) { return true ; } const balance = paymentMethod === 'onchain' ? onchainBalance : offchainBalance ; if ( activeTab === 'newsfeed' ) { if ( ! this . hasValidImpressions ( impressions ) ) { return true ; } if ( ! this . hasBoostFunds ( impressions , balance , rate ) ) { return true ; } return false ; } if ( ! this . hasValidBid ( tokens ) ) { return true ; } if ( ! this . hasOfferFunds ( tokens , balance ) ) { return true ; } if ( ! targetUser ) { return true ; } return false ; } ) ) ; } constructor ( private api , private toast , private web3Wallet , private boostContract ) { } ngOnDestroy ( ) { this . reset ( ) ; } returns { Observable < BalanceResponse > } public fetchBalance ( ) { return this . api . get ( 'api/v2/blockchain/wallet/balance' ) . pipe ( take ( 1 ) , map ( response => { const addresses = response . addresses ; for ( let address of addresses ) { if ( address . address === 'offchain' ) { this . offchainBalance$ . next ( toFriendlyCryptoVal ( address . balance ) ) ; } if ( address . address . startsWith ( '0x' ) ) { this . onchainBalance$ . next ( toFriendlyCryptoVal ( address . balance ) ) ; } } return response ; } ) , catchError ( e => this . handleError ( e ) ) ) ; } returns { Promise < BoostActivityPostResponse > } public async submitBoostAsync ( ) { const prepared = await this . prepareBoostPayload ( ) . toPromise ( ) ; if ( this . activeTab$ . getValue ( ) === 'offer' ) { const payload = await this . assemblePeerBoostPayload ( prepared ) ; return this . postPeerBoost ( payload ) . toPromise ( ) ; } const payload = await this . assembleDirectBoostPayload ( prepared ) ; return this . postBoostActivity ( payload ) . toPromise ( ) ; } returns { Observable < PrepareResponse > } private prepareBoostPayload ( ) { return this . entity$ . pipe ( take ( 1 ) , switchMap ( entity => this . api . get ( `api/v2/boost/prepare/${ entity . guid }` ) ) , catchError ( e => this . handleError ( e ) ) ) ; } param { BoostPayload } payload returns { Observable < BoostActivityPostResponse > } private postBoostActivity ( payload ) { return this . entity$ . pipe ( take ( 1 ) , switchMap ( entity => { const ownerGuid = entity . type === 'user' ? entity . guid : entity . owner_guid ; return this . api . post ( `api/v2/boost/${ entity . type }/${ entity . guid }/${ ownerGuid }` , payload ) ; } ) , catchError ( e => this . handleError ( e ) ) ) ; } param { BoostPayload } payload returns { ApiResponse } private postPeerBoost ( payload ) { return this . entity$ . pipe ( take ( 1 ) , switchMap ( entity => { return this . api . post ( `api/v2/boost/peer/${ entity . guid }/${ entity . owner_guid }` , payload ) ; } ) , catchError ( e => this . handleError ( e ) ) ) ; } param { PreparedResponse } preparedResponse returns { Promise < BoostPayload > } private async assembleDirectBoostPayload ( preparedResponse ) { const paymentMethod = this . paymentMethod$ . getValue ( ) ; const impressions = this . impressions$ . getValue ( ) ; let response = { guid : preparedResponse . guid , bidType : 'tokens' , impressions : impressions , checksum : preparedResponse . checksum , } ; if ( paymentMethod === 'offchain' ) { return { ... response , paymentMethod : { method : 'offchain' , address : 'offchain' , } , } ; } if ( paymentMethod === 'onchain' ) { return { ... response , paymentMethod : { ... ( await this . getOnchainPaymentMethod ( preparedResponse . guid , impressions , preparedResponse . checksum ) ) , } , } ; } } param { PreparedResponse } preparedResponse returns { Promise < PeerBoostPayload > } private async assemblePeerBoostPayload ( preparedResponse ) { const paymentMethod = this . paymentMethod$ . getValue ( ) ; const tokens = this . tokens$ . getValue ( ) ; let response = { guid : preparedResponse . guid , bid : this . getBid ( tokens ) , currency : 'tokens' , checksum : preparedResponse . checksum , destination : this . targetUser$ . getValue ( ) . guid , } ; if ( paymentMethod === 'offchain' ) { return { ... response , paymentMethod : { method : 'offchain' , address : 'offchain' , } , } ; } if ( paymentMethod === 'onchain' ) { return { ... response , paymentMethod : { ... ( await this . getOnchainPaymentMethod ( preparedResponse . guid , tokens , preparedResponse . checksum ) ) , } , } ; } } param { string } guid param { number } impressions param { string } checksum private async getOnchainPaymentMethod ( guid , tokens , checksum ) { const rate = this . rate$ . getValue ( ) ; if ( this . web3Wallet . isUnavailable ( ) ) { throw new Error ( 'No Ethereum wallets available on your browser.' ) ; } if ( ! ( await this . web3Wallet . unlock ( ) ) ) { throw new Error ( 'Your Ethereum wallet is locked or connected to another network.' ) ; } let amount = tokens ; if ( this . activeTab$ . getValue ( ) !== 'offer' ) { const tokensFixRate = this . rate$ . getValue ( ) / 10000 ; amount = Math . ceil ( < number > tokens / tokensFixRate ) / 10000 ; } return { method : 'onchain' , txHash : await this . boostContract . create ( guid , amount , checksum ) , address : await this . web3Wallet . getCurrentWallet ( true ) , } ; } param { number } amount returns { string } private getBid ( amount ) { const tokenDec = new BN ( 10 ) . pow ( new BN ( 18 ) ) ; return new BN ( amount || 0 ) . mul ( tokenDec ) . toString ( ) ; } private handleError ( e ) { console . error ( e ) ; this . toast . error ( e ) ; return of ( null ) ; } param { number } impressions param { number } balance param { number } rate returns { boolean } private hasBoostFunds ( impressions , balance , rate ) { let tokenCost = impressions / rate ; return balance >= tokenCost ; } param { number } tokenCost param { number } balance returns { boolean } private hasOfferFunds ( tokenCost , balance ) { return balance >= tokenCost ; } param { number } impressions returns { boolean } private hasValidImpressions ( impressions ) { return ( impressions <= MAXIMUM_SINGLE_BOOST_IMPRESSIONS && impressions >= MINIMUM_SINGLE_BOOST_IMPRESSIONS ) ; } param { number } tokens returns { boolean } private hasValidBid ( tokens ) { return tokens >= MINIMUM_BOOST_OFFER_TOKENS ; } param { BoostableEntity } entity returns { boolean } private isScheduled ( entity ) { return Number ( entity . time_created ) * 1000 > Date . now ( ) ; } returns { void } public reset ( ) { this . rate$ . next ( DEFAULT_BOOST_RATE ) ; this . activeTab$ . next ( DEFAULT_ACTIVE_TAB ) ; this . paymentMethod$ . next ( DEFAULT_PAYMENT_METHOD ) ; this . impressions$ . next ( DEFAULT_IMPRESSIONS ) ; this . tokens$ . next ( DEFAULT_TOKENS ) ; this . entity$ . next ( DEFAULT_ENTITY ) ; this . targetUser$ . next ( DEFAULT_TARGET_USER ) ; this . onchainBalance$ . next ( DEFAULT_BALANCE ) ; this . offchainBalance$ . next ( DEFAULT_BALANCE ) ; } }