import { Injectable } from '@angular/core' ; import { BehaviorSubject } from 'rxjs/BehaviorSubject' ; import { environment } from 'environments/environment' ; import { StoreService } from 'app/store/store.service' ; import { Account , Address , AliasList , Api , Asset , AssetList , Balance , BlockList , Transaction , TransactionId , TransactionList , TransactionMiningSubtype , TransactionType , UnconfirmedTransactionList , AddressPrefix , GetAccountTransactionsArgs , Alias } from '@signumjs/core' ; import { decryptAES , encryptAES , generateMasterKeys , hashSHA256 , Keys } from '@signumjs/crypto' ; import { Amount } from '@signumjs/util' ; import { HttpClientFactory , HttpError } from '@signumjs/http' ; import { ApiService } from '../../api.service' ; import { I18nService } from 'app/layout/components/i18n/i18n.service' ; import { NetworkService } from '../../network/network.service' ; import { KeyDecryptionException } from '../../util/exceptions/KeyDecryptionException' ; import { adjustLegacyAddressPrefix } from '../../util/adjustLegacyAddressPrefix' ; import { uniqBy } from 'lodash' ; interface SetAccountInfoRequest { name : string ; description : string ; deadline : number ; feePlanck : string ; pin : string ; keys : Keys ; } interface SetRewardRecipientRequest { recipientId : string ; deadline : number ; feePlanck : string ; pin : string ; keys : Keys ; } interface SetAliasRequest { aliasName : string ; aliasURI : string ; deadline : number ; feeNQT : string ; pin : string ; keys : Keys ; } interface SetCommitmentRequest { amountPlanck : string ; feePlanck : string ; pin : string ; keys : Keys ; isRevoking : boolean ; } @ Injectable ( { providedIn : 'root' } ) export class AccountService { public currentAccount$ : BehaviorSubject < Account > = new BehaviorSubject ( undefined ) ; private api : Api ; private transactionsSeenInNotifications : string [ ] = [ ] ; private accountPrefix : AddressPrefix . MainNet | AddressPrefix . TestNet ; constructor ( private storeService , private networkService , private apiService , private i18nService ) { this . storeService . settings . subscribe ( ( ) => { this . api = this . apiService . api ; } ) ; this . networkService . isMainNet$ . subscribe ( ( ) => { this . accountPrefix = this . networkService . isMainNet ( ) ? AddressPrefix . MainNet : AddressPrefix . TestNet ; } ) ; } public updateCurrentAccount ( account ) { this . currentAccount$ . next ( account ) ; } public async getAddedCommitments ( account ) { return this . api . account . getAccountTransactions ( { accountId : account . account , type : TransactionType . Mining , subtype : TransactionMiningSubtype . AddCommitment , includeIndirect : false , } ) ; } public async getAccountTransactions ( args ) { args . includeIndirect = true ; try { const transactions = await this . api . account . getAccountTransactions ( args ) ; return Promise . resolve ( transactions ) ; } catch ( e ) { const EC_INVALID_ARG = 4 ; if ( e . data . errorCode === EC_INVALID_ARG ) { return await this . api . account . getAccountTransactions ( args ) ; } else { throw e ; } } } public generateSendTransactionQRCodeAddress ( id , amountNQT ? , feeSuggestionType ? , feeNQT ? , immutable ? ) { return this . api . account . generateSendTransactionQRCodeAddress ( id , amountNQT , feeSuggestionType , feeNQT , immutable ) ; } public getAlias ( name ) { return this . api . alias . getAliasByName ( name ) ; } public getAliases ( id ) { return this . api . account . getAliases ( id ) ; } public getAssets ( id ) { return this . api . asset . getAllAssets ( id ) ; } public setAlias ( { aliasName , aliasURI , feeNQT , deadline , pin , keys } ) { const senderPrivateKey = this . getPrivateKey ( keys , pin ) ; return this . api . account . setAlias ( aliasName , aliasURI , feeNQT , keys . publicKey , senderPrivateKey , deadline ) ; } private getPrivateKey ( keys , pin ) { try { const privateKey = decryptAES ( keys . signPrivateKey , hashSHA256 ( pin ) ) ; if ( ! privateKey ) { throw new Error ( 'Key Decryption Exception' ) ; } return privateKey ; } catch ( e ) { throw new KeyDecryptionException ( ) ; } } public getAccountBalance ( id ) { return this . api . account . getAccountBalance ( id ) ; } public getUnconfirmedTransactions ( id ) { return this . api . account . getUnconfirmedAccountTransactions ( id ) ; } public async getAccount ( accountId ) { const supportsPocPlus = await this . apiService . supportsPocPlus ( ) ; const includeCommittedAmount = supportsPocPlus || undefined ; const account = await this . api . account . getAccount ( { accountId , includeCommittedAmount , } ) ; return adjustLegacyAddressPrefix ( account ) ; } public getCurrentAccount ( ) { return Promise . resolve ( this . currentAccount$ . getValue ( ) ) ; } public setAccountInfo ( { name , description , feePlanck , deadline , pin , keys } ) { const senderPrivateKey = this . getPrivateKey ( keys , pin ) ; return this . api . account . setAccountInfo ( { name , description , feePlanck , senderPrivateKey , senderPublicKey : keys . publicKey , deadline } ) ; } public setRewardRecipient ( { recipientId , feePlanck , deadline , pin , keys } ) { const senderPrivateKey = this . getPrivateKey ( keys , pin ) ; return this . api . account . setRewardRecipient ( { recipientId , senderPrivateKey , senderPublicKey : keys . publicKey , deadline , feePlanck , } ) ; } public async getRewardRecipient ( recipientId ) { const { rewardRecipient } = await this . api . account . getRewardRecipient ( recipientId ) ; return rewardRecipient ? this . api . account . getAccount ( { accountId : rewardRecipient } ) : null ; } public setCommitment ( { amountPlanck , feePlanck , pin , keys , isRevoking } ) { const senderPrivateKey = this . getPrivateKey ( keys , pin ) ; const args = { amountPlanck , senderPrivateKey , senderPublicKey : keys . publicKey , feePlanck , } ; return isRevoking ? this . api . account . removeCommitment ( args ) : this . api . account . addCommitment ( args ) ; } public createActiveAccount ( { passphrase , pin = '' } ) { return new Promise ( async ( resolve , reject ) => { const account = new Account ( ) ; account . type = 'active' ; account . confirmed = false ; const keys = generateMasterKeys ( passphrase ) ; const encryptedKey = encryptAES ( keys . signPrivateKey , hashSHA256 ( pin ) ) ; const encryptedSignKey = encryptAES ( keys . agreementPrivateKey , hashSHA256 ( pin ) ) ; account . keys = { publicKey : keys . publicKey , signPrivateKey : encryptedKey , agreementPrivateKey : encryptedSignKey } ; account . pinHash = hashSHA256 ( pin + keys . publicKey ) ; const address = Address . fromPublicKey ( keys . publicKey , this . accountPrefix ) ; account . account = address . getNumericId ( ) ; account . accountRS = address . getReedSolomonAddress ( ) ; await this . selectAccount ( account ) ; const savedAccount = await this . synchronizeAccount ( account ) ; resolve ( savedAccount ) ; } ) ; } public async createOfflineAccount ( reedSolomonAddress ) { const account = new Account ( ) ; const address = Address . fromReedSolomonAddress ( reedSolomonAddress ) ; const existingAccount = await this . storeService . findAccount ( address . getNumericId ( ) ) ; if ( existingAccount === undefined ) { account . type = 'offline' ; account . confirmed = false ; account . accountRS = reedSolomonAddress ; account . account = address . getNumericId ( ) ; await this . selectAccount ( account ) ; return this . synchronizeAccount ( account ) ; } else { throw new Error ( 'Address already imported!' ) ; } } public removeAccount ( account ) { return this . storeService . removeAccount ( account ) . catch ( error => error ) ; } public selectAccount ( account ) { return new Promise ( ( resolve , reject ) => { this . storeService . selectAccount ( account ) . then ( acc => { this . synchronizeAccount ( acc ) ; } ) ; this . updateCurrentAccount ( account ) ; resolve ( account ) ; } ) ; } public synchronizeAccount ( account ) { return new Promise ( async ( resolve , reject ) => { await this . syncAccountDetails ( account ) ; await this . syncAccountTransactions ( account ) ; await this . syncAccountUnconfirmedTransactions ( account ) ; if ( account . account === this . currentAccount$ . getValue ( ) . account ) { this . updateCurrentAccount ( account ) ; } this . storeService . saveAccount ( account ) . catch ( reject ) ; resolve ( account ) ; } ) ; } public isNewTransaction ( transactionId ) { return ( ! this . transactionsSeenInNotifications [ transactionId ] ) ; } public sendNewTransactionNotification ( transaction ) { if ( transaction . type !== TransactionType . Payment ) { return ; } this . transactionsSeenInNotifications [ transaction . transaction ] = true ; const incoming = transaction . recipient === this . currentAccount$ . value . account ; const amount = Amount . fromPlanck ( transaction . amountNQT ) ; const totalAmount = amount . clone ( ) . add ( Amount . fromPlanck ( transaction . feeNQT ) ) ; let header = '' ; let body = '' ; if ( incoming ) { header = this . i18nService . getTranslation ( 'youve_got_burst' ) ; body = this . i18nService . getTranslation ( 'youve_got_from' ) . replace ( '__a__' , transaction . recipientRS ) . replace ( '__b__' , amount . toString ( ) ) . replace ( '__c__' , transaction . senderRS ) ; } else { header = this . i18nService . getTranslation ( 'you_sent_burst' ) ; body = this . i18nService . getTranslation ( 'you_sent_to' ) . replace ( '__a__' , transaction . senderRS ) . replace ( '__b__' , totalAmount . toString ( ) ) . replace ( '__c__' , transaction . recipientRS ) ; } return window . Notification && new window . Notification ( header , { body , title : 'Phoenix' } ) ; } public async syncAccountUnconfirmedTransactions ( account ) { try { const orderByTimestamp = ( a , b ) => a . timestamp > b . timestamp ? - 1 : 1 ; const unconfirmedTransactionsResponse = await this . getUnconfirmedTransactions ( account . account ) ; const unconfirmed = unconfirmedTransactionsResponse . unconfirmedTransactions . sort ( orderByTimestamp ) ; account . transactions = uniqBy ( unconfirmed . concat ( account . transactions ) , 'transaction' ) ; if ( window . Notification ) { unconfirmed . filter ( ( { transaction } ) => this . isNewTransaction ( transaction ) ) . map ( ( transaction ) => this . sendNewTransactionNotification ( transaction ) ) ; } } catch ( e ) { console . log ( e ) ; } } public async syncAccountTransactions ( account ) { try { const { account : accountId , transactions } = account ; let transactionList = transactions . slice ( 0 , 500 ) ; if ( transactions . length > 0 ) { const timestamp = transactions [ 0 ] . timestamp . toString ( 10 ) ; const { transactions : recentTransactions } = await this . getAccountTransactions ( { accountId , timestamp } ) ; transactionList = recentTransactions . concat ( transactions ) ; } else { transactionList = ( await this . getAccountTransactions ( { accountId } ) ) . transactions ; } account . transactions = uniqBy ( transactionList , 'transaction' ) ; } catch ( e ) { account . transactions = [ ] ; } } private async syncAccountDetails ( account ) { try { const remoteAccount = await this . getAccount ( account . account ) ; account . name = remoteAccount . name ; account . description = remoteAccount . description ; account . assetBalances = remoteAccount . assetBalances ; account . unconfirmedAssetBalances = remoteAccount . unconfirmedAssetBalances ; account . committedBalanceNQT = remoteAccount . committedBalanceNQT ; account . balanceNQT = remoteAccount . balanceNQT ; account . unconfirmedBalanceNQT = remoteAccount . unconfirmedBalanceNQT ; account . accountRSExtended = remoteAccount . accountRSExtended ; account . confirmed = ! ! remoteAccount . publicKey ; } catch ( e ) { account . confirmed = false ; console . log ( e ) ; } } public async activateAccount ( account ) { try { if ( ! account . keys ) { console . warn ( 'Account does not have keys...ignored' ) ; return ; } const isMainNet = this . networkService . isMainNet ( ) ; const activatorUrl = isMainNet ? environment . activatorServiceUrl . mainNet : environment . activatorServiceUrl . testNet ; const http = HttpClientFactory . createHttpClient ( activatorUrl ) ; const payload = { account : account . account , publickey : account . keys . publicKey , ref : `phoenix-${ environment . version }` } ; await http . post ( '/api/activate' , payload ) ; } catch ( e ) { if ( e instanceof HttpError ) { const message = e . data && e . data . message ; throw new Error ( message || 'Unknown Error while requesting activation service' ) ; } throw e ; } } public async getForgedBlocks ( account ) { return this . api . account . getAccountBlocks ( { accountId : account . account } ) ; } }