diff --git a/assets/gnosis_node_list.yml b/assets/gnosis_node_list.yml new file mode 100644 index 000000000..3eb74075e --- /dev/null +++ b/assets/gnosis_node_list.yml @@ -0,0 +1,4 @@ +- + uri: gnosis-rpc.publicnode.com + useSSL: true + isDefault: true diff --git a/assets/images/gno.png b/assets/images/gno.png new file mode 100644 index 000000000..dd01c3187 Binary files /dev/null and b/assets/images/gno.png differ diff --git a/assets/images/gnosis_icon.png b/assets/images/gnosis_icon.png new file mode 100644 index 000000000..dd01c3187 Binary files /dev/null and b/assets/images/gnosis_icon.png differ diff --git a/assets/images/xdai.png b/assets/images/xdai.png new file mode 100644 index 000000000..10b2efaa0 Binary files /dev/null and b/assets/images/xdai.png differ diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 4b0f9521e..81f55651b 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -233,6 +233,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const zano = CryptoCurrency(title: 'ZANO', tag: 'ZANO', fullName: 'Zano', raw: 96, name: 'zano', iconPath: 'assets/images/zano_icon.png', decimals: 12); static const flip = CryptoCurrency(title: 'FLIP', tag: 'ETH', fullName: 'Chainflip', raw: 97, name: 'flip', iconPath: 'assets/images/flip_icon.png', decimals: 18); static const deuro = CryptoCurrency(title: 'DEURO', tag: 'ETH', fullName: 'Decentralized Euro', raw: 98, name: 'deuro', iconPath: 'assets/images/deuro_icon.png', decimals: 18); + static const xdai = CryptoCurrency(title: 'XDAI', tag: 'XDAI', fullName: 'xDAI', raw: 99, name: 'xDAI', iconPath: 'assets/images/xdai.png', decimals: 18); static final Map _rawCurrencyMap = [...all, ...havenCurrencies].fold>({}, (acc, item) { diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart index 0f913cb79..0231e2441 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -34,6 +34,8 @@ CryptoCurrency currencyForWalletType(WalletType type, {bool? isTestnet}) { return CryptoCurrency.zano; case WalletType.decred: return CryptoCurrency.dcr; + case WalletType.gnosis: + return CryptoCurrency.xdai; case WalletType.none: throw Exception( 'Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType'); @@ -60,6 +62,8 @@ WalletType? walletTypeForCurrency(CryptoCurrency currency) { return WalletType.banano; case CryptoCurrency.maticpoly: return WalletType.polygon; + case CryptoCurrency.xdai: + return WalletType.gnosis; case CryptoCurrency.sol: return WalletType.solana; case CryptoCurrency.trx: diff --git a/cw_core/lib/erc20_token.dart b/cw_core/lib/erc20_token.dart index fd76d28fc..d8ffdb4c5 100644 --- a/cw_core/lib/erc20_token.dart +++ b/cw_core/lib/erc20_token.dart @@ -70,6 +70,7 @@ class Erc20Token extends CryptoCurrency with HiveObjectMixin { static const boxName = 'Erc20Tokens'; static const ethereumBoxName = 'EthereumErc20Tokens'; static const polygonBoxName = 'PolygonErc20Tokens'; + static const gnosisBoxName = 'GnosisErc20Tokens'; @override bool operator ==(other) => diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 38fcde9e1..385ba4332 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -100,6 +100,7 @@ class Node extends HiveObject with Keyable { case WalletType.banano: case WalletType.ethereum: case WalletType.polygon: + case WalletType.gnosis: case WalletType.solana: case WalletType.tron: case WalletType.zano: @@ -163,6 +164,7 @@ class Node extends HiveObject with Keyable { case WalletType.bitcoinCash: case WalletType.ethereum: case WalletType.polygon: + case WalletType.gnosis: case WalletType.solana: case WalletType.tron: return requestElectrumServer(); diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index 40a2b31d5..0114339ba 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -10,6 +10,7 @@ const walletTypes = [ WalletType.litecoin, WalletType.haven, WalletType.ethereum, + WalletType.gnosis, WalletType.bitcoinCash, WalletType.nano, WalletType.banano, @@ -65,7 +66,10 @@ enum WalletType { zano, @HiveField(14) - decred + decred, + + @HiveField(15) + gnosis } int serializeToInt(WalletType type) { @@ -98,6 +102,8 @@ int serializeToInt(WalletType type) { return 12; case WalletType.decred: return 13; + case WalletType.gnosis: + return 14; case WalletType.none: return -1; } @@ -133,6 +139,8 @@ WalletType deserializeFromInt(int raw) { return WalletType.zano; case 13: return WalletType.decred; + case 14: + return WalletType.gnosis; default: throw Exception( 'Unexpected token: $raw for WalletType deserializeFromInt'); @@ -169,6 +177,8 @@ String walletTypeToString(WalletType type) { return 'Zano'; case WalletType.decred: return 'Decred'; + case WalletType.gnosis: + return 'Gnosis'; case WalletType.none: return ''; } @@ -204,6 +214,8 @@ String walletTypeToDisplayName(WalletType type) { return 'Zano (ZANO)'; case WalletType.decred: return 'Decred (DCR)'; + case WalletType.gnosis: + return 'Gnosis (xDAI)'; case WalletType.none: return ''; } @@ -242,6 +254,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type, {bool isTestnet = fal return CryptoCurrency.zano; case WalletType.decred: return CryptoCurrency.dcr; + case WalletType.gnosis: + return CryptoCurrency.xdai; case WalletType.none: throw Exception( 'Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency'); @@ -278,6 +292,8 @@ WalletType? cryptoCurrencyToWalletType(CryptoCurrency type) { return WalletType.zano; case CryptoCurrency.dcr: return WalletType.decred; + case CryptoCurrency.xdai: + return WalletType.gnosis; default: return null; } diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index a1b253dd8..d0e8eab9d 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -621,7 +621,9 @@ abstract class EVMChainWalletBase } else { balance.remove(token); } - } catch (_) {} + } catch (e) { + print(e); + } } } diff --git a/cw_gnosis/.gitignore b/cw_gnosis/.gitignore new file mode 100644 index 000000000..96486fd93 --- /dev/null +++ b/cw_gnosis/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/cw_gnosis/LICENSE b/cw_gnosis/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_gnosis/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_gnosis/analysis_options.yaml b/cw_gnosis/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_gnosis/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/cw_gnosis/devtools_options.yaml b/cw_gnosis/devtools_options.yaml new file mode 100644 index 000000000..fa0b357c4 --- /dev/null +++ b/cw_gnosis/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/cw_gnosis/lib/default_gnosis_erc20_tokens.dart b/cw_gnosis/lib/default_gnosis_erc20_tokens.dart new file mode 100644 index 000000000..d98def594 --- /dev/null +++ b/cw_gnosis/lib/default_gnosis_erc20_tokens.dart @@ -0,0 +1,54 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; + +class DefaultGnosisErc20Tokens { + final List _defaultTokens = [ + Erc20Token( + name: "Wrapped Ether", + symbol: "WETH", + contractAddress: "0x6a023ccd1ff6f2045c3309768ead9e68f978f6e1", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Tether USD on xDai", + symbol: "USDT", + contractAddress: "0x4ECaBa5870353805a9F068101A40E0f32ed605C6", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "USD Coin", + symbol: "USDC.e", + contractAddress: "0x2a22f9c3b484c3629090FeED35F17Ff8F88f76F0", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "Gnosis", + symbol: "GNO", + contractAddress: "0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb", + decimal: 18, + enabled: true, + ), + Erc20Token( + name: "Decentralized Euro", + symbol: "DEURO", + contractAddress: "0xac90f343820D8299Ac72a06A7674491b07d45f03", + decimal: 18, + enabled: true, + ), + ]; + + List get initialGnosisErc20Tokens => _defaultTokens.map((token) { + String? iconPath; + try { + iconPath = CryptoCurrency.all + .firstWhere((element) => + element.title.toUpperCase() == token.symbol.split(".").first.toUpperCase()) + .iconPath; + } catch (_) {} + + return Erc20Token.copyWith(token, iconPath, 'XDAI'); + }).toList(); +} diff --git a/cw_gnosis/lib/gnosis_client.dart b/cw_gnosis/lib/gnosis_client.dart new file mode 100644 index 000000000..444abb02a --- /dev/null +++ b/cw_gnosis/lib/gnosis_client.dart @@ -0,0 +1,92 @@ +import 'dart:convert'; + +import 'package:cw_evm/evm_chain_client.dart'; +import 'package:cw_evm/.secrets.g.dart' as secrets; +import 'package:cw_evm/evm_chain_transaction_model.dart'; +import 'package:flutter/foundation.dart'; +import 'package:web3dart/web3dart.dart'; + +class GnosisClient extends EVMChainClient { + @override + Transaction createTransaction({ + required EthereumAddress from, + required EthereumAddress to, + required EtherAmount amount, + EtherAmount? maxPriorityFeePerGas, + Uint8List? data, + int? maxGas, + EtherAmount? gasPrice, + EtherAmount? maxFeePerGas, + }) { + return Transaction( + from: from, + to: to, + value: amount, + // data: data, + maxGas: maxGas, + // gasPrice: gasPrice, + // maxFeePerGas: maxFeePerGas, + // maxPriorityFeePerGas: maxPriorityFeePerGas, + ); + } + + @override + Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction) => signedTransaction; + + @override + int get chainId => 137; + + @override + Future> fetchTransactions(String address, + {String? contractAddress}) async { + try { + final response = await httpClient.get(Uri.https("api.gnosisscan.io", "/v2/api", { + "chainid": "$chainId", + "module": "account", + "action": contractAddress != null ? "tokentx" : "txlist", + if (contractAddress != null) "contractaddress": contractAddress, + "address": address, + "apikey": secrets.etherScanApiKey, + })); + + final jsonResponse = json.decode(response.body) as Map; + + if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) { + return (jsonResponse['result'] as List) + .map( + (e) => EVMChainTransactionModel.fromJson(e as Map, 'XDAI'), + ) + .toList(); + } + + return []; + } catch (e) { + return []; + } + } + + @override + Future> fetchInternalTransactions(String address) async { + try { + final response = await httpClient.get(Uri.https("api.gnosisscan.io", "/v2/api", { + "chainid": "$chainId", + "module": "account", + "action": "txlistinternal", + "address": address, + "apikey": secrets.etherScanApiKey, + })); + + final jsonResponse = json.decode(response.body) as Map; + + if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) { + return (jsonResponse['result'] as List) + .map((e) => EVMChainTransactionModel.fromJson(e as Map, 'XDAI')) + .toList(); + } + + return []; + } catch (_) { + return []; + } + } +} diff --git a/cw_gnosis/lib/gnosis_mnemonics_exception.dart b/cw_gnosis/lib/gnosis_mnemonics_exception.dart new file mode 100644 index 000000000..d84afd100 --- /dev/null +++ b/cw_gnosis/lib/gnosis_mnemonics_exception.dart @@ -0,0 +1,5 @@ +class GnosisMnemonicIsIncorrectException implements Exception { + @override + String toString() => + 'Polygon mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; +} diff --git a/cw_gnosis/lib/gnosis_transaction_history.dart b/cw_gnosis/lib/gnosis_transaction_history.dart new file mode 100644 index 000000000..ed4b42d3c --- /dev/null +++ b/cw_gnosis/lib/gnosis_transaction_history.dart @@ -0,0 +1,20 @@ +import 'dart:core'; + +import 'package:cw_evm/evm_chain_transaction_history.dart'; +import 'package:cw_evm/evm_chain_transaction_info.dart'; +import 'package:cw_gnosis/gnosis_transaction_info.dart'; + +class GnosisTransactionHistory extends EVMChainTransactionHistory { + GnosisTransactionHistory({ + required super.walletInfo, + required super.password, + required super.encryptionFileUtils, + }); + + @override + String getTransactionHistoryFileName() => 'gnosis_transactions.json'; + + @override + EVMChainTransactionInfo getTransactionInfo(Map val) => + GnosisTransactionInfo.fromJson(val); +} diff --git a/cw_gnosis/lib/gnosis_transaction_info.dart b/cw_gnosis/lib/gnosis_transaction_info.dart new file mode 100644 index 000000000..1c5574582 --- /dev/null +++ b/cw_gnosis/lib/gnosis_transaction_info.dart @@ -0,0 +1,39 @@ +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_evm/evm_chain_transaction_info.dart'; + +class GnosisTransactionInfo extends EVMChainTransactionInfo { + GnosisTransactionInfo({ + required super.id, + required super.height, + required super.ethAmount, + required super.ethFee, + required super.tokenSymbol, + required super.direction, + required super.isPending, + required super.date, + required super.confirmations, + required super.to, + required super.from, + super.exponent, + }); + + factory GnosisTransactionInfo.fromJson(Map data) { + return GnosisTransactionInfo( + id: data['id'] as String, + height: data['height'] as int, + ethAmount: BigInt.parse(data['amount']), + exponent: data['exponent'] as int, + ethFee: BigInt.parse(data['fee']), + direction: parseTransactionDirectionFromInt(data['direction'] as int), + date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), + isPending: data['isPending'] as bool, + confirmations: data['confirmations'] as int, + tokenSymbol: data['tokenSymbol'] as String, + to: data['to'], + from: data['from'], + ); + } + + @override + String get feeCurrency => 'xDAI'; +} diff --git a/cw_gnosis/lib/gnosis_wallet.dart b/cw_gnosis/lib/gnosis_wallet.dart new file mode 100644 index 000000000..ca379a93b --- /dev/null +++ b/cw_gnosis/lib/gnosis_wallet.dart @@ -0,0 +1,163 @@ +import 'dart:convert'; + +import 'package:cw_core/cake_hive.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/encryption_file_utils.dart'; +import 'package:cw_core/erc20_token.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_keys_file.dart'; +import 'package:cw_evm/evm_chain_transaction_history.dart'; +import 'package:cw_evm/evm_chain_transaction_info.dart'; +import 'package:cw_evm/evm_chain_transaction_model.dart'; +import 'package:cw_evm/evm_chain_wallet.dart'; +import 'package:cw_evm/evm_erc20_balance.dart'; +import 'package:cw_gnosis/default_gnosis_erc20_tokens.dart'; +import 'package:cw_gnosis/gnosis_client.dart'; +import 'package:cw_gnosis/gnosis_transaction_history.dart'; +import 'package:cw_gnosis/gnosis_transaction_info.dart'; + +class GnosisWallet extends EVMChainWallet { + GnosisWallet({ + required super.walletInfo, + required super.password, + super.mnemonic, + super.initialBalance, + super.privateKey, + required super.client, + required super.encryptionFileUtils, + super.passphrase, + }) : super(nativeCurrency: CryptoCurrency.xdai); + + @override + Future initErc20TokensBox() async { + final boxName = "${walletInfo.name.replaceAll(" ", "_")}_ ${Erc20Token.gnosisBoxName}"; + if (await CakeHive.boxExists(boxName)) { + evmChainErc20TokensBox = await CakeHive.openBox(boxName); + } else { + evmChainErc20TokensBox = await CakeHive.openBox(boxName.replaceAll(" ", "")); + } + } + + @override + void addInitialTokens([bool isMigration = false]) { + final initialErc20Tokens = DefaultGnosisErc20Tokens().initialGnosisErc20Tokens; + + for (final token in initialErc20Tokens) { + if (!evmChainErc20TokensBox.containsKey(token.contractAddress)) { + if (isMigration) token.enabled = false; + evmChainErc20TokensBox.put(token.contractAddress, token); + } + } + } + + @override + List get getDefaultTokenContractAddresses => + DefaultGnosisErc20Tokens().initialGnosisErc20Tokens.map((e) => e.contractAddress).toList(); + + @override + Future checkIfScanProviderIsEnabled() async { + bool isPolygonScanEnabled = (await sharedPrefs.future).getBool("use_gnosisscan") ?? true; + return isPolygonScanEnabled; + } + + @override + String getTransactionHistoryFileName() => 'gnosis_transactions.json'; + + @override + Erc20Token createNewErc20TokenObject(Erc20Token token, String? iconPath) { + return Erc20Token( + name: token.name, + symbol: token.symbol, + contractAddress: token.contractAddress, + decimal: token.decimal, + enabled: token.enabled, + tag: token.tag ?? "XDAI", + iconPath: iconPath, + isPotentialScam: token.isPotentialScam, + ); + } + + @override + EVMChainTransactionInfo getTransactionInfo( + EVMChainTransactionModel transactionModel, String address) { + final model = GnosisTransactionInfo( + id: transactionModel.hash, + height: transactionModel.blockNumber, + ethAmount: transactionModel.amount, + direction: transactionModel.from == address + ? TransactionDirection.outgoing + : TransactionDirection.incoming, + isPending: false, + date: transactionModel.date, + confirmations: transactionModel.confirmations, + ethFee: BigInt.from(transactionModel.gasUsed) * transactionModel.gasPrice, + exponent: transactionModel.tokenDecimal ?? 18, + tokenSymbol: transactionModel.tokenSymbol ?? "XDAI", + to: transactionModel.to, + from: transactionModel.from, + ); + return model; + } + + @override + EVMChainTransactionHistory setUpTransactionHistory( + WalletInfo walletInfo, String password, EncryptionFileUtils encryptionFileUtils) { + return GnosisTransactionHistory( + walletInfo: walletInfo, + password: password, + encryptionFileUtils: encryptionFileUtils, + ); + } + + static Future open({ + required String name, + required String password, + required WalletInfo walletInfo, + required EncryptionFileUtils encryptionFileUtils, + }) async { + final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); + final path = await pathForWallet(name: name, type: walletInfo.type); + + Map? data; + try { + final jsonSource = await encryptionFileUtils.read(path: path, password: password); + + data = json.decode(jsonSource) as Map; + } catch (e) { + if (!hasKeysFile) rethrow; + } + + final balance = EVMChainERC20Balance.fromJSON(data?['balance'] as String?) ?? + EVMChainERC20Balance(BigInt.zero); + + final WalletKeysData keysData; + // Migrate wallet from the old scheme to then new .keys file scheme + if (!hasKeysFile) { + final mnemonic = data!['mnemonic'] as String?; + final privateKey = data['private_key'] as String?; + final passphrase = data['passphrase'] as String?; + + keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey, passphrase: passphrase); + } else { + keysData = await WalletKeysFile.readKeysFile( + name, + walletInfo.type, + password, + encryptionFileUtils, + ); + } + + return GnosisWallet( + walletInfo: walletInfo, + password: password, + mnemonic: keysData.mnemonic, + privateKey: keysData.privateKey, + passphrase: keysData.passphrase, + initialBalance: balance, + client: GnosisClient(), + encryptionFileUtils: encryptionFileUtils, + ); + } +} diff --git a/cw_gnosis/lib/gnosis_wallet_service.dart b/cw_gnosis/lib/gnosis_wallet_service.dart new file mode 100644 index 000000000..33757fd7d --- /dev/null +++ b/cw_gnosis/lib/gnosis_wallet_service.dart @@ -0,0 +1,162 @@ +import 'package:bip39/bip39.dart' as bip39; +import 'package:cw_core/encryption_file_utils.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart'; +import 'package:cw_evm/evm_chain_wallet_service.dart'; +import 'package:cw_gnosis/gnosis_mnemonics_exception.dart'; +import 'package:cw_gnosis/gnosis_wallet.dart'; +import 'package:cw_gnosis/gnosis_client.dart'; + +class GnosisWalletService extends EVMChainWalletService { + GnosisWalletService( + super.walletInfoSource, super.isDirect, { + required this.client, + }); + + late GnosisClient client; + + @override + WalletType getType() => WalletType.gnosis; + + @override + Future create(EVMChainNewWalletCredentials credentials, {bool? isTestnet}) async { + final strength = credentials.seedPhraseLength == 24 ? 256 : 128; + + final mnemonic = credentials.mnemonic ?? bip39.generateMnemonic(strength: strength); + + final wallet = GnosisWallet( + walletInfo: credentials.walletInfo!, + mnemonic: mnemonic, + password: credentials.password!, + passphrase: credentials.passphrase, + client: client, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + return wallet; + } + + @override + Future openWallet(String name, String password) async { + final walletInfo = + walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType())); + + try { + final wallet = await GnosisWallet.open( + name: name, + password: password, + walletInfo: walletInfo, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + + await wallet.init(); + wallet.addInitialTokens(true); + await wallet.save(); + saveBackup(name); + return wallet; + } catch (_) { + await restoreWalletFilesFromBackup(name); + + final wallet = await GnosisWallet.open( + name: name, + password: password, + walletInfo: walletInfo, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + + await wallet.init(); + await wallet.save(); + return wallet; + } + } + + @override + Future restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials, + {bool? isTestnet}) async { + final wallet = GnosisWallet( + password: credentials.password!, + privateKey: credentials.privateKey, + walletInfo: credentials.walletInfo!, + client: client, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + return wallet; + } + + @override + Future restoreFromHardwareWallet( + EVMChainRestoreWalletFromHardware credentials) async { + credentials.walletInfo!.derivationInfo = DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/44'/60'/${credentials.hwAccountData.accountIndex}'/0/0" + ); + credentials.walletInfo!.hardwareWalletType = credentials.hardwareWalletType; + credentials.walletInfo!.address = credentials.hwAccountData.address; + + final wallet = GnosisWallet( + walletInfo: credentials.walletInfo!, + password: credentials.password!, + client: client, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + Future restoreFromSeed(EVMChainRestoreWalletFromSeedCredentials credentials, + {bool? isTestnet}) async { + if (!bip39.validateMnemonic(credentials.mnemonic)) { + throw GnosisMnemonicIsIncorrectException(); + } + + final wallet = GnosisWallet( + password: credentials.password!, + mnemonic: credentials.mnemonic, + walletInfo: credentials.walletInfo!, + passphrase: credentials.passphrase, + client: client, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + Future rename(String currentName, String password, String newName) async { + final currentWalletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType())); + final currentWallet = await GnosisWallet.open( + password: password, + name: currentName, + walletInfo: currentWalletInfo, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + + await currentWallet.renameWalletFiles(newName); + await saveBackup(newName); + + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); + } +} diff --git a/cw_gnosis/pubspec.yaml b/cw_gnosis/pubspec.yaml new file mode 100644 index 000000000..c15119ba6 --- /dev/null +++ b/cw_gnosis/pubspec.yaml @@ -0,0 +1,74 @@ +name: cw_gnosis +description: A new Flutter package project. +version: 0.0.1 +publish_to: none +homepage: https://cakewallet.com + +environment: + sdk: '>=3.0.6 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + cw_core: + path: ../cw_core + cw_ethereum: + path: ../cw_ethereum + cw_evm: + path: ../cw_evm + web3dart: ^2.7.1 + hive: ^2.2.3 + bip39: ^1.0.6 + collection: ^1.17.1 + +dependency_overrides: + web3dart: + git: + url: https://github.com/cake-tech/web3dart.git + ref: cake + watcher: ^1.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + build_runner: ^2.4.15 + + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index 344b5391d..69404474b 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -2,6 +2,7 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/validator.dart'; import 'package:cake_wallet/entities/mnemonic_item.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/gnosis/gnosis.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/polygon/polygon.dart'; @@ -40,6 +41,8 @@ class SeedValidator extends Validator { return nano!.getNanoWordList(language); case WalletType.polygon: return polygon!.getPolygonWordList(language); + case WalletType.gnosis: + return gnosis!.getGnosisWordList(language); case WalletType.solana: return solana!.getSolanaWordList(language); case WalletType.tron: diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index b44e56a98..764c526a5 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -80,6 +80,7 @@ class WalletCreationService { case WalletType.bitcoinCash: case WalletType.ethereum: case WalletType.polygon: + case WalletType.gnosis: case WalletType.solana: case WalletType.tron: return true; diff --git a/lib/di.dart b/lib/di.dart index 1d925150f..f0b0537d9 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -30,6 +30,7 @@ import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/hardware_wallet/require_hardware_wallet_connection.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; +import 'package:cake_wallet/gnosis/gnosis.dart'; import 'package:cake_wallet/haven/cw_haven.dart'; import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart'; import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart'; @@ -1140,6 +1141,9 @@ Future setup({ case WalletType.polygon: return polygon!.createPolygonWalletService( _walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); + case WalletType.gnosis: + return gnosis!.createGnosisWalletService( + _walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); case WalletType.solana: return solana!.createSolanaWalletService( _walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 45234d5ec..763b84599 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -36,6 +36,7 @@ const cakeWalletLitecoinElectrumUri = 'ltc-electrum.cakewallet.com:50002'; const havenDefaultNodeUri = 'nodes.havenprotocol.org:443'; const ethereumDefaultNodeUri = 'ethereum-rpc.publicnode.com'; const polygonDefaultNodeUri = 'polygon-bor-rpc.publicnode.com'; +const gnosisDefaultNodeUri = 'gnosis-rpc.publicnode.com'; const cakeWalletBitcoinCashDefaultNodeUri = 'bitcoincash.stackwallet.com:50002'; const nanoDefaultNodeUri = 'nano.nownodes.io'; const nanoDefaultPowNodeUri = 'rpc.nano.to'; @@ -511,6 +512,15 @@ Future defaultSettingsMigration( enabled: true, ); break; + case 50: + await addWalletNodeList(nodes: nodes, type: WalletType.gnosis); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.gnosis, + currentNodePreferenceKey: PreferencesKey.currentGnosisNodeIdKey, + ); + break; default: break; } @@ -607,6 +617,8 @@ String _getDefaultNodeUri(WalletType type) { return cakeWalletBitcoinCashDefaultNodeUri; case WalletType.polygon: return polygonDefaultNodeUri; + case WalletType.gnosis: + return gnosisDefaultNodeUri; case WalletType.solana: return solanaDefaultNodeUri; case WalletType.tron: @@ -1042,6 +1054,7 @@ Future checkCurrentNodes( final currentHavenNodeId = sharedPreferences.getInt(PreferencesKey.currentHavenNodeIdKey); final currentEthereumNodeId = sharedPreferences.getInt(PreferencesKey.currentEthereumNodeIdKey); final currentPolygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey); + final currentGnosisNodeId = sharedPreferences.getInt(PreferencesKey.currentGnosisNodeIdKey); final currentNanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final currentNanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey); final currentDecredNodeId = sharedPreferences.getInt(PreferencesKey.currentDecredNodeIdKey); @@ -1063,6 +1076,8 @@ Future checkCurrentNodes( nodeSource.values.firstWhereOrNull((node) => node.key == currentEthereumNodeId); final currentPolygonNodeServer = nodeSource.values.firstWhereOrNull((node) => node.key == currentPolygonNodeId); + final currentGnosisNodeServer = + nodeSource.values.firstWhereOrNull((node) => node.key == currentGnosisNodeId); final currentNanoNodeServer = nodeSource.values.firstWhereOrNull((node) => node.key == currentNanoNodeId); final currentDecredNodeServer = @@ -1146,6 +1161,12 @@ Future checkCurrentNodes( await sharedPreferences.setInt(PreferencesKey.currentPolygonNodeIdKey, node.key as int); } + if (currentGnosisNodeServer == null) { + final node = Node(uri: gnosisDefaultNodeUri, type: WalletType.gnosis); + await nodeSource.add(node); + await sharedPreferences.setInt(PreferencesKey.currentGnosisNodeIdKey, node.key as int); + } + if (currentSolanaNodeServer == null) { final node = Node(uri: solanaDefaultNodeUri, type: WalletType.solana); await nodeSource.add(node); diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index bb489e715..4d6966f65 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -31,6 +31,9 @@ Future> loadDefaultNodes(WalletType type) async { case WalletType.polygon: path = 'assets/polygon_node_list.yml'; break; + case WalletType.gnosis: + path = 'assets/gnosis_node_list.yml'; + break; case WalletType.solana: path = 'assets/solana_node_list.yml'; break; diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 9e384f462..e2c2b3882 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -8,6 +8,7 @@ class PreferencesKey { static const currentZanoNodeIdKey = 'current_node_id_zano'; static const currentEthereumNodeIdKey = 'current_node_id_eth'; static const currentPolygonNodeIdKey = 'current_node_id_matic'; + static const currentGnosisNodeIdKey = 'current_node_id_gnosis'; static const currentNanoNodeIdKey = 'current_node_id_nano'; static const currentNanoPowNodeIdKey = 'current_node_id_nano_pow'; static const currentDecredNodeIdKey = 'current_node_id_decred'; @@ -48,6 +49,7 @@ class PreferencesKey { static const litecoinTransactionPriority = 'current_fee_priority_litecoin'; static const ethereumTransactionPriority = 'current_fee_priority_ethereum'; static const polygonTransactionPriority = 'current_fee_priority_polygon'; + static const gnosisTransactionPriority = 'current_fee_priority_gnosis'; static const bitcoinCashTransactionPriority = 'current_fee_priority_bitcoin_cash'; static const zanoTransactionPriority = 'current_fee_priority_zano'; static const wowneroTransactionPriority = 'current_fee_priority_wownero'; @@ -72,6 +74,7 @@ class PreferencesKey { static const pinNativeTokenAtTop = 'pin_native_token_at_top'; static const useEtherscan = 'use_etherscan'; static const usePolygonScan = 'use_polygonscan'; + static const useGnosisScan = 'use_gnosisscan'; static const useTronGrid = 'use_trongrid'; static const useMempoolFeeAPI = 'use_mempool_fee_api'; static const defaultNanoRep = 'default_nano_representative'; diff --git a/lib/entities/priority_for_wallet_type.dart b/lib/entities/priority_for_wallet_type.dart index 5307250d5..a4b8ffbe6 100644 --- a/lib/entities/priority_for_wallet_type.dart +++ b/lib/entities/priority_for_wallet_type.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/gnosis/gnosis.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/wownero/wownero.dart'; @@ -25,6 +26,8 @@ List priorityForWalletType(WalletType type) { return bitcoinCash!.getTransactionPriorities(); case WalletType.polygon: return polygon!.getTransactionPriorities(); + case WalletType.gnosis: + return gnosis!.getTransactionPriorities(); // no such thing for nano/banano/solana/tron: case WalletType.nano: case WalletType.banano: diff --git a/lib/reactions/bip39_wallet_utils.dart b/lib/reactions/bip39_wallet_utils.dart index a46adb6b1..adb126818 100644 --- a/lib/reactions/bip39_wallet_utils.dart +++ b/lib/reactions/bip39_wallet_utils.dart @@ -4,6 +4,7 @@ bool isBIP39Wallet(WalletType walletType) { switch (walletType) { case WalletType.ethereum: case WalletType.polygon: + case WalletType.gnosis: case WalletType.solana: case WalletType.tron: case WalletType.bitcoin: diff --git a/lib/reactions/wallet_connect.dart b/lib/reactions/wallet_connect.dart index 37a0b92ec..b80fb4237 100644 --- a/lib/reactions/wallet_connect.dart +++ b/lib/reactions/wallet_connect.dart @@ -6,8 +6,9 @@ import 'package:cw_core/wallet_type.dart'; bool isEVMCompatibleChain(WalletType walletType) { switch (walletType) { - case WalletType.polygon: case WalletType.ethereum: + case WalletType.polygon: + case WalletType.gnosis: return true; default: return false; @@ -16,8 +17,9 @@ bool isEVMCompatibleChain(WalletType walletType) { bool isNFTACtivatedChain(WalletType walletType) { switch (walletType) { - case WalletType.polygon: case WalletType.ethereum: + case WalletType.polygon: + case WalletType.gnosis: case WalletType.solana: return true; default: @@ -27,8 +29,9 @@ bool isNFTACtivatedChain(WalletType walletType) { bool isWalletConnectCompatibleChain(WalletType walletType) { switch (walletType) { - case WalletType.polygon: case WalletType.ethereum: + case WalletType.polygon: + case WalletType.gnosis: case WalletType.solana: return true; default: diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index b394e96ad..cc940d6ee 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -33,6 +33,7 @@ class MenuWidgetState extends State { 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.gnosisIcon = Image.asset('assets/images/gnosis_icon.png'), this.solanaIcon = Image.asset('assets/images/sol_icon.png'), this.tronIcon = Image.asset('assets/images/trx_icon.png'), this.wowneroIcon = Image.asset('assets/images/wownero_icon.png'), @@ -59,6 +60,7 @@ class MenuWidgetState extends State { Image nanoIcon; Image bananoIcon; Image polygonIcon; + Image gnosisIcon; Image solanaIcon; Image tronIcon; Image wowneroIcon; @@ -245,6 +247,8 @@ class MenuWidgetState extends State { return bananoIcon; case WalletType.polygon: return polygonIcon; + case WalletType.gnosis: + return gnosisIcon; case WalletType.solana: return solanaIcon; case WalletType.tron: diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index c88590475..96e8d4741 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -26,6 +26,7 @@ import 'package:cake_wallet/entities/seed_type.dart'; import 'package:cake_wallet/entities/sort_balance_types.dart'; import 'package:cake_wallet/entities/wallet_list_order_types.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/gnosis/gnosis.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; import 'package:cw_core/transaction_priority.dart'; @@ -105,6 +106,7 @@ abstract class SettingsStoreBase with Store { required this.pinNativeTokenAtTop, required this.useEtherscan, required this.usePolygonScan, + required this.useGnosisScan, required this.useTronGrid, required this.useMempoolFeeAPI, required this.defaultNanoRep, @@ -134,6 +136,7 @@ abstract class SettingsStoreBase with Store { TransactionPriority? initialLitecoinTransactionPriority, TransactionPriority? initialEthereumTransactionPriority, TransactionPriority? initialPolygonTransactionPriority, + TransactionPriority? initialGnosisTransactionPriority, TransactionPriority? initialBitcoinCashTransactionPriority, TransactionPriority? initialZanoTransactionPriority, TransactionPriority? initialDecredTransactionPriority, @@ -215,6 +218,10 @@ abstract class SettingsStoreBase with Store { priority[WalletType.polygon] = initialPolygonTransactionPriority; } + if (initialGnosisTransactionPriority != null) { + priority[WalletType.gnosis] = initialGnosisTransactionPriority; + } + if (initialBitcoinCashTransactionPriority != null) { priority[WalletType.bitcoinCash] = initialBitcoinCashTransactionPriority; } @@ -277,6 +284,9 @@ abstract class SettingsStoreBase with Store { case WalletType.polygon: key = PreferencesKey.polygonTransactionPriority; break; + case WalletType.gnosis: + key = PreferencesKey.gnosisTransactionPriority; + break; case WalletType.zano: key = PreferencesKey.zanoTransactionPriority; break; @@ -429,6 +439,11 @@ abstract class SettingsStoreBase with Store { (bool usePolygonScan) => _sharedPreferences.setBool(PreferencesKey.usePolygonScan, usePolygonScan)); + reaction( + (_) => useGnosisScan, + (bool useGnosisScan) => + _sharedPreferences.setBool(PreferencesKey.useGnosisScan, useGnosisScan)); + reaction((_) => useTronGrid, (bool useTronGrid) => _sharedPreferences.setBool(PreferencesKey.useTronGrid, useTronGrid)); @@ -773,6 +788,9 @@ abstract class SettingsStoreBase with Store { @observable bool usePolygonScan; + @observable + bool useGnosisScan; + @observable bool useTronGrid; @@ -908,6 +926,7 @@ abstract class SettingsStoreBase with Store { TransactionPriority? litecoinTransactionPriority; TransactionPriority? ethereumTransactionPriority; TransactionPriority? polygonTransactionPriority; + TransactionPriority? gnosisTransactionPriority; TransactionPriority? bitcoinCashTransactionPriority; TransactionPriority? wowneroTransactionPriority; TransactionPriority? zanoTransactionPriority; @@ -929,6 +948,10 @@ abstract class SettingsStoreBase with Store { polygonTransactionPriority = polygon?.deserializePolygonTransactionPriority( sharedPreferences.getInt(PreferencesKey.polygonTransactionPriority)!); } + if (sharedPreferences.getInt(PreferencesKey.gnosisTransactionPriority) != null) { + gnosisTransactionPriority = gnosis?.deserializeGnosisTransactionPriority( + sharedPreferences.getInt(PreferencesKey.gnosisTransactionPriority)!); + } if (sharedPreferences.getInt(PreferencesKey.bitcoinCashTransactionPriority) != null) { bitcoinCashTransactionPriority = bitcoinCash?.deserializeBitcoinCashTransactionPriority( sharedPreferences.getInt(PreferencesKey.bitcoinCashTransactionPriority)!); @@ -955,6 +978,7 @@ abstract class SettingsStoreBase with Store { wowneroTransactionPriority ??= wownero?.getDefaultTransactionPriority(); decredTransactionPriority ??= decred?.getDecredTransactionPriorityMedium(); polygonTransactionPriority ??= polygon?.getDefaultTransactionPriority(); + gnosisTransactionPriority ??= gnosis?.getDefaultTransactionPriority(); zanoTransactionPriority ??= zano?.getDefaultTransactionPriority(); final currentBalanceDisplayMode = BalanceDisplayMode.deserialize( @@ -999,6 +1023,7 @@ abstract class SettingsStoreBase with Store { : defaultSeedPhraseLength; final useEtherscan = sharedPreferences.getBool(PreferencesKey.useEtherscan) ?? true; final usePolygonScan = sharedPreferences.getBool(PreferencesKey.usePolygonScan) ?? true; + final useGnosisScan = sharedPreferences.getBool(PreferencesKey.useGnosisScan) ?? true; final useTronGrid = sharedPreferences.getBool(PreferencesKey.useTronGrid) ?? true; final useMempoolFeeAPI = sharedPreferences.getBool(PreferencesKey.useMempoolFeeAPI) ?? true; final defaultNanoRep = sharedPreferences.getString(PreferencesKey.defaultNanoRep) ?? ""; @@ -1043,6 +1068,7 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getInt(PreferencesKey.currentBitcoinCashNodeIdKey); final ethereumNodeId = sharedPreferences.getInt(PreferencesKey.currentEthereumNodeIdKey); final polygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey); + final gnosisNodeId = sharedPreferences.getInt(PreferencesKey.currentGnosisNodeIdKey); final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final nanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey); final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); @@ -1062,6 +1088,8 @@ abstract class SettingsStoreBase with Store { nodeSource.values.firstWhereOrNull((e) => e.uriRaw == ethereumDefaultNodeUri); final polygonNode = nodeSource.get(polygonNodeId) ?? nodeSource.values.firstWhereOrNull((e) => e.uriRaw == polygonDefaultNodeUri); + final gnosisNode = nodeSource.get(gnosisNodeId) ?? + nodeSource.values.firstWhereOrNull((e) => e.uriRaw == gnosisDefaultNodeUri); final bitcoinCashElectrumServer = nodeSource.get(bitcoinCashElectrumServerId) ?? nodeSource.values.firstWhereOrNull((e) => e.uriRaw == cakeWalletBitcoinCashDefaultNodeUri); final nanoNode = nodeSource.get(nanoNodeId) ?? @@ -1132,6 +1160,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.polygon] = polygonNode; } + if (gnosisNode != null) { + nodes[WalletType.gnosis] = gnosisNode; + } + if (bitcoinCashElectrumServer != null) { nodes[WalletType.bitcoinCash] = bitcoinCashElectrumServer; } @@ -1304,6 +1336,7 @@ abstract class SettingsStoreBase with Store { pinNativeTokenAtTop: pinNativeTokenAtTop, useEtherscan: useEtherscan, usePolygonScan: usePolygonScan, + useGnosisScan: useGnosisScan, useTronGrid: useTronGrid, useMempoolFeeAPI: useMempoolFeeAPI, defaultNanoRep: defaultNanoRep, @@ -1349,6 +1382,7 @@ abstract class SettingsStoreBase with Store { shouldRequireTOTP2FAForAllSecurityAndBackupSettings, initialEthereumTransactionPriority: ethereumTransactionPriority, initialPolygonTransactionPriority: polygonTransactionPriority, + initialGnosisTransactionPriority: gnosisTransactionPriority, initialSyncMode: savedSyncMode, initialSyncAll: savedSyncAll, shouldShowYatPopup: shouldShowYatPopup, @@ -1398,6 +1432,11 @@ abstract class SettingsStoreBase with Store { priority[WalletType.polygon] = polygon!.deserializePolygonTransactionPriority( sharedPreferences.getInt(PreferencesKey.polygonTransactionPriority)!); } + if (gnosis != null && + sharedPreferences.getInt(PreferencesKey.gnosisTransactionPriority) != null) { + priority[WalletType.gnosis] = gnosis!.deserializeGnosisTransactionPriority( + sharedPreferences.getInt(PreferencesKey.gnosisTransactionPriority)!); + } if (bitcoinCash != null && sharedPreferences.getInt(PreferencesKey.bitcoinCashTransactionPriority) != null) { priority[WalletType.bitcoinCash] = bitcoinCash!.deserializeBitcoinCashTransactionPriority( @@ -1484,6 +1523,7 @@ abstract class SettingsStoreBase with Store { pinNativeTokenAtTop = sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop) ?? true; useEtherscan = sharedPreferences.getBool(PreferencesKey.useEtherscan) ?? true; usePolygonScan = sharedPreferences.getBool(PreferencesKey.usePolygonScan) ?? true; + useGnosisScan = sharedPreferences.getBool(PreferencesKey.usePolygonScan) ?? true; useTronGrid = sharedPreferences.getBool(PreferencesKey.useTronGrid) ?? true; useMempoolFeeAPI = sharedPreferences.getBool(PreferencesKey.useMempoolFeeAPI) ?? true; defaultNanoRep = sharedPreferences.getString(PreferencesKey.defaultNanoRep) ?? ""; @@ -1516,6 +1556,7 @@ abstract class SettingsStoreBase with Store { final havenNodeId = sharedPreferences.getInt(PreferencesKey.currentHavenNodeIdKey); final ethereumNodeId = sharedPreferences.getInt(PreferencesKey.currentEthereumNodeIdKey); final polygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey); + final gnosisNodeId = sharedPreferences.getInt(PreferencesKey.currentGnosisNodeIdKey); final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); final tronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); @@ -1528,6 +1569,7 @@ abstract class SettingsStoreBase with Store { final havenNode = nodeSource.get(havenNodeId); final ethereumNode = nodeSource.get(ethereumNodeId); final polygonNode = nodeSource.get(polygonNodeId); + final gnosisNode = nodeSource.get(gnosisNodeId); final bitcoinCashNode = nodeSource.get(bitcoinCashElectrumServerId); final nanoNode = nodeSource.get(nanoNodeId); final solanaNode = nodeSource.get(solanaNodeId); @@ -1560,6 +1602,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.polygon] = polygonNode; } + if (gnosisNode != null) { + nodes[WalletType.gnosis] = gnosisNode; + } + if (bitcoinCashNode != null) { nodes[WalletType.bitcoinCash] = bitcoinCashNode; } @@ -1716,6 +1762,9 @@ abstract class SettingsStoreBase with Store { case WalletType.polygon: await _sharedPreferences.setInt(PreferencesKey.currentPolygonNodeIdKey, node.key as int); break; + case WalletType.gnosis: + await _sharedPreferences.setInt(PreferencesKey.currentGnosisNodeIdKey, node.key as int); + break; case WalletType.solana: await _sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, node.key as int); break; diff --git a/lib/view_model/advanced_privacy_settings_view_model.dart b/lib/view_model/advanced_privacy_settings_view_model.dart index 803744590..6299dfcfd 100644 --- a/lib/view_model/advanced_privacy_settings_view_model.dart +++ b/lib/view_model/advanced_privacy_settings_view_model.dart @@ -35,9 +35,10 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { // convert to switch case so that it give a syntax error when adding a new wallet type // thus we don't forget about it switch (type) { - case WalletType.ethereum: case WalletType.bitcoinCash: + case WalletType.ethereum: case WalletType.polygon: + case WalletType.gnosis: case WalletType.solana: case WalletType.tron: return true; diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 30bd1c8b3..e191e959e 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -850,6 +850,7 @@ abstract class DashboardViewModelBase with Store { case WalletType.bitcoinCash: case WalletType.ethereum: case WalletType.polygon: + case WalletType.gnosis: case WalletType.solana: case WalletType.nano: case WalletType.banano: diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart index 4794746c4..81649bcf5 100644 --- a/lib/view_model/dashboard/home_settings_view_model.dart +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/erc20_token_info_moralis.dart'; import 'package:cake_wallet/entities/sort_balance_types.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/gnosis/gnosis.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/solana/solana.dart'; @@ -200,6 +201,9 @@ abstract class HomeSettingsViewModelBase with Store { case WalletType.polygon: defaultTokenAddresses = polygon!.getDefaultTokenContractAddresses(); break; + case WalletType.gnosis: + defaultTokenAddresses = gnosis!.getDefaultTokenContractAddresses(); + break; case WalletType.solana: defaultTokenAddresses = solana!.getDefaultTokenContractAddresses(); break; diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 808ccabea..e2c1197e1 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -3,6 +3,7 @@ import 'package:cake_wallet/entities/balance_display_mode.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/gnosis/gnosis.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; @@ -185,6 +186,13 @@ class TransactionListItem extends ActionListItem with Keyable { cryptoAmount: polygon!.formatterPolygonAmountToDouble(transaction: transaction), price: price); break; + case WalletType.gnosis: + final asset = gnosis!.assetOfTransaction(balanceViewModel.wallet, transaction); + final price = balanceViewModel.fiatConvertationStore.prices[asset]; + amount = calculateFiatAmountRaw( + cryptoAmount: gnosis!.formatterGnosisAmountToDouble(transaction: transaction), + price: price); + break; case WalletType.nano: amount = calculateFiatAmountRaw( cryptoAmount: double.parse(nanoUtil!.getRawAsUsableString( diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 58b5e5756..714984151 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -779,6 +779,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with depositCurrency = CryptoCurrency.dcr; receiveCurrency = CryptoCurrency.xmr; break; + case WalletType.gnosis: + depositCurrency = CryptoCurrency.xdai; + receiveCurrency = CryptoCurrency.xmr; + break; case WalletType.none: break; } diff --git a/lib/view_model/node_list/node_create_or_edit_view_model.dart b/lib/view_model/node_list/node_create_or_edit_view_model.dart index 7e4e73915..fd5e8db51 100644 --- a/lib/view_model/node_list/node_create_or_edit_view_model.dart +++ b/lib/view_model/node_list/node_create_or_edit_view_model.dart @@ -73,6 +73,7 @@ abstract class NodeCreateOrEditViewModelBase with Store { switch (_walletType) { case WalletType.ethereum: case WalletType.polygon: + case WalletType.gnosis: case WalletType.solana: case WalletType.banano: case WalletType.nano: diff --git a/lib/view_model/send/fees_view_model.dart b/lib/view_model/send/fees_view_model.dart index f6dd0f201..8eecdbb2e 100644 --- a/lib/view_model/send/fees_view_model.dart +++ b/lib/view_model/send/fees_view_model.dart @@ -3,6 +3,7 @@ import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/gnosis/gnosis.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/store/app_store.dart'; @@ -89,6 +90,8 @@ abstract class FeesViewModelBase extends WalletChangeListenerViewModel with Stor return transactionPriority == bitcoinCash!.getBitcoinCashTransactionPrioritySlow(); case WalletType.polygon: return transactionPriority == polygon!.getPolygonTransactionPrioritySlow(); + case WalletType.gnosis: + return transactionPriority == gnosis!.getGnosisTransactionPrioritySlow(); case WalletType.decred: return transactionPriority == decred!.getDecredTransactionPrioritySlow(); case WalletType.none: diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index 6c3588404..c34332797 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/gnosis/gnosis.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/solana/solana.dart'; @@ -110,6 +111,9 @@ abstract class OutputBase with Store { case WalletType.polygon: _amount = polygon!.formatterPolygonParseAmount(_cryptoAmount); break; + case WalletType.gnosis: + _amount = gnosis!.formatterGnosisParseAmount(_cryptoAmount); + break; case WalletType.wownero: _amount = wownero!.formatterWowneroParseAmount(amount: _cryptoAmount); break; @@ -186,6 +190,10 @@ abstract class OutputBase with Store { return polygon!.formatterPolygonAmountToDouble(amount: BigInt.from(fee)); } + if (_wallet.type == WalletType.gnosis) { + return gnosis!.formatterGnosisAmountToDouble(amount: BigInt.from(fee)); + } + if (_wallet.type == WalletType.zano) { return zano!.formatterIntAmountToDouble(amount: fee, currency: cryptoCurrencyHandler(), forFee: true); } @@ -296,6 +304,7 @@ abstract class OutputBase with Store { case WalletType.monero: case WalletType.ethereum: case WalletType.polygon: + case WalletType.gnosis: case WalletType.solana: case WalletType.tron: case WalletType.haven: diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index 067ca73f9..982d1535c 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -73,6 +73,9 @@ abstract class TransactionDetailsViewModelBase with Store { case WalletType.polygon: _addPolygonListItems(tx, dateFormat); break; + case WalletType.gnosis: + _addGnosisListItems(tx, dateFormat); + break; case WalletType.solana: _addSolanaListItems(tx, dateFormat); break; @@ -183,6 +186,8 @@ abstract class TransactionDetailsViewModelBase with Store { return 'https://nanexplorer.com/banano/block/${txId}'; case WalletType.polygon: return 'https://polygonscan.com/tx/${txId}'; + case WalletType.gnosis: + return 'https://gnosisscan.io/tx/${txId}'; case WalletType.solana: return 'https://solscan.io/tx/${txId}'; case WalletType.tron: @@ -217,6 +222,8 @@ abstract class TransactionDetailsViewModelBase with Store { return S.current.view_transaction_on + 'nanexplorer.com'; case WalletType.polygon: return S.current.view_transaction_on + 'polygonscan.com'; + case WalletType.gnosis: + return S.current.view_transaction_on + 'gnosisscan.io'; case WalletType.solana: return S.current.view_transaction_on + 'solscan.io'; case WalletType.tron: @@ -516,6 +523,56 @@ abstract class TransactionDetailsViewModelBase with Store { items.addAll(_items); } + void _addGnosisListItems(TransactionInfo tx, DateFormat dateFormat) { + final _items = [ + StandartListItem( + title: S.current.transaction_details_transaction_id, + value: tx.txHash, + key: ValueKey('standard_list_item_transaction_details_id_key'), + ), + StandartListItem( + title: S.current.transaction_details_date, + value: dateFormat.format(tx.date), + key: ValueKey('standard_list_item_transaction_details_date_key'), + ), + StandartListItem( + title: S.current.confirmations, + value: tx.confirmations.toString(), + key: ValueKey('standard_list_item_transaction_confirmations_key'), + ), + StandartListItem( + title: S.current.transaction_details_height, + value: '${tx.height}', + key: ValueKey('standard_list_item_transaction_details_height_key'), + ), + StandartListItem( + title: S.current.transaction_details_amount, + value: tx.amountFormatted(), + key: ValueKey('standard_list_item_transaction_details_amount_key'), + ), + if (tx.feeFormatted()?.isNotEmpty ?? false) + StandartListItem( + title: S.current.transaction_details_fee, + value: tx.feeFormatted()!, + key: ValueKey('standard_list_item_transaction_details_fee_key'), + ), + if (showRecipientAddress && tx.to != null && tx.direction == TransactionDirection.outgoing) + StandartListItem( + title: S.current.transaction_details_recipient_address, + value: tx.to!, + key: ValueKey('standard_list_item_transaction_details_recipient_address_key'), + ), + if (tx.direction == TransactionDirection.incoming && tx.from != null) + StandartListItem( + title: S.current.transaction_details_source_address, + value: tx.from!, + key: ValueKey('standard_list_item_transaction_details_source_address_key'), + ), + ]; + + items.addAll(_items); + } + void _addSolanaListItems(TransactionInfo tx, DateFormat dateFormat) { final _items = [ StandartListItem( diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 910f081ee..7918931f3 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -340,6 +340,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo return NanoURI(amount: amount, address: address.address); case WalletType.polygon: return PolygonURI(amount: amount, address: address.address); + case WalletType.gnosis: + return EthereumURI(amount: amount, address: address.address); // ToDo: ? case WalletType.solana: return SolanaURI(amount: amount, address: address.address); case WalletType.tron: diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index da5d04f59..1249a9921 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -124,6 +124,7 @@ abstract class WalletKeysViewModelBase with Store { break; case WalletType.ethereum: case WalletType.polygon: + case WalletType.gnosis: case WalletType.solana: case WalletType.tron: items.addAll([ diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index 86d1be65f..6c7f5aa40 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/core/new_wallet_arguments.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/gnosis/gnosis.dart'; import 'package:cake_wallet/zano/zano.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/solana/solana.dart'; @@ -111,6 +112,13 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { mnemonic: newWalletArguments!.mnemonic, passphrase: passphrase, ); + case WalletType.gnosis: + return gnosis!.createGnosisNewWalletCredentials( + name: name, + password: walletPassword, + mnemonic: newWalletArguments!.mnemonic, + passphrase: passphrase, + ); case WalletType.solana: return solana!.createSolanaNewWalletCredentials( name: name, diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index 6e00ba4cc..1a9d60e91 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/core/generate_wallet_password.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/gnosis/gnosis.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/polygon/polygon.dart'; @@ -38,6 +39,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { type == WalletType.monero || type == WalletType.haven || type == WalletType.wownero, hasRestoreFromPrivateKey = type == WalletType.ethereum || type == WalletType.polygon || + type == WalletType.gnosis || type == WalletType.nano || type == WalletType.banano || type == WalletType.solana || @@ -59,6 +61,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { case WalletType.haven: case WalletType.ethereum: case WalletType.polygon: + case WalletType.gnosis: case WalletType.decred: availableModes = [WalletRestoreMode.seed, WalletRestoreMode.keys]; break; @@ -150,6 +153,13 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { password: password, passphrase: passphrase, ); + case WalletType.gnosis: + return gnosis!.createGnosisRestoreWalletFromSeedCredentials( + name: name, + mnemonic: seed, + password: password, + passphrase: passphrase, + ); case WalletType.solana: return solana!.createSolanaRestoreWalletFromSeedCredentials( name: name,