Add Robinhood Chain (#3398)

* fix android CI

* feat: Add Robinhood Chain, all major flows done. minor issue now is our providers don't support it yet as its still pretty early, two weeks from launch

* feat: Add Robinhood Chain, all major flows done. minor issue now is our providers don't support it yet as its still pretty early, two weeks from launch

* Cleanup

* auto-reformat

* fix ui issue and contact listing for tokens

* fix: missing icons and crash for fee priority in settings

* chore: cleanup

* fixes from feedbacks

* fix: review issues and sync to latest dev
feat: switch history api to etherscan, add history toggle to privacy settings, exclude robETH from exchange pickers, skip tokens discovery for chains that moralis hasn't indexed, add tests

* fix: address Robinhood review comments

* fix: show Solana token icons in history

* fix: address review comments and default Robinhood to PublicNode

* fix:  revert filter and refuse unmapped networks in provider

---------

Co-authored-by: Robert Malikowski <malikowskirobert@gmail.com>
This commit is contained in:
David Adegoke
2026-10-04 13:22:05 +02:00
committed by GitHub
co-authored by Robert Malikowski
parent 77e4b946eb
commit 046e57c598
143 changed files with 1214 additions and 159 deletions
+1
View File
@@ -137,6 +137,7 @@ class DFXBuyProvider extends BuyProvider {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
case WalletType.solana:
case WalletType.tron:
return wallet.signMessage(message);
@@ -108,6 +108,7 @@ class RobinhoodBuyProvider extends BuyProvider {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
case WalletType.solana:
case WalletType.tron:
case WalletType.dogecoin:
+3
View File
@@ -113,6 +113,7 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.eth:
case CryptoCurrency.baseEth:
case CryptoCurrency.arbEth:
case CryptoCurrency.robEth:
case CryptoCurrency.mana:
case CryptoCurrency.matic:
case CryptoCurrency.maticpoly:
@@ -271,6 +272,7 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.avaxc:
case CryptoCurrency.baseEth:
case CryptoCurrency.arbEth:
case CryptoCurrency.robEth:
return [42];
case CryptoCurrency.bch:
return [42, 54, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35];
@@ -382,6 +384,7 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.maticpoly:
case CryptoCurrency.baseEth:
case CryptoCurrency.arbEth:
case CryptoCurrency.robEth:
case CryptoCurrency.arb:
pattern = '0x[0-9a-zA-Z]+';
case CryptoCurrency.nano:
+1
View File
@@ -37,6 +37,7 @@ class SeedValidator extends Validator<MnemonicItem> {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
return evm!.getEVMWordList(language);
case WalletType.bitcoinCash:
return getBitcoinWordList(language);
+1
View File
@@ -81,6 +81,7 @@ class WalletCreationService {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
case WalletType.solana:
case WalletType.tron:
case WalletType.dogecoin:
+1
View File
@@ -1234,6 +1234,7 @@ Future<void> setup({
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
return evm!.createEVMWalletService(param1, SettingsStoreBase.walletPasswordDirectInput);
case WalletType.bitcoinCash:
return bitcoinCash!.createBitcoinCashWalletService(
@@ -64,6 +64,7 @@ const dogecoinDefaultNodeUri = 'dogecoin.stackwallet.com:50022';
const baseDefaultNodeUri = 'base-rpc.publicnode.com';
const arbitrumDefaultNodeUri = 'arbitrum-one-rpc.publicnode.com';
const bscDefaultNodeUri = 'bsc-dataseed.bnbchain.org';
const robinhoodDefaultNodeUri = "robinhood-rpc.publicnode.com";
const zcashDefaultNodeUri = 'zec.rocks:443';
Future<void> defaultSettingsMigration(
@@ -660,6 +661,12 @@ Future<void> defaultSettingsMigration(
break;
case 72:
await createDefaultChartsData();
await addWalletNodeList(type: WalletType.robinhood);
await _changeDefaultNode(
sharedPreferences: sharedPreferences,
type: WalletType.robinhood,
currentNodePreferenceKey: PreferencesKey.currentRobinhoodNodeIdKey,
);
break;
default:
break;
+1
View File
@@ -16,6 +16,7 @@ const Map<WalletType, String> nodePreferenceKeys = {
WalletType.base: PreferencesKey.currentBaseNodeIdKey,
WalletType.arbitrum: PreferencesKey.currentArbitrumNodeIdKey,
WalletType.bsc: PreferencesKey.currentBscNodeIdKey,
WalletType.robinhood: PreferencesKey.currentRobinhoodNodeIdKey,
WalletType.nano: PreferencesKey.currentNanoNodeIdKey,
WalletType.decred: PreferencesKey.currentDecredNodeIdKey,
WalletType.bitcoinCash: PreferencesKey.currentBitcoinCashNodeIdKey,
+2
View File
@@ -13,6 +13,7 @@ class PreferencesKey {
static const currentBaseNodeIdKey = 'current_node_id_base';
static const currentArbitrumNodeIdKey = 'current_node_id_arbitrum';
static const currentBscNodeIdKey = 'current_node_id_bsc';
static const currentRobinhoodNodeIdKey = "current_node_id_robinhood";
static const currentNanoNodeIdKey = 'current_node_id_nano';
static const currentNanoPowNodeIdKey = 'current_node_id_nano_pow';
static const currentDecredNodeIdKey = 'current_node_id_decred';
@@ -92,6 +93,7 @@ class PreferencesKey {
static const useBaseScan = 'use_base_scan';
static const useArbiScan = 'use_arbitrum_scan';
static const useBscScan = 'use_bscscan';
static const useRobinhoodScan = "use_robinhood_scan";
static const useTronGrid = 'use_trongrid';
static const useMempoolFeeAPI = 'use_mempool_fee_api';
static const evmHiddenChainIds = 'evm_hidden_chain_ids';
@@ -30,6 +30,7 @@ List<TransactionPriority> priorityForWalletType(WalletType type) {
case WalletType.dogecoin:
return dogecoin!.getTransactionPriorities();
case WalletType.arbitrum:
case WalletType.robinhood:
case WalletType.nano:
case WalletType.banano:
case WalletType.solana:
+5 -2
View File
@@ -199,10 +199,13 @@ class CWEVM extends EVM {
@override
Future<Erc20Token?> getErc20Token(WalletBase wallet, String contractAddress) {
final evmWallet = wallet as EVMChainWallet;
final chainName = EVMChainUtils.getDefaultTokenSymbol(evmWallet.selectedChainId).toLowerCase();
return evmWallet.getErc20Token(contractAddress, chainName);
return evmWallet.getErc20Token(contractAddress, getMoralisChainName(wallet));
}
@override
String? getMoralisChainName(WalletBase wallet) =>
EVMChainUtils.getMoralisChainName((wallet as EVMChainWallet).selectedChainId);
@override
CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) {
transaction as EVMChainTransactionInfo;
@@ -356,6 +356,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
return 'cchain';
case 'ARB':
return 'arbitrum';
case "ROB":
return "hood";
default:
return tag.toLowerCase();
}
@@ -366,6 +368,7 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
'POLY' => 'MATIC',
'AVAXC' => 'CCHAIN',
'ARBITRUM' => 'ARB',
"HOOD" => "ROB",
_ => network,
};
}
@@ -50,6 +50,7 @@ class NearIntentsExchangeProvider extends ExchangeProvider {
'AVAXC': '0x1111111111111111111111111111111111111111',
'ARB': '0x1111111111111111111111111111111111111111',
'BASE': '0x1111111111111111111111111111111111111111',
"ROB": "0x1111111111111111111111111111111111111111",
// Others
'SOL': '11111111111111111111111111111111',
@@ -516,6 +517,7 @@ class NearIntentsExchangeProvider extends ExchangeProvider {
return switch (tag) {
'TRX' => 'tron',
'AVAXC' => 'avax',
"ROB" => "hood",
_ => tag?.toLowerCase(),
};
}
@@ -524,6 +526,7 @@ class NearIntentsExchangeProvider extends ExchangeProvider {
return switch (blockchain) {
'tron' => 'TRX',
'avax' => 'AVAXC',
"hood" => "ROB",
_ => blockchain?.toUpperCase(),
};
}
@@ -457,6 +457,8 @@ class SideShiftExchangeProvider extends ExchangeProvider {
return 'zcash';
case 'AVAXC':
return 'avax';
case "ROB":
return "robinhood";
default:
return tag.toLowerCase();
}
@@ -54,6 +54,10 @@ class SwapTradeExchangeProvider extends ExchangeProvider {
required CryptoCurrency to,
required bool isFixedRateMode,
}) async {
if (!_isPairSupported(from, to)) {
return null;
}
try {
final uri = Uri.https(apiAuthority, getCoins);
final response = await ProxyWrapper().get(clearnetUri: uri);
@@ -96,6 +100,7 @@ class SwapTradeExchangeProvider extends ExchangeProvider {
required bool isReceiveAmount}) async {
try {
if (amount == 0) return 0.0;
if (!_isPairSupported(from, to)) return 0.0;
if (isFixedRateMode && !supportsFixedRate) {
return 0.0;
}
@@ -191,13 +196,24 @@ class SwapTradeExchangeProvider extends ExchangeProvider {
required bool isFixedRateMode,
required bool isSendAll,
}) async {
final sendNetwork = _networkFor(request.fromCurrency);
final receiveNetwork = _networkFor(request.toCurrency);
if (sendNetwork == null || receiveNetwork == null) {
throw TradeNotCreatedException(
description,
description: "SwapTrade has no network for "
"${request.fromCurrency.title} ${request.fromCurrency.tag ?? ""} or "
"${request.toCurrency.title} ${request.toCurrency.tag ?? ""}",
);
}
try {
final params = <String, dynamic>{};
var body = <String, dynamic>{
'coin_send': _normalizeCurrency(request.fromCurrency),
'coin_send_network': _networkFor(request.fromCurrency),
'coin_send_network': sendNetwork,
'coin_receive': _normalizeCurrency(request.toCurrency),
'coin_receive_network': _networkFor(request.toCurrency),
'coin_receive_network': receiveNetwork,
'amount_send': request.fromAmount,
'recipient': request.toAddress,
'ref': 'cake',
@@ -396,22 +412,16 @@ class SwapTradeExchangeProvider extends ExchangeProvider {
}
}
String _networkFor(CryptoCurrency currency) {
final network = switch (currency) {
CryptoCurrency.eth => 'ETH',
CryptoCurrency.bnb => 'BNB_BSC',
CryptoCurrency.usdterc20 => 'USDT_ERC20',
CryptoCurrency.usdttrc20 => 'TRX_USDT_S2UZ',
CryptoCurrency.usdtbsc => 'USDT_BSC',
CryptoCurrency.sol => 'SOL',
CryptoCurrency.btc => 'BTC',
CryptoCurrency.xmr => 'XMR',
CryptoCurrency.ltc => 'LTC',
CryptoCurrency.ada => 'ADA',
CryptoCurrency.bch => 'BCH',
CryptoCurrency.zec => 'ZEC',
_ => currency.title.toUpperCase(),
};
return network;
}
bool _isPairSupported(CryptoCurrency from, CryptoCurrency to) =>
_networkFor(from) != null && _networkFor(to) != null;
String? _networkFor(CryptoCurrency currency) =>
switch ((currency.title.toUpperCase(), currency.tag?.toUpperCase())) {
("BNB", "BSC") => "BNB_BSC",
("USDT", "ETH") => "USDT_ERC20",
("USDT", "TRX") => "TRX_USDT_S2UZ",
("USDT", "BSC") => "USDT_BSC",
(final title, null) => title,
_ => null,
};
}
+4 -1
View File
@@ -612,7 +612,10 @@ class _NewSendPageState extends State<NewSendPage> {
subtitle:
"~${output.estimatedFee} ${widget.sendViewModel.currencySymbol} (${output.estimatedFeeFiatAmount} ${widget.sendViewModel.fiatCurrency})",
// Without fee priorities the row does nothing,
// so it must not be announced as interactive.
// so it must not be announced as interactive or
// show a tappable arrow.
showArrow: widget
.sendViewModel.feesViewModel.hasFeesPriority,
onTap: widget.sendViewModel.feesViewModel
.hasFeesPriority
? () =>
@@ -481,6 +481,7 @@ class _MultiNetworkPickerBodyState extends State<_MultiNetworkPickerBody> {
String _shortChainLabel(CryptoCurrency c) {
if (cryptoCurrencyOrTokenToWalletType(c) == WalletType.bsc) return 'BSC';
if (cryptoCurrencyOrTokenToWalletType(c) == WalletType.robinhood) return "ROB";
return chainNameForCurrency(c);
}
@@ -493,7 +494,7 @@ class _MultiNetworkPickerBodyState extends State<_MultiNetworkPickerBody> {
}
bool _isL2NativeEth(CryptoCurrency c) =>
c == CryptoCurrency.arbEth || c == CryptoCurrency.baseEth;
c == CryptoCurrency.arbEth || c == CryptoCurrency.baseEth || c == CryptoCurrency.robEth;
_SelSection? _selectedSection({
required List<CryptoCurrency> recents,
@@ -5,6 +5,7 @@ import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cw_core/crypto_currency.dart';
import "package:cw_core/currency_for_wallet_type.dart";
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
@@ -36,6 +37,7 @@ class ReceiveInfoBox extends StatelessWidget {
case WalletType.polygon:
case WalletType.zano:
case WalletType.bsc:
case WalletType.robinhood:
if (autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.disabled) return null;
return ReceiveInfoBox(
iconPath: "",
@@ -43,8 +45,7 @@ class ReceiveInfoBox extends StatelessWidget {
onDismissed: onDismissed,
bottomWidget: InfoboxCurrencyRow(
currencies: supportedCurrencies ?? [],
chainIconPath:
"assets/new-ui/chain_badges/${walletTypeToString(type).toLowerCase()}.svg",
chainIconPath: walletTypeToCryptoCurrency(type).chainIconPath ?? "",
));
default:
if (autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.disabled) return null;
@@ -1,9 +1,9 @@
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
import "package:cw_core/currency_for_wallet_type.dart";
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter_svg/flutter_svg.dart';
class ReceiveTokenDisplay extends StatelessWidget {
const ReceiveTokenDisplay({super.key, required this.addressListViewModel});
@@ -35,7 +35,7 @@ class ReceiveTokenDisplay extends StatelessWidget {
children: [
CakeImageWidget(
imageUrl:
"assets/new-ui/chain_badges/${addressListViewModel.wallet.type == WalletType.bsc ? 'bnb' : walletTypeToString(addressListViewModel.wallet.type).toLowerCase()}.svg",
walletTypeToCryptoCurrency(addressListViewModel.wallet.type).chainIconPath,
width: 16,
height: 16,
colorFilter:
+5
View File
@@ -11,6 +11,7 @@ const List<WalletType> walletConnectCompatibleChains = [
WalletType.base,
WalletType.arbitrum,
WalletType.bsc,
WalletType.robinhood,
WalletType.solana,
];
@@ -39,6 +40,7 @@ bool isEVMCompatibleChain(WalletType walletType) {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
return true;
default:
return false;
@@ -93,6 +95,8 @@ String getChainNameSpaceAndIdBasedOnWalletType(WalletType walletType, {int? chai
return EVMChainId.arbitrum.chain();
case WalletType.bsc:
return EVMChainId.bsc.chain();
case WalletType.robinhood:
return EVMChainId.robinhood.chain();
case WalletType.solana:
return SolanaChainId.mainnet.chain();
default:
@@ -107,6 +111,7 @@ List<String> getChainSupportedMethodsOnWalletType(WalletType walletType) {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
return EVMSupportedMethods.values.map((e) => e.name).toList();
case WalletType.solana:
return SolanaSupportedMethods.values.map((e) => e.name).toList();
+3
View File
@@ -7,6 +7,7 @@ bool isBIP39Wallet(WalletType walletType) {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
case WalletType.solana:
case WalletType.tron:
case WalletType.bitcoin:
@@ -49,6 +50,7 @@ bool hasTokens(WalletType walletType) {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
return true;
default:
return false;
@@ -61,6 +63,7 @@ String tokenStandardFor(WalletType walletType) {
case WalletType.polygon:
case WalletType.arbitrum:
case WalletType.base:
case WalletType.robinhood:
return 'ERC-20';
case WalletType.bsc:
return 'BEP-20';
@@ -388,9 +388,12 @@ class _ContactListBodyState extends State<ContactListBody> {
}
Widget generateContactRaw(BuildContext context, ContactRecord contact, bool isLast) {
final image = contact.type == CryptoCurrency.baseEth
? 'assets/new-ui/crypto_full_icons/base.svg'
: contact.type.iconPath;
const l2NativeChainIcons = {
CryptoCurrency.baseEth: "assets/new-ui/crypto_full_icons/base.svg",
CryptoCurrency.arbEth: "assets/new-ui/crypto_full_icons/arbitrum.svg",
CryptoCurrency.robEth: "assets/new-ui/crypto_full_icons/robinhood.svg",
};
final image = l2NativeChainIcons[contact.type] ?? contact.type.iconPath;
final currencyIcon = (image != null && image.isNotEmpty)
? CakeImageWidget(imageUrl: image, height: 24, width: 24)
: const SizedBox(height: 24, width: 24);
@@ -2,6 +2,7 @@ import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/setting_action_button.dart';
import 'package:cake_wallet/src/widgets/setting_actions.dart';
import 'package:flutter/material.dart';
import "package:flutter_svg/svg.dart";
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
@@ -36,6 +37,7 @@ class MenuWidgetState extends State<MenuWidget> {
this.baseIcon = Image.asset('assets/new-ui/crypto_full_icons/base.svg'),
this.arbitrumIcon = Image.asset('assets/new-ui/crypto_full_icons/arbitrum.svg'),
this.bscIcon = Image.asset('assets/new-ui/crypto_full_icons/bnb.svg'),
this.robinhoodIcon = SvgPicture.asset("assets/new-ui/crypto_full_icons/robinhood.svg"),
this.solanaIcon = Image.asset('assets/new-ui/crypto_full_icons/solana.svg'),
this.tronIcon = Image.asset('assets/new-ui/crypto_full_icons/tron.svg'),
this.wowneroIcon = Image.asset('assets/new-ui/crypto_full_icons/wownero.svg'),
@@ -67,6 +69,7 @@ class MenuWidgetState extends State<MenuWidget> {
Image baseIcon;
Image arbitrumIcon;
Image bscIcon;
Widget robinhoodIcon;
Image solanaIcon;
Image tronIcon;
Image wowneroIcon;
@@ -261,6 +264,8 @@ class MenuWidgetState extends State<MenuWidget> {
return arbitrumIcon;
case WalletType.bsc:
return bscIcon;
case WalletType.robinhood:
return robinhoodIcon;
case WalletType.tron:
return tronIcon;
case WalletType.wownero:
@@ -93,6 +93,14 @@ class ConnectionSyncPage extends BasePage {
onChanged: (val) {
_connectionSyncViewModel.setUseBaseScan(val);
}),
if (_connectionSyncViewModel.canUseRobinhoodScan)
ListItemToggle(
keyValue: "can_use_robinhoodscan",
label: S.of(context).robinhoodscan_history,
value: _connectionSyncViewModel.useRobinhoodScan,
onChanged: (val) {
_connectionSyncViewModel.setUseRobinhoodScan(val);
}),
if (_connectionSyncViewModel.canUseArbiScan)
ListItemToggle(
keyValue: "can_use_arbiscan",
@@ -6,6 +6,7 @@ enum EVMChainId {
base,
arbitrum,
bsc,
robinhood,
}
extension EVMChainIdX on EVMChainId {
@@ -24,6 +25,7 @@ extension EVMChainIdX on EVMChainId {
EVMChainId.base => 8453,
EVMChainId.arbitrum => 42161,
EVMChainId.bsc => 56,
EVMChainId.robinhood => 4663,
};
}
@@ -18,6 +18,7 @@ class KeyServiceImpl implements WalletConnectKeyService {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
return evm!.getPrivateKey(wallet);
case WalletType.solana:
return solana!.getPrivateKey(wallet);
@@ -33,6 +34,7 @@ class KeyServiceImpl implements WalletConnectKeyService {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
return evm!.getPublicKey(wallet);
case WalletType.solana:
return solana!.getPublicKey(wallet);
@@ -52,6 +54,7 @@ class KeyServiceImpl implements WalletConnectKeyService {
'eip155:137',
'eip155:8453',
'eip155:42161',
"eip155:4663",
],
privateKey: _getPrivateKeyForWallet(wallet),
publicKey: _getPublicKeyForWallet(wallet),
+28
View File
@@ -120,6 +120,7 @@ abstract class SettingsStoreBase with Store {
required this.useBaseScan,
required this.useArbiScan,
required this.useBscScan,
required this.useRobinhoodScan,
required this.usePolygonScan,
required this.useTronGrid,
required this.useMempoolFeeAPI,
@@ -533,6 +534,11 @@ abstract class SettingsStoreBase with Store {
reaction((_) => useBscScan,
(bool useBscScan) => _sharedPreferences.setBool(PreferencesKey.useBscScan, useBscScan));
reaction(
(_) => useRobinhoodScan,
(bool useRobinhoodScan) =>
_sharedPreferences.setBool(PreferencesKey.useRobinhoodScan, useRobinhoodScan));
reaction((_) => useTronGrid,
(bool useTronGrid) => _sharedPreferences.setBool(PreferencesKey.useTronGrid, useTronGrid));
@@ -978,6 +984,9 @@ abstract class SettingsStoreBase with Store {
@observable
bool useBscScan;
@observable
bool useRobinhoodScan;
@observable
bool useTronGrid;
@@ -1144,6 +1153,8 @@ abstract class SettingsStoreBase with Store {
return PreferencesKey.currentArbitrumNodeIdKey;
case 56:
return PreferencesKey.currentBscNodeIdKey;
case 4663:
return PreferencesKey.currentRobinhoodNodeIdKey;
default:
// Default to Ethereum for unknown chainIds
return PreferencesKey.currentEthereumNodeIdKey;
@@ -1338,6 +1349,7 @@ abstract class SettingsStoreBase with Store {
final useBaseScan = sharedPreferences.getBool(PreferencesKey.useBaseScan) ?? true;
final useArbiScan = sharedPreferences.getBool(PreferencesKey.useArbiScan) ?? true;
final useBscScan = sharedPreferences.getBool(PreferencesKey.useBscScan) ?? true;
final useRobinhoodScan = sharedPreferences.getBool(PreferencesKey.useRobinhoodScan) ?? true;
final useTronGrid = sharedPreferences.getBool(PreferencesKey.useTronGrid) ?? true;
final useMempoolFeeAPI = sharedPreferences.getBool(PreferencesKey.useMempoolFeeAPI) ?? true;
final useBlinkProtection = sharedPreferences.getBool(PreferencesKey.useBlinkProtection) ?? true;
@@ -1402,6 +1414,7 @@ abstract class SettingsStoreBase with Store {
final baseNodeId = sharedPreferences.getInt(PreferencesKey.currentBaseNodeIdKey);
final arbitrumNodeId = sharedPreferences.getInt(PreferencesKey.currentArbitrumNodeIdKey);
final bscNodeId = sharedPreferences.getInt(PreferencesKey.currentBscNodeIdKey);
final robinhoodNodeId = sharedPreferences.getInt(PreferencesKey.currentRobinhoodNodeIdKey);
final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey);
final nanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey);
final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey);
@@ -1455,6 +1468,8 @@ abstract class SettingsStoreBase with Store {
nodeSource.firstWhereOrNull((e) => e.uriRaw == zcashDefaultNodeUri);
final bscNode = nodeSource.firstWhereOrNull((e) => e.id == bscNodeId) ??
nodeSource.firstWhereOrNull((e) => e.uriRaw == bscDefaultNodeUri);
final robinhoodNode = nodeSource.firstWhereOrNull((e) => e.id == robinhoodNodeId) ??
nodeSource.firstWhereOrNull((e) => e.uriRaw == robinhoodDefaultNodeUri);
final packageInfo = await PackageInfo.fromPlatform();
final deviceName = await _getDeviceName() ?? '';
@@ -1523,6 +1538,10 @@ abstract class SettingsStoreBase with Store {
nodes[WalletType.bsc] = bscNode;
}
if (robinhoodNode != null) {
nodes[WalletType.robinhood] = robinhoodNode;
}
if (bitcoinCashElectrumServer != null) {
nodes[WalletType.bitcoinCash] = bitcoinCashElectrumServer;
}
@@ -1725,6 +1744,7 @@ abstract class SettingsStoreBase with Store {
useBaseScan: useBaseScan,
useArbiScan: useArbiScan,
useBscScan: useBscScan,
useRobinhoodScan: useRobinhoodScan,
useTronGrid: useTronGrid,
useMempoolFeeAPI: useMempoolFeeAPI,
useBlinkProtection: useBlinkProtection,
@@ -1972,6 +1992,7 @@ abstract class SettingsStoreBase with Store {
useBaseScan = sharedPreferences.getBool(PreferencesKey.useBaseScan) ?? true;
useArbiScan = sharedPreferences.getBool(PreferencesKey.useArbiScan) ?? true;
useBscScan = sharedPreferences.getBool(PreferencesKey.useBscScan) ?? true;
useRobinhoodScan = sharedPreferences.getBool(PreferencesKey.useRobinhoodScan) ?? true;
useTronGrid = sharedPreferences.getBool(PreferencesKey.useTronGrid) ?? true;
useMempoolFeeAPI = sharedPreferences.getBool(PreferencesKey.useMempoolFeeAPI) ?? true;
useBlinkProtection = sharedPreferences.getBool(PreferencesKey.useBlinkProtection) ?? true;
@@ -2014,6 +2035,7 @@ abstract class SettingsStoreBase with Store {
final baseNodeId = sharedPreferences.getInt(PreferencesKey.currentBaseNodeIdKey);
final arbitrumNodeId = sharedPreferences.getInt(PreferencesKey.currentArbitrumNodeIdKey);
final bscNodeId = sharedPreferences.getInt(PreferencesKey.currentBscNodeIdKey);
final robinhoodNodeId = sharedPreferences.getInt(PreferencesKey.currentRobinhoodNodeIdKey);
final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey);
final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey);
final tronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey);
@@ -2031,6 +2053,7 @@ abstract class SettingsStoreBase with Store {
final baseNode = await Node.get(baseNodeId ?? -1);
final arbitrumNode = await Node.get(arbitrumNodeId ?? -1);
final bscNode = await Node.get(bscNodeId ?? -1);
final robinhoodNode = await Node.get(robinhoodNodeId ?? -1);
final bitcoinCashNode = await Node.get(bitcoinCashElectrumServerId ?? -1);
final nanoNode = await Node.get(nanoNodeId ?? -1);
final solanaNode = await Node.get(solanaNodeId ?? -1);
@@ -2077,6 +2100,10 @@ abstract class SettingsStoreBase with Store {
nodes[WalletType.bsc] = bscNode;
}
if (robinhoodNode != null) {
nodes[WalletType.robinhood] = robinhoodNode;
}
if (bitcoinCashNode != null) {
nodes[WalletType.bitcoinCash] = bitcoinCashNode;
}
@@ -2230,6 +2257,7 @@ abstract class SettingsStoreBase with Store {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
final chainId = evm!.getChainIdByWalletType(node.type);
final preferenceKey = _getEVMNodePreferenceKey(chainId);
await _sharedPreferences.setInt(preferenceKey, node.id);
+4
View File
@@ -12,6 +12,8 @@ String getQrImage(WalletType type) {
return 'assets/images/arbitrum_chain_QR.svg';
case WalletType.bsc:
return 'assets/images/bnb_chain_QR.svg';
case WalletType.robinhood:
return "assets/images/eth_chain_qr.svg";
case WalletType.solana:
return 'assets/images/sol_chain_qr.svg';
case WalletType.tron:
@@ -55,6 +57,8 @@ String getChainMonoImage(WalletType type) {
return 'assets/images/arbitrum_chain_mono.svg';
case WalletType.bsc:
return 'assets/images/bnb_chain_mono.svg';
case WalletType.robinhood:
return "assets/images/robinhood_chain_mono.svg";
case WalletType.solana:
return 'assets/images/sol_chain_mono.svg';
case WalletType.tron:
+7 -3
View File
@@ -2,7 +2,6 @@ import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/evm/evm.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/tron/tron.dart';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/erc20_token.dart';
@@ -12,7 +11,6 @@ import "package:cw_core/utils/print_verbose.dart";
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:hive/hive.dart';
class TokenUtilities {
static Future<List<Erc20Token>> loadAllUniqueEvmTokens() async {
@@ -192,6 +190,7 @@ class TokenUtilities {
WalletType.base => 8453,
WalletType.arbitrum => 42161,
WalletType.bsc => 56,
WalletType.robinhood => 4663,
_ => 1,
};
@@ -298,7 +297,12 @@ class TokenUtilities {
// Only check EVM registry for currencies that might be EVM-related
final isPotentialEVM = isNativeToken(currency) ||
(tag != null &&
(tag == "ETH" || tag == "POL" || tag == "BASE" || tag == "ARB" || tag == "BSC"));
(tag == "ETH" ||
tag == "POL" ||
tag == "BASE" ||
tag == "ARB" ||
tag == "BSC" ||
tag == "ROB"));
if (isPotentialEVM) {
// Try by tag first if available (e.g., 'POL', 'BASE', 'ARB')
@@ -56,6 +56,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
case WalletType.solana:
case WalletType.tron:
case WalletType.zcash:
@@ -100,6 +101,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store {
WalletType.base,
WalletType.arbitrum,
WalletType.bsc,
WalletType.robinhood,
WalletType.tron,
WalletType.solana,
WalletType.monero,
@@ -219,6 +219,7 @@ abstract class BalanceViewModelBase with Store {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
case WalletType.solana:
case WalletType.tron:
return S.current.xmr_full_balance;
@@ -1300,6 +1300,7 @@ abstract class DashboardViewModelBase with Store {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
case WalletType.solana:
case WalletType.nano:
case WalletType.banano:
@@ -194,10 +194,10 @@ abstract class HomeSettingsViewModelBase with Store {
return false;
}
bool isPotentialScamViaMoralis = await _isPotentialScamTokenViaMoralis(
contractAddress,
getChainNameBasedOnWalletType(_balanceViewModel.wallet.type),
);
final moralisChainName = evm!.getMoralisChainName(_balanceViewModel.wallet);
final isPotentialScamViaMoralis = moralisChainName != null &&
await _isPotentialScamTokenViaMoralis(contractAddress, moralisChainName);
bool isUnverifiedContract = await _isContractUnverified(
contractAddress,
@@ -221,6 +221,7 @@ abstract class HomeSettingsViewModelBase with Store {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
defaultTokenAddresses = evm!.getDefaultTokenContractAddresses(_balanceViewModel.wallet);
break;
case WalletType.solana:
@@ -262,6 +263,7 @@ abstract class HomeSettingsViewModelBase with Store {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
defaultTokenSymbols = evm!.getDefaultTokenSymbols(_balanceViewModel.wallet);
break;
case WalletType.solana:
@@ -207,6 +207,7 @@ class TransactionListItem extends ActionListItem with Keyable {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
final asset = assetOfTransaction;
final price = balanceViewModel.fiatConversionStore.prices[asset];
amount = calculateFiatAmountRaw(
@@ -277,7 +277,8 @@ abstract class ExchangeTradeViewModelBase with Store {
_isTokenBelongingToWallet(CryptoCurrency.arbEth) ||
_isTokenBelongingToWallet(CryptoCurrency.trx) ||
_isTokenBelongingToWallet(CryptoCurrency.sol) ||
_isTokenBelongingToWallet(CryptoCurrency.bnb);
_isTokenBelongingToWallet(CryptoCurrency.bnb) ||
_isTokenBelongingToWallet(CryptoCurrency.robEth);
if (!canSend) {
return 'Wallet currency ${wallet.currency.title} does not match trade from currency ${tradeFrom.title} or is not a supported token for this wallet.';
@@ -346,6 +347,7 @@ abstract class ExchangeTradeViewModelBase with Store {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
return _createERC681URI(fromCurrency, inputAddress, amount);
case WalletType.solana:
return SolanaURI(
@@ -1547,6 +1547,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
depositCurrency = CryptoCurrency.bnb;
receiveCurrency = CryptoCurrency.xmr;
break;
case WalletType.robinhood:
depositCurrency = CryptoCurrency.robEth;
receiveCurrency = CryptoCurrency.xmr;
break;
case WalletType.solana:
depositCurrency = CryptoCurrency.sol;
receiveCurrency = CryptoCurrency.xmr;
@@ -126,6 +126,7 @@ abstract class NodeCreateOrEditViewModelBase with Store {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
case WalletType.solana:
case WalletType.banano:
case WalletType.nano:
@@ -35,6 +35,7 @@ class WalletRestoreFromQRCode {
'base-wallet': WalletType.base,
'arbitrum-wallet': WalletType.arbitrum,
'bsc-wallet': WalletType.bsc,
"robinhood-wallet": WalletType.robinhood,
'nano-wallet': WalletType.nano,
'nano_wallet': WalletType.nano,
'bitcoincash': WalletType.bitcoinCash,
+3 -2
View File
@@ -109,6 +109,7 @@ abstract class FeesViewModelBase extends WalletChangeListenerViewModel with Stor
case WalletType.solana:
case WalletType.tron:
case WalletType.arbitrum:
case WalletType.robinhood:
case WalletType.zcash:
return false;
}
@@ -128,8 +129,8 @@ abstract class FeesViewModelBase extends WalletChangeListenerViewModel with Stor
wallet.type != WalletType.banano &&
wallet.type != WalletType.solana &&
wallet.type != WalletType.tron &&
wallet.chainId !=
42161; // Wallet type is generic for all EVM chains, so we need to check the chainId
wallet.chainId != 42161 &&
wallet.chainId != 4663;
@computed
bool get isElectrumWallet =>
+3 -1
View File
@@ -189,12 +189,14 @@ abstract class OutputBase with Store {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
final isNative = [
CryptoCurrency.eth,
CryptoCurrency.maticpoly,
CryptoCurrency.baseEth,
CryptoCurrency.arbEth,
CryptoCurrency.bnb
CryptoCurrency.bnb,
CryptoCurrency.robEth
].contains(cryptoCurrencyHandler());
final fee = isNative
+3
View File
@@ -293,6 +293,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
case WalletType.tron:
case WalletType.solana:
case WalletType.bitcoin:
@@ -1273,6 +1274,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
WalletType.solana,
WalletType.tron,
WalletType.arbitrum,
WalletType.robinhood,
WalletType.zcash,
].contains(wallet.type)) {
throw Exception('Priority is null for wallet type: ${wallet.type}');
@@ -1311,6 +1313,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
return evm!.createEVMTransactionCredentials(
outputs,
priority: priority,
@@ -205,6 +205,9 @@ abstract class ConnectionSyncViewModelBase with Store {
@computed
bool get useBaseScan => _settingsStore.useBaseScan;
@computed
bool get useRobinhoodScan => _settingsStore.useRobinhoodScan;
@computed
bool get useArbiScan => _settingsStore.useArbiScan;
@@ -223,6 +226,9 @@ abstract class ConnectionSyncViewModelBase with Store {
@computed
bool get canUseBaseScan => _wallet.chainId == 8453;
@computed
bool get canUseRobinhoodScan => _wallet.chainId == 4663;
@computed
bool get canUseArbiScan => _wallet.chainId == 42161;
@@ -271,6 +277,12 @@ abstract class ConnectionSyncViewModelBase with Store {
evm!.updateScanProviderUsageState(_wallet, value);
}
@action
void setUseRobinhoodScan(bool value) {
_settingsStore.useRobinhoodScan = value;
evm!.updateScanProviderUsageState(_wallet, value);
}
@action
void setUseTronGrid(bool value) {
_settingsStore.useTronGrid = value;
@@ -104,6 +104,7 @@ abstract class OtherSettingsViewModelBase with Store {
WalletType.solana,
WalletType.tron,
WalletType.arbitrum,
WalletType.robinhood,
].contains(_wallet.type));
String getDisplayPriority(dynamic priority) {
@@ -476,6 +476,8 @@ abstract class TransactionDetailsViewModelBase with Store {
return "https://arbiscan.io/tx/${txId}";
case WalletType.bsc:
return "https://bscscan.com/tx/${txId}";
case WalletType.robinhood:
return "https://robinhoodchain.blockscout.com/tx/${txId}";
case WalletType.polygon:
return "https://polygonscan.com/tx/${txId}";
case WalletType.nano:
@@ -562,6 +562,12 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
"assets/images/usdt_wallet_icon.svg",
"assets/images/more_tokens.svg",
];
case 4663:
return [
"assets/new-ui/crypto_full_icons/robinhood.svg",
"assets/images/usdg_icon.png",
"assets/images/more_tokens.svg",
];
default:
return [
"assets/new-ui/crypto_full_icons/ethereum.svg",
@@ -171,6 +171,7 @@ abstract class WalletKeysViewModelBase with Store {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
case WalletType.solana:
case WalletType.tron:
items.addAll([
@@ -321,6 +322,8 @@ abstract class WalletKeysViewModelBase with Store {
return 'arbitrum-wallet';
case WalletType.bsc:
return 'bsc-wallet';
case WalletType.robinhood:
return "robinhood-wallet";
case WalletType.solana:
return 'solana-wallet';
case WalletType.tron:
+1
View File
@@ -83,6 +83,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
return evm!.createEVMNewWalletCredentials(
name: name,
password: walletPassword,
@@ -54,6 +54,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
case WalletType.decred:
case WalletType.bitcoin:
case WalletType.litecoin:
@@ -97,6 +98,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
WalletType.base,
WalletType.arbitrum,
WalletType.bsc,
WalletType.robinhood,
WalletType.nano,
WalletType.banano,
WalletType.solana,
@@ -179,6 +181,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
return evm!.createEVMRestoreWalletFromSeedCredentials(
name: name,
mnemonic: seed,
@@ -288,6 +291,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
case WalletType.base:
case WalletType.arbitrum:
case WalletType.bsc:
case WalletType.robinhood:
return evm!.createEVMRestoreWalletFromPrivateKey(
name: name,
password: password,
@@ -1,6 +1,5 @@
import 'dart:async';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/core/wallet_loading_service.dart';
import 'package:cw_core/wallet_base.dart';
@@ -32,13 +31,6 @@ abstract class WalletSwitcherViewModelBase with Store {
Future<List<WalletInfo>> getWallets(WalletType? walletType) async {
final wiList = await WalletInfo.getAll();
if (walletType == null) return wiList;
// For EVM-compatible wallet types, show all EVM-compatible wallets
// This allows users to switch between any EVM wallet regardless of the specific chain
if (isEVMCompatibleChain(walletType)) {
return wiList.where((wallet) => isEVMCompatibleChain(wallet.type)).toList();
}
return wiList.where((wallet) => wallet.type == walletType).toList();
}