/**
 * Copyright (c) 2020 BlockDev AG
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */ import { AppState , Identity , IdentityRegistrationStatus , SSEEventType } from "mysterium-vpn-js" ; import { action , computed , makeObservable , observable , reaction , runInAction , toJS } from "mobx" ; import retry from "async-retry" ; import _ from "lodash" ; import { RootStore } from "../store" ; import { eventBus , tequilapi } from "../tequilapi" ; import { log } from "../../shared/log/log" ; import { decimalPart , fmtMoney } from "../payment/display" ; import { PushTopic } from "../../shared/push/topics" ; import { subscribePush , unsubscribePush } from "../push/push" ; import { ExportIdentityOpts , ImportIdentityOpts , mysteriumNodeIPC } from "../../shared/node/mysteriumNodeIPC" ; import { analytics } from "../analytics/analytics" ; import { EventName } from "../analytics/event" ; import { locations } from "../navigation/locations" ; const registrationPossible = ( status ) : boolean => { return ! [ IdentityRegistrationStatus . Registered , IdentityRegistrationStatus . InProgress , IdentityRegistrationStatus . Unknown , ] . includes ( status ) ; } ; export class IdentityStore { loading = false ; identity ? : Identity ; unlocked = false ; identities : Identity [ ] = [ ] ; root : RootStore ; constructor ( root ) { makeObservable ( this , { loading : observable , identity : observable , unlocked : observable , identities : observable , refreshIdentity : action , create : action , loadIdentity : action , identityExists : computed , fetchIdentity : action , hasIdentities : action , unlock : action , register : action , registerWithReferralToken : action , setLoading : action , setIdentity : action , setIdentities : action , exportIdentity : action , importIdentityChooseFile : action , importIdentity : action , } ) ; this . root = root ; } setupReactions ( ) { eventBus . on ( SSEEventType . AppStateChange , ( state ) => { this . setIdentities ( state . identities ?? [ ] ) ; } ) ; reaction ( ( ) => this . identities , async ( identities ) => { this . refreshIdentity ( identities ) ; } , { name : "Refresh identity from node state" } ) ; const reportBalanceUpdate = _ . debounce ( ( amount ) => { analytics . event ( EventName . balance_update , { balance : Number ( fmtMoney ( { amount , currency : this . root . payment . appCurrency , } , { showCurrency : false } ) ) , } ) ; } , 60000 ) ; reaction ( ( ) => this . identity ?. balance , ( balance ) => { if ( balance != null && balance / decimalPart ( ) < 0.5 ) { subscribePush ( PushTopic . LessThanHalfMyst ) ; } else { unsubscribePush ( PushTopic . LessThanHalfMyst ) ; } reportBalanceUpdate ( this . root . identity . identity ?. balance ?? 0 ) ; } ) ; reaction ( ( ) => this . identity ?. balance , this . registerOnBalanceChange ) ; reaction ( ( ) => this . identity ?. registrationStatus , this . navigateOnStatusChange ) ; } registerOnBalanceChange = async ( balance ? ) : Promise < void > => { if ( ! this . identity ) { return ; } if ( ! registrationPossible ( this . identity . registrationStatus ) ) { return ; } if ( ! balance ) { return ; } if ( balance < ( this . root . payment . registrationFee ?? 0 ) ) { log . info ( "Balance is insufficient to register" ) ; return ; } log . info ( "Balance is sufficient for registration" ) ; return await this . register ( this . identity ) ; } ; navigateOnStatusChange = ( status ? ) : void => { switch ( status ) { case IdentityRegistrationStatus . InProgress : case IdentityRegistrationStatus . RegistrationError : this . root . router . push ( locations . registering ) ; return ; case IdentityRegistrationStatus . Registered : this . root . router . push ( locations . proposals ) ; } } ; async hasIdentities ( ) { const ids = await tequilapi . identityList ( ) ; return ids . length > 0 ; } async loadIdentity ( ) { log . info ( "Loading identity" ) ; const identity = await this . fetchIdentity ( ) ; this . setIdentity ( identity ) ; await this . unlock ( ) ; log . info ( "Identity loaded:" , identity ) ; } get identityExists ( ) : boolean { return this . identity ?. id != null ; } async fetchIdentity ( ) { const ids = await tequilapi . identityList ( ) ; if ( ids . length < 1 ) { return undefined ; } const current = await tequilapi . identityCurrent ( { passphrase : "" } ) . catch ( ( reason ) => { throw Error ( "Could not get current identity ref: " + reason ) ; } ) ; const MAX_RETRIES = 10 ; return await retry ( async ( ) => { return tequilapi . identity ( current . id ) ; } , { retries : MAX_RETRIES , onRetry : ( e , attempt ) => log . warn ( `Failed to get identity (${ attempt }/${ MAX_RETRIES }): ${ e . message }` ) , } ) . catch ( ( reason ) => { throw Error ( "Could not get identity: " + reason ) ; } ) ; } refreshIdentity = ( identities ) : void => { if ( ! this . identity ) { return ; } const matchingId = identities . find ( ( id ) => id . id == this . identity ?. id ) ; this . setIdentity ( matchingId ) ; } ; async create ( ) { try { await tequilapi . identityCreate ( "" ) ; } catch ( err ) { log . error ( "Failed to create ID" , err ) ; } } async unlock ( ) { if ( this . unlocked ) { return ; } if ( ! this . identity ) { return ; } const i = this . identity . id ; try { await tequilapi . identityUnlock ( i , "" , 10000 ) ; runInAction ( ( ) => { this . unlocked = true ; } ) ; } catch ( err ) { log . error ( "Failed to unlock identity" , err ) ; } } async registerIfNeeded ( ) { if ( ! this . identity ) { log . info ( "No ID" ) ; return ; } const { id , registrationStatus : status , balance } = this . identity ; if ( ! registrationPossible ( status ) ) { log . info ( `Status ${ status } cannot be registered` ) ; return ; } const eligibleForFreeRegistration = await tequilapi . freeRegistrationEligibility ( id ) . then ( ( res ) => res . eligible ) ; log . info ( "Free registration eligibility:" , eligibleForFreeRegistration ) ; if ( eligibleForFreeRegistration ) { log . info ( "Attempting to register for free" ) ; await this . register ( this . identity ) ; return ; } await this . root . payment . fetchTransactorFees ( ) ; log . info ( "Registration fee:" , this . root . payment . registrationFee ) ; if ( balance < ( this . root . payment . registrationFee ?? 0 ) ) { log . info ( "Balance is insufficient to register" ) ; return ; } log . info ( "Balance is sufficient for registration" ) ; await this . register ( this . identity ) ; } async register ( id , referralToken ? ) { log . info ( "Registering MysteriumID: " , toJS ( id ) , referralToken ? `token: ${ referralToken }` : "" ) ; await this . root . payment . fetchTransactorFees ( ) ; try { await tequilapi . identityRegister ( id . id , { stake : 0 , referralToken } ) ; } catch ( err ) { log . error ( "Failed to register identity" , err ) ; } } async registerWithReferralToken ( token ) { if ( ! this . identity ) { return ; } this . setLoading ( true ) ; try { await tequilapi . identityRegister ( this . identity ?. id , { token } ) ; } finally { this . setLoading ( false ) ; } } async refreshBalance ( ) { if ( ! this . identity ) { return ; } await tequilapi . identityBalanceRefresh ( this . identity . id ) ; } setLoading = ( b ) : void => { this . loading = b ; } ; setIdentity = ( identity ? ) : void => { this . identity = identity ; } ; setIdentities = ( identities ) : void => { this . identities = identities ; } ; async exportIdentity ( opts ) { const res = await mysteriumNodeIPC . exportIdentity ( opts ) ; if ( res . error ) { return Promise . reject ( res . error ) ; } return String ( res . result ) ; } importIdentityChooseFile ( ) { return mysteriumNodeIPC . importIdentityChooseFile ( ) ; } async importIdentity ( opts ) { const res = await mysteriumNodeIPC . importIdentity ( opts ) ; if ( res . error ) { return Promise . reject ( res . error ) ; } await this . loadIdentity ( ) ; if ( this . identity && this . identity ?. registrationStatus !== IdentityRegistrationStatus . Registered ) { await this . register ( this . identity ) ; } return String ( res . result ) ; } }