Merge pull request #1448 from reubenyap/codex/rsfiro-app-config
What changed, and why it matters
This commit adds support for a new Ethereum token called rsFIRO and makes the list of default Ethereum tokens configurable for each app flavor (Stack Wallet, Stack Duo, Campfire). It also includes a database migration so existing users get rsFIRO added automatically. There is no obvious security vulnerability in the changes, but the migration logic has a subtle edge case that could, in theory, affect token data consistency.
No immediate security action required. Reviewers should verify the rsFIRO contract address and decimals against official sources, confirm the migration's case-insensitive address check matches the database's address normalization, and ensure app configuration scripts cannot be tampered with during build.
Security signals we found
Database migration inserts a hardcoded ERC-20 contract address into user data based on app configuration
Migration checks for existing contract by case-insensitive address comparison before insertion
Token icon rendering now branches on contract address equality, which is a presentation-layer change
Default token lists moved from shared Dart code into shell-generated app configuration files
No input validation, cryptography, network, or permission changes observed
Evidence from the diff
The patch introduces DefaultTokens.rsFiro (ERC-20 contract 0x2744ea5…aeddc2, 8 decimals) and replaces the global DefaultTokens.list with per-app _defaultEthTokens lists generated by configuration scripts. UI code now uses AppConfig.defaultEthTokens and special-cases rsFIRO icons via local SVG assets. A new DB migration (16→17) inserts rsFIRO into the user’s token catalog if the app config includes it and no matching address exists. Tests verify the migration inserts rsFIRO once and preserves existing/custom tokens.
Changed components
lib/utilities/default_eth_tokens.dartlib/app_config.dartlib/db/db_version_migration.dartscripts/app_config/configure_*.shlib/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dartlib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dartlib/widgets/icon_widgets/eth_token_icon.dartlib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dartlib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dartlib/pages/shopinbit/shopinbit_payment_shared.dartInspect captured patch +454 / −295
### asset_sources/svg/campfire/rsfiro.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 254.12 254.12" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="56.64" x2="197.49" y1="43.13" y2="210.99"><stop offset="0" stop-color="#dd704f"/><stop offset="1" stop-color="#8f314f"/><stop offset="1" stop-color="#ff9a03"/></linearGradient><circle cx="127.06" cy="127.06" fill="#fff" r="127.06"/><path d="m28.53 174.99c-7.06-14.48-11.03-30.74-11.03-47.93 0-60.51 49.05-109.56 109.56-109.56 31.32 0 59.55 13.16 79.52 34.24h-51.46c-2.54 0-4.87 1.41-6.06 3.65l-30.58 57.97h-39.35c-3.77 0-6.85 3.08-6.85 6.85v13.69c0 3.77 3.08 6.85 6.85 6.85h24.9l-16.13 30.58c-1.19 2.25-3.52 3.65-6.06 3.65h-53.31zm143.75-95.86c-2.54 0-4.87 1.41-6.06 3.65l-16.13 30.58h24.9c3.77 0 6.85 3.08 6.85 6.85v13.69c0 3.77-3.08 6.85-6.85 6.85h-39.35l-30.58 57.97c-1.19 2.25-3.52 3.65-6.06 3.65h-51.46c19.97 21.07 48.2 34.24 79.52 34.24 60.51 0 109.56-49.05 109.56-109.56 0-17.19-3.97-33.45-11.03-47.93h-53.31z" fill="url(#a)"/><g fill="#fff"><path d="m45.95 47.64h11.48l.41 8.22h.82c1.09-3.36 2.82-5.69 5.18-7s5.52-1.96 9.48-1.96v11.3c-3.89 0-6.92.53-9.07 1.59s-3.69 2.77-4.61 5.13c-.92 2.37-1.38 5.63-1.38 9.8v20.54h-12.3v-47.61z"/><path d="m89.3 95.62c-3.28-.25-6.15-.56-8.61-.93v-8.87c5.06.56 10.04.84 14.96.84 2.6 0 4.53-.15 5.79-.47 1.26-.31 2.08-.81 2.46-1.49.37-.68.56-1.74.56-3.17s-.12-2.55-.36-3.17-.67-1.06-1.28-1.31-1.74-.47-3.38-.65l-10.56-1.31c-3.76-.43-6.3-1.71-7.64-3.83-1.33-2.12-2-5.48-2-10.08 0-3.86.77-6.82 2.31-8.87s3.67-3.47 6.41-4.25c2.73-.78 6.18-1.17 10.35-1.17 2.6 0 5.35.14 8.25.42s5.28.64 7.12 1.07v8.78c-4.58-.62-9.19-.93-13.84-.93-2.32 0-4.08.11-5.28.33s-2.05.69-2.56 1.4c-.51.72-.77 1.79-.77 3.22s.08 2.35.26 2.94c.17.59.55 1.01 1.13 1.26s1.59.47 3.02.65l10.66 1.21c2.32.31 4.18.84 5.59 1.59 1.4.75 2.49 2.07 3.28 3.97.78 1.9 1.18 4.65 1.18 8.26 0 4.05-.7 7.16-2.1 9.34s-3.38 3.66-5.94 4.43-5.89 1.17-9.99 1.17c-2.73 0-5.74-.13-9.02-.37z"/></g></svg>
\ No newline at end of file
### asset_sources/svg/stack_duo/rsfiro.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 254.12 254.12" xmlns="http://www.w3.org/2000/svg"/>
\ No newline at end of file
### asset_sources/svg/stack_wallet/rsfiro.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 254.12 254.12" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="56.64" x2="197.49" y1="43.13" y2="210.99"><stop offset="0" stop-color="#dd704f"/><stop offset="1" stop-color="#8f314f"/><stop offset="1" stop-color="#ff9a03"/></linearGradient><circle cx="127.06" cy="127.06" fill="#fff" r="127.06"/><path d="m28.53 174.99c-7.06-14.48-11.03-30.74-11.03-47.93 0-60.51 49.05-109.56 109.56-109.56 31.32 0 59.55 13.16 79.52 34.24h-51.46c-2.54 0-4.87 1.41-6.06 3.65l-30.58 57.97h-39.35c-3.77 0-6.85 3.08-6.85 6.85v13.69c0 3.77 3.08 6.85 6.85 6.85h24.9l-16.13 30.58c-1.19 2.25-3.52 3.65-6.06 3.65h-53.31zm143.75-95.86c-2.54 0-4.87 1.41-6.06 3.65l-16.13 30.58h24.9c3.77 0 6.85 3.08 6.85 6.85v13.69c0 3.77-3.08 6.85-6.85 6.85h-39.35l-30.58 57.97c-1.19 2.25-3.52 3.65-6.06 3.65h-51.46c19.97 21.07 48.2 34.24 79.52 34.24 60.51 0 109.56-49.05 109.56-109.56 0-17.19-3.97-33.45-11.03-47.93h-53.31z" fill="url(#a)"/><g fill="#fff"><path d="m45.95 47.64h11.48l.41 8.22h.82c1.09-3.36 2.82-5.69 5.18-7s5.52-1.96 9.48-1.96v11.3c-3.89 0-6.92.53-9.07 1.59s-3.69 2.77-4.61 5.13c-.92 2.37-1.38 5.63-1.38 9.8v20.54h-12.3v-47.61z"/><path d="m89.3 95.62c-3.28-.25-6.15-.56-8.61-.93v-8.87c5.06.56 10.04.84 14.96.84 2.6 0 4.53-.15 5.79-.47 1.26-.31 2.08-.81 2.46-1.49.37-.68.56-1.74.56-3.17s-.12-2.55-.36-3.17-.67-1.06-1.28-1.31-1.74-.47-3.38-.65l-10.56-1.31c-3.76-.43-6.3-1.71-7.64-3.83-1.33-2.12-2-5.48-2-10.08 0-3.86.77-6.82 2.31-8.87s3.67-3.47 6.41-4.25c2.73-.78 6.18-1.17 10.35-1.17 2.6 0 5.35.14 8.25.42s5.28.64 7.12 1.07v8.78c-4.58-.62-9.19-.93-13.84-.93-2.32 0-4.08.11-5.28.33s-2.05.69-2.56 1.4c-.51.72-.77 1.79-.77 3.22s.08 2.35.26 2.94c.17.59.55 1.01 1.13 1.26s1.59.47 3.02.65l10.66 1.21c2.32.31 4.18.84 5.59 1.59 1.4.75 2.49 2.07 3.28 3.97.78 1.9 1.18 4.65 1.18 8.26 0 4.05-.7 7.16-2.1 9.34s-3.38 3.66-5.94 4.43-5.89 1.17-9.99 1.17c-2.73 0-5.74-.13-9.02-.37z"/></g></svg>
\ No newline at end of file
### lib/app_config.dart
@@ -1,6 +1,8 @@
// ignore: unused_import
import 'dart:io';
+import 'models/isar/models/ethereum/eth_contract.dart';
+import 'utilities/default_eth_tokens.dart';
import 'wallets/crypto_currency/crypto_currency.dart';
import 'wallets/crypto_currency/intermediate/frost_currency.dart';
@@ -28,6 +30,8 @@ abstract class AppConfig {
static List<CryptoCurrency> get coins => _supportedCoins;
+ static List<EthContract> get defaultEthTokens => _defaultEthTokens;
+
static ({String from, String fromFuzzyNet, String to, String toFuzzyNet})
get swapDefaults => _swapDefaults;
### lib/db/db_version_migration.dart
@@ -24,6 +24,7 @@ import '../services/mixins/wallet_db.dart';
import '../services/wallets_service.dart';
import '../utilities/amount/amount.dart';
import '../utilities/constants.dart';
+import '../utilities/default_eth_tokens.dart';
import '../utilities/flutter_secure_storage_interface.dart';
import '../utilities/logger.dart';
import '../utilities/prefs.dart';
@@ -379,9 +380,8 @@ class DbVersionMigrator with WalletDB {
final mwcMigrationWalletsService = WalletsService();
final mwcMigrationWalletNames =
await mwcMigrationWalletsService.walletNames;
- final mwcIdentifier = Mimblewimblecoin(
- CryptoCurrencyNetwork.main,
- ).identifier;
+ final mwcIdentifier = Mimblewimblecoin(CryptoCurrencyNetwork.main)
+ .identifier;
for (final walletId in mwcMigrationWalletNames.keys) {
if (mwcMigrationWalletNames[walletId]!.coinIdentifier ==
mwcIdentifier) {
@@ -399,6 +399,30 @@ class DbVersionMigrator with WalletDB {
// try to continue migrating
return await migrate(16, secureStore: secureStore);
+ case 16:
+ if (AppConfig.defaultEthTokens.contains(DefaultTokens.rsFiro)) {
+ final db = MainDB.instance;
+ await db.initMainDB();
+ if (await db.getEthContracts().isNotEmpty() &&
+ await db
+ .getEthContracts()
+ .filter()
+ .addressEqualTo(
+ DefaultTokens.rsFiro.address,
+ caseSensitive: false,
+ )
+ .isEmpty()) {
+ await db.putEthContract(DefaultTokens.rsFiro);
+ }
+ }
+
+ await DB.instance.put<dynamic>(
+ boxName: DB.boxNameDBInfo,
+ key: "hive_data_version",
+ value: 17,
+ );
+ return await migrate(17, secureStore: secureStore);
+
default:
// finally return
return;
### lib/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart
@@ -15,6 +15,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:isar_community/isar.dart';
+import '../../../app_config.dart';
import '../../../db/isar/main_db.dart';
import '../../../models/isar/models/ethereum/eth_contract.dart';
import '../../../models/isar/models/solana/sol_contract.dart';
@@ -25,7 +26,6 @@ import '../../../providers/global/wallets_provider.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/assets.dart';
import '../../../utilities/constants.dart';
-import '../../../utilities/default_eth_tokens.dart';
import '../../../utilities/default_sol_tokens.dart';
import '../../../utilities/text_styles.dart';
import '../../../utilities/util.dart';
@@ -100,11 +100,10 @@ class _EditWalletTokensViewState extends ConsumerState<EditWalletTokensView> {
}
Future<void> onNextPressed() async {
- final selectedTokens =
- tokenEntities
- .where((e) => e.selected)
- .map((e) => e.token.address)
- .toList();
+ final selectedTokens = tokenEntities
+ .where((e) => e.selected)
+ .map((e) => e.token.address)
+ .toList();
final wallet = ref.read(pWallets).getWallet(widget.walletId);
@@ -119,13 +118,11 @@ class _EditWalletTokensViewState extends ConsumerState<EditWalletTokensView> {
Navigator.of(context).pop(42);
} else {
if (isDesktop) {
- Navigator.of(
- context,
- ).popUntil(ModalRoute.withName(DesktopHomeView.routeName));
+ Navigator.of(context)
+ .popUntil(ModalRoute.withName(DesktopHomeView.routeName));
} else {
- await Navigator.of(
- context,
- ).pushNamedAndRemoveUntil(HomeView.routeName, (route) => false);
+ await Navigator.of(context)
+ .pushNamedAndRemoveUntil(HomeView.routeName, (route) => false);
}
if (mounted) {
unawaited(
@@ -160,9 +157,8 @@ class _EditWalletTokensViewState extends ConsumerState<EditWalletTokensView> {
),
);
} else {
- final result = await Navigator.of(
- context,
- ).pushNamed(AddCustomTokenView.routeName);
+ final result = await Navigator.of(context)
+ .pushNamed(AddCustomTokenView.routeName);
contract = result as EthContract?;
}
@@ -177,7 +173,9 @@ class _EditWalletTokensViewState extends ConsumerState<EditWalletTokensView> {
tokenEntities.add(
AddTokenListElementData(contract!)..selected = true,
);
- tokenEntities.sort((a, b) => a.token.name.compareTo(b.token.name));
+ tokenEntities.sort(
+ (a, b) => a.token.name.compareTo(b.token.name),
+ );
}
});
}
@@ -199,9 +197,7 @@ class _EditWalletTokensViewState extends ConsumerState<EditWalletTokensView> {
),
);
} else {
- final result = await Navigator.of(
- context,
- ).pushNamed(
+ final result = await Navigator.of(context).pushNamed(
AddCustomSolanaTokenView.routeName,
arguments: widget.walletId,
);
@@ -228,9 +224,7 @@ class _EditWalletTokensViewState extends ConsumerState<EditWalletTokensView> {
if (tokenEntities
.where((e) => e.token.address == token!.address)
.isEmpty) {
- tokenEntities.add(
- AddTokenListElementData(token!)..selected = true,
- );
+ tokenEntities.add(AddTokenListElementData(token!)..selected = true);
tokenEntities.sort((a, b) => a.token.name.compareTo(b.token.name));
}
});
@@ -268,7 +262,7 @@ class _EditWalletTokensViewState extends ConsumerState<EditWalletTokensView> {
.findAllSync();
if (contracts.isEmpty) {
- contracts.addAll(DefaultTokens.list);
+ contracts.addAll(AppConfig.defaultEthTokens);
MainDB.instance
.putEthContracts(contracts)
.then(
@@ -448,9 +442,8 @@ class _EditWalletTokensViewState extends ConsumerState<EditWalletTokensView> {
_searchTerm = value;
});
},
- style: STextStyles.desktopTextMedium(
- context,
- ).copyWith(height: 2),
+ style: STextStyles.desktopTextMedium(context)
+ .copyWith(height: 2),
decoration:
standardInputDecoration(
"Search",
@@ -516,9 +509,9 @@ class _EditWalletTokensViewState extends ConsumerState<EditWalletTokensView> {
} else {
return Background(
child: Scaffold(
- backgroundColor: Theme.of(
- context,
- ).extension<StackColors>()!.background,
+ backgroundColor: Theme.of(context)
+ .extension<StackColors>()!
+ .background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
@@ -533,14 +526,14 @@ class _EditWalletTokensViewState extends ConsumerState<EditWalletTokensView> {
child: AppBarIconButton(
size: 36,
shadows: const [],
- color: Theme.of(
- context,
- ).extension<StackColors>()!.background,
+ color: Theme.of(context)
+ .extension<StackColors>()!
+ .background,
icon: SvgPicture.asset(
Assets.svg.circlePlusFilled,
- color: Theme.of(
- context,
- ).extension<StackColors>()!.topNavIconPrimary,
+ color: Theme.of(context)
+ .extension<StackColors>()!
+ .topNavIconPrimary,
width: 20,
height: 20,
),
### lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart
@@ -20,6 +20,7 @@ import '../../../../services/exchange/change_now/change_now_exchange.dart';
import '../../../../services/exchange/exchange_data_loading_service.dart';
import '../../../../themes/stack_colors.dart';
import '../../../../utilities/assets.dart';
+import '../../../../utilities/default_eth_tokens.dart';
import '../../../../utilities/text_styles.dart';
import '../../../../utilities/util.dart';
import '../../../../widgets/app_icon.dart';
@@ -54,18 +55,14 @@ class _AddTokenListElementState extends ConsumerState<AddTokenListElement> {
super.initState();
ExchangeDataLoadingService.instance.isar.then((isar) async {
- final currency =
- await isar.currencies
- .where()
- .exchangeNameEqualTo(ChangeNowExchange.exchangeName)
- .filter()
- .tokenContractEqualTo(
- widget.data.token.address,
- caseSensitive: false,
- )
- .and()
- .imageIsNotEmpty()
- .findFirst();
+ final currency = await isar.currencies
+ .where()
+ .exchangeNameEqualTo(ChangeNowExchange.exchangeName)
+ .filter()
+ .tokenContractEqualTo(widget.data.token.address, caseSensitive: false)
+ .and()
+ .imageIsNotEmpty()
+ .findFirst();
if (mounted) {
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -86,50 +83,54 @@ class _AddTokenListElementState extends ConsumerState<AddTokenListElement> {
return RoundedWhiteContainer(
padding: EdgeInsets.all(isDesktop ? 16 : 12),
- borderColor:
- isDesktop
- ? Theme.of(context).extension<StackColors>()!.backgroundAppBar
- : null,
+ borderColor: isDesktop
+ ? Theme.of(context).extension<StackColors>()!.backgroundAppBar
+ : null,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
- currency != null
- ? SvgPicture.network(
- currency!.image,
- width: iconSize,
- height: iconSize,
- placeholderBuilder:
- (_) => AppIcon(width: iconSize, height: iconSize),
- )
- : AppIcon(width: iconSize, height: iconSize),
+ if (widget.data.token is EthContract &&
+ widget.data.token.address.toLowerCase() ==
+ DefaultTokens.rsFiro.address)
+ SvgPicture.asset(
+ Assets.svg.rsFiro,
+ width: iconSize,
+ height: iconSize,
+ )
+ else
+ currency != null
+ ? SvgPicture.network(
+ currency!.image,
+ width: iconSize,
+ height: iconSize,
+ placeholderBuilder: (_) =>
+ AppIcon(width: iconSize, height: iconSize),
+ )
+ : AppIcon(width: iconSize, height: iconSize),
const SizedBox(width: 12),
ConditionalParent(
condition: isDesktop,
- builder:
- (child) => Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- child,
- const SizedBox(height: 2),
- Text(
- widget.data.token.symbol,
- style: STextStyles.desktopTextExtraExtraSmall(
- context,
- ),
- overflow: TextOverflow.ellipsis,
- ),
- ],
+ builder: (child) => Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ child,
+ const SizedBox(height: 2),
+ Text(
+ widget.data.token.symbol,
+ style: STextStyles.desktopTextExtraExtraSmall(context),
+ overflow: TextOverflow.ellipsis,
),
+ ],
+ ),
child: Text(
isDesktop
? mainLabel
: "$mainLabel (${widget.data.token.symbol})",
- style:
- isDesktop
- ? STextStyles.desktopTextSmall(context)
- : STextStyles.w600_14(context),
+ style: isDesktop
+ ? STextStyles.desktopTextSmall(context)
+ : STextStyles.w600_14(context),
overflow: TextOverflow.ellipsis,
),
),
@@ -138,21 +139,20 @@ class _AddTokenListElementState extends ConsumerState<AddTokenListElement> {
const SizedBox(width: 4),
isDesktop
? Checkbox(
- value: widget.data.selected,
- onChanged:
- (newValue) =>
- setState(() => widget.data.selected = newValue!),
- )
+ value: widget.data.selected,
+ onChanged: (newValue) =>
+ setState(() => widget.data.selected = newValue!),
+ )
: SizedBox(
- height: 20,
- width: 40,
- child: DraggableSwitchButton(
- isOn: widget.data.selected,
- onValueChanged: (newValue) {
- widget.data.selected = newValue;
- },
+ height: 20,
+ width: 40,
+ child: DraggableSwitchButton(
+ isOn: widget.data.selected,
+ onValueChanged: (newValue) {
+ widget.data.selected = newValue;
+ },
+ ),
),
- ),
],
),
);
### lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart
@@ -28,7 +28,6 @@ import '../../../providers/providers.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/assets.dart';
import '../../../utilities/constants.dart';
-import '../../../utilities/default_eth_tokens.dart';
import '../../../utilities/default_sol_tokens.dart';
import '../../../utilities/text_styles.dart';
import '../../../utilities/util.dart';
@@ -103,17 +102,15 @@ class _AddWalletViewState extends ConsumerState<AddWalletView> {
if (isDesktop) {
contract = await showDialog(
context: context,
- builder:
- (context) => const DesktopDialog(
- maxWidth: 580,
- maxHeight: 500,
- child: AddCustomTokenView(),
- ),
+ builder: (context) => const DesktopDialog(
+ maxWidth: 580,
+ maxHeight: 500,
+ child: AddCustomTokenView(),
+ ),
);
} else {
- contract = await Navigator.of(
- context,
- ).pushNamed(AddCustomTokenView.routeName);
+ contract = await Navigator.of(context)
+ .pushNamed(AddCustomTokenView.routeName);
}
if (contract != null) {
@@ -137,17 +134,15 @@ class _AddWalletViewState extends ConsumerState<AddWalletView> {
if (isDesktop) {
token = await showDialog(
context: context,
- builder:
- (context) => const DesktopDialog(
- maxWidth: 580,
- maxHeight: 500,
- child: AddCustomSolanaTokenView(),
- ),
+ builder: (context) => const DesktopDialog(
+ maxWidth: 580,
+ maxHeight: 500,
+ child: AddCustomSolanaTokenView(),
+ ),
);
} else {
- token = await Navigator.of(
- context,
- ).pushNamed(AddCustomSolanaTokenView.routeName);
+ token = await Navigator.of(context)
+ .pushNamed(AddCustomSolanaTokenView.routeName);
}
if (token != null) {
@@ -176,11 +171,13 @@ class _AddWalletViewState extends ConsumerState<AddWalletView> {
}
if (AppConfig.coins.whereType<Ethereum>().isNotEmpty) {
- final contracts =
- MainDB.instance.getEthContracts().sortByName().findAllSync();
+ final contracts = MainDB.instance
+ .getEthContracts()
+ .sortByName()
+ .findAllSync();
if (contracts.isEmpty) {
- contracts.addAll(DefaultTokens.list);
+ contracts.addAll(AppConfig.defaultEthTokens);
MainDB.instance
.putEthContracts(contracts)
.then(
@@ -274,60 +271,60 @@ class _AddWalletViewState extends ConsumerState<AddWalletView> {
_searchTerm = value;
});
},
- style: STextStyles.desktopTextMedium(
- context,
- ).copyWith(height: 2),
- decoration: standardInputDecoration(
- "Search",
- _searchFocusNode,
- context,
- ).copyWith(
- contentPadding: const EdgeInsets.symmetric(
- vertical: 10,
- ),
- prefixIcon: Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 16,
- // vertical: 20,
- ),
- child: SvgPicture.asset(
- Assets.svg.search,
- width: 24,
- height: 24,
- color:
- Theme.of(context)
+ style: STextStyles.desktopTextMedium(context)
+ .copyWith(height: 2),
+ decoration:
+ standardInputDecoration(
+ "Search",
+ _searchFocusNode,
+ context,
+ ).copyWith(
+ contentPadding: const EdgeInsets.symmetric(
+ vertical: 10,
+ ),
+ prefixIcon: Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ // vertical: 20,
+ ),
+ child: SvgPicture.asset(
+ Assets.svg.search,
+ width: 24,
+ height: 24,
+ color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultSearchIconLeft,
- ),
- ),
- suffixIcon:
- _searchFieldController.text.isNotEmpty
+ ),
+ ),
+ suffixIcon:
+ _searchFieldController.text.isNotEmpty
? Padding(
- padding: const EdgeInsets.only(
- right: 10,
- ),
- child: UnconstrainedBox(
- child: Row(
- children: [
- TextFieldIconButton(
- child: const XIcon(
- width: 24,
- height: 24,
+ padding: const EdgeInsets.only(
+ right: 10,
+ ),
+ child: UnconstrainedBox(
+ child: Row(
+ children: [
+ TextFieldIconButton(
+ child: const XIcon(
+ width: 24,
+ height: 24,
+ ),
+ onTap: () async {
+ setState(() {
+ _searchFieldController
+ .text =
+ "";
+ _searchTerm = "";
+ });
+ },
),
- onTap: () async {
- setState(() {
- _searchFieldController
- .text = "";
- _searchTerm = "";
- });
- },
- ),
- ],
+ ],
+ ),
),
- ),
- )
+ )
: null,
- ),
+ ),
),
),
),
@@ -365,7 +362,10 @@ class _AddWalletViewState extends ConsumerState<AddWalletView> {
if (solTokenEntities.isNotEmpty)
ExpandingSubListItem(
title: "Solana tokens",
- entities: filter(_searchTerm, solTokenEntities),
+ entities: filter(
+ _searchTerm,
+ solTokenEntities,
+ ),
initialState: ExpandableState.expanded,
animationDurationMultiplier: 0.5,
trailing: AddCustomTokenSelector(
@@ -395,8 +395,9 @@ class _AddWalletViewState extends ConsumerState<AddWalletView> {
} else {
return Background(
child: Scaffold(
- backgroundColor:
- Theme.of(context).extension<StackColors>()!.background,
+ backgroundColor: Theme.of(context)
+ .extension<StackColors>()!
+ .background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
@@ -419,58 +420,61 @@ class _AddWalletViewState extends ConsumerState<AddWalletView> {
Constants.size.circularBorderRadius,
),
child: Semantics(
- label:
- "Search Text Field. Inputs Text To Search In Wallets.",
+ label: "Search Text Field. Inputs Text To Search In Wallets.",
excludeSemantics: true,
child: TextField(
autofocus: isDesktop,
autocorrect: !isDesktop,
enableSuggestions: !isDesktop,
controller: _searchFieldController,
focusNode: _searchFocusNode,
- onChanged:
- (value) => setState(() => _searchTerm = value),
+ onChanged: (value) =>
+ setState(() => _searchTerm = value),
style: STextStyles.field(context),
- decoration: standardInputDecoration(
- "Search",
- _searchFocusNode,
- context,
- desktopMed: isDesktop,
- ).copyWith(
- prefixIcon: Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 10,
- vertical: 16,
- ),
- child: SvgPicture.asset(
- Assets.svg.search,
- width: 16,
- height: 16,
- ),
- ),
- suffixIcon:
- _searchFieldController.text.isNotEmpty
+ decoration:
+ standardInputDecoration(
+ "Search",
+ _searchFocusNode,
+ context,
+ desktopMed: isDesktop,
+ ).copyWith(
+ prefixIcon: Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 10,
+ vertical: 16,
+ ),
+ child: SvgPicture.asset(
+ Assets.svg.search,
+ width: 16,
+ height: 16,
+ ),
+ ),
+ suffixIcon:
+ _searchFieldController.text.isNotEmpty
? Padding(
- padding: const EdgeInsets.only(right: 0),
- child: UnconstrainedBox(
- child: Row(
- children: [
- TextFieldIconButton(
- child: const XIcon(),
- onTap: () async {
- setState(() {
- _searchFieldController.text =
- "";
- _searchTerm = "";
- });
- },
- ),
- ],
+ padding: const EdgeInsets.only(
+ right: 0,
+ ),
+ child: UnconstrainedBox(
+ child: Row(
+ children: [
+ TextFieldIconButton(
+ child: const XIcon(),
+ onTap: () async {
+ setState(() {
+ _searchFieldController
+ .text =
+ "";
+ _searchTerm = "";
+ });
+ },
+ ),
+ ],
+ ),
),
- ),
- )
+ )
: null,
- ),
+ ),
),
),
),
### lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart
@@ -26,6 +26,7 @@ import '../../../../themes/coin_icon_provider.dart';
import '../../../../themes/stack_colors.dart';
import '../../../../utilities/assets.dart';
import '../../../../utilities/constants.dart';
+import '../../../../utilities/default_eth_tokens.dart';
import '../../../../utilities/text_styles.dart';
import '../../../../utilities/util.dart';
import '../../../../widgets/app_icon.dart';
@@ -137,40 +138,46 @@ class _CoinSelectItemState extends ConsumerState<CoinSelectItem> {
constraints: BoxConstraints(minHeight: isDesktop ? 70 : 0),
child: Row(
children: [
- tokenImageUri != null
- ? tokenImageUri!.toLowerCase().endsWith(".svg")
- ? SvgPicture.network(
- tokenImageUri!,
- width: 26,
- height: 26,
- placeholderBuilder: (_) =>
- const AppIcon(width: 26, height: 26),
- )
- : Image.network(
- tokenImageUri!,
- width: 26,
- height: 26,
- errorBuilder: (_, _, _) => SvgPicture.file(
- File(
- ref.watch(
- coinIconProvider(
- widget.entity.cryptoCurrency,
+ if (widget.entity is EthTokenEntity &&
+ (widget.entity as EthTokenEntity).token.address
+ .toLowerCase() ==
+ DefaultTokens.rsFiro.address)
+ SvgPicture.asset(Assets.svg.rsFiro, width: 26, height: 26)
+ else
+ tokenImageUri != null
+ ? tokenImageUri!.toLowerCase().endsWith(".svg")
+ ? SvgPicture.network(
+ tokenImageUri!,
+ width: 26,
+ height: 26,
+ placeholderBuilder: (_) =>
+ const AppIcon(width: 26, height: 26),
+ )
+ : Image.network(
+ tokenImageUri!,
+ width: 26,
+ height: 26,
+ errorBuilder: (_, _, _) => SvgPicture.file(
+ File(
+ ref.watch(
+ coinIconProvider(
+ widget.entity.cryptoCurrency,
+ ),
),
),
+ width: 26,
+ height: 26,
),
- width: 26,
- height: 26,
- ),
- )
- : SvgPicture.file(
- File(
- ref.watch(
- coinIconProvider(widget.entity.cryptoCurrency),
+ )
+ : SvgPicture.file(
+ File(
+ ref.watch(
+ coinIconProvider(widget.entity.cryptoCurrency),
+ ),
),
+ width: 26,
+ height: 26,
),
- width: 26,
- height: 26,
- ),
SizedBox(width: isDesktop ? 12 : 10),
Text(
"${widget.entity.name} (${widget.entity.ticker})",
@@ -187,9 +194,9 @@ class _CoinSelectItemState extends ConsumerState<CoinSelectItem> {
height: 24,
child: SvgPicture.asset(
Assets.svg.check,
- color: Theme.of(
- context,
- ).extension<StackColors>()!.accentColorDark,
+ color: Theme.of(context)
+ .extension<StackColors>()!
+ .accentColorDark,
),
),
),
### lib/pages/shopinbit/shopinbit_payment_shared.dart
@@ -20,9 +20,7 @@ import '../../widgets/background.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import 'shopinbit_send_from_view.dart';
-final String kShopInBitUsdtContractAddress = DefaultTokens.list
- .firstWhere((t) => t.symbol == "USDT")
- .address;
+final String kShopInBitUsdtContractAddress = DefaultTokens.usdt.address;
// Address + amount pulled out of one of the API's payment_links entries.
class ShopInBitPaymentTarget {
### lib/utilities/assets.dart
@@ -117,6 +117,8 @@ class _SVG {
final coinControl = const _COIN_CONTROL();
+ String get rsFiro => "assets/svg/rsfiro.svg";
+
String get peers => "assets/svg/peers.svg";
String get fusing => "assets/svg/fusing.svg";
String get upFromLine => "assets/svg/up-from-line.svg";
### lib/utilities/constants.dart
@@ -40,7 +40,7 @@ abstract class Constants {
// Enable Logger.print statements
static const bool disableLogger = false;
- static const int currentDataVersion = 16;
+ static const int currentDataVersion = 17;
static const int rescanV1 = 1;
### lib/utilities/default_eth_tokens.dart
@@ -11,41 +11,46 @@
import '../models/isar/models/ethereum/eth_contract.dart';
abstract class DefaultTokens {
- static List<EthContract> list = [
- EthContract(
- address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
- name: "USD Coin",
- symbol: "USDC",
- decimals: 6,
- type: EthContractType.erc20,
- ),
- EthContract(
- address: "0xdac17f958d2ee523a2206206994597c13d831ec7",
- name: "Tether",
- symbol: "USDT",
- decimals: 6,
- type: EthContractType.erc20,
- ),
- EthContract(
- address: "0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce",
- name: "Shiba Inu",
- symbol: "SHIB",
- decimals: 18,
- type: EthContractType.erc20,
- ),
- EthContract(
- address: "0x514910771af9ca656af840dff83e8264ecf986ca",
- name: "Chainlink",
- symbol: "LINK",
- decimals: 18,
- type: EthContractType.erc20,
- ),
- EthContract(
- address: "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984",
- name: "Uniswap",
- symbol: "UNI",
- decimals: 18,
- type: EthContractType.erc20,
- ),
- ];
+ static final usdc = EthContract(
+ address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
+ name: "USD Coin",
+ symbol: "USDC",
+ decimals: 6,
+ type: EthContractType.erc20,
+ );
+ static final usdt = EthContract(
+ address: "0xdac17f958d2ee523a2206206994597c13d831ec7",
+ name: "Tether",
+ symbol: "USDT",
+ decimals: 6,
+ type: EthContractType.erc20,
+ );
+ static final shib = EthContract(
+ address: "0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce",
+ name: "Shiba Inu",
+ symbol: "SHIB",
+ decimals: 18,
+ type: EthContractType.erc20,
+ );
+ static final link = EthContract(
+ address: "0x514910771af9ca656af840dff83e8264ecf986ca",
+ name: "Chainlink",
+ symbol: "LINK",
+ decimals: 18,
+ type: EthContractType.erc20,
+ );
+ static final uni = EthContract(
+ address: "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984",
+ name: "Uniswap",
+ symbol: "UNI",
+ decimals: 18,
+ type: EthContractType.erc20,
+ );
+ static final rsFiro = EthContract(
+ address: "0x2744ea5ac9b11cb5e3cd63d3a88e858336aeddc2",
+ name: "rsFIRO",
+ symbol: "rsFIRO",
+ decimals: 8,
+ type: EthContractType.erc20,
+ );
}
### lib/widgets/icon_widgets/eth_token_icon.dart
@@ -19,6 +19,8 @@ import '../../models/isar/exchange_cache/currency.dart';
import '../../services/exchange/change_now/change_now_exchange.dart';
import '../../services/exchange/exchange_data_loading_service.dart';
import '../../themes/coin_icon_provider.dart';
+import '../../utilities/assets.dart';
+import '../../utilities/default_eth_tokens.dart';
import '../../wallets/crypto_currency/crypto_currency.dart';
import '../loading_indicator.dart';
@@ -38,10 +40,13 @@ class EthTokenIcon extends ConsumerStatefulWidget {
class _EthTokenIconState extends ConsumerState<EthTokenIcon> {
String? imageUrl;
+ bool get _isRsFiro =>
+ widget.contractAddress.toLowerCase() == DefaultTokens.rsFiro.address;
@override
void initState() {
super.initState();
+ if (_isRsFiro) return;
ExchangeDataLoadingService.instance.isar.then((isar) async {
final currency = await isar.currencies
@@ -67,6 +72,14 @@ class _EthTokenIconState extends ConsumerState<EthTokenIcon> {
@override
Widget build(BuildContext context) {
+ if (_isRsFiro) {
+ return SvgPicture.asset(
+ Assets.svg.rsFiro,
+ width: widget.size,
+ height: widget.size,
+ );
+ }
+
if (imageUrl == null || imageUrl!.isEmpty) {
return SvgPicture.file(
File(ref.watch(coinIconProvider(Ethereum(.main)))),
### scripts/app_config/configure_campfire.sh
@@ -75,8 +75,15 @@ const ({String light, String dark})? _appIconAsset = (
final List<CryptoCurrency> _supportedCoins = List.unmodifiable([
Firo(CryptoCurrencyNetwork.main),
+ Ethereum(CryptoCurrencyNetwork.main),
]);
+final List<EthContract> _defaultEthTokens = [
+ DefaultTokens.rsFiro,
+ DefaultTokens.usdc,
+ DefaultTokens.usdt,
+];
+
final ({String from, String fromFuzzyNet, String to, String toFuzzyNet})
_swapDefaults = (
from: "BTC",
@@ -85,4 +92,4 @@ _swapDefaults = (
toFuzzyNet: "firo",
);
-EOF
\ No newline at end of file
+EOF
### scripts/app_config/configure_stack_duo.sh
@@ -80,6 +80,8 @@ final List<CryptoCurrency> _supportedCoins = List.unmodifiable([
BitcoinFrost(CryptoCurrencyNetwork.test4),
]);
+const List<EthContract> _defaultEthTokens = [];
+
final ({String from, String fromFuzzyNet, String to, String toFuzzyNet})
_swapDefaults = (
from: "BTC",
@@ -88,4 +90,4 @@ _swapDefaults = (
toFuzzyNet: "xmr",
);
-EOF
\ No newline at end of file
+EOF
### scripts/app_config/configure_stack_wallet.sh
@@ -135,6 +135,15 @@ final List<CryptoCurrency> _supportedCoins = List.unmodifiable([
Xelis(CryptoCurrencyNetwork.test),
]);
+final List<EthContract> _defaultEthTokens = [
+ DefaultTokens.usdc,
+ DefaultTokens.usdt,
+ DefaultTokens.shib,
+ DefaultTokens.link,
+ DefaultTokens.uni,
+ DefaultTokens.rsFiro,
+];
+
final ({String from, String fromFuzzyNet, String to, String toFuzzyNet})
_swapDefaults = (
from: "BTC",
@@ -143,4 +152,4 @@ _swapDefaults = (
toFuzzyNet: "xmr",
);
-EOF
\ No newline at end of file
+EOF
### scripts/ensure_test_app_config.sh
@@ -80,6 +80,15 @@ final List<CryptoCurrency> _supportedCoins = List.unmodifiable([
Xelis(CryptoCurrencyNetwork.test),
]);
+final List<EthContract> _defaultEthTokens = [
+ DefaultTokens.usdc,
+ DefaultTokens.usdt,
+ DefaultTokens.shib,
+ DefaultTokens.link,
+ DefaultTokens.uni,
+ DefaultTokens.rsFiro,
+];
+
final ({String from, String fromFuzzyNet, String to, String toFuzzyNet})
_swapDefaults = (
from: "BTC",
### test/db_version_migration_test.dart
@@ -1,14 +1,61 @@
-// import 'package:flutter_test/flutter_test.dart';
-// import 'package:hive_test/hive_test.dart';
+import 'dart:io';
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:isar_community/isar.dart';
+import 'package:stackwallet/db/db_version_migration.dart';
+import 'package:stackwallet/db/hive/db.dart';
+import 'package:stackwallet/db/isar/main_db.dart';
+import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart';
+import 'package:stackwallet/utilities/constants.dart';
+import 'package:stackwallet/utilities/default_eth_tokens.dart';
+import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
+
+import 'hive/hive_ce_test_utils.dart';
void main() {
- // setUp(() async {
- // await setUpTestHive();
- // });
- //
- // // no migration to test yet
- //
- // tearDown(() async {
- // await tearDownTestHive();
- // });
+ test('v17 adds rsFIRO once and preserves the token catalog', () async {
+ await setUpHiveCeTest();
+ addTearDown(tearDownHiveCeTest);
+ final info = await DB.instance.hive.openBox<dynamic>(DB.boxNameDBInfo);
+ final directory = await Directory.systemTemp.createTemp(
+ 'rsfiro_migration_',
+ );
+ addTearDown(() => directory.delete(recursive: true));
+ await Isar.initializeIsarCore(download: true);
+ final isar = await Isar.open(
+ [EthContractSchema],
+ directory: directory.path,
+ inspector: false,
+ );
+ addTearDown(() => isar.close(deleteFromDisk: true));
+ final db = MainDB.instance;
+ await db.initMainDB(mock: isar);
+ Future<void> migrate(int version) =>
+ DbVersionMigrator().migrate(version, secureStore: FakeSecureStorage());
+
+ await migrate(16);
+ expect(Constants.currentDataVersion, 17);
+ expect(info.get('hive_data_version'), 17);
+ expect(await db.getEthContracts().isEmpty(), isTrue);
+
+ final custom = DefaultTokens.usdc.copyWith(name: 'Custom token');
+ await db.putEthContract(custom);
+ await migrate(16);
+ expect(await db.getEthContracts().count(), 2);
+ expect((await db.getEthContract(custom.address))!.name, 'Custom token');
+
+ final existing = (await db.getEthContract(DefaultTokens.rsFiro.address))!
+ .copyWith(
+ address: DefaultTokens.rsFiro.address.toUpperCase(),
+ abi: 'keep',
+ );
+ await db.putEthContract(existing);
+ await migrate(16);
+ expect(await db.getEthContracts().count(), 2);
+ expect((await db.getEthContract(existing.address))!.abi, 'keep');
+
+ await isar.writeTxn(() => isar.ethContracts.delete(existing.id));
+ await migrate(info.get('hive_data_version') as int);
+ expect(await db.getEthContracts().count(), 1);
+ });
}
### test/widgets/eth_token_icon_test.dart
@@ -0,0 +1,32 @@
+import 'package:flutter/widgets.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:stackwallet/utilities/assets.dart';
+import 'package:stackwallet/utilities/default_eth_tokens.dart';
+import 'package:stackwallet/widgets/icon_widgets/eth_token_icon.dart';
+
+void main() {
+ testWidgets('rsFIRO uses the per-app SVG by contract address', (
+ tester,
+ ) async {
+ final token = DefaultTokens.rsFiro;
+ expect(token.address, '0x2744ea5ac9b11cb5e3cd63d3a88e858336aeddc2');
+ expect(token.decimals, 8);
+ await tester.pumpWidget(
+ ProviderScope(
+ child: Center(
+ child: EthTokenIcon(
+ contractAddress: token.address.toUpperCase(),
+ size: 26,
+ ),
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+ final svg = tester.widget<SvgPicture>(find.byType(SvgPicture));
+ expect((svg.bytesLoader as SvgAssetLoader).assetName, Assets.svg.rsFiro);
+ expect(tester.getSize(find.byType(SvgPicture)), const Size.square(26));
+ expect(tester.takeException(), isNull);
+ });
+}Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.