Add logic flow for adding erc20 tokens

This commit is contained in:
OmarHatem 2023-06-21 03:46:58 +03:00
parent 4b35ad3b21
commit 55b5780958
13 changed files with 254 additions and 27 deletions

View file

@ -3,6 +3,7 @@ import 'dart:math';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_ethereum/ethereum_balance.dart'; import 'package:cw_ethereum/ethereum_balance.dart';
import 'package:cw_ethereum/models/erc20_token.dart';
import 'package:cw_ethereum/pending_ethereum_transaction.dart'; import 'package:cw_ethereum/pending_ethereum_transaction.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
@ -102,7 +103,10 @@ class EthereumClient {
required int gas, required int gas,
required EthereumTransactionPriority priority, required EthereumTransactionPriority priority,
required CryptoCurrency currency, required CryptoCurrency currency,
String? contractAddress,
}) async { }) async {
assert(currency == CryptoCurrency.eth || contractAddress != null);
bool _isEthereum = currency == CryptoCurrency.eth; bool _isEthereum = currency == CryptoCurrency.eth;
final price = await _client!.getGasPrice(); final price = await _client!.getGasPrice();
@ -128,7 +132,7 @@ class EthereumClient {
final erc20 = Erc20( final erc20 = Erc20(
client: _client!, client: _client!,
address: EthereumAddress.fromHex(_erc20Currencies[currency]!), address: EthereumAddress.fromHex(contractAddress!),
); );
final originalAmount = BigInt.parse(amount) / BigInt.from(pow(10, 18)); final originalAmount = BigInt.parse(amount) / BigInt.from(pow(10, 18));
@ -211,25 +215,22 @@ I/flutter ( 4474): Gas Used: 53000
// )); // ));
// } // }
Future<Map<CryptoCurrency, ERC20Balance>> fetchERC20Balances(EthereumAddress userAddress) async { Future<ERC20Balance> fetchERC20Balances(
final Map<CryptoCurrency, ERC20Balance> erc20Balances = {}; EthereumAddress userAddress, String contractAddress) async {
final erc20 = Erc20(address: EthereumAddress.fromHex(contractAddress), client: _client!);
final balance = await erc20.balanceOf(userAddress);
for (var currency in _erc20Currencies.keys) { int exponent = (await erc20.decimals()).toInt();
final contractAddress = _erc20Currencies[currency]!;
try { return ERC20Balance(balance, exponent: exponent);
final erc20 = Erc20(address: EthereumAddress.fromHex(contractAddress), client: _client!); }
final balance = await erc20.balanceOf(userAddress);
int exponent = (await erc20.decimals()).toInt(); Future<Erc20Token> addErc20Token(String contractAddress) async {
final erc20 = Erc20(address: EthereumAddress.fromHex(contractAddress), client: _client!);
final name = await erc20.name();
final symbol = await erc20.symbol();
erc20Balances[currency] = ERC20Balance(balance, exponent: exponent); return Erc20Token(name: name, symbol: symbol, contractAddress: contractAddress);
} catch (e) {
continue;
}
}
return erc20Balances;
} }
void stop() { void stop() {

View file

@ -19,6 +19,8 @@ import 'package:cw_ethereum/ethereum_transaction_info.dart';
import 'package:cw_ethereum/ethereum_transaction_priority.dart'; import 'package:cw_ethereum/ethereum_transaction_priority.dart';
import 'package:cw_ethereum/ethereum_wallet_addresses.dart'; import 'package:cw_ethereum/ethereum_wallet_addresses.dart';
import 'package:cw_ethereum/file.dart'; import 'package:cw_ethereum/file.dart';
import 'package:cw_ethereum/models/erc20_token.dart';
import 'package:hive/hive.dart';
import 'package:hex/hex.dart'; import 'package:hex/hex.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:web3dart/web3dart.dart'; import 'package:web3dart/web3dart.dart';
@ -47,11 +49,17 @@ abstract class EthereumWalletBase
{CryptoCurrency.eth: initialBalance ?? ERC20Balance(BigInt.zero)}), {CryptoCurrency.eth: initialBalance ?? ERC20Balance(BigInt.zero)}),
super(walletInfo) { super(walletInfo) {
this.walletInfo = walletInfo; this.walletInfo = walletInfo;
if (!Hive.isAdapterRegistered(Erc20Token.typeId)) {
Hive.registerAdapter(Erc20TokenAdapter());
}
} }
final String _mnemonic; final String _mnemonic;
final String _password; final String _password;
late final Box<Erc20Token> erc20TokensBox;
late final EthPrivateKey _privateKey; late final EthPrivateKey _privateKey;
late EthereumClient _client; late EthereumClient _client;
@ -71,6 +79,7 @@ abstract class EthereumWalletBase
late ObservableMap<CryptoCurrency, ERC20Balance> balance; late ObservableMap<CryptoCurrency, ERC20Balance> balance;
Future<void> init() async { Future<void> init() async {
erc20TokensBox = await Hive.openBox<Erc20Token>(Erc20Token.boxName);
await walletAddresses.init(); await walletAddresses.init();
_privateKey = await getPrivateKey(_mnemonic, _password); _privateKey = await getPrivateKey(_mnemonic, _password);
transactionHistory = EthereumTransactionHistory(); transactionHistory = EthereumTransactionHistory();
@ -247,7 +256,15 @@ abstract class EthereumWalletBase
Future<void> _updateBalance() async { Future<void> _updateBalance() async {
balance[currency] = await _fetchBalances(); balance[currency] = await _fetchBalances();
balance.addAll(await _client.fetchERC20Balances(_privateKey.address));
/// Get Erc20 Tokens balances
for (var token in erc20TokensBox.values) {
try {
final currency = erc20Currencies.firstWhere((element) => element.title == token.symbol);
balance[currency] =
await _client.fetchERC20Balances(_privateKey.address, token.contractAddress);
} catch (_) {}
}
await save(); await save();
} }
@ -270,7 +287,24 @@ abstract class EthereumWalletBase
Future<void>? updateBalance() => null; Future<void>? updateBalance() => null;
List<CryptoCurrency> get erc20Currencies => _client.erc20Currencies.keys.toList(); List<CryptoCurrency> get erc20Currencies => erc20TokensBox.values
.map((token) => CryptoCurrency.all.firstWhere(
(currency) => currency.title.toLowerCase() == token.symbol.toLowerCase(),
orElse: () => CryptoCurrency(
name: token.name,
title: token.symbol,
fullName: token.name,
),
))
.toList();
Future<CryptoCurrency> addErc20Token(String contractAddress) async {
final token = await _client.addErc20Token(contractAddress);
erc20TokensBox.add(token);
return CryptoCurrency(name: token.name, title: token.symbol, fullName: token.name);
}
void _onNewTransaction(FilterEvent event) { void _onNewTransaction(FilterEvent event) {
_updateBalance(); _updateBalance();

View file

@ -0,0 +1,35 @@
import 'package:cw_core/keyable.dart';
import 'package:hive/hive.dart';
part 'erc20_token.g.dart';
@HiveType(typeId: Erc20Token.typeId)
class Erc20Token extends HiveObject with Keyable {
@HiveField(0)
final String name;
@HiveField(1)
final String symbol;
@HiveField(2)
final String contractAddress;
Erc20Token({required this.name, required this.symbol, required this.contractAddress});
static const typeId = 12;
static const boxName = 'Erc20Tokens';
@override
bool operator ==(other) =>
other is Erc20Token &&
(other.name == name && other.symbol == symbol && other.contractAddress == contractAddress);
@override
int get hashCode => name.hashCode ^ symbol.hashCode ^ contractAddress.hashCode;
@override
dynamic get keyIndex {
_keyIndex ??= key;
return _keyIndex;
}
dynamic _keyIndex;
}

View file

@ -19,6 +19,7 @@ import 'package:cake_wallet/src/screens/buy/payfura_page.dart';
import 'package:cake_wallet/src/screens/dashboard/desktop_dashboard_page.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_dashboard_page.dart';
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart';
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart';
import 'package:cake_wallet/src/screens/dashboard/home_settings_page.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart';
import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart';
import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart';
@ -42,6 +43,7 @@ import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart';
import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart'; import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart';
import 'package:cake_wallet/view_model/anonpay_details_view_model.dart'; import 'package:cake_wallet/view_model/anonpay_details_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/market_place_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/market_place_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart';
import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart'; import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart';
@ -1015,8 +1017,11 @@ Future setup({
getIt.registerFactoryParam<AdvancedPrivacySettingsViewModel, WalletType, void>( getIt.registerFactoryParam<AdvancedPrivacySettingsViewModel, WalletType, void>(
(type, _) => AdvancedPrivacySettingsViewModel(type, getIt.get<SettingsStore>())); (type, _) => AdvancedPrivacySettingsViewModel(type, getIt.get<SettingsStore>()));
getIt.registerFactory<HomeSettingsPage>( getIt.registerFactoryParam<HomeSettingsPage, BalanceViewModel, void>((balanceViewModel, _) =>
() => AdvancedPrivacySettingsViewModel(type, getIt.get<SettingsStore>())); HomeSettingsPage(getIt.get<HomeSettingsViewModel>(param1: balanceViewModel)));
getIt.registerFactoryParam<HomeSettingsViewModel, BalanceViewModel, void>(
(balanceViewModel, _) => HomeSettingsViewModel(getIt.get<SettingsStore>(), balanceViewModel));
_isSetupFinished = true; _isSetupFinished = true;
} }

View file

@ -40,6 +40,7 @@ class PreferencesKey {
static const lastAuthTimeMilliseconds = 'last_auth_time_milliseconds'; static const lastAuthTimeMilliseconds = 'last_auth_time_milliseconds';
static const lastPopupDate = 'last_popup_date'; static const lastPopupDate = 'last_popup_date';
static const lastAppReviewDate = 'last_app_review_date'; static const lastAppReviewDate = 'last_app_review_date';
static const sortBalanceBy = 'sort_balance_by';

View file

@ -0,0 +1,17 @@
enum SortBalanceBy {
FiatBalance,
Gross,
Alphabetical;
@override
String toString() {
switch (this) {
case SortBalanceBy.FiatBalance:
return "S.current.fiat_balance";
case SortBalanceBy.Gross:
return "S.current.gross";
case SortBalanceBy.Alphabetical:
return "S.current.alphabetical";
}
}
}

View file

@ -81,8 +81,14 @@ class CWEthereum extends Ethereum {
int formatterEthereumParseAmount(String amount) => EthereumFormatter.parseEthereumAmount(amount); int formatterEthereumParseAmount(String amount) => EthereumFormatter.parseEthereumAmount(amount);
@override @override
List<CryptoCurrency> getERC20Currencies(Object wallet) { List<CryptoCurrency> getERC20Currencies(WalletBase wallet) {
final ethereumWallet = wallet as EthereumWallet; final ethereumWallet = wallet as EthereumWallet;
return ethereumWallet.erc20Currencies; return ethereumWallet.erc20Currencies;
} }
@override
Future<CryptoCurrency> addErc20Token(WalletBase wallet, String contractAddress) async {
final ethereumWallet = wallet as EthereumWallet;
return await ethereumWallet.addErc20Token(contractAddress);
}
} }

View file

@ -11,6 +11,7 @@ import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart';
import 'package:cake_wallet/src/screens/buy/webview_page.dart'; import 'package:cake_wallet/src/screens/buy/webview_page.dart';
import 'package:cake_wallet/src/screens/buy/payfura_page.dart'; import 'package:cake_wallet/src/screens/buy/payfura_page.dart';
import 'package:cake_wallet/src/screens/buy/pre_order_page.dart'; import 'package:cake_wallet/src/screens/buy/pre_order_page.dart';
import 'package:cake_wallet/src/screens/dashboard/home_settings_page.dart';
import 'package:cake_wallet/src/screens/restore/sweeping_wallet_page.dart'; import 'package:cake_wallet/src/screens/restore/sweeping_wallet_page.dart';
import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart';
import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart';
@ -97,8 +98,6 @@ import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/node.dart'; import 'package:cw_core/node.dart';
import 'buy/moonpay/moonpay_buy_provider.dart';
late RouteSettings currentRouteSettings; late RouteSettings currentRouteSettings;
Route<dynamic> createRoute(RouteSettings settings) { Route<dynamic> createRoute(RouteSettings settings) {

View file

@ -0,0 +1,85 @@
import 'package:cake_wallet/entities/sort_balance_types.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class HomeSettingsPage extends BasePage {
HomeSettingsPage(this._homeSettingsViewModel);
final HomeSettingsViewModel _homeSettingsViewModel;
final TextEditingController _searchController = TextEditingController();
// TODO: add localization
@override
String? get title => "S.current.home_screen_settings";
@override
Widget body(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: [
Observer(
builder: (_) => SettingsPickerCell<SortBalanceBy>(
title: "S.current.sort_by",
items: SortBalanceBy.values,
selectedItem: _homeSettingsViewModel.sortBalanceBy,
onItemSelected: _homeSettingsViewModel.setSortBalanceBy,
),
),
Row(
children: [
TextFormField(
controller: _searchController,
style: TextStyle(color: Theme.of(context).primaryTextTheme.titleLarge!.color!),
decoration: InputDecoration(
hintText: "S.of(context).search_token",
prefixIcon: Image.asset("assets/images/search_icon.png"),
filled: true,
fillColor: Theme.of(context).accentTextTheme.displaySmall!.color!,
alignLabelWithHint: false,
contentPadding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Colors.transparent),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Colors.transparent),
),
),
),
IconButton(
onPressed: () {},
style: IconButton.styleFrom(
shape: CircleBorder(),
backgroundColor: Theme.of(context).accentTextTheme.bodySmall!.color!,
),
icon: Icon(
Icons.add,
color: Theme.of(context).primaryTextTheme.titleLarge!.color!,
size: 22.0,
),
),
],
),
ListView.builder(
itemCount: _homeSettingsViewModel.tokens.length,
shrinkWrap: true,
itemBuilder: (context, index) {
return SettingsSwitcherCell(
title: _homeSettingsViewModel.tokens[index],
value: false,
onValueChange: (_, bool value) {},
);
},
),
],
),
);
}
}

View file

@ -49,9 +49,8 @@ class BalancePage extends StatelessWidget {
), ),
if (dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled) if (dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled)
InkWell( InkWell(
onTap: () { onTap: () => Navigator.pushNamed(context, Routes.homeSettings,
Navigator.pushNamed(context, Routes.homeSettings) arguments: dashboardViewModel.balanceViewModel),
},
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Image.asset( child: Image.asset(

View file

@ -4,6 +4,7 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/pin_code_required_duration.dart'; import 'package:cake_wallet/entities/pin_code_required_duration.dart';
import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/sort_balance_types.dart';
import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/transaction_priority.dart';
@ -57,6 +58,7 @@ abstract class SettingsStoreBase with Store {
required this.isBitcoinBuyEnabled, required this.isBitcoinBuyEnabled,
required this.actionlistDisplayMode, required this.actionlistDisplayMode,
required this.pinTimeOutDuration, required this.pinTimeOutDuration,
required this.sortBalanceBy,
TransactionPriority? initialBitcoinTransactionPriority, TransactionPriority? initialBitcoinTransactionPriority,
TransactionPriority? initialMoneroTransactionPriority, TransactionPriority? initialMoneroTransactionPriority,
TransactionPriority? initialHavenTransactionPriority, TransactionPriority? initialHavenTransactionPriority,
@ -214,6 +216,11 @@ abstract class SettingsStoreBase with Store {
(ExchangeApiMode mode) => (ExchangeApiMode mode) =>
sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, mode.serialize())); sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, mode.serialize()));
reaction(
(_) => sortBalanceBy,
(SortBalanceBy sortBalanceBy) =>
_sharedPreferences.setInt(PreferencesKey.sortBalanceBy, sortBalanceBy.index));
this.nodes.observe((change) { this.nodes.observe((change) {
if (change.newValue != null && change.key != null) { if (change.newValue != null && change.key != null) {
_saveCurrentNode(change.newValue!, change.key!); _saveCurrentNode(change.newValue!, change.key!);
@ -293,6 +300,9 @@ abstract class SettingsStoreBase with Store {
@observable @observable
ObservableMap<WalletType, TransactionPriority> priority; ObservableMap<WalletType, TransactionPriority> priority;
@observable
SortBalanceBy sortBalanceBy;
String appVersion; String appVersion;
String deviceName; String deviceName;
@ -393,6 +403,8 @@ abstract class SettingsStoreBase with Store {
final pinCodeTimeOutDuration = timeOutDuration != null final pinCodeTimeOutDuration = timeOutDuration != null
? PinCodeRequiredDuration.deserialize(raw: timeOutDuration) ? PinCodeRequiredDuration.deserialize(raw: timeOutDuration)
: defaultPinCodeTimeOutDuration; : defaultPinCodeTimeOutDuration;
final sortBalanceBy =
SortBalanceBy.values[sharedPreferences.getInt(PreferencesKey.sortBalanceBy) ?? 0];
// If no value // If no value
if (pinLength == null || pinLength == 0) { if (pinLength == null || pinLength == 0) {
@ -463,6 +475,7 @@ abstract class SettingsStoreBase with Store {
initialPinLength: pinLength, initialPinLength: pinLength,
pinTimeOutDuration: pinCodeTimeOutDuration, pinTimeOutDuration: pinCodeTimeOutDuration,
initialLanguageCode: savedLanguageCode, initialLanguageCode: savedLanguageCode,
sortBalanceBy: sortBalanceBy,
initialMoneroTransactionPriority: moneroTransactionPriority, initialMoneroTransactionPriority: moneroTransactionPriority,
initialBitcoinTransactionPriority: bitcoinTransactionPriority, initialBitcoinTransactionPriority: bitcoinTransactionPriority,
initialHavenTransactionPriority: havenTransactionPriority, initialHavenTransactionPriority: havenTransactionPriority,
@ -541,6 +554,8 @@ abstract class SettingsStoreBase with Store {
languageCode = sharedPreferences.getString(PreferencesKey.currentLanguageCode) ?? languageCode; languageCode = sharedPreferences.getString(PreferencesKey.currentLanguageCode) ?? languageCode;
shouldShowYatPopup = shouldShowYatPopup =
sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? shouldShowYatPopup; sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? shouldShowYatPopup;
sortBalanceBy = SortBalanceBy
.values[sharedPreferences.getInt(PreferencesKey.sortBalanceBy) ?? sortBalanceBy.index];
final nodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); final nodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey);
final bitcoinElectrumServerId = final bitcoinElectrumServerId =

View file

@ -0,0 +1,29 @@
import 'package:cake_wallet/entities/sort_balance_types.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:mobx/mobx.dart';
part 'home_settings_view_model.g.dart';
class HomeSettingsViewModel = HomeSettingsViewModelBase with _$HomeSettingsViewModel;
abstract class HomeSettingsViewModelBase with Store {
HomeSettingsViewModelBase(this._settingsStore, this._balanceViewModel) {
}
final SettingsStore _settingsStore;
final BalanceViewModel _balanceViewModel;
@computed
SortBalanceBy get sortBalanceBy => _settingsStore.sortBalanceBy;
@action
void setSortBalanceBy(SortBalanceBy value) => _settingsStore.sortBalanceBy = value;
@observable
bool pinNativeToken = false;
List<String> get tokens =>
_balanceViewModel.balances.keys.map((e) => e.fullName ?? e.title).toList();
}

View file

@ -524,7 +524,8 @@ abstract class Ethereum {
}); });
int formatterEthereumParseAmount(String amount); int formatterEthereumParseAmount(String amount);
List<CryptoCurrency> getERC20Currencies(Object wallet); List<CryptoCurrency> getERC20Currencies(WalletBase wallet);
Future<CryptoCurrency> addErc20Token(WalletBase wallet, String contractAddress);
} }
"""; """;