import { Injectable } from "@angular/core" ; import { Delegate } from "ark-ts" ; import { Network , NetworkType } from "ark-ts/model" ; import * as lodash from "lodash" ; import { EMPTY , Observable , Subject , throwError } from "rxjs" ; import { debounceTime , map } from "rxjs/operators" ; import { v4 as uuid } from "uuid" ; import * as constants from "@/app/app.constants" ; import { Contact , Profile , Wallet , WalletKeys } from "@/models/model" ; import { StoredNetwork } from "@/models/stored-network" ; import { TranslatableObject } from "@/models/translate" ; import { AuthProvider } from "@/services/auth/auth" ; import { ForgeProvider } from "@/services/forge/forge" ; import { StorageProvider } from "@/services/storage/storage" ; import { UserDataService } from "./user-data.interface" ; @ Injectable ( ) export class UserDataServiceImpl implements UserDataService { public get isDevNet ( ) : boolean { return ( this . currentNetwork && this . currentNetwork . type === NetworkType . Devnet ) ; } public get isMainNet ( ) : boolean { return ( this . currentNetwork && this . currentNetwork . type === NetworkType . Mainnet ) ; } public get defaultNetworks ( ) : Network [ ] { if ( ! this . _defaultNetworks ) { this . _defaultNetworks = Network . getAll ( ) ; } return this . _defaultNetworks ; } public profiles : { [ key ] : Profile ; } = { } ; public networks : Record < string , StoredNetwork > = { } ; public currentProfile : Profile ; public currentNetwork : StoredNetwork ; public currentWallet : Wallet ; public onActivateNetwork$ : Subject < StoredNetwork > = new Subject ( ) ; public onUpdateNetwork$ : Subject < StoredNetwork > = new Subject ( ) ; public onCreateWallet$ : Subject < Wallet > = new Subject ( ) ; public onUpdateWallet$ : Subject < Wallet > = new Subject ( ) ; public onSelectProfile$ : Subject < Profile > = new Subject ( ) ; private _defaultNetworks : Network [ ] ; constructor ( private storageProvider , private authProvider , private forgeProvider ) { this . loadAllData ( ) ; this . onLogin ( ) ; this . onClearStorage ( ) ; this . onUpdateNetwork$ . subscribe ( ( network ) => ( this . currentNetwork = network ) ) ; } private static mapContact = ( contacts , contact , address ) : void => { contact . address = address ; contacts [ address ] = contact ; } ; addOrUpdateNetwork ( network , networkId ? ) { if ( ! networkId ) { networkId = this . generateUniqueId ( ) ; } const { [ networkId ] : _ , ... networks } = this . networks ; networks [ networkId ] = network ; this . networks = networks ; return this . storageProvider . set ( constants . STORAGE_NETWORKS , this . networks ) . pipe ( map ( ( ) => { return { network : this . networks [ networkId ] , id : networkId , } ; } ) ) ; } getNetworkById ( networkId ) { return this . networks [ networkId ] ; } removeNetworkById ( networkId ) { const { [ networkId ] : _ , ... networks } = this . networks ; this . networks = networks ; return this . storageProvider . set ( constants . STORAGE_NETWORKS , this . networks ) ; } addProfile ( profile ) { this . profiles [ this . generateUniqueId ( ) ] = profile ; return this . saveProfiles ( ) ; } getProfileByName ( name ) { const profile = lodash . find ( this . profiles , ( id ) => id . name . toLowerCase ( ) === name . toLowerCase ( ) ) ; if ( profile ) { return new Profile ( ) . deserialize ( profile ) ; } } getProfileById ( profileId ) { if ( this . profiles [ profileId ] ) { return new Profile ( ) . deserialize ( this . profiles [ profileId ] ) ; } } removeProfileById ( profileId ) { const { [ profileId ] : _ , ... profiles } = this . profiles ; this . profiles = profiles ; return this . saveProfiles ( ) ; } saveProfiles ( profiles = this . profiles ) { const currentProfile = this . authProvider . loggedProfileId ; if ( currentProfile ) { this . setCurrentProfile ( currentProfile , false ) ; } return this . storageProvider . set ( constants . STORAGE_PROFILES , profiles ) ; } encryptSecondPassphrase ( wallet , pinCode , secondPassphrase , profileId = this . authProvider . loggedProfileId ) { if ( lodash . isUndefined ( profileId ) ) { return ; } if ( wallet && ! wallet . cipherSecondKey ) { wallet . cipherSecondKey = this . forgeProvider . encrypt ( secondPassphrase , pinCode , wallet . address , wallet . iv ) ; return this . updateWallet ( wallet , profileId , true ) ; } return this . saveProfiles ( ) ; } addWallet ( wallet , passphrase , pinCode , profileId = this . authProvider . loggedProfileId ) { if ( lodash . isUndefined ( profileId ) ) { return throwError ( "EMPTY_PROFILE_ID" ) ; } const profile = this . getProfileById ( profileId ) ; if ( ! profile ) { return throwError ( "PROFILE_NOT_FOUND" ) ; } if ( passphrase ) { const iv = this . forgeProvider . generateIv ( ) ; wallet . iv = iv ; const cipherKey = this . forgeProvider . encrypt ( passphrase , pinCode , wallet . address , iv ) ; wallet . cipherKey = cipherKey ; } if ( ! profile . wallets [ wallet . address ] || profile . wallets [ wallet . address ] . isWatchOnly ) { this . onCreateWallet$ . next ( wallet ) ; return this . saveWallet ( wallet , profileId , true ) ; } return this . saveProfiles ( ) ; } updateWalletEncryption ( oldPassword , newPassword ) { for ( const profileId in this . profiles ) { if ( profileId ) { const profile = this . profiles [ profileId ] ; for ( const walletId in profile . wallets ) { if ( walletId ) { const wallet = profile . wallets [ walletId ] ; if ( wallet . isWatchOnly ) { continue ; } const key = this . forgeProvider . decrypt ( wallet . cipherKey , oldPassword , wallet . address , wallet . iv ) ; wallet . cipherKey = this . forgeProvider . encrypt ( key , newPassword , wallet . address , wallet . iv ) ; if ( wallet . cipherSecondKey ) { const secondKey = this . forgeProvider . decrypt ( wallet . cipherSecondKey , oldPassword , wallet . address , wallet . iv ) ; wallet . cipherSecondKey = this . forgeProvider . encrypt ( secondKey , newPassword , wallet . address , wallet . iv ) ; } this . updateWallet ( wallet , profileId ) ; } } } } return this . saveProfiles ( ) ; } removeWalletByAddress ( address , profileId = this . authProvider . loggedProfileId ) { delete this . profiles [ profileId ] . wallets [ address ] ; return this . saveProfiles ( ) ; } ensureWalletDelegateProperties ( wallet , delegateOrUserName ) { if ( ! wallet ) { return throwError ( "WALLET_EMPTY" ) ; } const userName = ! delegateOrUserName || typeof delegateOrUserName === "string" ? ( delegateOrUserName as string ) : delegateOrUserName . username ; if ( ! userName ) { return throwError ( "USERNAME_EMPTY" ) ; } if ( wallet . isDelegate && wallet . username === userName ) { return EMPTY ; } wallet . isDelegate = true ; wallet . username = userName ; return this . updateWallet ( wallet , this . currentProfile . profileId , true ) ; } getWalletByAddress ( address , profileId = this . authProvider . loggedProfileId ) { if ( ! address || lodash . isUndefined ( profileId ) ) { return ; } const profile = this . getProfileById ( profileId ) ; let wallet = new Wallet ( ) ; if ( profile . wallets [ address ] ) { wallet = wallet . deserialize ( profile . wallets [ address ] ) ; wallet . loadTransactions ( wallet . transactions ) ; return wallet ; } return null ; } updateWallet ( wallet , profileId , notificate = false ) { if ( lodash . isUndefined ( profileId ) ) { return throwError ( "EMPTY_PROFILE_ID" ) ; } const profile = this . getProfileById ( profileId ) ; if ( profile && profile . wallets [ wallet . address ] ) { return this . saveWallet ( wallet , profileId , notificate ) ; } return EMPTY ; } saveWallet ( wallet , profileId = this . authProvider . loggedProfileId , notificate = false ) { if ( lodash . isUndefined ( profileId ) ) { return throwError ( "EMPTY_PROFILE_ID" ) ; } const profile = this . getProfileById ( profileId ) ; wallet . lastUpdate = new Date ( ) . getTime ( ) ; profile . wallets [ wallet . address ] = wallet ; this . profiles [ profileId ] = profile ; if ( notificate ) { this . onUpdateWallet$ . next ( wallet ) ; } return this . saveProfiles ( ) ; } public setWalletLabel ( wallet , label ) { if ( ! wallet ) { return throwError ( { key : "VALIDATION.INVALID_WALLET" , } as TranslatableObject ) ; } if ( label && lodash . some ( this . currentProfile . wallets , ( w ) => w . label && w . label . toLowerCase ( ) === label . toLowerCase ( ) && w . address !== wallet . address ) ) { return throwError ( { key : "VALIDATION.LABEL_EXISTS" , parameters : { label } , } as TranslatableObject ) ; } wallet . label = label ; return this . updateWallet ( wallet , this . currentProfile . profileId ) ; } public getWalletLabel ( walletOrAddress , profileId ? ) { let wallet ; if ( typeof walletOrAddress === "string" ) { wallet = this . getWalletByAddress ( walletOrAddress , profileId ) ; } else { wallet = walletOrAddress ; } if ( ! wallet ) { return null ; } return wallet . username || wallet . label ; } setCurrentWallet ( wallet ) { this . currentWallet = wallet ; } clearCurrentWallet ( ) { this . currentWallet = undefined ; } loadProfiles ( ) { return this . storageProvider . getObject ( constants . STORAGE_PROFILES ) . pipe ( map ( ( profiles ) => { return lodash . mapValues ( profiles , ( profile , profileId ) => ( { ... profile , profileId , contacts : lodash . transform ( profile . contacts , UserDataServiceImpl . mapContact , { } ) , } ) ) ; } ) ) ; } loadNetworks ( ) { return new Observable ( ( observer ) => { this . storageProvider . getObject ( constants . STORAGE_NETWORKS ) . subscribe ( ( networks ) => { if ( ! networks || lodash . isEmpty ( networks ) ) { const uniqueDefaults = { } ; for ( const network of this . defaultNetworks ) { uniqueDefaults [ this . generateUniqueId ( ) ] = network ; } this . storageProvider . set ( constants . STORAGE_NETWORKS , uniqueDefaults ) ; observer . next ( uniqueDefaults ) ; } else { observer . next ( networks ) ; } observer . complete ( ) ; } ) ; } ) ; } getKeysByWallet ( wallet , password ) { if ( ! wallet . cipherKey && ! wallet . cipherSecondKey ) { return ; } const keys = { } ; if ( wallet . cipherKey ) { keys . key = this . forgeProvider . decrypt ( wallet . cipherKey , password , wallet . address , wallet . iv ) ; } if ( wallet . cipherSecondKey ) { keys . secondKey = this . forgeProvider . decrypt ( wallet . cipherSecondKey , password , wallet . address , wallet . iv ) ; } return keys ; } private setCurrentNetwork ( ) { if ( ! this . currentProfile ) { return ; } const network = new StoredNetwork ( ) ; Object . assign ( network , this . networks [ this . currentProfile . networkId ] ) ; this . onActivateNetwork$ . next ( network ) ; this . currentNetwork = network ; } private setCurrentProfile ( profileId , broadcast = true ) { if ( profileId && this . profiles [ profileId ] ) { const profile = new Profile ( ) . deserialize ( this . profiles [ profileId ] ) ; this . currentProfile = profile ; if ( broadcast ) { this . onSelectProfile$ . next ( profile ) ; } } else { this . currentProfile = null ; this . authProvider . logout ( false ) ; } } private loadAllData ( ) { this . loadProfiles ( ) . subscribe ( ( profiles ) => ( this . profiles = profiles ) ) ; this . loadNetworks ( ) . subscribe ( ( networks ) => ( this . networks = networks ) ) ; } private onLogin ( ) { return this . authProvider . onLogin$ . subscribe ( ( id ) => { this . setCurrentProfile ( id ) ; this . setCurrentNetwork ( ) ; } ) ; } private onClearStorage ( ) { this . storageProvider . onClear$ . pipe ( debounceTime ( 100 ) ) . subscribe ( ( ) => { this . loadAllData ( ) ; this . setCurrentProfile ( null ) ; } ) ; } private generateUniqueId ( ) { return uuid ( ) ; } }