diff --git a/front/src/app/app.component.html b/front/src/app/app.component.html index 2ec51955..bebadafc 100644 --- a/front/src/app/app.component.html +++ b/front/src/app/app.component.html @@ -1,7 +1,7 @@ -
+
Loading
diff --git a/front/src/app/app.component.spec.ts b/front/src/app/app.component.spec.ts index 3049b0e3..df74344b 100644 --- a/front/src/app/app.component.spec.ts +++ b/front/src/app/app.component.spec.ts @@ -34,10 +34,4 @@ describe('AppComponent', () => { it('should create the app', async(() => { expect(comp).toBeTruthy(); })); - /** - * DEFAULT VALUES - */ - it(`should have as isPageLoading 'false'`, async(() => { - expect(comp.isPageLoading).toEqual(false); - })); }); diff --git a/front/src/app/app.component.ts b/front/src/app/app.component.ts index 93a1cbef..e99d16d4 100644 --- a/front/src/app/app.component.ts +++ b/front/src/app/app.component.ts @@ -9,20 +9,7 @@ import { Subscription } from 'rxjs'; styleUrls: ['./app.component.scss'] }) @AutoUnsubscribe('_subsArr$') -export class AppComponent implements OnInit { - isPageLoading = false; - - private _subsArr$: Subscription[] = []; - - constructor(private _layoutService: LayoutService, private _cdr: ChangeDetectorRef) { - } - - ngOnInit() { - this._subsArr$.push( - this._layoutService.isPageLoading.subscribe((state: boolean) => { - this.isPageLoading = state; - this._cdr.detectChanges(); - }) - ); +export class AppComponent { + constructor(public layoutService: LayoutService) { } } diff --git a/front/src/app/components/interactor/interactor.component.html b/front/src/app/components/interactor/interactor.component.html index a742cf30..4890c9cf 100644 --- a/front/src/app/components/interactor/interactor.component.html +++ b/front/src/app/components/interactor/interactor.component.html @@ -26,7 +26,7 @@
Interact with a Smart rows="5" required > -
+
or select template:
- - -
-
+
+
+ +
+
+ +
+
+
- + +
+
+ +
+ It seems that you have metamask installed but GoChain is not configured, please use the following + guide to configure it. +
+
+ + To use wallet without private key, please install or enable + MetaMask + and + + configure it to work with Gochain + + +
diff --git a/front/src/app/scenes/wallet-main/wallet-main.component.ts b/front/src/app/scenes/wallet-main/wallet-main.component.ts index 1e6c371e..1dea2ab3 100644 --- a/front/src/app/scenes/wallet-main/wallet-main.component.ts +++ b/front/src/app/scenes/wallet-main/wallet-main.component.ts @@ -10,6 +10,8 @@ import {WalletService} from '../../services/wallet.service'; import {PasswordField} from '../../models/password-field.model'; /*UTILS*/ import {META_TITLES} from '../../utils/constants'; +import {LayoutService} from '../../services/layout.service'; +import {filter, flatMap} from 'rxjs/operators'; @Component({ selector: 'app-wallet-main', @@ -42,19 +44,42 @@ export class WalletMainComponent implements OnInit { private _fb: FormBuilder, private _toastrService: ToastrService, private _router: Router, + private _layoutService: LayoutService, ) { } ngOnInit() { + /*this._layoutService.onLoading();*/ this._metaService.setTitle(META_TITLES.WALLET.title); + /*this.walletService.metamaskConfigured$.pipe( + filter((v: boolean) => { + if (!v) { + this._layoutService.offLoading(); + } + return v; + }), + flatMap(() => this.walletService.openAccount()), + ).subscribe(() => { + this._layoutService.offLoading(); + this._router.navigate(['/wallet/account']); + }, (err) => { + this._toastrService.danger(err); + this._layoutService.offLoading(); + });*/ } - onPrivateKeySubmit() { - const privateKey: string = this.privateKeyForm.get('privateKey').value; - this.walletService.openAccount(privateKey).subscribe((ok: boolean) => { - if (ok) { - this._router.navigate(['/wallet/account']); + onSubmit(metamask: boolean = false) { + let privateKey: string = null; + if (!metamask) { + privateKey = this.privateKeyForm.get('privateKey').value; + if (!privateKey) { + this._toastrService.danger('Please enter private key'); + return; } - }); + } + this.walletService.openAccount(privateKey).subscribe( + () => this._router.navigate(['/wallet/account']), + (err) => this._toastrService.danger(err), + ); } } diff --git a/front/src/app/services/common.service.ts b/front/src/app/services/common.service.ts index 3d9d77ee..75850631 100644 --- a/front/src/app/services/common.service.ts +++ b/front/src/app/services/common.service.ts @@ -17,11 +17,11 @@ import {Stats} from '../models/stats.model'; import {Contract} from '../models/contract.model'; import {SignerData, SignerStat} from '../models/signer-stats'; import {SignerNode} from '../models/signer-node'; +import {AbiItem} from 'web3-utils'; /*UTILS*/ import {ContractAbi, ContractEventsAbi, ContractAbiByID, AbiItemIDed} from '../utils/types'; import {FunctionName} from '../utils/enums'; import {objIsEmpty} from '../utils/functions'; -import {AbiItem} from 'web3-utils'; @Injectable() export class CommonService implements Resolve { @@ -36,7 +36,7 @@ export class CommonService implements Resolve { }); } return this._rpcProvider$.pipe( - filter(v => !!v), + filter(v => !!v), take(1), ); } @@ -47,7 +47,7 @@ export class CommonService implements Resolve { this.initAbi(); } return this._abi$.pipe( - filter(v => v !== null), + filter(v => v !== null), take(1), ); } @@ -58,7 +58,7 @@ export class CommonService implements Resolve { this.initAbi(); } return this._abiByID$.pipe( - filter(v => v !== null), + filter(v => v !== null), take(1), ); } @@ -72,7 +72,7 @@ export class CommonService implements Resolve { }); } return this._eventsAbi$.pipe( - filter(v => v !== null), + filter(v => v !== null), take(1), ); } diff --git a/front/src/app/services/layout.service.ts b/front/src/app/services/layout.service.ts index a18610a3..f5ac7ded 100644 --- a/front/src/app/services/layout.service.ts +++ b/front/src/app/services/layout.service.ts @@ -11,7 +11,7 @@ import {ThemeColor} from '../utils/enums'; providedIn: 'root', }) export class LayoutService { - isPageLoading: BehaviorSubject = new BehaviorSubject(false); + isPageLoading$: BehaviorSubject = new BehaviorSubject(false); themeColor$: BehaviorSubject = new BehaviorSubject(ThemeColor.LIGHT); themeSettings: ThemeSettings; mobileMenuState: BehaviorSubject = new BehaviorSubject(false); @@ -30,14 +30,14 @@ export class LayoutService { } toggleLoading() { - this.isPageLoading.next(!this.isPageLoading.value); + this.isPageLoading$.next(!this.isPageLoading$.value); } onLoading() { - this.isPageLoading.next(true); + this.isPageLoading$.next(true); } offLoading() { - this.isPageLoading.next(false); + this.isPageLoading$.next(false); } } diff --git a/front/src/app/services/wallet.service.ts b/front/src/app/services/wallet.service.ts index 1bd61c9f..8507c43f 100644 --- a/front/src/app/services/wallet.service.ts +++ b/front/src/app/services/wallet.service.ts @@ -1,14 +1,15 @@ /*CORE*/ import {Injectable} from '@angular/core'; import {Router} from '@angular/router'; -import {BehaviorSubject, forkJoin, Observable, of} from 'rxjs'; -import {concatMap, filter, map, take, finalize, catchError, mergeMap} from 'rxjs/operators'; +import {BehaviorSubject, forkJoin, Observable, of, throwError} from 'rxjs'; +import {concatMap, filter, map, mergeMap, take, tap} from 'rxjs/operators'; import {fromPromise} from 'rxjs/internal-compatibility'; /*WEB3*/ import Web3 from 'web3'; import {SignedTransaction, Transaction as Web3Tx, TransactionConfig, TransactionReceipt} from 'web3-core'; import {Account} from 'web3-eth-accounts'; -import {AbiItem, fromWei, toWei, isAddress} from 'web3-utils'; +import {Contract as Web3Contract} from 'web3-eth-contract'; +import {AbiItem, fromWei, isAddress, toWei} from 'web3-utils'; /*SERVICES*/ import {ToastrService} from '../modules/toastr/toastr.service'; import {CommonService} from './common.service'; @@ -17,77 +18,184 @@ import {Transaction} from '../models/transaction.model'; /*UTILS*/ import {objIsEmpty} from '../utils/functions'; +interface IWallet { + w3: Web3; + + send(tx: TransactionConfig): Observable; + + call(tx: TransactionConfig): Observable; + + logIn(privateKey: string): Observable; + + logOut(): void; +} + +abstract class Wallet { + w3: Web3; + + constructor(w3: Web3) { + this.w3 = w3; + } + + call(tx: TransactionConfig): Observable { + return fromPromise(this.w3.eth.call(tx)); + } + + logOut(): void { + + } +} + +class MetamaskStrategy extends Wallet implements IWallet { + + send(tx: TransactionConfig): Observable { + return fromPromise(this.w3.eth.sendTransaction(tx)); + } + + logIn(): Observable { + return fromPromise((window as any).ethereum.enable()).pipe( + map((accounts: string[]) => { + return accounts[0]; + }), + ); + } +} + +class PrivateKeyStrategy extends Wallet implements IWallet { + private account: Account; + + send(tx: TransactionConfig): Observable { + return fromPromise(this.w3.eth.accounts.signTransaction(tx, this.account.privateKey)).pipe( + mergeMap((signedTx: SignedTransaction) => fromPromise(this.w3.eth.sendSignedTransaction(signedTx.rawTransaction))), + ); + } + + logIn(privateKey: string): Observable { + if (privateKey.length === 64 && privateKey.indexOf('0x') !== 0) { + privateKey = '0x' + privateKey; + } + if (privateKey.length === 66) { + let account: Account; + try { + account = this.w3.eth.accounts.privateKeyToAccount(privateKey); + } catch (e) { + return throwError(e); + } + this.account = account; + return of(this.account.address); + } + return throwError('Given private key is not valid'); + } + + logOut(): void { + this.account = null; + } +} + @Injectable() export class WalletService { isProcessing = false; // ACCOUNT INFO - account: Account; + accountAddress: string; accountBalance: string; receipt: TransactionReceipt; - private _web3Callable$: BehaviorSubject = new BehaviorSubject(null); - private _web3Payable$: BehaviorSubject = new BehaviorSubject(null); + private _metamaskIntalled$: BehaviorSubject = new BehaviorSubject(false); + + get metamaskIntalled$(): Observable { + return this.ready$.pipe( + mergeMap(() => this._metamaskIntalled$), + filter(v => v !== null), + ); + } + + get metamaskConfigured$(): Observable { + return this.ready$.pipe( + mergeMap(() => this._metamaskConfigured$), + filter(v => v !== null), + ); + } + + private _metamaskConfigured$: BehaviorSubject = new BehaviorSubject(null); - get w3Call(): Observable { - return this._web3Callable$.pipe( - filter(v => !!v), - take(1), + private _logged$: BehaviorSubject = new BehaviorSubject(false); + + get logged$(): Observable { + return this.ready$.pipe( + mergeMap(() => this._logged$), ); } - get w3Pay(): Observable { - return this._web3Payable$.pipe( - filter(v => !!v), - take(1), +// used for paid + private _walletContext: IWallet; + + // used for interaction with chain, only free methods + private _w3: Web3; + private _metamaskw3: Web3; + + get w3$(): Observable { + return this.ready$.pipe( + map(() => this._w3), + ); + } + + private _ready$: BehaviorSubject = new BehaviorSubject(false); + get ready$(): Observable { + return this._ready$.pipe( + filter(v => !!v), + take(1), ); } constructor( private _toastrService: ToastrService, private _commonService: CommonService, - private _router: Router, ) { - this._commonService.rpcProvider$ - .pipe( - filter(value => !!value), - ) - .subscribe((rpcProvider: string) => { - const metaMaskProvider = new Web3(Web3.givenProvider, null, {transactionConfirmationBlocks: 1,}); - const web3Provider = new Web3(new Web3.providers.HttpProvider(rpcProvider), null, {transactionConfirmationBlocks: 1,}); - this._web3Callable$.next(web3Provider); - if (!metaMaskProvider.currentProvider) { - this._web3Payable$.error('Metamask is not enabled'); + this._commonService.rpcProvider$.subscribe((rpcProvider: string) => { + this.initProvider(rpcProvider); + }); + } + + initProvider(rpcProvider: string): void { + const metaMaskProvider = new Web3(Web3.givenProvider, null, {transactionConfirmationBlocks: 1}); + const web3Provider = new Web3(new Web3.providers.HttpProvider(rpcProvider), null, {transactionConfirmationBlocks: 1}); + this._w3 = web3Provider; + if (!metaMaskProvider.currentProvider) { + this._metamaskConfigured$.next(false); + this._ready$.next(true); + return; + } + web3Provider.eth.net.getId((web3err, web3NetID) => { + if (web3err) { + this._toastrService.danger('Metamask is enabled but can\'t get Gochain network id'); + this._metamaskIntalled$.next(true); + this._metamaskConfigured$.next(false); + this._ready$.next(true); + return; + } + metaMaskProvider.eth.net.getId((metamaskErr, metamask3NetID) => { + if (metamaskErr) { + this._toastrService.danger('Metamask is enabled but can\'t get network id from Metamask'); + this._metamaskIntalled$.next(true); + this._metamaskConfigured$.next(false); + this._ready$.next(true); return; } - web3Provider.eth.net.getId((err, web3NetID) => { - if (err) { - this._toastrService.danger('Metamask is enabled but can\'t get network id'); - this._web3Payable$.error(`Failed to get network id: ${err}`); - return; - } - metaMaskProvider.eth.net.getId((err, metamask3NetID) => { - if (err) { - this._toastrService.danger('Metamask is enabled but can\'t get network id from Metamask'); - this._web3Payable$.error(`Failed to Metamask network id: ${err}`); - return; - } - if (web3NetID !== metamask3NetID) { - this._toastrService.danger('Metamask is enabled but networks are different'); - this._web3Payable$.error(`Metamask network ID (${metamask3NetID}) doesn't match expected (${web3NetID})`); - return; - } - this._web3Payable$.next(web3Provider); - }); - }); + if (web3NetID !== metamask3NetID) { + this._toastrService.warning('Metamask is enabled but networks are different'); + this._metamaskIntalled$.next(true); + this._metamaskConfigured$.next(false); + this._ready$.next(true); + return; + } + this._metamaskw3 = metaMaskProvider; + this._metamaskIntalled$.next(true); + this._metamaskConfigured$.next(true); + this._ready$.next(true); }); - } - - sendSignedTx(signed: SignedTransaction): Observable { - return this.w3Call.pipe(concatMap((web3: Web3) => { - return fromPromise(web3.eth.sendSignedTransaction(signed.rawTransaction)); - })) + }); } /** @@ -96,21 +204,22 @@ export class WalletService { * @param abi * @param params */ - call(addr: string, abi: AbiItem, params: any[]): Observable | null { - let web3; - return (abi.constant ? this.w3Call : this.w3Pay).pipe( - mergeMap((_web3: Web3) => { - web3 = _web3; - const encoded: string = web3.eth.abi.encodeFunctionCall(abi, params); - return fromPromise(web3.eth.call({ + call(addr: string, abi: AbiItem, params: any[]): Observable { + return this.ready$.pipe( + mergeMap(() => { + const encoded: string = this._w3.eth.abi.encodeFunctionCall(abi, params); + const tx: TransactionConfig = { + from: this.accountAddress, to: addr, data: encoded, - })) - }),map((res: string) => { - if (!res || res==='0x' || res==='0X') { + }; + return abi.constant ? this._w3.eth.call(tx) : this._walletContext.call(tx); + }), + map((res: string) => { + if (!res || res === '0x' || res === '0X') { return null; } - const decoded: object = web3.eth.abi.decodeParameters(abi.outputs, res); + const decoded: object = this._w3.eth.abi.decodeParameters(abi.outputs, res); if (objIsEmpty(decoded)) { return null; } @@ -124,11 +233,12 @@ export class WalletService { * @param txHash */ getTxData(txHash: string): Observable { - return this.w3Call.pipe(concatMap((web3: Web3) => { - return forkJoin([ - fromPromise(web3.eth.getTransaction(txHash)), - fromPromise(web3.eth.getTransactionReceipt(txHash)), - ]).pipe( + return this.ready$.pipe( + concatMap(() => { + return forkJoin([ + fromPromise(this._w3.eth.getTransaction(txHash)), + fromPromise(this._w3.eth.getTransactionReceipt(txHash)), + ]).pipe( map((res: [Web3Tx, TransactionReceipt]) => { if (!res[0]) { return null; @@ -148,22 +258,24 @@ export class WalletService { finalTx.block_number = tx.blockNumber; finalTx.gas_fee = '' + (+tx.gasPrice * txReceipt.gasUsed); finalTx.contract_address = - (txReceipt.contractAddress && txReceipt.contractAddress !== '0x0000000000000000000000000000000000000000') - ? txReceipt.contractAddress - : null; + (txReceipt.contractAddress && txReceipt.contractAddress !== '0x0000000000000000000000000000000000000000') + ? txReceipt.contractAddress + : null; finalTx.status = txReceipt.status; finalTx.created_at = new Date(); } return finalTx; }), - ); - })); + ); + })); } estimateGas(tx: TransactionConfig): Observable { - return this.w3Call.pipe(concatMap((web3: Web3) => { - return fromPromise(web3.eth.estimateGas(tx)); - })); + return this.ready$.pipe( + concatMap(() => { + return fromPromise(this._w3.eth.estimateGas(tx)); + }), + ); } // WALLET METHODS @@ -176,11 +288,12 @@ export class WalletService { */ sendGo(to: string, value: string, gas: string): void { if (this.isProcessing) { + this._toastrService.warning('another process in action'); return; } if (to.length !== 42 || !isAddress(to)) { - this._toastrService.danger('ERROR: Invalid TO address.'); + this._toastrService.danger('Invalid TO address.'); return; } @@ -192,6 +305,7 @@ export class WalletService { } const tx: TransactionConfig = { + from: this.accountAddress, to, value, gas @@ -215,6 +329,7 @@ export class WalletService { } const tx: TransactionConfig = { + from: this.accountAddress, data: byteCode, gas }; @@ -228,24 +343,14 @@ export class WalletService { */ sendTx(tx: TransactionConfig): void { this.isProcessing = true; - this.w3Call.subscribe((web3: Web3) => { - const p: Promise = web3.eth.getTransactionCount(this.account.address); - fromPromise(p).pipe( - concatMap(nonce => { - tx.nonce = nonce; - const p2: Promise = web3.eth.accounts.signTransaction(tx, this.account.privateKey); - return fromPromise(p2); - }), - concatMap((signed: SignedTransaction) => { - return this.sendSignedTx(signed); - }) - ).subscribe((receipt: TransactionReceipt) => { - this.receipt = receipt; - this.getBalance(); - }, err => { - this._toastrService.danger(err); - this.resetProcessing(); - }); + this.ready$.pipe( + mergeMap(() => this._walletContext.send(tx)), + ).subscribe((receipt: TransactionReceipt) => { + this.receipt = receipt; + this.getBalance(); + }, (err) => { + this._toastrService.danger(err); + this.resetProcessing(); }); } @@ -257,45 +362,57 @@ export class WalletService { // ACCOUNT METHODS createAccount(): Observable { - return this.w3Call.pipe(map((web3: Web3) => { - return web3.eth.accounts.create(); - })); + return this.ready$.pipe( + map(() => this._w3.eth.accounts.create()), + ); } - openAccount(privateKey: string): Observable { + openAccount(privateKey: string = null): Observable { this.isProcessing = true; - if (privateKey.length === 64 && privateKey.indexOf('0x') !== 0) { - privateKey = '0x' + privateKey; - } - if (privateKey.length === 66) { - return this.w3Call.pipe(map((web3: Web3) => { - this.account = web3.eth.accounts.privateKeyToAccount(privateKey); + return this.ready$.pipe( + mergeMap(() => { + this._walletContext = privateKey === null ? new MetamaskStrategy(this._metamaskw3) : new PrivateKeyStrategy(this._w3); + return this._walletContext.logIn(privateKey); + }), + tap((accountAddress: string) => { + this.accountAddress = accountAddress; + this._logged$.next(true); + this.isProcessing = false; this.getBalance(); - return true; - }), catchError(err => { - this._toastrService.danger(err); - return of(false); - }), finalize(()=> this.isProcessing = false )); - } - this.isProcessing = false; - this._toastrService.danger('Given private key is not valid'); - return of(false); + }, (err) => { + this.isProcessing = false; + }), + ); } closeAccount(): void { - this.account = null; this.accountBalance = null; - this._router.navigate(['wallet']); + this.accountAddress = null; + this._logged$.next(false); + this._walletContext.logOut(); } getBalance() { - this.w3Call.pipe(concatMap((web3: Web3) => { - return fromPromise(web3.eth.getBalance(this.account.address)); - })).subscribe((balance: string) => { - this._toastrService.info('Updated balance.'); - this.accountBalance = fromWei(balance, 'ether').toString(); - }, err => { - this._toastrService.danger(err); + this.ready$.pipe( + concatMap(() => fromPromise(this._w3.eth.getBalance(this.accountAddress))), + ).subscribe((balance: string) => { + this._toastrService.info('Updated balance.'); + this.accountBalance = fromWei(balance, 'ether').toString(); + }, err => { + this._toastrService.danger(err); }); } + + + initContract(addrHash: string, abiItems: AbiItem[]): Observable { + return this.ready$.pipe( + map(() => new this._w3.eth.Contract(abiItems, addrHash)) + ); + } + + getBlockNumber(): Observable { + return this.ready$.pipe( + mergeMap(() => this._w3.eth.getBlockNumber()), + ); + } }