From 9adb38d51bb8c2c632ef8ec9e8c105e24969a956 Mon Sep 17 00:00:00 2001 From: CharlVS <77973576+CharlVS@users.noreply.github.com> Date: Mon, 16 Oct 2023 14:35:01 +0200 Subject: [PATCH] Update coin configs at runtime --- lib/app_config/coin_converter.dart | 5 +- lib/app_config/coins_updater.dart | 147 +++++++++++++++++++++++++++++ lib/model/coin.dart | 7 +- lib/services/mm_service.dart | 3 +- 4 files changed, 154 insertions(+), 8 deletions(-) create mode 100644 lib/app_config/coins_updater.dart diff --git a/lib/app_config/coin_converter.dart b/lib/app_config/coin_converter.dart index f524304e5..24b0f6861 100644 --- a/lib/app_config/coin_converter.dart +++ b/lib/app_config/coin_converter.dart @@ -1,14 +1,13 @@ import 'dart:convert'; import 'package:flutter/services.dart'; +import 'package:komodo_dex/app_config/coins_updater.dart'; import '../model/coin_type.dart'; import '../utils/utils.dart'; Future> convertCoinsConfigToAppConfig() async { - final String coins = - await rootBundle.loadString('assets/coins_config.json', cache: false); - // 561 coins + final String coins = await CoinUpdater().getConfig(); Map coinsResponse = jsonDecode(coins); List allCoinsList = []; diff --git a/lib/app_config/coins_updater.dart b/lib/app_config/coins_updater.dart new file mode 100644 index 000000000..d41c20694 --- /dev/null +++ b/lib/app_config/coins_updater.dart @@ -0,0 +1,147 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; +import 'package:flutter/services.dart'; +import 'package:http/http.dart' as http; +import 'package:komodo_dex/utils/log.dart'; +import 'package:path_provider/path_provider.dart'; + +/// Provides methods for fetching coin data either from local assets or a remote Git repository. +/// +/// `CoinUpdater` uses a singleton pattern to ensure a single instance throughout the app's lifecycle. +/// It checks first if there is a cached version of the data. If not, it uses the bundled asset. +/// In the background, it tries to fetch the most recent data and cache it for future launches. +/// +/// Usage: +/// ```dart +/// final coinUpdater = CoinUpdater(); +/// final config = await coinUpdater.getConfig(); +/// final coins = await coinUpdater.getCoins(); +/// ``` +/// +/// NB! [coinsRepoBranch] and [coinsRepoUrl] only take effect for the next +/// launch of the app. So 2x restart is needed to switch to a different branch. +/// +/// TODO: Implement coin icon fetching. +class CoinUpdater { + factory CoinUpdater() => _instance; + + CoinUpdater._internal(); + + static final CoinUpdater _instance = CoinUpdater._internal(); + + /// The branch of the coins repository to use. + //! QA: change branch name here and then restart twice after logging in. + static const coinsRepoBranch = 'master'; + + static const coinsRepoUrl = + 'https://raw.githubusercontent.com/KomodoPlatform/coins'; + + static const isUpdateEnabled = true; + + final String localAssetPathConfig = 'assets/coins_config.json'; + final String localAssetPathCoins = 'assets/coins.json'; + + String get remotePathConfig => + '$coinsRepoUrl/$coinsRepoBranch/utils/coins_config.json'; + String get remotePathCoins => '$coinsRepoUrl/$coinsRepoBranch/coins'; + + String _cachedConfig; + String _cachedCoins; + + Future _fetchAsset(String path) async { + return await rootBundle.loadString(path); + } + + Future _getLocalFile(String filename) async { + final directory = await getApplicationDocumentsDirectory(); + return File('${directory.path}/$filename'); + } + + Future _fetchOrCache( + String localPath, + String remoteUrl, + String cacheName, + String cacheProperty, + ) async { + try { + if (cacheProperty != null) { + return cacheProperty; + } + + File cacheFile = await _getLocalFile(cacheName); + + if (await cacheFile.exists()) { + cacheProperty = await cacheFile.readAsString(); + return cacheProperty; + } else { + String localData = await _fetchAsset(localPath); + if (isUpdateEnabled) { + scheduleMicrotask( + () => _updateCacheInBackground(remoteUrl, cacheFile), + ); + } + cacheProperty = localData; + return localData; + } + } catch (e) { + // If there's an error, first try to return the cached value, + // if that's null too, then fall back to the local asset. + if (cacheProperty != null) { + return cacheProperty; + } else { + return await _fetchAsset(localPath); + } + } + } + + void _updateCacheInBackground(String remoteUrl, File cacheFile) async { + final ReceivePort receivePort = ReceivePort(); + await Isolate.spawn( + _isolateEntry, + [remoteUrl, cacheFile.path], + onExit: receivePort.sendPort, + ); + receivePort.listen((data) { + // Close the receive port when the isolate is done + receivePort.close(); + + Log( + 'CoinUpdater', + 'Coin updater updated coins to latest commit on branch ' + '$coinsRepoBranch from $coinsRepoUrl. \n $remoteUrl', + ); + }); + } + + static void _isolateEntry(List data) async { + final String remoteUrl = data[0]; + final String filePath = data[1]; + + final response = await http.get(Uri.parse(remoteUrl)); + if (response.statusCode == 200) { + final file = File(filePath); + file.writeAsString(response.body); + } + } + + Future getConfig() async { + _cachedConfig = await _fetchOrCache( + localAssetPathConfig, + remotePathConfig, + 'coins_config_cache.json', + _cachedConfig, + ); + return _cachedConfig; + } + + Future getCoins() async { + _cachedCoins = await _fetchOrCache( + localAssetPathCoins, + remotePathCoins, + 'coins_cache.json', + _cachedCoins, + ); + return _cachedCoins; + } +} diff --git a/lib/model/coin.dart b/lib/model/coin.dart index 197e84e18..8a67a50b9 100644 --- a/lib/model/coin.dart +++ b/lib/model/coin.dart @@ -2,6 +2,7 @@ import 'dart:collection'; import 'dart:convert'; import 'package:flutter/services.dart'; +import 'package:komodo_dex/app_config/coins_updater.dart'; import '../app_config/app_config.dart'; import '../app_config/coin_converter.dart'; @@ -33,8 +34,7 @@ Future> get coins async { _coinsInvoked = true; Log('coin:29', 'Loading coins.jsonā€¦'); - const ci = 'assets/coins.json'; - final cis = await rootBundle.loadString(ci, cache: false); + final String cis = await CoinUpdater().getCoins(); final List cil = json.decode(cis); final Map> cim = {}; for (dynamic js in cil) cim[js['coin']] = Map.from(js); @@ -195,8 +195,7 @@ class Coin { if (lightWalletDServers != null) 'light_wallet_d_servers': List.from(lightWalletDServers.map((x) => x)), - - }; + }; String getTxFeeSatoshi() { int txFeeRes = 0; diff --git a/lib/services/mm_service.dart b/lib/services/mm_service.dart index 99db8a322..c5718a877 100644 --- a/lib/services/mm_service.dart +++ b/lib/services/mm_service.dart @@ -8,6 +8,7 @@ import 'package:flutter/services.dart' show EventChannel, MethodChannel, rootBundle, SystemChannels; import 'package:http/http.dart' as http; import 'package:http/http.dart'; +import 'package:komodo_dex/app_config/coins_updater.dart'; import 'package:package_info_plus/package_info_plus.dart'; import '../app_config/app_config.dart'; @@ -457,7 +458,7 @@ class MMService { Future> readJsonCoinInit() async { try { - return jsonDecode(await rootBundle.loadString('assets/coins.json')); + return jsonDecode(await CoinUpdater().getCoins()); } catch (e) { if (kDebugMode) { Log('mm_service', 'readJsonCoinInit] $e');