CW-555-Add-Solana-Wallet (#1272)

* chore: Create cw_solana package and clean up files

* feat: Add Solana Wallet - Create, Restore form seed, restore from Key, Restore from QR, Send, Receive, transaction history, spl tokens

* fix: Make transactions file specific to solana only for solana transactions

* chore: Revert inject app details script

* fix: Fix issue with node and switch current node to main beta instead of testnet

* fix: Fix merge conflicts and adjust migration version

* fix: Fetch spl token error

Signed-off-by: Blazebrain <davidadegoke16@gmail.com>

* fix: Diplay and activate spl tokens bug

* fix: Review and fixes

* fix: reverted formatting for cryptocurrency class

* fix: Review comments, split sending flow into signing and sending separately, fix issues

* fix: Revert throwing unimplenented error

* chore: Fix comment

* chore: Fix comment

* fix: Errors in flow

* Update provider_types.dart [skip ci]

* fix: Issues with solana wallet

* Update solana_wallet.dart [skip ci]

* fix: Review comments

* fix: Date time config

* fix: Revert bash script for app details

* fix: Error with balance, displaying fees, fixing sent or received identifier bug, displaying token symbol with token transaction item in transactions list

* fix: Issues with address validation when sending spl tokens and walletconnect initial setup

* fix: Issues with sending, fetching transactions history, almost wrapping up walletconnect

* fix: Adjust imports that would affect monerocom building successfully

* fix: Refine transaction direction and continue work on walletconnect

* feat: Display SPL token transfers in the transaction history and finally settle the transaction direction

* fix: Delay in transactions history dispaly, show native token transactions first, then process spl token transactions

* feat: Switch node and revert solana chain id to previous id

* fix: Remove print statement

* fix: Remove await for transactions, fetch all transaction histories instantly and adjust solana send success message

* chore: Code refactoring and streamlined wallet type check for solana send success message

* fix: Make timeout error for node silent and add spl token images

---------

Signed-off-by: Blazebrain <davidadegoke16@gmail.com>
Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
Adegoke David
2024-02-23 15:39:19 +02:00
committed by GitHub
co-authored by Omar Hatem
parent 5a7ea87543
commit 109bba4301
133 changed files with 5356 additions and 353 deletions
@@ -37,6 +37,7 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
final bitcoinCashIcon = Image.asset('assets/images/bch_icon.png', height: 24, width: 24);
final nanoIcon = Image.asset('assets/images/nano_icon.png', height: 24, width: 24);
final bananoIcon = Image.asset('assets/images/nano_icon.png', height: 24, width: 24);
final solanaIcon = Image.asset('assets/images/sol_icon.png', height: 24, width: 24);
final nonWalletTypeIcon = Image.asset('assets/images/close.png', height: 24, width: 24);
Image _newWalletImage(BuildContext context) => Image.asset(
@@ -153,6 +154,8 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
return bananoIcon;
case WalletType.polygon:
return polygonIcon;
case WalletType.solana:
return solanaIcon;
default:
return nonWalletTypeIcon;
}
+27 -21
View File
@@ -8,6 +8,7 @@ import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -16,12 +17,12 @@ class EditTokenPage extends BasePage {
EditTokenPage({
Key? key,
required this.homeSettingsViewModel,
this.erc20token,
this.token,
this.initialContractAddress,
}) : assert(erc20token == null || initialContractAddress == null);
}) : assert(token == null || initialContractAddress == null);
final HomeSettingsViewModel homeSettingsViewModel;
final Erc20Token? erc20token;
final CryptoCurrency? token;
final String? initialContractAddress;
@override
@@ -31,7 +32,7 @@ class EditTokenPage extends BasePage {
Widget body(BuildContext context) {
return EditTokenPageBody(
homeSettingsViewModel: homeSettingsViewModel,
erc20token: erc20token,
token: token,
initialContractAddress: initialContractAddress,
);
}
@@ -41,12 +42,12 @@ class EditTokenPageBody extends StatefulWidget {
const EditTokenPageBody({
Key? key,
required this.homeSettingsViewModel,
this.erc20token,
this.token,
this.initialContractAddress,
}) : super(key: key);
final HomeSettingsViewModel homeSettingsViewModel;
final Erc20Token? erc20token;
final CryptoCurrency? token;
final String? initialContractAddress;
@override
@@ -73,11 +74,15 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
void initState() {
super.initState();
if (widget.erc20token != null) {
_contractAddressController.text = widget.erc20token!.contractAddress;
_tokenNameController.text = widget.erc20token!.name;
_tokenSymbolController.text = widget.erc20token!.symbol;
_tokenDecimalController.text = widget.erc20token!.decimal.toString();
String? address;
if (widget.token != null) {
address = widget.homeSettingsViewModel.getTokenAddressBasedOnWallet(widget.token!);
_contractAddressController.text = address ?? '';
_tokenNameController.text = widget.token!.name;
_tokenSymbolController.text = widget.token!.title;
_tokenDecimalController.text = widget.token!.decimals.toString();
}
if (widget.initialContractAddress != null) {
@@ -91,7 +96,7 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
}
final contractAddress = _contractAddressController.text;
if (contractAddress.isNotEmpty && contractAddress != widget.erc20token?.contractAddress) {
if (contractAddress.isNotEmpty && contractAddress != address) {
setState(() {
_showDisclaimer = true;
});
@@ -139,7 +144,9 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
color: Theme.of(context)
.extension<TransactionTradeTheme>()!
.detailsTitlesColor,
),
),
),
@@ -172,12 +179,12 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
Expanded(
child: PrimaryButton(
onPressed: () async {
if (widget.erc20token != null) {
await widget.homeSettingsViewModel.deleteErc20Token(widget.erc20token!);
if (widget.token != null) {
await widget.homeSettingsViewModel.deleteToken(widget.token!);
}
Navigator.pop(context);
},
text: widget.erc20token != null ? S.of(context).delete : S.of(context).cancel,
text: widget.token != null ? S.of(context).delete : S.of(context).cancel,
color: Colors.red,
textColor: Colors.white,
),
@@ -188,7 +195,7 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
onPressed: () async {
if (_formKey.currentState!.validate() &&
(!_showDisclaimer || _disclaimerChecked)) {
await widget.homeSettingsViewModel.addErc20Token(Erc20Token(
await widget.homeSettingsViewModel.addToken(Erc20Token(
name: _tokenNameController.text,
symbol: _tokenSymbolController.text,
contractAddress: _contractAddressController.text,
@@ -214,14 +221,13 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
void _getTokenInfo() async {
if (_contractAddressController.text.isNotEmpty) {
final token =
await widget.homeSettingsViewModel.getErc20Token(_contractAddressController.text);
final token = await widget.homeSettingsViewModel.getToken(_contractAddressController.text);
if (token != null) {
if (_tokenNameController.text.isEmpty) _tokenNameController.text = token.name;
if (_tokenSymbolController.text.isEmpty) _tokenSymbolController.text = token.symbol;
if (_tokenSymbolController.text.isEmpty) _tokenSymbolController.text = token.title;
if (_tokenDecimalController.text.isEmpty)
_tokenDecimalController.text = token.decimal.toString();
_tokenDecimalController.text = token.decimals.toString();
}
}
}
@@ -5,6 +5,7 @@ import 'package:cake_wallet/entities/sort_balance_types.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
import 'package:cake_wallet/themes/extensions/address_theme.dart';
@@ -117,7 +118,7 @@ class HomeSettingsPage extends BasePage {
return SettingsSwitcherCell(
title: "${token.name} "
"(${token.symbol})",
"(${token.title})",
value: token.enabled,
onValueChange: (_, bool value) {
_homeSettingsViewModel.changeTokenAvailability(token, value);
@@ -128,20 +129,16 @@ class HomeSettingsPage extends BasePage {
'token': token,
});
},
leading: token.iconPath != null
? Container(
child: Image.asset(
token.iconPath!,
height: 30.0,
width: 30.0,
),
)
: Container(
leading: CakeImageWidget(
imageUrl: token.iconPath,
height: 40,
width: 40,
displayOnError: Container(
height: 30.0,
width: 30.0,
child: Center(
child: Text(
token.symbol.substring(0, min(token.symbol.length, 2)),
token.title.substring(0, min(token.title.length, 2)),
style: TextStyle(fontSize: 11),
),
),
@@ -149,7 +146,8 @@ class HomeSettingsPage extends BasePage {
shape: BoxShape.circle,
color: Colors.grey.shade400,
),
),
),
),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(30),
@@ -6,6 +6,7 @@ import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/dashboard/pages/nft_listing_page.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/home_screen_account_widget.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/src/screens/exchange_trade/information_page.dart';
import 'package:cake_wallet/src/widgets/introducing_card.dart';
import 'package:cake_wallet/store/settings_store.dart';
@@ -333,15 +334,11 @@ class BalanceRowWidget extends StatelessWidget {
child: Center(
child: Column(
children: [
currency.iconPath != null
? Container(
child: Image.asset(
currency.iconPath!,
height: 40.0,
width: 40.0,
),
)
: Container(
CakeImageWidget(
imageUrl: currency.iconPath,
height: 40,
width: 40,
displayOnError: Container(
height: 30.0,
width: 30.0,
child: Center(
@@ -355,6 +352,7 @@ class BalanceRowWidget extends StatelessWidget {
color: Colors.grey.shade400,
),
),
),
const SizedBox(height: 10),
Text(
currency.title,
@@ -2,7 +2,7 @@ import 'package:cake_wallet/entities/wallet_nft_response.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/menu_widget.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/nft_image_tile_widget.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/src/widgets/gradient_background.dart';
import 'package:cake_wallet/themes/extensions/balance_page_theme.dart';
import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart';
@@ -94,7 +94,7 @@ class NFTDetailsPage extends BasePage {
.syncedBackgroundColor,
),
child: NFTImageWidget(
child: CakeImageWidget(
imageUrl: nftAsset.normalizedMetadata?.imageUrl,
),
),
@@ -18,23 +18,23 @@ class MenuWidget extends StatefulWidget {
class MenuWidgetState extends State<MenuWidget> {
MenuWidgetState()
: this.menuWidth = 0,
this.screenWidth = 0,
this.screenHeight = 0,
this.headerHeight = 120,
this.tileHeight = 60,
this.fromTopEdge = 50,
this.fromBottomEdge = 25,
this.moneroIcon = Image.asset('assets/images/monero_menu.png'),
this.bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png'),
this.litecoinIcon = Image.asset('assets/images/litecoin_menu.png'),
this.havenIcon = Image.asset('assets/images/haven_menu.png'),
this.ethereumIcon = Image.asset('assets/images/eth_icon.png'),
this.nanoIcon = Image.asset('assets/images/nano_icon.png'),
this.bananoIcon = Image.asset('assets/images/nano_icon.png'),
: this.menuWidth = 0,
this.screenWidth = 0,
this.screenHeight = 0,
this.headerHeight = 120,
this.tileHeight = 60,
this.fromTopEdge = 50,
this.fromBottomEdge = 25,
this.moneroIcon = Image.asset('assets/images/monero_menu.png'),
this.bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png'),
this.litecoinIcon = Image.asset('assets/images/litecoin_menu.png'),
this.havenIcon = Image.asset('assets/images/haven_menu.png'),
this.ethereumIcon = Image.asset('assets/images/eth_icon.png'),
this.nanoIcon = Image.asset('assets/images/nano_icon.png'),
this.bananoIcon = Image.asset('assets/images/nano_icon.png'),
this.bitcoinCashIcon = Image.asset('assets/images/bch_icon.png'),
this.polygonIcon = Image.asset('assets/images/matic_icon.png');
this.polygonIcon = Image.asset('assets/images/matic_icon.png'),
this.solanaIcon = Image.asset('assets/images/sol_icon.png');
final largeScreen = 731;
@@ -56,7 +56,7 @@ class MenuWidgetState extends State<MenuWidget> {
Image nanoIcon;
Image bananoIcon;
Image polygonIcon;
Image solanaIcon;
@override
void initState() {
@@ -224,6 +224,8 @@ class MenuWidgetState extends State<MenuWidget> {
return bananoIcon;
case WalletType.polygon:
return polygonIcon;
case WalletType.solana:
return solanaIcon;
default:
throw Exception('No icon for ${type.toString()}');
}
@@ -1,9 +1,8 @@
import 'package:cake_wallet/entities/wallet_nft_response.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/nft_image_tile_widget.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/themes/extensions/balance_page_theme.dart';
import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class NFTTileWidget extends StatelessWidget {
@@ -38,7 +37,7 @@ class NFTTileWidget extends StatelessWidget {
),
color: Theme.of(context).extension<SyncIndicatorTheme>()!.syncedBackgroundColor,
),
child: NFTImageWidget(
child: CakeImageWidget(
imageUrl: nftAsset.normalizedMetadata?.imageUrl,
),
),
+10 -2
View File
@@ -14,6 +14,7 @@ import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/utils/request_review_handler.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
@@ -439,10 +440,17 @@ class SendPage extends BasePage {
}
if (state is TransactionCommitted) {
String alertContent;
if (sendViewModel.walletType == WalletType.solana) {
alertContent =
'${S.of(_dialogContext).send_success(sendViewModel.selectedCryptoCurrency.toString())}. ${S.of(_dialogContext).waitFewSecondForTxUpdate}';
} else {
alertContent = S.of(_dialogContext).send_success(
sendViewModel.selectedCryptoCurrency.toString());
}
return AlertWithOneAction(
alertTitle: '',
alertContent: S.of(_dialogContext).send_success(
sendViewModel.selectedCryptoCurrency.toString()),
alertContent: alertContent,
buttonText: S.of(_dialogContext).ok,
buttonAction: () {
Navigator.of(_dialogContext).pop();
+1 -1
View File
@@ -321,7 +321,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
? sendViewModel.allAmountValidator
: sendViewModel.amountValidator,
),
if (!sendViewModel.isBatchSending)
if (!sendViewModel.isBatchSending && sendViewModel.shouldDisplaySendALL)
Positioned(
top: 2,
right: 0,
@@ -107,7 +107,7 @@ class ConnectionSyncPage extends BasePage {
);
},
),
if (isEVMCompatibleChain(dashboardViewModel.wallet.type)) ...[
if (isWalletConnectCompatibleChain(dashboardViewModel.wallet.type)) ...[
WalletConnectTile(
onTap: () => Navigator.of(context).pushNamed(Routes.walletConnectConnectionsListing),
),
@@ -26,7 +26,7 @@ class OtherSettingsPage extends BasePage {
padding: EdgeInsets.only(top: 10),
child: Column(
children: [
if (!_otherSettingsViewModel.changeRepresentativeEnabled)
if (_otherSettingsViewModel.displayTransactionPriority)
SettingsPickerCell(
title: S.current.settings_fee_priority,
items: priorityForWalletType(_otherSettingsViewModel.walletType),
@@ -1,4 +1,5 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/receive_page_theme.dart';
import 'package:flutter/material.dart';
@@ -26,12 +27,11 @@ class PairingItemWidget extends StatelessWidget {
'$year-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}';
return ListTile(
leading: CircleAvatar(
backgroundImage: (metadata.icons.isNotEmpty
? NetworkImage(metadata.icons[0])
: const AssetImage(
'assets/images/default_icon.png',
)) as ImageProvider<Object>,
leading: CakeImageWidget(
imageUrl: metadata.icons.isNotEmpty ? metadata.icons[0]: null,
displayOnError: CircleAvatar(
backgroundImage: AssetImage('assets/images/default_icon.png'),
),
),
title: Text(
metadata.name,
@@ -103,6 +103,7 @@ class WalletListBodyState extends State<WalletListBody> {
final bitcoinCashIcon = Image.asset('assets/images/bch_icon.png', height: 24, width: 24);
final nanoIcon = Image.asset('assets/images/nano_icon.png', height: 24, width: 24);
final polygonIcon = Image.asset('assets/images/matic_icon.png', height: 24, width: 24);
final solanaIcon = Image.asset('assets/images/sol_icon.png', height: 24, width: 24);
final scrollController = ScrollController();
final double tileHeight = 60;
Flushbar<void>? _progressBar;
@@ -313,6 +314,8 @@ class WalletListBodyState extends State<WalletListBody> {
return nanoIcon;
case WalletType.polygon:
return polygonIcon;
case WalletType.solana:
return solanaIcon;
default:
return nonWalletTypeIcon;
}
@@ -2,25 +2,45 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class NFTImageWidget extends StatelessWidget {
const NFTImageWidget({
class CakeImageWidget extends StatelessWidget {
CakeImageWidget({
required this.imageUrl,
});
Widget? displayOnError,
this.height,
this.width,
}) : _displayOnError = displayOnError ?? Icon(Icons.error);
final String? imageUrl;
final double? height;
final double? width;
final Widget? _displayOnError;
@override
Widget build(BuildContext context) {
try {
if (imageUrl == null) return Icon(Icons.error);
if (imageUrl == null) return _displayOnError!;
if (imageUrl!.contains('assets/images')) {
return Image.asset(
imageUrl!,
height: height,
width: width,
);
}
if (imageUrl!.contains('.svg')) {
return SvgPicture.network(imageUrl!);
return SvgPicture.network(
imageUrl!,
height: height,
width: width,
);
}
return Image.network(
imageUrl!,
fit: BoxFit.cover,
height: height,
width: width,
loadingBuilder: (BuildContext _, Widget child, ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) {
return child;
@@ -31,7 +51,7 @@ class NFTImageWidget extends StatelessWidget {
errorBuilder: (_, __, ___) => Icon(Icons.error),
);
} catch (_) {
return Icon(Icons.error);
return _displayOnError!;
}
}
}