feat: load rsFIRO icons from per-app asset sources
What changed, and why it matters
This commit simply swaps the source of the rsFIRO token icon from a network-downloaded image to a local SVG file bundled inside the app. It is a cosmetic/user-interface change with no security implications.
No security action needed; treat as routine UI/asset maintenance.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch adds per-app rsfiro.svg assets and updates three UI widgets (AddTokenListElement, CoinSelectItem, EthTokenIcon) to render a local SVG when the Ethereum contract address matches DefaultTokens.rsFiro. It also adds a widget test verifying the asset path. No cryptographic, networking, or permission changes are present.
Changed components
asset_sources/svg/campfire/rsfiro.svgasset_sources/svg/stack_duo/rsfiro.svgasset_sources/svg/stack_wallet/rsfiro.svglib/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/utilities/assets.dartlib/widgets/icon_widgets/eth_token_icon.darttest/widgets/eth_token_icon_test.dartInspect captured patch +98 / −2
### asset_sources/svg/campfire/rsfiro.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 254.12 254.12">
+<defs>
+<linearGradient id="linear-gradient" x1="56.64" y1="43.13" x2="197.49" y2="210.99" gradientUnits="userSpaceOnUse">
+<stop offset="0" stop-color="#dd704f"/>
+<stop offset="1" stop-color="#8f314f"/>
+<stop offset="1" stop-color="#ff9a03"/>
+</linearGradient>
+</defs>
+<g id="Layer_1-2">
+<circle fill="#fff" cx="127.06" cy="127.06" r="127.06"/>
+<path fill="url(#linear-gradient)" 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.65H28.53ZM172.28,79.13c-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"/>
+<path fill="#fff" d="M45.95,47.64h11.48l.41,8.22h.82c1.09-3.36,2.82-5.69,5.18-7,2.36-1.31,5.52-1.96,9.48-1.96v11.3c-3.89,0-6.92.53-9.07,1.59-2.15,1.06-3.69,2.77-4.61,5.13-.92,2.37-1.38,5.63-1.38,9.8v20.54h-12.3v-47.61Z"/>
+<path fill="#fff" 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.17c-.24-.62-.67-1.06-1.28-1.31-.61-.25-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.87,1.54-2.05,3.67-3.47,6.41-4.25,2.73-.78,6.18-1.17,10.35-1.17,2.6,0,5.35.14,8.25.42,2.9.28,5.28.64,7.12,1.07v8.78c-4.58-.62-9.19-.93-13.84-.93-2.32,0-4.08.11-5.28.33-1.2.22-2.05.69-2.56,1.4-.51.72-.77,1.79-.77,3.22s.08,2.35.26,2.94c.17.59.55,1.01,1.13,1.26.58.25,1.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.34-1.4,2.18-3.38,3.66-5.94,4.43s-5.89,1.17-9.99,1.17c-2.73,0-5.74-.13-9.02-.37Z"/>
+</g>
+</svg>
### asset_sources/svg/stack_duo/rsfiro.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 254.12 254.12"/>
### asset_sources/svg/stack_wallet/rsfiro.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 254.12 254.12">
+<defs>
+<linearGradient id="linear-gradient" x1="56.64" y1="43.13" x2="197.49" y2="210.99" gradientUnits="userSpaceOnUse">
+<stop offset="0" stop-color="#dd704f"/>
+<stop offset="1" stop-color="#8f314f"/>
+<stop offset="1" stop-color="#ff9a03"/>
+</linearGradient>
+</defs>
+<g id="Layer_1-2">
+<circle fill="#fff" cx="127.06" cy="127.06" r="127.06"/>
+<path fill="url(#linear-gradient)" 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.65H28.53ZM172.28,79.13c-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"/>
+<path fill="#fff" d="M45.95,47.64h11.48l.41,8.22h.82c1.09-3.36,2.82-5.69,5.18-7,2.36-1.31,5.52-1.96,9.48-1.96v11.3c-3.89,0-6.92.53-9.07,1.59-2.15,1.06-3.69,2.77-4.61,5.13-.92,2.37-1.38,5.63-1.38,9.8v20.54h-12.3v-47.61Z"/>
+<path fill="#fff" 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.17c-.24-.62-.67-1.06-1.28-1.31-.61-.25-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.87,1.54-2.05,3.67-3.47,6.41-4.25,2.73-.78,6.18-1.17,10.35-1.17,2.6,0,5.35.14,8.25.42,2.9.28,5.28.64,7.12,1.07v8.78c-4.58-.62-9.19-.93-13.84-.93-2.32,0-4.08.11-5.28.33-1.2.22-2.05.69-2.56,1.4-.51.72-.77,1.79-.77,3.22s.08,2.35.26,2.94c.17.59.55,1.01,1.13,1.26.58.25,1.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.34-1.4,2.18-3.38,3.66-5.94,4.43s-5.89,1.17-9.99,1.17c-2.73,0-5.74-.13-9.02-.37Z"/>
+</g>
+</svg>
### 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';
@@ -95,7 +96,16 @@ class _AddTokenListElementState extends ConsumerState<AddTokenListElement> {
children: [
Row(
children: [
- currency != null
+ 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,
### 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,7 +138,12 @@ class _CoinSelectItemState extends ConsumerState<CoinSelectItem> {
constraints: BoxConstraints(minHeight: isDesktop ? 70 : 0),
child: Row(
children: [
- tokenImageUri != null
+ 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!,
### 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/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)))),
### 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 15/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.