From 1e1e910d38e20d5f183c0fafa8171ae4c057589c Mon Sep 17 00:00:00 2001 From: keeri Date: Mon, 13 Dec 2021 14:59:46 +0000 Subject: [PATCH 1/3] automatically refresh tx list upon receive or new receivable tx fix transactions being able to appear simultaneously as "receivable" and "received" due to async nature of requests --- .../account-details.component.html | 6 +- .../account-details.component.ts | 211 +++++++++++++++++- .../components/receive/receive.component.ts | 6 +- src/app/services/wallet.service.ts | 29 ++- 4 files changed, 233 insertions(+), 19 deletions(-) diff --git a/src/app/components/account-details/account-details.component.html b/src/app/components/account-details/account-details.component.html index 3bb1028..ec305c8 100644 --- a/src/app/components/account-details/account-details.component.html +++ b/src/app/components/account-details/account-details.component.html @@ -258,12 +258,12 @@ diff --git a/src/app/components/account-details/account-details.component.ts b/src/app/components/account-details/account-details.component.ts index 773be38..93db825 100644 --- a/src/app/components/account-details/account-details.component.ts +++ b/src/app/components/account-details/account-details.component.ts @@ -42,7 +42,9 @@ export class AccountDetailsComponent implements OnInit, OnDestroy { walletAccount = null; - timeoutIdAllowingRefresh: any = null; + timeoutIdAllowingManualRefresh: any = null; + timeoutIdAllowingInstantAutoRefresh: any = null; + timeoutIdQueuedAutoRefresh: any = null; qrModal: any = null; mobileAccountMenuModal: any = null; mobileTransactionMenuModal: any = null; @@ -66,7 +68,11 @@ export class AccountDetailsComponent implements OnInit, OnDestroy { routerSub = null; priceSub = null; - statsRefreshEnabled = true; + initialLoadDone = false; + manualRefreshAllowed = true; + instantAutoRefreshAllowed = true; + shouldQueueAutoRefresh = false; + autoRefreshReasonBlockUpdate = null; dateStringToday = ''; dateStringYesterday = ''; @@ -148,6 +154,10 @@ export class AccountDetailsComponent implements OnInit, OnDestroy { this.account.pendingFiat = this.util.nano.rawToMnano(this.account.pending || 0).times(this.price.price.lastPrice).toNumber(); }); + this.wallet.wallet.pendingBlocksUpdate$.subscribe(async receivableBlockUpdate => { + this.onReceivableBlockUpdate(receivableBlockUpdate); + }); + const UIkit = window['UIkit']; const qrModal = UIkit.modal('#qr-code-modal'); this.qrModal = qrModal; @@ -159,6 +169,7 @@ export class AccountDetailsComponent implements OnInit, OnDestroy { this.mobileTransactionMenuModal = mobileTransactionMenuModal; await this.loadAccountDetails(); + this.initialLoadDone = true; this.addressBook.loadAddressBook(); this.populateRepresentativeList(); @@ -262,14 +273,180 @@ export class AccountDetailsComponent implements OnInit, OnDestroy { this.repLabel = null; } - async loadAccountDetails(refresh= false) { - if (refresh && !this.statsRefreshEnabled) return; - this.statsRefreshEnabled = false; + onRefreshButtonClick() { + if (!this.manualRefreshAllowed) return; - if (this.timeoutIdAllowingRefresh != null) { - clearTimeout(this.timeoutIdAllowingRefresh); + this.loadAccountDetails(); + } + + isReceivableBlockUpdateRelevant(receivableBlockUpdate) { + let isRelevant = true; + + if (receivableBlockUpdate.account !== this.accountID) { + isRelevant = false; + return isRelevant; } - this.timeoutIdAllowingRefresh = setTimeout(() => this.statsRefreshEnabled = true, 5000); + + const sourceHashToFind = receivableBlockUpdate.sourceHash; + + const alreadyInReceivableBlocks = + this.pendingBlocks.some( + (knownReceivableBlock) => + (knownReceivableBlock.hash === sourceHashToFind) + ); + + if (receivableBlockUpdate.hasBeenReceived === true) { + const destinationHashToFind = receivableBlockUpdate.destinationHash; + + const alreadyInAccountHistory = + this.accountHistory.some( + (knownAccountHistoryBlock) => + (knownAccountHistoryBlock.hash === destinationHashToFind) + ); + + if ( + (alreadyInAccountHistory === true) + && (alreadyInReceivableBlocks === false) + ) { + isRelevant = false; + return isRelevant; + } + } else { + if (alreadyInReceivableBlocks === true) { + isRelevant = false; + return isRelevant; + } + } + + return isRelevant; + } + + onReceivableBlockUpdate(receivableBlockUpdate) { + if (receivableBlockUpdate === null) { + return; + } + + const isRelevantUpdate = + this.isReceivableBlockUpdateRelevant(receivableBlockUpdate); + + if (isRelevantUpdate === false) { + return; + } + + this.loadAccountDetailsThrottled({ receivableBlockUpdate }); + } + + loadAccountDetailsThrottled(params) { + this.autoRefreshReasonBlockUpdate = ( + (params.receivableBlockUpdate != null) + ? params.receivableBlockUpdate + : null + ); + + if (this.initialLoadDone === false) { + return; + } + + if (this.instantAutoRefreshAllowed === true) { + this.loadAccountDetails(); + return; + } + + if (this.loadingAccountDetails === true) { + // Queue refresh once the loading is done + this.shouldQueueAutoRefresh = true; + } else { + // Queue refresh now + this.loadAccountDetailsDelayed(3000); + } + } + + enableManualRefreshDelayed(delayMS) { + if (this.timeoutIdAllowingManualRefresh != null) { + clearTimeout(this.timeoutIdAllowingManualRefresh); + } + + this.timeoutIdAllowingManualRefresh = + setTimeout( + () => { + this.manualRefreshAllowed = true; + }, + delayMS + ); + } + + enableInstantAutoRefreshDelayed(delayMS) { + if (this.timeoutIdAllowingInstantAutoRefresh != null) { + clearTimeout(this.timeoutIdAllowingInstantAutoRefresh); + } + + this.timeoutIdAllowingInstantAutoRefresh = + setTimeout( + () => { + this.instantAutoRefreshAllowed = true; + }, + delayMS + ); + } + + loadAccountDetailsDelayed(delayMS) { + if (this.timeoutIdQueuedAutoRefresh != null) { + clearTimeout(this.timeoutIdQueuedAutoRefresh); + } + + if(this.autoRefreshReasonBlockUpdate !== null) { + const isUpdateStillRelevant = + this.isReceivableBlockUpdateRelevant(this.autoRefreshReasonBlockUpdate); + + if (isUpdateStillRelevant === false) { + this.enableRefreshesEventually(); + return; + } + } + + this.timeoutIdQueuedAutoRefresh = + setTimeout( + () => { + this.loadAccountDetails(); + }, + delayMS + ); + } + + onAccountDetailsLoadStart() { + this.instantAutoRefreshAllowed = false; + this.manualRefreshAllowed = false; + + if (this.timeoutIdAllowingManualRefresh != null) { + clearTimeout(this.timeoutIdAllowingManualRefresh); + } + + if (this.timeoutIdAllowingInstantAutoRefresh != null) { + clearTimeout(this.timeoutIdAllowingInstantAutoRefresh); + } + + if (this.timeoutIdQueuedAutoRefresh != null) { + clearTimeout(this.timeoutIdQueuedAutoRefresh); + } + } + + enableRefreshesEventually() { + this.enableInstantAutoRefreshDelayed(3000); + this.enableManualRefreshDelayed(5000); + } + + onAccountDetailsLoadDone() { + if (this.shouldQueueAutoRefresh === true) { + this.shouldQueueAutoRefresh = false; + this.loadAccountDetailsDelayed(3000); + return; + } + + this.enableRefreshesEventually(); + } + + async loadAccountDetails() { + this.onAccountDetailsLoadStart(); this.pendingBlocks = []; @@ -288,11 +465,13 @@ export class AccountDetailsComponent implements OnInit, OnDestroy { if (accountID !== this.accountID) { // Navigated to a different account while account info was loading + this.onAccountDetailsLoadDone(); return; } if (!this.account) { this.loadingAccountDetails = false; + this.onAccountDetailsLoadDone(); return; } @@ -316,6 +495,7 @@ export class AccountDetailsComponent implements OnInit, OnDestroy { if (accountID !== this.accountID) { // Navigated to a different account while incoming tx were loading + this.onAccountDetailsLoadDone(); return; } @@ -380,10 +560,12 @@ export class AccountDetailsComponent implements OnInit, OnDestroy { if (accountID !== this.accountID) { // Navigated to a different account while account history was loading + this.onAccountDetailsLoadDone(); return; } this.loadingAccountDetails = false; + this.onAccountDetailsLoadDone(); } getAccountLabel(accountID, defaultLabel) { @@ -458,9 +640,18 @@ export class AccountDetailsComponent implements OnInit, OnDestroy { ); if (h.type === 'state') { - // For Open and receive blocks, we need to look up block info to get originating account if (h.subtype === 'open' || h.subtype === 'receive') { + // Look up block info to get sender account additionalBlocksInfo.push({ hash: h.hash, link: h.link }); + + // Remove a receivable block if this is a receive for it + const sourceHashToFind = h.link; + + this.pendingBlocks = + this.pendingBlocks.filter( + (knownReceivableBlock) => + (knownReceivableBlock.hash !== sourceHashToFind) + ); } else if (h.subtype === 'change') { h.link_as_account = h.representative; h.addressBookName = ( @@ -790,6 +981,8 @@ export class AccountDetailsComponent implements OnInit, OnDestroy { receivableBlock.loading = false; await this.wallet.reloadBalances(); + + this.loadAccountDetailsThrottled({}); } async generateSend() { diff --git a/src/app/components/receive/receive.component.ts b/src/app/components/receive/receive.component.ts index fb86b76..dcdc683 100644 --- a/src/app/components/receive/receive.component.ts +++ b/src/app/components/receive/receive.component.ts @@ -85,7 +85,11 @@ export class ReceiveComponent implements OnInit, OnDestroy { this.selAccountInit = true; }); - this.walletService.wallet.pendingBlocksUpdate$.subscribe(async acc => { + this.walletService.wallet.pendingBlocksUpdate$.subscribe(async receivableBlockUpdate => { + if (receivableBlockUpdate === null) { + return; + } + this.updatePendingBlocks(); }); diff --git a/src/app/services/wallet.service.ts b/src/app/services/wallet.service.ts index 3626982..7909b4d 100644 --- a/src/app/services/wallet.service.ts +++ b/src/app/services/wallet.service.ts @@ -38,6 +38,13 @@ export interface Block { source: string; } +export interface ReceivableBlockUpdate { + account: string; + sourceHash: string; + destinationHash: string|null; + hasBeenReceived: boolean; +} + export interface FullWallet { type: WalletType; seedBytes: any; @@ -58,7 +65,7 @@ export interface FullWallet { locked: boolean; password: string; pendingBlocks: Block[]; - pendingBlocksUpdate$: BehaviorSubject; + pendingBlocksUpdate$: BehaviorSubject; newWallet$: BehaviorSubject; refresh$: BehaviorSubject; } @@ -106,7 +113,7 @@ export class WalletService { locked: false, password: '', pendingBlocks: [], - pendingBlocksUpdate$: new BehaviorSubject(false), + pendingBlocksUpdate$: new BehaviorSubject(null), newWallet$: new BehaviorSubject(false), refresh$: new BehaviorSubject(false), }; @@ -953,8 +960,13 @@ export class WalletService { if (existingHash) return false; // Already added this.wallet.pendingBlocks.push({ account: accountID, hash: blockHash, amount: amount, source: source }); - this.wallet.pendingBlocksUpdate$.next(true); - this.wallet.pendingBlocksUpdate$.next(false); + this.wallet.pendingBlocksUpdate$.next({ + account: accountID, + sourceHash: blockHash, + destinationHash: null, + hasBeenReceived: false, + }); + this.wallet.pendingBlocksUpdate$.next(null); return true; } @@ -1008,8 +1020,13 @@ export class WalletService { // list also updated with reloadBalances but not if called too fast this.removePendingBlock(nextBlock.hash); await this.reloadBalances(); - this.wallet.pendingBlocksUpdate$.next(true); - this.wallet.pendingBlocksUpdate$.next(false); + this.wallet.pendingBlocksUpdate$.next({ + account: nextBlock.account, + sourceHash: nextBlock.hash, + destinationHash: newHash, + hasBeenReceived: true, + }); + this.wallet.pendingBlocksUpdate$.next(null); } else { if (this.isLedgerWallet()) { this.processingPending = false; From 982aede1380df935bfda8639d8da8619f2dc65ed Mon Sep 17 00:00:00 2001 From: keeri Date: Mon, 13 Dec 2021 15:01:36 +0000 Subject: [PATCH 2/3] fix lint --- src/app/components/account-details/account-details.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/account-details/account-details.component.ts b/src/app/components/account-details/account-details.component.ts index 93db825..dbf9c20 100644 --- a/src/app/components/account-details/account-details.component.ts +++ b/src/app/components/account-details/account-details.component.ts @@ -394,7 +394,7 @@ export class AccountDetailsComponent implements OnInit, OnDestroy { clearTimeout(this.timeoutIdQueuedAutoRefresh); } - if(this.autoRefreshReasonBlockUpdate !== null) { + if (this.autoRefreshReasonBlockUpdate !== null) { const isUpdateStillRelevant = this.isReceivableBlockUpdateRelevant(this.autoRefreshReasonBlockUpdate); From ecd67bc6279f951552bdc388c0d11a7a19cb1532 Mon Sep 17 00:00:00 2001 From: keeri Date: Mon, 13 Dec 2021 15:07:19 +0000 Subject: [PATCH 3/3] fix condition being checked outside timeout callback --- .../account-details.component.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/app/components/account-details/account-details.component.ts b/src/app/components/account-details/account-details.component.ts index dbf9c20..92903ca 100644 --- a/src/app/components/account-details/account-details.component.ts +++ b/src/app/components/account-details/account-details.component.ts @@ -394,19 +394,19 @@ export class AccountDetailsComponent implements OnInit, OnDestroy { clearTimeout(this.timeoutIdQueuedAutoRefresh); } - if (this.autoRefreshReasonBlockUpdate !== null) { - const isUpdateStillRelevant = - this.isReceivableBlockUpdateRelevant(this.autoRefreshReasonBlockUpdate); - - if (isUpdateStillRelevant === false) { - this.enableRefreshesEventually(); - return; - } - } - this.timeoutIdQueuedAutoRefresh = setTimeout( () => { + if (this.autoRefreshReasonBlockUpdate !== null) { + const isUpdateStillRelevant = + this.isReceivableBlockUpdateRelevant(this.autoRefreshReasonBlockUpdate); + + if (isUpdateStillRelevant === false) { + this.enableRefreshesEventually(); + return; + } + } + this.loadAccountDetails(); }, delayMS