Skip to content

Commit

Permalink
feat(wallet_sync): cache hash calls to the explorer
Browse files Browse the repository at this point in the history
During the synchronization process, the wallet was calling multiple
times to the hash endpoint of the Witnet Block Explorer. It occurred
when the user had multiple accounts and had sent funds between them.
During the wallet synchronization, if two accounts of the same wallet
had sent funds between them, both would call the explorer with the
same hash of a valueTransferOutput. Because of that, the number of
repeated calls relied on the number of times accounts of the same
wallet have interacted between them.

To avoid multiple calls, we have implemented an in-memory cache using
a "read through" pattern to fetch the Witnet Block Explorer API only if
it wasn't called before.
  • Loading branch information
Tommytrg committed Aug 25, 2023
1 parent 0e28359 commit f3faadf
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 54 deletions.
25 changes: 19 additions & 6 deletions lib/bloc/crypto/crypto_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import 'package:my_wit_wallet/bloc/crypto/api_crypto.dart';
import 'package:my_wit_wallet/bloc/explorer/api_explorer.dart';
import 'package:my_wit_wallet/shared/api_database.dart';
import 'package:my_wit_wallet/shared/locator.dart';
import 'package:my_wit_wallet/util/storage/cache/read_through.dart';
import 'package:my_wit_wallet/util/storage/database/account.dart';
import 'package:my_wit_wallet/util/storage/database/balance_info.dart';
import 'package:my_wit_wallet/util/storage/database/encrypt/password.dart';
Expand All @@ -40,7 +41,18 @@ class CryptoBloc extends Bloc<CryptoEvent, CryptoState> {
ApiExplorer apiExplorer = Locator.instance.get<ApiExplorer>();
ApiDatabase db = Locator.instance.get<ApiDatabase>();

// TODO: remove late because is always being defined in the constructor
// Map<hash, valueTransferInfo>
late ReadThrough<ValueTransferInfo> _vttGetThroughBlockExplorer;

CryptoBloc(initialState) : super(initialState) {
_vttGetThroughBlockExplorer = ReadThrough(
(String hash) async => await apiExplorer.hash(hash),
(ValueTransferInfo valueTransferInfo) async =>
await db.addVtt(valueTransferInfo),
(String hash) async => await db.getVtt(hash),
);

on<CryptoInitializeWalletEvent>(_cryptoInitializeWalletEvent);
on<CryptoInitWalletDoneEvent>(_cryptoInitWalletDoneEvent);
on<CryptoReadyEvent>(_cryptoReadyEvent);
Expand All @@ -49,7 +61,6 @@ class CryptoBloc extends Bloc<CryptoEvent, CryptoState> {

Future<void> _cryptoInitializeWalletEvent(
CryptoInitializeWalletEvent event, Emitter<CryptoState> emit) async {
print("----------2-----------");
/// setup default default structure for database and unlock it
emit(
CryptoInitializingWalletState(
Expand Down Expand Up @@ -237,7 +248,6 @@ class CryptoBloc extends Bloc<CryptoEvent, CryptoState> {
ApiDatabase db = Locator.instance<ApiDatabase>();
String key = await db.getKeychain();
final masterKey = key != '' ? key : event.password;
print("----------1-----------");
apiCrypto.setInitialWalletData(
event.walletName,
event.keyData,
Expand Down Expand Up @@ -292,10 +302,11 @@ class CryptoBloc extends Bloc<CryptoEvent, CryptoState> {
try {
for (int i = 0; i < account.vttHashes.length; i++) {
String _hash = account.vttHashes.elementAt(i);
var result = await apiExplorer.hash(_hash);
ValueTransferInfo valueTransferInfo = result as ValueTransferInfo;
account.vtts.add(valueTransferInfo);
await db.addVtt(valueTransferInfo);
ValueTransferInfo? valueTransferInfo =
await _vttGetThroughBlockExplorer.get(_hash);
if (valueTransferInfo != null) {
account.vtts.add(valueTransferInfo);
}
}
return account;
} catch (e) {
Expand Down Expand Up @@ -334,7 +345,9 @@ class CryptoBloc extends Bloc<CryptoEvent, CryptoState> {
}

Future<Account> _syncAccount(Account account) async {
// In this function vttHashes are fullfilled
try {
// Is not ignoring repeated value transfers
final addressValueTransfers = await apiExplorer.address(
value: account.address,
tab: 'value_transfers') as AddressValueTransfers;
Expand Down
32 changes: 32 additions & 0 deletions lib/util/storage/cache/read_through.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Generic implementation of read throught cache pattern
class ReadThrough<T> {
// Consume the value if it's not already stored
Future<T?> Function(String) _fetch;
// Save the value in cache
Future<bool> Function(T) _save;
// Read Value from Cache
Future<T?> Function(String) _read;

ReadThrough(this._fetch, this._save, this._read);

Future<bool> _insert(T value) async {
return await _save(value);
}

// Check if value is stored and call the fetch function if it's not found
Future<T?> get(String key) async {
T? storedValue = await _read(key);

if (storedValue != null) {
return storedValue;
} else {
T? value = await _fetch(key);

if (value != null) {
await _insert(value);
}

return value;
}
}
}
48 changes: 0 additions & 48 deletions test/storage/read_through_in_memory_cache_test.dart

This file was deleted.

64 changes: 64 additions & 0 deletions test/storage/read_through_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import 'package:my_wit_wallet/util/storage/cache/read_through.dart';
import 'package:test/test.dart';

void main() {
group('Storage implementation of read through cache pattern in memory', () {
test('Should call fetch callback if it\'s first time getting the value',
() async {
int timesCalled = 0;
String expectedValue = "Potato";
bool saveCalled = false;

ReadThrough<String> cache = ReadThrough((p0) async {
timesCalled = timesCalled += 1;

await Future.delayed(Duration(seconds: 0));

return expectedValue;
}, (p1) async {
saveCalled = true;
return true;
}, (p0) async {
return null;
});

expect(await cache.get("whatever"), expectedValue);
expect(saveCalled, true);
expect(timesCalled, 1);
});

test(
'Should NOT call fetch callback if it\'s the second time getting the value',
() async {
int timesCalled = 0;
String expectedValue = "Potato";
bool saveCalled = false;

ReadThrough<String> cache = ReadThrough((p0) async {
timesCalled = timesCalled += 1;

await Future.delayed(Duration(seconds: 0));

return expectedValue;
}, (p1) async {
saveCalled = true;
return true;
}, (p0) async {
if (timesCalled == 0) {
return null;
} else {
return expectedValue;
}
});

// call get multiple times
expect(await cache.get(expectedValue), expectedValue);
expect(await cache.get(expectedValue), expectedValue);
expect(await cache.get(expectedValue), expectedValue);
expect(await cache.get(expectedValue), expectedValue);
expect(saveCalled, true);

expect(timesCalled, 1);
});
});
}

0 comments on commit f3faadf

Please sign in to comment.