Merge branch 'staging' into masternode-payout-ui
What changed, and why it matters
This commit is a routine feature merge that adds support for a new Ethereum token called rsFIRO across several app variants. It updates token lists, adds an icon, and includes a database migration so existing users automatically see the new token. There is no indication this change fixes a security vulnerability or introduces a known dangerous behavior.
No security action required. Treat as a normal feature merge. If reviewing for release, verify the rsFIRO contract address and decimals (8) against an authoritative source, and confirm the migration preserves user-added custom tokens as the included test suggests.
Security signals we found
No security-relevant code changes observed
New asset and token configuration only
Database migration is additive and idempotent (checks for existing contract before insert)
No changes to networking, parsing, cryptography, or permissions
Evidence from the diff
The merge from ‘staging’ into ‘masternode-payout-ui’ primarily introduces rsFIRO (0x2744ea5ac9b11cb5e3cd63d3a88e858336aeddc2) as a default ERC-20 token. It refactors DefaultTokens from a single list into named static fields, wires AppConfig.defaultEthTokens per app flavor, adds a DB migration from version 16 to 17 that inserts rsFIRO only if the app config includes it and no matching contract exists, and updates UI widgets to render a bundled SVG icon for rsFIRO. The diff also includes tests validating the migration and icon behavior. No security-relevant fixes (e.g., input validation, cryptography, authorization) are visible.
Changed components
lib/utilities/default_eth_tokens.dartlib/app_config.dartlib/db/db_version_migration.dartlib/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.dartscripts/app_config/configure_campfire.shscripts/app_config/configure_stack_wallet.shscripts/app_config/configure_stack_duo.shscripts/ensure_test_app_config.shInspect 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.