diff --git a/.github/workflows/automated_integration_test.yml b/.github/workflows/automated_integration_test.yml index 5ccf4aacb..8ff5c137a 100644 --- a/.github/workflows/automated_integration_test.yml +++ b/.github/workflows/automated_integration_test.yml @@ -164,6 +164,7 @@ jobs: echo "const bitcoinCashTestWalletSeeds = '${{ secrets.BITCOIN_CASH_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const polygonTestWalletSeeds = '${{ secrets.POLYGON_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const baseTestWalletSeeds = '${{ secrets.BASE_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart + echo "const arbitrumTestWalletSeeds = '${{ secrets.ARBITRUM_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const solanaTestWalletSeeds = '${{ secrets.SOLANA_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const solanaTestWalletSeeds2 = '${{ secrets.SOLANA_TEST_WALLET_SEEDS2 }}';" >> lib/.secrets.g.dart echo "const tronTestWalletSeeds = '${{ secrets.TRON_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart @@ -178,6 +179,7 @@ jobs: echo "const bitcoinCashTestWalletReceiveAddress = '${{ secrets.BITCOIN_CASH_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const polygonTestWalletReceiveAddress = '${{ secrets.POLYGON_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const baseTestWalletReceiveAddress = '${{ secrets.BASE_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart + echo "const arbitrumTestWalletReceiveAddress = '${{ secrets.ARBITRUM_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const solanaTestWalletReceiveAddress = '${{ secrets.SOLANA_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const tronTestWalletReceiveAddress = '${{ secrets.TRON_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const nanoTestWalletReceiveAddress = '${{ secrets.NANO_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart diff --git a/.github/workflows/no_restricted_imports.yaml b/.github/workflows/no_restricted_imports.yaml index 2b07c27cd..18b228dcb 100644 --- a/.github/workflows/no_restricted_imports.yaml +++ b/.github/workflows/no_restricted_imports.yaml @@ -21,6 +21,7 @@ jobs: "cw_nano" "cw_polygon" "cw_base" + "cw_arbitrum" "cw_solana" "cw_tron" "cw_wownero" diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index a8ea6aab2..02f6e1b4c 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -158,6 +158,7 @@ jobs: echo "const bitcoinCashTestWalletSeeds = '${{ secrets.BITCOIN_CASH_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const polygonTestWalletSeeds = '${{ secrets.POLYGON_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const baseTestWalletSeeds = '${{ secrets.BASE_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart + echo "const arbitrumTestWalletSeeds = '${{ secrets.ARBITRUM_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const solanaTestWalletSeeds = '${{ secrets.SOLANA_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const tronTestWalletSeeds = '${{ secrets.TRON_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const nanoTestWalletSeeds = '${{ secrets.NANO_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart @@ -170,6 +171,7 @@ jobs: echo "const bitcoinCashTestWalletReceiveAddress = '${{ secrets.BITCOIN_CASH_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const polygonTestWalletReceiveAddress = '${{ secrets.POLYGON_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const baseTestWalletReceiveAddress = '${{ secrets.BASE_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart + echo "const arbitrumTestWalletReceiveAddress = '${{ secrets.ARBITRUM_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const solanaTestWalletReceiveAddress = '${{ secrets.SOLANA_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const tronTestWalletReceiveAddress = '${{ secrets.TRON_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const nanoTestWalletReceiveAddress = '${{ secrets.NANO_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 0e736ee64..903f77a75 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -151,6 +151,7 @@ jobs: echo "const bitcoinCashTestWalletSeeds = '${{ secrets.BITCOIN_CASH_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const polygonTestWalletSeeds = '${{ secrets.POLYGON_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const baseTestWalletSeeds = '${{ secrets.BASE_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart + echo "const arbitrumTestWalletSeeds = '${{ secrets.ARBITRUM_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const solanaTestWalletSeeds = '${{ secrets.SOLANA_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const tronTestWalletSeeds = '${{ secrets.TRON_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const nanoTestWalletSeeds = '${{ secrets.NANO_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart @@ -164,6 +165,7 @@ jobs: echo "const bitcoinCashTestWalletReceiveAddress = '${{ secrets.BITCOIN_CASH_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const polygonTestWalletReceiveAddress = '${{ secrets.POLYGON_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const baseTestWalletReceiveAddress = '${{ secrets.BASE_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart + echo "const arbitrumTestWalletReceiveAddress = '${{ secrets.ARBITRUM_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const solanaTestWalletReceiveAddress = '${{ secrets.SOLANA_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const tronTestWalletReceiveAddress = '${{ secrets.TRON_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const nanoTestWalletReceiveAddress = '${{ secrets.NANO_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart diff --git a/.gitignore b/.gitignore index be83f1a62..cd2230504 100644 --- a/.gitignore +++ b/.gitignore @@ -142,6 +142,7 @@ lib/zano/zano.dart lib/decred/decred.dart lib/dogecoin/dogecoin.dart lib/base/base.dart +lib/arbitrum/arbitrum.dart ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png diff --git a/README.md b/README.md index 1266d08a5..43a72e0ad 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Cake Wallet includes support for several cryptocurrencies, including: * Bitcoin Cash (BCH) * Polygon (POL) * Base (BASE) +* Arbitrum (ARB) * Solana (SOL) * Tron (TRX) * Nano (XNO) diff --git a/analysis_options.yaml b/analysis_options.yaml index 8b07015a2..4f6f0f6e9 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -25,6 +25,7 @@ analyzer: lib/wownero/cw_wownero.dart, lib/zano/cw_zano.dart, lib/base/cw_base.dart, + lib/arbitrum/cw_arbitrum.dart, ] language: strict-casts: true diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index 287554cd3..6b3b2cb2d 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -109,6 +109,9 @@ + + + diff --git a/assets/arbitrum_node_list.yml b/assets/arbitrum_node_list.yml new file mode 100644 index 000000000..63894f18a --- /dev/null +++ b/assets/arbitrum_node_list.yml @@ -0,0 +1,12 @@ +- + uri: arbitrum.nownodes.io + is_default: true + useSSL: true + isEnabledForAutoSwitching: true +- + uri: arbitrum.drpc.org + useSSL: true +- + uri: arbitrum-one-rpc.publicnode.com + useSSL: true + isEnabledForAutoSwitching: true \ No newline at end of file diff --git a/assets/images/arbitrum_chain_QR.svg b/assets/images/arbitrum_chain_QR.svg new file mode 100644 index 000000000..fa3338346 --- /dev/null +++ b/assets/images/arbitrum_chain_QR.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/arbitrum_chain_mono.svg b/assets/images/arbitrum_chain_mono.svg new file mode 100644 index 000000000..480dd1dad --- /dev/null +++ b/assets/images/arbitrum_chain_mono.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/crypto/arbitrum.webp b/assets/images/crypto/arbitrum.webp new file mode 100644 index 000000000..b0c78bd9f Binary files /dev/null and b/assets/images/crypto/arbitrum.webp differ diff --git a/cakewallet.bat b/cakewallet.bat index 8baa910e8..d601741d5 100644 --- a/cakewallet.bat +++ b/cakewallet.bat @@ -1,5 +1,5 @@ @echo off -set cw_win_app_config=--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --dogecoin --base +set cw_win_app_config=--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --dogecoin --base --arbitrum set cw_root=%cd% set cw_archive_name=Cake Wallet.zip set cw_archive_path=%cw_root%\%cw_archive_name% @@ -24,7 +24,7 @@ IF NOT EXIST "%secrets_file_path%" ( ) ELSE (echo === Using previously/already generated secrets file: %secrets_file_path% ===) echo === Generating mobx models === -for /d %%i in (cw_core cw_monero cw_bitcoin cw_ethereum cw_evm cw_polygon cw_nano cw_bitcoin_cash cw_solana cw_tron cw_base.) do ( +for /d %%i in (cw_core cw_monero cw_bitcoin cw_ethereum cw_evm cw_polygon cw_nano cw_bitcoin_cash cw_solana cw_tron cw_base cw_arbitrum.) do ( cd %%i call flutter pub get > nul call dart run build_runner build --delete-conflicting-outputs > nul diff --git a/cw_arbitrum/.gitignore b/cw_arbitrum/.gitignore new file mode 100644 index 000000000..eb6c05cd3 --- /dev/null +++ b/cw_arbitrum/.gitignore @@ -0,0 +1,31 @@ +# 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/ +.flutter-plugins +.flutter-plugins-dependencies +build/ diff --git a/cw_arbitrum/.metadata b/cw_arbitrum/.metadata new file mode 100644 index 000000000..6f8d1aa9c --- /dev/null +++ b/cw_arbitrum/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "fcf2c11572af6f390246c056bc905eca609533a0" + channel: "[user-branch]" + +project_type: package diff --git a/cw_arbitrum/CHANGELOG.md b/cw_arbitrum/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/cw_arbitrum/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/cw_arbitrum/LICENSE b/cw_arbitrum/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_arbitrum/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_arbitrum/README.md b/cw_arbitrum/README.md new file mode 100644 index 000000000..4a260d8d2 --- /dev/null +++ b/cw_arbitrum/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/cw_arbitrum/analysis_options.yaml b/cw_arbitrum/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_arbitrum/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_arbitrum/lib/arbirtrum_mnemonics_exception.dart b/cw_arbitrum/lib/arbirtrum_mnemonics_exception.dart new file mode 100644 index 000000000..8af5612db --- /dev/null +++ b/cw_arbitrum/lib/arbirtrum_mnemonics_exception.dart @@ -0,0 +1,5 @@ +class ArbitrumMnemonicIsIncorrectException implements Exception { + @override + String toString() => + 'Arbitrum mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; +} diff --git a/cw_arbitrum/lib/arbitrum_client.dart b/cw_arbitrum/lib/arbitrum_client.dart new file mode 100644 index 000000000..f9fb36d34 --- /dev/null +++ b/cw_arbitrum/lib/arbitrum_client.dart @@ -0,0 +1,107 @@ +import 'dart:convert'; + +import 'package:cw_evm/.secrets.g.dart' as secrets; +import 'package:cw_evm/evm_chain_client.dart'; +import 'package:cw_evm/evm_chain_transaction_model.dart'; +import 'package:flutter/foundation.dart'; +import 'package:web3dart/web3dart.dart'; + +class ArbitrumClient extends EVMChainClient { + @override + Transaction createTransaction({ + required EthereumAddress from, + required EthereumAddress to, + required EtherAmount amount, + EtherAmount? maxPriorityFeePerGas, + Uint8List? data, + int? maxGas, + EtherAmount? gasPrice, + EtherAmount? maxFeePerGas, + }) { + EtherAmount? finalGasPrice = gasPrice; + + if (gasPrice == null && maxFeePerGas != null) { + // If we have EIP-1559 parameters but no legacy gasPrice, then use maxFeePerGas as gasPrice + finalGasPrice = maxFeePerGas; + } + + return Transaction( + from: from, + to: to, + value: amount, + data: data, + maxGas: maxGas, + gasPrice: finalGasPrice, + // maxFeePerGas: maxFeePerGas, + // maxPriorityFeePerGas: maxPriorityFeePerGas, + ); + } + + @override + Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction) => signedTransaction; + + @override + int get chainId => 42161; + + @override + Future> fetchTransactions( + String address, { + String? contractAddress, + }) async { + try { + final response = await client.get( + Uri.https("api.etherscan.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) { + final res = (jsonResponse['result'] as List); + + res.removeWhere((e) => e['value'] == '0'); + + return res + .map((e) => EVMChainTransactionModel.fromJson(e as Map, 'ETH')) + .toList(); + } + + return []; + } catch (e) { + return []; + } + } + + @override + Future> fetchInternalTransactions(String address) async { + try { + final response = await client.get( + Uri.https("api.etherscan.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, 'ETH')) + .toList(); + } + + return []; + } catch (_) { + return []; + } + } +} diff --git a/cw_arbitrum/lib/arbitrum_transaction_history.dart b/cw_arbitrum/lib/arbitrum_transaction_history.dart new file mode 100644 index 000000000..1319c7aea --- /dev/null +++ b/cw_arbitrum/lib/arbitrum_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_arbitrum/arbitrum_transaction_info.dart'; + +class ArbitrumTransactionHistory extends EVMChainTransactionHistory { + ArbitrumTransactionHistory({ + required super.walletInfo, + required super.password, + required super.encryptionFileUtils, + }); + + @override + String getTransactionHistoryFileName() => 'arbitrum_transactions.json'; + + @override + EVMChainTransactionInfo getTransactionInfo(Map val) => + ArbitrumTransactionInfo.fromJson(val); +} diff --git a/cw_arbitrum/lib/arbitrum_transaction_info.dart b/cw_arbitrum/lib/arbitrum_transaction_info.dart new file mode 100644 index 000000000..d12499991 --- /dev/null +++ b/cw_arbitrum/lib/arbitrum_transaction_info.dart @@ -0,0 +1,41 @@ +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_evm/evm_chain_transaction_info.dart'; + +class ArbitrumTransactionInfo extends EVMChainTransactionInfo { + ArbitrumTransactionInfo({ + 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.contractAddress, + super.exponent, + }); + + factory ArbitrumTransactionInfo.fromJson(Map data) { + return ArbitrumTransactionInfo( + 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'], + contractAddress: data['contractAddress'], + ); + } + + @override + String get feeCurrency => 'ETH'; +} diff --git a/cw_arbitrum/lib/arbitrum_wallet.dart b/cw_arbitrum/lib/arbitrum_wallet.dart new file mode 100644 index 000000000..6181f11a2 --- /dev/null +++ b/cw_arbitrum/lib/arbitrum_wallet.dart @@ -0,0 +1,179 @@ +import 'dart:convert'; + +import 'package:cw_arbitrum/arbitrum_client.dart'; +import 'package:cw_arbitrum/arbitrum_transaction_history.dart'; +import 'package:cw_arbitrum/arbitrum_transaction_info.dart'; +import 'package:cw_arbitrum/default_arbitrum_erc20_tokens.dart'; +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'; + +class ArbitrumWallet extends EVMChainWallet { + ArbitrumWallet({ + required super.walletInfo, + required super.password, + required super.derivationInfo, + super.mnemonic, + super.initialBalance, + super.privateKey, + required super.client, + required super.encryptionFileUtils, + super.passphrase, + }) : super(nativeCurrency: CryptoCurrency.arbEth); + + @override + bool get hasPriorityFee => false; + + @override + Future initErc20TokensBox() async { + final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${Erc20Token.arbitrumBoxName}"; + + evmChainErc20TokensBox = await CakeHive.openBox(boxName); + } + + @override + void addInitialTokens() { + final initialErc20Tokens = DefaultArbitrumErc20Tokens().initialArbitrumErc20Tokens; + + for (final token in initialErc20Tokens) { + if (!evmChainErc20TokensBox.containsKey(token.contractAddress)) { + evmChainErc20TokensBox.put(token.contractAddress, token); + } else { + // update existing token + final existingToken = evmChainErc20TokensBox.get(token.contractAddress); + evmChainErc20TokensBox.put( + token.contractAddress, + Erc20Token.copyWith(token, enabled: existingToken!.enabled), + ); + } + } + } + + @override + List get getDefaultTokenContractAddresses => DefaultArbitrumErc20Tokens() + .initialArbitrumErc20Tokens + .map((e) => e.contractAddress) + .toList(); + + @override + Future checkIfScanProviderIsEnabled() async { + return (await sharedPrefs.future).getBool("use_arbiscan") ?? true; + } + + @override + String getTransactionHistoryFileName() => 'arbitrum_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 ?? 'ETH', + iconPath: iconPath, + isPotentialScam: token.isPotentialScam, + ); + } + + @override + EVMChainTransactionInfo getTransactionInfo( + EVMChainTransactionModel transactionModel, + String address, + ) { + final model = ArbitrumTransactionInfo( + 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 ?? "ETH", + to: transactionModel.to, + from: transactionModel.from, + contractAddress: transactionModel.contractAddress, + ); + return model; + } + + @override + EVMChainTransactionHistory setUpTransactionHistory( + WalletInfo walletInfo, + String password, + EncryptionFileUtils encryptionFileUtils, + ) { + return ArbitrumTransactionHistory( + 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 ArbitrumWallet( + walletInfo: walletInfo, + derivationInfo: await walletInfo.getDerivationInfo(), + password: password, + mnemonic: keysData.mnemonic, + privateKey: keysData.privateKey, + passphrase: keysData.passphrase, + initialBalance: balance, + client: ArbitrumClient(), + encryptionFileUtils: encryptionFileUtils, + ); + } +} diff --git a/cw_arbitrum/lib/arbitrum_wallet_service.dart b/cw_arbitrum/lib/arbitrum_wallet_service.dart new file mode 100644 index 000000000..91e0b86ee --- /dev/null +++ b/cw_arbitrum/lib/arbitrum_wallet_service.dart @@ -0,0 +1,174 @@ +import 'package:bip39/bip39.dart' as bip39; +import 'package:cw_arbitrum/arbirtrum_mnemonics_exception.dart'; +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_arbitrum/arbitrum_wallet.dart'; +import 'package:cw_arbitrum/arbitrum_client.dart'; + +class ArbitrumWalletService extends EVMChainWalletService { + ArbitrumWalletService(super.isDirect, {required this.client}); + + late ArbitrumClient client; + + @override + WalletType getType() => WalletType.arbitrum; + + @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 = ArbitrumWallet( + walletInfo: credentials.walletInfo!, + derivationInfo: await credentials.walletInfo!.getDerivationInfo(), + 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 = await WalletInfo.get(name, getType()); + if (walletInfo == null) { + throw Exception('Wallet not found'); + } + + try { + final wallet = await ArbitrumWallet.open( + name: name, + password: password, + walletInfo: walletInfo, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + saveBackup(name); + return wallet; + } catch (_) { + await restoreWalletFilesFromBackup(name); + + final wallet = await ArbitrumWallet.open( + name: name, + password: password, + walletInfo: walletInfo, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + return wallet; + } + } + + @override + Future restoreFromKeys( + EVMChainRestoreWalletFromPrivateKey credentials, { + bool? isTestnet, + }) async { + final wallet = ArbitrumWallet( + password: credentials.password!, + privateKey: credentials.privateKey, + walletInfo: credentials.walletInfo!, + derivationInfo: await credentials.walletInfo!.getDerivationInfo(), + client: client, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + return wallet; + } + + @override + Future restoreFromHardwareWallet( + EVMChainRestoreWalletFromHardware credentials, + ) async { + final derivationInfo = await credentials.walletInfo!.getDerivationInfo(); + derivationInfo.derivationType = DerivationType.bip39; + derivationInfo.derivationPath = "m/44'/60'/${credentials.hwAccountData.accountIndex}'/0/0"; + await derivationInfo.save(); + credentials.walletInfo!.hardwareWalletType = credentials.hardwareWalletType; + credentials.walletInfo!.address = credentials.hwAccountData.address; + await credentials.walletInfo!.save(); + + final wallet = ArbitrumWallet( + walletInfo: credentials.walletInfo!, + derivationInfo: derivationInfo, + 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 ArbitrumMnemonicIsIncorrectException(); + } + + final wallet = ArbitrumWallet( + password: credentials.password!, + mnemonic: credentials.mnemonic, + walletInfo: credentials.walletInfo!, + derivationInfo: await credentials.walletInfo!.getDerivationInfo(), + 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 = await WalletInfo.get(currentName, getType()); + if (currentWalletInfo == null) { + throw Exception('Wallet not found'); + } + final currentWallet = await ArbitrumWallet.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; + + newWalletInfo.save(); + } +} diff --git a/cw_arbitrum/lib/cw_arbitrum.dart b/cw_arbitrum/lib/cw_arbitrum.dart new file mode 100644 index 000000000..298576d81 --- /dev/null +++ b/cw_arbitrum/lib/cw_arbitrum.dart @@ -0,0 +1,5 @@ +/// A Calculator. +class Calculator { + /// Returns [value] plus 1. + int addOne(int value) => value + 1; +} diff --git a/cw_arbitrum/lib/default_arbitrum_erc20_tokens.dart b/cw_arbitrum/lib/default_arbitrum_erc20_tokens.dart new file mode 100644 index 000000000..25cc5739b --- /dev/null +++ b/cw_arbitrum/lib/default_arbitrum_erc20_tokens.dart @@ -0,0 +1,71 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; + +class DefaultArbitrumErc20Tokens { + final List _defaultTokens = [ + Erc20Token( + name: "USD Coin", + symbol: "USDC", + contractAddress: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "USDC.e", + symbol: "USDC.e", + contractAddress: "0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "Wrapped BTC", + symbol: "WBTC", + contractAddress: "0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f", + decimal: 8, + enabled: true, + ), + Erc20Token( + name: "Chainlink Token", + symbol: "LINK", + contractAddress: "0xf97f4df75117a78c1A5a0DBb814Af92458539FB4", + decimal: 18, + enabled: true, + ), + Erc20Token( + name: "Wrapped liquid staked Ether 2.0", + symbol: "wstETH", + contractAddress: "0x0fBcbaEA96Ce0cF7Ee00A8c19c3ab6f5Dc8E1921", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Wrapped Ether", + symbol: "WETH", + contractAddress: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "DAI", + symbol: "DAI", + contractAddress: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", + decimal: 18, + enabled: false, + ), + ]; + + List get initialArbitrumErc20Tokens => _defaultTokens.map((token) { + String? iconPath; + if (token.iconPath?.isEmpty ?? true) { + try { + iconPath = CryptoCurrency.all + .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase()) + .iconPath; + } catch (_) {} + } else { + iconPath = token.iconPath; + } + + return Erc20Token.copyWith(token, icon: iconPath, tag: 'ARB'); + }).toList(); +} diff --git a/cw_arbitrum/pubspec.yaml b/cw_arbitrum/pubspec.yaml new file mode 100644 index 000000000..f6cb36577 --- /dev/null +++ b/cw_arbitrum/pubspec.yaml @@ -0,0 +1,73 @@ +name: cw_arbitrum +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/to/asset-from-package + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # 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/to/font-from-package diff --git a/cw_arbitrum/test/cw_arbitrum_test.dart b/cw_arbitrum/test/cw_arbitrum_test.dart new file mode 100644 index 000000000..5cf35628f --- /dev/null +++ b/cw_arbitrum/test/cw_arbitrum_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:cw_arbitrum/cw_arbitrum.dart'; + +void main() { + test('adds one to input values', () { + final calculator = Calculator(); + expect(calculator.addOne(2), 3); + expect(calculator.addOne(-7), -6); + expect(calculator.addOne(0), 1); + }); +} diff --git a/cw_base/lib/base_wallet.dart b/cw_base/lib/base_wallet.dart index d1287da78..f3e5878a4 100644 --- a/cw_base/lib/base_wallet.dart +++ b/cw_base/lib/base_wallet.dart @@ -45,9 +45,13 @@ class BaseWallet extends EVMChainWallet { for (final token in initialErc20Tokens) { if (!evmChainErc20TokensBox.containsKey(token.contractAddress)) { evmChainErc20TokensBox.put(token.contractAddress, token); - } else { // update existing token + } else { + // update existing token final existingToken = evmChainErc20TokensBox.get(token.contractAddress); - evmChainErc20TokensBox.put(token.contractAddress, Erc20Token.copyWith(token, enabled: existingToken!.enabled)); + evmChainErc20TokensBox.put( + token.contractAddress, + Erc20Token.copyWith(token, enabled: existingToken!.enabled), + ); } } } @@ -58,7 +62,7 @@ class BaseWallet extends EVMChainWallet { @override Future checkIfScanProviderIsEnabled() async { - return (await sharedPrefs.future).getBool("use_basescan") ?? true; + return (await sharedPrefs.future).getBool("use_basescan") ?? true; } @override diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 55938227e..4ae3aca19 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -117,6 +117,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen CryptoCurrency.deps, CryptoCurrency.baseEth, CryptoCurrency.usde, + CryptoCurrency.arbEth, ]; static const havenCurrencies = [ @@ -245,6 +246,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const cbbtc = CryptoCurrency(title: 'CBBTC', tag: 'ETH', fullName: 'Coinbase Wrapped BTC', raw: 103, name: 'cbbtc', iconPath: 'assets/images/cbbtc_icon.png', decimals: 8); static const baseEth = CryptoCurrency(title: 'ETH', tag: 'BASE', fullName: 'Ethereum', raw: 104, name: 'baseth', iconPath: 'assets/images/crypto/base_icon.webp', decimals: 18); static const usde = CryptoCurrency(title: 'USDE', tag: 'BASE', fullName: 'Ethena USDE', raw: 105, name: 'usde', iconPath: 'assets/images/crypto/ethena-usde-logo.png', decimals: 18); + static const arbEth = CryptoCurrency(title: 'ETH', tag: 'ARB', fullName: 'Arbitrum', raw: 106, name: 'arbeth', iconPath: 'assets/images/crypto/arbitrum.webp', 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 5bceeb1b4..fe6899c4b 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -18,6 +18,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type, {bool isTestnet = fal return CryptoCurrency.eth; case WalletType.base: return CryptoCurrency.baseEth; + case WalletType.arbitrum: + return CryptoCurrency.arbEth; case WalletType.bitcoinCash: return CryptoCurrency.bch; case WalletType.nano: diff --git a/cw_core/lib/erc20_token.dart b/cw_core/lib/erc20_token.dart index d090acdca..cbc42adcb 100644 --- a/cw_core/lib/erc20_token.dart +++ b/cw_core/lib/erc20_token.dart @@ -71,6 +71,7 @@ class Erc20Token extends CryptoCurrency with HiveObjectMixin { static const ethereumBoxName = 'EthereumErc20Tokens'; static const polygonBoxName = 'PolygonErc20Tokens'; static const baseBoxName = 'BaseErc20Tokens'; + static const arbitrumBoxName = 'ArbitrumErc20Tokens'; @override bool operator ==(other) => diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 4908641b8..a8bd39aec 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -107,6 +107,7 @@ class Node extends HiveObject with Keyable { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.solana: case WalletType.tron: case WalletType.zano: @@ -171,6 +172,7 @@ class Node extends HiveObject with Keyable { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.solana: case WalletType.tron: case WalletType.dogecoin: diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index dfcc2c460..4aa0bd96c 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -86,6 +86,8 @@ abstract class WalletBase updateEstimatedFeesParams(TransactionPriority priority) async {} + // void fetchTransactionsAsync( // void Function(TransactionType transaction) onTransactionLoaded, // {void Function() onFinished}); diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index 01ae3725d..70a61d12f 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -20,6 +20,7 @@ const walletTypes = [ WalletType.decred, WalletType.dogecoin, WalletType.base, + WalletType.arbitrum, ]; @HiveType(typeId: WALLET_TYPE_TYPE_ID) @@ -74,6 +75,9 @@ enum WalletType { @HiveField(16) base, + + @HiveField(17) + arbitrum, } int serializeToInt(WalletType type) { @@ -110,6 +114,8 @@ int serializeToInt(WalletType type) { return 14; case WalletType.base: return 15; + case WalletType.arbitrum: + return 16; case WalletType.none: return -1; } @@ -149,6 +155,8 @@ WalletType deserializeFromInt(int raw) { return WalletType.dogecoin; case 15: return WalletType.base; + case 16: + return WalletType.arbitrum; default: throw Exception('Unexpected token: $raw for WalletType deserializeFromInt'); } @@ -188,6 +196,8 @@ String walletTypeToString(WalletType type) { return 'Dogecoin'; case WalletType.base: return 'Base'; + case WalletType.arbitrum: + return 'Arbitrum'; case WalletType.none: return ''; } @@ -227,6 +237,8 @@ String walletTypeToDisplayName(WalletType type) { return 'Dogecoin (DOGE)'; case WalletType.base: return 'Base (BASE)'; + case WalletType.arbitrum: + return 'Arbitrum (ARB)'; case WalletType.none: return ''; } @@ -266,6 +278,8 @@ WalletType? cryptoCurrencyToWalletType(CryptoCurrency type) { return WalletType.dogecoin; case CryptoCurrency.baseEth: return WalletType.base; + case CryptoCurrency.arbEth: + return WalletType.arbitrum; default: return null; } diff --git a/cw_evm/lib/evm_chain_client.dart b/cw_evm/lib/evm_chain_client.dart index fbc4bad95..574c2b4f9 100644 --- a/cw_evm/lib/evm_chain_client.dart +++ b/cw_evm/lib/evm_chain_client.dart @@ -163,7 +163,7 @@ abstract class EVMChainClient { required BigInt gasFee, required int estimatedGasUnits, required int maxFeePerGas, - required EVMChainTransactionPriority priority, + required EVMChainTransactionPriority? priority, required CryptoCurrency currency, required String feeCurrency, required int exponent, @@ -172,15 +172,20 @@ abstract class EVMChainClient { int? gasPrice, }) async { assert(currency == CryptoCurrency.eth || - currency == CryptoCurrency.maticpoly || currency == CryptoCurrency.baseEth || + currency == CryptoCurrency.maticpoly || + currency == CryptoCurrency.baseEth || + currency == CryptoCurrency.arbEth || contractAddress != null); - bool isNativeToken = currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly || currency == CryptoCurrency.baseEth; + bool isNativeToken = currency == CryptoCurrency.eth || + currency == CryptoCurrency.maticpoly || + currency == CryptoCurrency.baseEth || + currency == CryptoCurrency.arbEth; final Transaction transaction = createTransaction( from: privateKey.address, to: EthereumAddress.fromHex(toAddress), - maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip), + maxPriorityFeePerGas: priority != null ? EtherAmount.fromInt(EtherUnit.gwei, priority.tip) : null, amount: isNativeToken ? EtherAmount.inWei(amount) : EtherAmount.zero(), data: data != null ? hexToBytes(data) : null, maxGas: estimatedGasUnits, @@ -229,7 +234,7 @@ abstract class EVMChainClient { required String feeCurrency, required int estimatedGasUnits, required int maxFeePerGas, - required EVMChainTransactionPriority priority, + required EVMChainTransactionPriority? priority, required int exponent, required String contractAddress, int? gasPrice, @@ -237,7 +242,7 @@ abstract class EVMChainClient { final Transaction transaction = createTransaction( from: privateKey.address, to: EthereumAddress.fromHex(contractAddress), - maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip), + maxPriorityFeePerGas:priority != null ? EtherAmount.fromInt(EtherUnit.gwei, priority.tip) : null, amount: EtherAmount.zero(), maxGas: estimatedGasUnits, maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas), diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index e1058da64..75b08b0fc 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -115,11 +115,13 @@ abstract class EVMChainWalletBase late final EVMChainClient _client; - int gasPrice = 0; - int? gasBaseFee = 0; - int estimatedGasUnits = 0; + bool hasPriorityFee = true; - Timer? _updateFeesTimer; + @observable + String? nativeTxEstimatedFee; + + @observable + String? erc20TxEstimatedFee; bool _isTransactionUpdating; @@ -168,6 +170,16 @@ abstract class EVMChainWalletBase EncryptionFileUtils encryptionFileUtils, ); + String _getUSDCContractAddress() { + return switch (_client.chainId) { + 1 => "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + 137 => "0x2791bca1f2de4661ed88a30c99a7a9449aa84174", + 8453 => "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + 42161 => "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + _ => throw Exception("Unsupported chain ID: ${_client.chainId}"), + }; + } + @override Future checkNodeHealth() async { try { @@ -175,26 +187,9 @@ abstract class EVMChainWalletBase await _client.getBalance(_evmChainPrivateKey.address, throwOnError: true); // Check USDC token balance - String usdcContractAddress; + String usdcContractAddress = _getUSDCContractAddress(); - switch (_client.chainId) { - case 1: - usdcContractAddress = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"; - break; - case 137: - usdcContractAddress = "0x2791bca1f2de4661ed88a30c99a7a9449aa84174"; - break; - case 8453: - usdcContractAddress = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"; - break; - default: - return true; - } - - await _client.fetchERC20Balances( - _evmChainPrivateKey.address, - usdcContractAddress, - ); + await _client.fetchERC20Balances(_evmChainPrivateKey.address, usdcContractAddress); return true; } catch (e) { @@ -279,30 +274,77 @@ abstract class EVMChainWalletBase } @override - int calculateEstimatedFee(TransactionPriority priority, int? amount) { - { - try { + int calculateEstimatedFee(TransactionPriority priority, int? amount) => 0; + + @override + Future updateEstimatedFeesParams(TransactionPriority priority) async => + await _getEstimatedFees(priority); + + Future _getEstimatedFees(TransactionPriority? priority) async { + final nativeFee = await _getNativeTxFee(priority); + nativeTxEstimatedFee = nativeFee.toString(); + + final erc20Fee = await _getErc20TxFee(priority); + erc20TxEstimatedFee = erc20Fee.toString(); + + printV('Native Estimated Fee: $nativeTxEstimatedFee'); + printV('ERC20 Estimated Fee: $erc20TxEstimatedFee'); + } + + Future _getNativeTxFee(TransactionPriority? priority) async { + try { + int priorityFee = 0; + if (hasPriorityFee) { if (priority is EVMChainTransactionPriority) { - final priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt(); - - int maxFeePerGas; - if (gasBaseFee != null) { - // MaxFeePerGas with EIP1559; - maxFeePerGas = gasBaseFee! + priorityFee; - } else { - // MaxFeePerGas with gasPrice; - maxFeePerGas = gasPrice; - printV('MaxFeePerGas with gasPrice: $maxFeePerGas'); - } - - final totalGasFee = estimatedGasUnits * maxFeePerGas; - return totalGasFee; + priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt(); } - - return 0; - } catch (e) { - return 0; } + + final gasPrice = await _client.getGasUnitPrice(); + final gasBaseFee = await _client.getGasBaseFee(); + + final gasUnits = await _client.getEstimatedGasUnitsForTransaction( + senderAddress: evmChainPrivateKey.address, + toAddress: evmChainPrivateKey.address, + gasPrice: EtherAmount.fromInt(EtherUnit.wei, gasPrice), + value: EtherAmount.fromBigInt(EtherUnit.wei, BigInt.from(0.0000000001)), + ); + + int maxFeePerGas = gasBaseFee != null ? (gasBaseFee + priorityFee) : (gasPrice + priorityFee); + final totalGasFee = gasUnits * maxFeePerGas; + return totalGasFee; + } catch (e) { + printV(e.toString()); + return 0; + } + } + + Future _getErc20TxFee(TransactionPriority? priority) async { + try { + int priorityFee = 0; + if (hasPriorityFee) { + if (priority is EVMChainTransactionPriority) { + priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt(); + } + } + + final gasPrice = await _client.getGasUnitPrice(); + final gasBaseFee = await _client.getGasBaseFee(); + + final gasUnits = await _client.getEstimatedGasUnitsForTransaction( + senderAddress: evmChainPrivateKey.address, + toAddress: evmChainPrivateKey.address, + contractAddress: _getUSDCContractAddress(), // Using USDC for default estimation + gasPrice: EtherAmount.fromInt(EtherUnit.wei, gasPrice), + value: EtherAmount.fromBigInt(EtherUnit.wei, BigInt.from(0.0000000001)), + ); + + int maxFeePerGas = gasBaseFee != null ? (gasBaseFee + priorityFee) : (gasPrice + priorityFee); + final totalGasFee = gasUnits * maxFeePerGas; + return totalGasFee; + } catch (e) { + printV(e.toString()); + return 0; } } @@ -316,76 +358,81 @@ abstract class EVMChainWalletBase required amount, required String? contractAddress, required String receivingAddressHex, - required TransactionPriority priority, + required TransactionPriority? priority, Uint8List? data, }) async { try { - if (priority is EVMChainTransactionPriority) { - final priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt(); - - int maxFeePerGas; - int adjustedGasPrice; - - bool isPolygon = _client.chainId == 137; - - if (gasBaseFee != null) { - // MaxFeePerGas with EIP1559; - maxFeePerGas = gasBaseFee! + priorityFee; - } else { - // MaxFeePerGas with gasPrice - maxFeePerGas = gasPrice + priorityFee; + int priorityFee = 0; + if (hasPriorityFee && priority != null) { + if (priority is EVMChainTransactionPriority) { + priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt(); } - - adjustedGasPrice = maxFeePerGas; - - // Polygon has a minimum priority fee of 25 gwei - if (isPolygon) { - int minPriorityFee = 25; - int minPriorityFeeWei = - EtherAmount.fromInt(EtherUnit.gwei, minPriorityFee).getInWei.toInt(); - - // Calculate user selected priority-based additional fee on top of minimum - int additionalPriorityFee = 0; - switch (priority) { - case EVMChainTransactionPriority.slow: - // We use minimum priority fee only - additionalPriorityFee = 0; - break; - case EVMChainTransactionPriority.medium: - // We add 15 gwei on top of minimum - additionalPriorityFee = EtherAmount.fromInt(EtherUnit.gwei, 15).getInWei.toInt(); - break; - case EVMChainTransactionPriority.fast: - // We add 35 gwei on top of minimum - additionalPriorityFee = EtherAmount.fromInt(EtherUnit.gwei, 35).getInWei.toInt(); - break; - } - - int totalPriorityFee = minPriorityFeeWei + additionalPriorityFee; - adjustedGasPrice = gasPrice + totalPriorityFee; - maxFeePerGas = gasPrice + totalPriorityFee; - } - - final estimatedGas = await _client.getEstimatedGasUnitsForTransaction( - contractAddress: contractAddress, - senderAddress: _evmChainPrivateKey.address, - value: EtherAmount.fromBigInt(EtherUnit.wei, amount!), - gasPrice: EtherAmount.fromInt(EtherUnit.wei, adjustedGasPrice), - toAddress: EthereumAddress.fromHex(receivingAddressHex), - maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas), - data: data, - ); - - final totalGasFee = estimatedGas * adjustedGasPrice; - - return GasParamsHandler( - estimatedGasUnits: estimatedGas, - estimatedGasFee: totalGasFee, - maxFeePerGas: maxFeePerGas, - gasPrice: adjustedGasPrice, - ); } - return GasParamsHandler.zero(); + + final gasBaseFee = await _client.getGasBaseFee(); + final gasPrice = await _client.getGasUnitPrice(); + + int maxFeePerGas; + int adjustedGasPrice; + + bool isPolygon = _client.chainId == 137; + + if (gasBaseFee != null) { + // MaxFeePerGas with EIP1559; + maxFeePerGas = gasBaseFee + priorityFee; + } else { + // MaxFeePerGas with gasPrice + maxFeePerGas = gasPrice + priorityFee; + } + + adjustedGasPrice = maxFeePerGas; + + // Polygon has a minimum priority fee of 25 gwei + if (isPolygon) { + int minPriorityFee = 25; + int minPriorityFeeWei = + EtherAmount.fromInt(EtherUnit.gwei, minPriorityFee).getInWei.toInt(); + + // Calculate user selected priority-based additional fee on top of minimum + int additionalPriorityFee = 0; + switch (priority) { + case EVMChainTransactionPriority.slow: + // We use minimum priority fee only + additionalPriorityFee = 0; + break; + case EVMChainTransactionPriority.medium: + // We add 15 gwei on top of minimum + additionalPriorityFee = EtherAmount.fromInt(EtherUnit.gwei, 15).getInWei.toInt(); + break; + case EVMChainTransactionPriority.fast: + // We add 35 gwei on top of minimum + additionalPriorityFee = EtherAmount.fromInt(EtherUnit.gwei, 35).getInWei.toInt(); + break; + } + + int totalPriorityFee = minPriorityFeeWei + additionalPriorityFee; + adjustedGasPrice = gasPrice + totalPriorityFee; + maxFeePerGas = gasPrice + totalPriorityFee; + } + + final estimatedGas = await _client.getEstimatedGasUnitsForTransaction( + contractAddress: contractAddress, + senderAddress: _evmChainPrivateKey.address, + value: EtherAmount.fromBigInt(EtherUnit.wei, amount!), + gasPrice: EtherAmount.fromInt(EtherUnit.wei, adjustedGasPrice), + toAddress: EthereumAddress.fromHex(receivingAddressHex), + maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas), + data: data, + ); + + final totalGasFee = estimatedGas * adjustedGasPrice; + + return GasParamsHandler( + estimatedGasUnits: estimatedGas, + estimatedGasFee: totalGasFee, + maxFeePerGas: maxFeePerGas, + gasPrice: adjustedGasPrice, + ); } catch (e) { return GasParamsHandler.zero(); } @@ -400,7 +447,6 @@ abstract class EVMChainWalletBase Future close({bool shouldCleanup = false}) async { _client.stop(); _transactionsUpdateTimer?.cancel(); - _updateFeesTimer?.cancel(); } @action @@ -440,30 +486,16 @@ abstract class EVMChainWalletBase await _updateBalance(); await _updateTransactions(); - await _updateEstimatedGasFeeParams(); + await _getEstimatedFees( + hasPriorityFee ? EVMChainTransactionPriority.medium : null, + ); // We're using medium priority for default estimation - _updateFeesTimer ??= Timer.periodic(const Duration(seconds: 30), (timer) async { - await _updateEstimatedGasFeeParams(); - }); syncStatus = SyncedSyncStatus(); } catch (e) { syncStatus = FailedSyncStatus(); } } - Future _updateEstimatedGasFeeParams() async { - gasBaseFee = await _client.getGasBaseFee(); - - gasPrice = await _client.getGasUnitPrice(); - - estimatedGasUnits = await _client.getEstimatedGasUnitsForTransaction( - senderAddress: _evmChainPrivateKey.address, - toAddress: _evmChainPrivateKey.address, - gasPrice: EtherAmount.fromInt(EtherUnit.wei, gasPrice), - value: EtherAmount.fromBigInt(EtherUnit.wei, BigInt.one), - ); - } - @override Future createTransaction(Object credentials) async { final _credentials = credentials as EVMChainTransactionCredentials; @@ -511,7 +543,7 @@ abstract class EVMChainWalletBase final gasFeesModel = await calculateActualEstimatedFeeForCreateTransaction( amount: totalAmount, receivingAddressHex: toAddress, - priority: _credentials.priority!, + priority: _credentials.priority, contractAddress: contractAddress, ); @@ -539,7 +571,7 @@ abstract class EVMChainWalletBase final gasFeesModel = await calculateActualEstimatedFeeForCreateTransaction( amount: totalAmount, receivingAddressHex: toAddress, - priority: _credentials.priority!, + priority: _credentials.priority, contractAddress: contractAddress, ); @@ -562,7 +594,7 @@ abstract class EVMChainWalletBase final refinedGasFeesModel = await calculateActualEstimatedFeeForCreateTransaction( amount: totalAmount, receivingAddressHex: toAddress, - priority: _credentials.priority!, + priority: _credentials.priority, contractAddress: contractAddress, ); @@ -577,7 +609,7 @@ abstract class EVMChainWalletBase totalAmount = (currencyBalance.balance - estimatedFeesForTransaction); } - // check the fees on the base currency (Eth/Polygon) + // check the fees on the base currency if (estimatedFeesForTransaction > balance[currency]!.balance) { throw EVMChainTransactionFeesException(currency.title); } @@ -599,9 +631,9 @@ abstract class EVMChainWalletBase toAddress: toAddress, amount: totalAmount, gasFee: estimatedFeesForTransaction, - priority: _credentials.priority!, + priority: _credentials.priority, currency: transactionCurrency, - feeCurrency: switch (_client.chainId) { 1 => "ETH", 137 => "POL", 8453 => "ETH", _ => "ETH" }, + feeCurrency: switch (_client.chainId) { 137 => "POL", _ => "ETH" }, maxFeePerGas: maxFeePerGasForTransaction, exponent: exponent, contractAddress: @@ -614,12 +646,11 @@ abstract class EVMChainWalletBase } Future createCallDataTransaction( - String to, - String dataHex, - BigInt valueWei, - EVMChainTransactionPriority priority, - ) async { - + String to, + String dataHex, + BigInt valueWei, + EVMChainTransactionPriority? priority, + ) async { // Estimate gas with the SAME call (sender, to, value, data) final gas = await calculateActualEstimatedFeeForCreateTransaction( amount: valueWei, // native value (usually 0 for ERC20 transfer) @@ -632,7 +663,8 @@ abstract class EVMChainWalletBase final nativeCurrency = switch (_client.chainId) { 137 => CryptoCurrency.maticpoly, 8453 => CryptoCurrency.baseEth, - _ => CryptoCurrency.eth, + 42161 => CryptoCurrency.arbEth, + _ => CryptoCurrency.eth, }; // Fallback for nodes that fail estimate (non-zero) @@ -656,9 +688,13 @@ abstract class EVMChainWalletBase ); } - - Future createApprovalTransaction(BigInt amount, String spender, - CryptoCurrency token, EVMChainTransactionPriority priority, String feeCurrency) async { + Future createApprovalTransaction( + BigInt amount, + String spender, + CryptoCurrency token, + EVMChainTransactionPriority? priority, + String feeCurrency, + ) async { final CryptoCurrency transactionCurrency = balance.keys.firstWhere((element) => element.title == token.title); assert(transactionCurrency is Erc20Token); @@ -840,10 +876,7 @@ abstract class EVMChainWalletBase } Future isApprovalRequired( - String tokenContract, - String spender, - BigInt requiredAmount) async { - + String tokenContract, String spender, BigInt requiredAmount) async { const zero = '0x0000000000000000000000000000000000000000'; const evmNative = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; @@ -859,9 +892,7 @@ abstract class EVMChainWalletBase chainId: _client.chainId, ); - final allowance = await erc20.allowance( - owner, - EthereumAddress.fromHex(spender)); + final allowance = await erc20.allowance(owner, EthereumAddress.fromHex(spender)); return allowance < requiredAmount; } catch (e) { @@ -898,7 +929,6 @@ abstract class EVMChainWalletBase @override Future updateTransactionsHistory() async => await _updateTransactions(); - List get erc20Currencies => evmChainErc20TokensBox.values.toList(); Future addErc20Token(Erc20Token token) async { @@ -998,6 +1028,8 @@ abstract class EVMChainWalletBase /// PolygonScan for Polygon. /// /// BaseScan for Base. + /// + /// ArbiScan for Arbitrum. void updateScanProviderUsageState(bool isEnabled) { if (isEnabled) { _updateTransactions(); diff --git a/integration_test/components/common_test_flows.dart b/integration_test/components/common_test_flows.dart index 53baf5dbd..333a6599d 100644 --- a/integration_test/components/common_test_flows.dart +++ b/integration_test/components/common_test_flows.dart @@ -356,6 +356,8 @@ class CommonTestFlows { return secrets.solanaTestWalletSeeds; case WalletType.base: return secrets.baseTestWalletSeeds; + case WalletType.arbitrum: + return secrets.arbitrumTestWalletSeeds; case WalletType.tron: return secrets.tronTestWalletSeeds; case WalletType.nano: @@ -392,6 +394,8 @@ class CommonTestFlows { return secrets.polygonTestWalletReceiveAddress; case WalletType.base: return secrets.baseTestWalletReceiveAddress; + case WalletType.arbitrum: + return secrets.arbitrumTestWalletReceiveAddress; case WalletType.solana: return secrets.solanaTestWalletReceiveAddress; case WalletType.tron: diff --git a/ios/Runner/InfoBase.plist b/ios/Runner/InfoBase.plist index 3b11e216b..59e3bb924 100644 --- a/ios/Runner/InfoBase.plist +++ b/ios/Runner/InfoBase.plist @@ -322,6 +322,26 @@ base-wallet + + CFBundleTypeRole + Editor + CFBundleURLName + arbitrum + CFBundleURLSchemes + + arbitrum + + + + CFBundleTypeRole + Viewer + CFBundleURLName + arbitrum-wallet + CFBundleURLSchemes + + arbitrum-wallet + + CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/lib/arbitrum/cw_arbitrum.dart b/lib/arbitrum/cw_arbitrum.dart new file mode 100644 index 000000000..e4e5b6d64 --- /dev/null +++ b/lib/arbitrum/cw_arbitrum.dart @@ -0,0 +1,244 @@ +part of 'arbitrum.dart'; + +class CWArbitrum extends Arbitrum { + @override + List getArbitrumWordList(String language) => EVMChainMnemonics.englishWordlist; + + WalletService createArbitrumWalletService(bool isDirect) => + ArbitrumWalletService(isDirect, client: ArbitrumClient()); + + @override + WalletCredentials createArbitrumNewWalletCredentials({ + required String name, + String? mnemonic, + WalletInfo? walletInfo, + String? password, + String? passphrase, + }) => + EVMChainNewWalletCredentials( + name: name, + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + passphrase: passphrase, + ); + + @override + WalletCredentials createArbitrumRestoreWalletFromSeedCredentials({ + required String name, + required String mnemonic, + required String password, + String? passphrase, + }) => + EVMChainRestoreWalletFromSeedCredentials( + name: name, + password: password, + mnemonic: mnemonic, + passphrase: passphrase, + ); + + @override + WalletCredentials createArbitrumRestoreWalletFromPrivateKey({ + required String name, + required String privateKey, + required String password, + }) => + EVMChainRestoreWalletFromPrivateKey(name: name, password: password, privateKey: privateKey); + + @override + WalletCredentials createArbitrumHardwareWalletCredentials({ + required String name, + required HardwareAccountData hwAccountData, + WalletInfo? walletInfo, + }) => + EVMChainRestoreWalletFromHardware( + name: name, + hwAccountData: hwAccountData, + walletInfo: walletInfo, + ); + + @override + String getAddress(WalletBase wallet) => (wallet as ArbitrumWallet).walletAddresses.address; + + @override + String getPrivateKey(WalletBase wallet) { + final privateKeyHolder = (wallet as ArbitrumWallet).evmChainPrivateKey; + if (privateKeyHolder is EthPrivateKey) return bytesToHex(privateKeyHolder.privateKey); + return ""; + } + + @override + String getPublicKey(WalletBase wallet) { + final privateKeyInUnitInt = (wallet as ArbitrumWallet).evmChainPrivateKey; + return privateKeyInUnitInt.address.hex; + } + + Object createArbitrumTransactionCredentials( + List outputs, { + required CryptoCurrency currency, + int? feeRate, + }) => + EVMChainTransactionCredentials( + outputs + .map( + (out) => OutputInfo( + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmount, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + formattedCryptoAmount: out.formattedCryptoAmount, + ), + ) + .toList(), + priority: null, + currency: currency, + feeRate: feeRate, + ); + + Object createArbitrumTransactionCredentialsRaw( + List outputs, { + required CryptoCurrency currency, + required int feeRate, + }) => + EVMChainTransactionCredentials( + outputs, + priority: null, + currency: currency, + feeRate: feeRate, + ); + + @override + int formatterArbitrumParseAmount(String amount) => EVMChainFormatter.parseEVMChainAmount(amount); + + @override + double formatterArbitrumAmountToDouble({ + TransactionInfo? transaction, + BigInt? amount, + int exponent = 18, + }) { + assert(transaction != null || amount != null); + + if (transaction != null) { + transaction as EVMChainTransactionInfo; + return transaction.ethAmount / BigInt.from(10).pow(transaction.exponent); + } else { + return (amount!) / BigInt.from(10).pow(exponent); + } + } + + @override + List getERC20Currencies(WalletBase wallet) => + (wallet as ArbitrumWallet).erc20Currencies; + + @override + Future addErc20Token(WalletBase wallet, CryptoCurrency token) => + (wallet as ArbitrumWallet).addErc20Token(token as Erc20Token); + + @override + Future deleteErc20Token(WalletBase wallet, CryptoCurrency token) => + (wallet as ArbitrumWallet).deleteErc20Token(token as Erc20Token); + + @override + Future removeTokenTransactionsInHistory(WalletBase wallet, CryptoCurrency token) => + (wallet as ArbitrumWallet).removeTokenTransactionsInHistory(token as Erc20Token); + + @override + Future getErc20Token(WalletBase wallet, String contractAddress) => + (wallet as ArbitrumWallet).getErc20Token(contractAddress, 'arbitrum'); + + @override + CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) { + transaction as EVMChainTransactionInfo; + if (transaction.tokenSymbol == CryptoCurrency.arbEth.title || + transaction.tokenSymbol == "ARB") { + return CryptoCurrency.arbEth; + } + + wallet as ArbitrumWallet; + + return wallet.erc20Currencies.firstWhere( + (element) => + transaction.contractAddress?.toLowerCase() == element.contractAddress?.toLowerCase(), + ); + } + + @override + void updateArbitrumScanUsageState(WalletBase wallet, bool isEnabled) => + (wallet as ArbitrumWallet).updateScanProviderUsageState(isEnabled); + + @override + Web3Client? getWeb3Client(WalletBase wallet) => (wallet as ArbitrumWallet).getWeb3Client(); + + @override + String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress; + + @override + Future createTokenApproval( + WalletBase wallet, + BigInt amount, + String spender, + CryptoCurrency token, + ) => + (wallet as EVMChainWallet).createApprovalTransaction( + amount, + spender, + token, + null, + "ARB", + ); + + @override + Future setHardwareWalletService(WalletBase wallet, HardwareWalletService service) async { + if (service is EVMChainLedgerService) { + ((wallet as EVMChainWallet).evmChainPrivateKey as EvmLedgerCredentials).setLedgerConnection( + service.ledgerConnection, (await wallet.walletInfo.getDerivationInfo()).derivationPath); + } else if (service is EVMChainBitboxService) { + ((wallet as EVMChainWallet).evmChainPrivateKey as EvmBitboxCredentials) + .setBitbox(service.manager, (await wallet.walletInfo.getDerivationInfo()).derivationPath); + } + return Future.value(); + } + + @override + HardwareWalletService getLedgerHardwareWalletService(ledger.LedgerConnection connection) => + EVMChainLedgerService(connection); + + @override + HardwareWalletService getBitboxHardwareWalletService(bitbox.BitboxManager manager) => + EVMChainBitboxService(manager, chainId: 42161); + + @override + List getDefaultTokenContractAddresses() => DefaultArbitrumErc20Tokens() + .initialArbitrumErc20Tokens + .map((e) => e.contractAddress) + .toList(); + + @override + bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress) { + final arbitrumWallet = wallet as ArbitrumWallet; + return arbitrumWallet.erc20Currencies.any( + (element) => element.contractAddress.toLowerCase() == contractAddress.toLowerCase(), + ); + } + + @override + Future isApprovalRequired( + WalletBase wallet, String tokenContract, String spender, BigInt requiredAmount) => + (wallet as EVMChainWallet).isApprovalRequired(tokenContract, spender, requiredAmount); + + @override + Future createRawCallDataTransaction( + WalletBase wallet, String to, String dataHex, BigInt valueWei) => + (wallet as EVMChainWallet).createCallDataTransaction(to, dataHex, valueWei, null); + + @override + String? getArbitrumNativeEstimatedFee(WalletBase wallet) => + (wallet as EVMChainWallet).nativeTxEstimatedFee; + + @override + String? getArbitrumERC20EstimatedFee(WalletBase wallet) => + (wallet as EVMChainWallet).erc20TxEstimatedFee; +} diff --git a/lib/base/cw_base.dart b/lib/base/cw_base.dart index 42a9ad03c..5b17abd02 100644 --- a/lib/base/cw_base.dart +++ b/lib/base/cw_base.dart @@ -190,7 +190,8 @@ class CWBase extends Base { String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress; @override - Future isApprovalRequired(WalletBase wallet, String tokenContract,String spender, BigInt requiredAmount) => + Future isApprovalRequired( + WalletBase wallet, String tokenContract, String spender, BigInt requiredAmount) => (wallet as EVMChainWallet).isApprovalRequired(tokenContract, spender, requiredAmount); @override @@ -210,10 +211,10 @@ class CWBase extends Base { ); @override - Future createRawCallDataTransaction(WalletBase wallet, String to, String dataHex, BigInt valueWei, - TransactionPriority priority) => - (wallet as EVMChainWallet).createCallDataTransaction(to, dataHex, valueWei, priority as EVMChainTransactionPriority); - + Future createRawCallDataTransaction(WalletBase wallet, String to, + String dataHex, BigInt valueWei, TransactionPriority priority) => + (wallet as EVMChainWallet).createCallDataTransaction( + to, dataHex, valueWei, priority as EVMChainTransactionPriority); @override Future setHardwareWalletService(WalletBase wallet, HardwareWalletService service) async { @@ -246,4 +247,12 @@ class CWBase extends Base { (element) => element.contractAddress.toLowerCase() == contractAddress.toLowerCase(), ); } + + @override + String? getBaseNativeEstimatedFee(WalletBase wallet) => + (wallet as EVMChainWallet).nativeTxEstimatedFee; + + @override + String? getBaseERC20EstimatedFee(WalletBase wallet) => + (wallet as EVMChainWallet).erc20TxEstimatedFee; } diff --git a/lib/buy/dfx/dfx_buy_provider.dart b/lib/buy/dfx/dfx_buy_provider.dart index c590d82b3..5f4c66854 100644 --- a/lib/buy/dfx/dfx_buy_provider.dart +++ b/lib/buy/dfx/dfx_buy_provider.dart @@ -136,6 +136,7 @@ class DFXBuyProvider extends BuyProvider { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.solana: case WalletType.tron: return wallet.signMessage(message); diff --git a/lib/buy/kryptonim/kryptonim.dart b/lib/buy/kryptonim/kryptonim.dart index cbe684686..4bedd9485 100644 --- a/lib/buy/kryptonim/kryptonim.dart +++ b/lib/buy/kryptonim/kryptonim.dart @@ -219,6 +219,7 @@ class KryptonimBuyProvider extends BuyProvider { String? blockchain = switch (cur.tag) { 'ETH' => 'Ethereum', 'BASE' => 'Base', + 'ARB' => 'Arbitrum', 'POL' => 'Polygon', 'AVAXC' => 'Avalanche', 'SOL' => 'Solana', @@ -231,6 +232,7 @@ class KryptonimBuyProvider extends BuyProvider { CryptoCurrency.ltc => 'Litecoin', CryptoCurrency.eth => 'Ethereum', CryptoCurrency.baseEth => 'Base', + CryptoCurrency.arbEth => 'Arbitrum', CryptoCurrency.maticpoly => 'Matic', _ => null, }; diff --git a/lib/buy/onramper/onramper_buy_provider.dart b/lib/buy/onramper/onramper_buy_provider.dart index 615d510bc..8d809a74f 100644 --- a/lib/buy/onramper/onramper_buy_provider.dart +++ b/lib/buy/onramper/onramper_buy_provider.dart @@ -324,6 +324,8 @@ class OnRamperBuyProvider extends BuyProvider { switch (tag) { case 'POL': return 'POLYGON'; + case 'ARB': + return 'ARBITRUM'; case 'ETH': return 'ETHEREUM'; case 'TRX': diff --git a/lib/buy/robinhood/robinhood_buy_provider.dart b/lib/buy/robinhood/robinhood_buy_provider.dart index ee529d3db..57ce1ad87 100644 --- a/lib/buy/robinhood/robinhood_buy_provider.dart +++ b/lib/buy/robinhood/robinhood_buy_provider.dart @@ -106,6 +106,7 @@ class RobinhoodBuyProvider extends BuyProvider { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.solana: case WalletType.tron: case WalletType.dogecoin: diff --git a/lib/core/open_crypto_pay/open_cryptopay_service.dart b/lib/core/open_crypto_pay/open_cryptopay_service.dart index d913a619c..cc94740f0 100644 --- a/lib/core/open_crypto_pay/open_cryptopay_service.dart +++ b/lib/core/open_crypto_pay/open_cryptopay_service.dart @@ -162,6 +162,8 @@ class OpenCryptoPayService { return "Ethereum"; case "POL": return "Polygon"; + case "ARB": + return "Arbitrum"; case "TRX": return "Tron"; case "SOL": diff --git a/lib/core/payment_uris.dart b/lib/core/payment_uris.dart index be4e79007..38f4f6d2e 100644 --- a/lib/core/payment_uris.dart +++ b/lib/core/payment_uris.dart @@ -101,6 +101,21 @@ class BaseURI extends PaymentURI { } } +class ArbitrumURI extends PaymentURI { + ArbitrumURI({required super.amount, required super.address}); + + @override + String toString() { + var base = 'arbitrum:$address'; + + if (amount.isNotEmpty) { + base += '?amount=${amount.replaceAll(',', '.')}'; + } + + return base; + } +} + class BitcoinCashURI extends PaymentURI { BitcoinCashURI({required super.amount, required super.address}); diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index 2643fd0d7..e7facde3b 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/validator.dart'; @@ -55,6 +56,8 @@ class SeedValidator extends Validator { return decred!.getDecredWordList(); case WalletType.base: return base!.getBaseWordList(language); + case WalletType.arbitrum: + return arbitrum!.getArbitrumWordList(language); case WalletType.none: case WalletType.haven: return []; diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index dec1c8022..7f09e7bfb 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -79,6 +79,7 @@ class WalletCreationService { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.solana: case WalletType.tron: case WalletType.dogecoin: diff --git a/lib/di.dart b/lib/di.dart index 1d4a0ac41..fde64bd72 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/anonpay/anonpay_api.dart'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/anypay/anypay_api.dart'; +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; @@ -1226,6 +1227,8 @@ Future setup({ return decred!.createDecredWalletService(_unspentCoinsInfoSource); case WalletType.base: return base!.createBaseWalletService(SettingsStoreBase.walletPasswordDirectInput); + case WalletType.arbitrum: + return arbitrum!.createArbitrumWalletService(SettingsStoreBase.walletPasswordDirectInput); case WalletType.haven: return HavenWalletService(); case WalletType.none: diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 83671474d..9f2ac0bce 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -50,6 +50,7 @@ const moneroWorldNodeUri = '.moneroworld.com'; const decredDefaultUri = "default-spv-nodes"; const dogecoinDefaultNodeUri = 'dogecoin.stackwallet.com:50022'; const baseDefaultNodeUri = 'base.nownodes.io'; +const arbitrumDefaultNodeUri = 'arbitrum.nownodes.io'; Future defaultSettingsMigration( {required int version, @@ -545,6 +546,16 @@ Future defaultSettingsMigration( currentNodePreferenceKey: PreferencesKey.currentBaseNodeIdKey, ); break; + case 53: + await addWalletNodeList(nodes: nodes, type: WalletType.arbitrum); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.arbitrum, + currentNodePreferenceKey: PreferencesKey.currentArbitrumNodeIdKey, + ); + break; + default: break; } @@ -655,6 +666,8 @@ String _getDefaultNodeUri(WalletType type) { return dogecoinDefaultNodeUri; case WalletType.base: return baseDefaultNodeUri; + case WalletType.arbitrum: + return arbitrumDefaultNodeUri; case WalletType.banano: case WalletType.none: return ''; @@ -1057,6 +1070,7 @@ Future checkCurrentNodes( final currentEthereumNodeId = sharedPreferences.getInt(PreferencesKey.currentEthereumNodeIdKey); final currentPolygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey); final currentBaseNodeId = sharedPreferences.getInt(PreferencesKey.currentBaseNodeIdKey); + final currentArbitrumNodeId = sharedPreferences.getInt(PreferencesKey.currentArbitrumNodeIdKey); final currentNanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final currentNanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey); final currentDecredNodeId = sharedPreferences.getInt(PreferencesKey.currentDecredNodeIdKey); @@ -1082,6 +1096,8 @@ Future checkCurrentNodes( nodeSource.values.firstWhereOrNull((node) => node.key == currentPolygonNodeId); final currentBaseNodeServer = nodeSource.values.firstWhereOrNull((node) => node.key == currentBaseNodeId); + final currentArbitrumNodeServer = + nodeSource.values.firstWhereOrNull((node) => node.key == currentArbitrumNodeId); final currentNanoNodeServer = nodeSource.values.firstWhereOrNull((node) => node.key == currentNanoNodeId); final currentDecredNodeServer = @@ -1179,6 +1195,12 @@ Future checkCurrentNodes( await sharedPreferences.setInt(PreferencesKey.currentBaseNodeIdKey, node.key as int); } + if (currentArbitrumNodeServer == null) { + final node = Node(uri: arbitrumDefaultNodeUri, type: WalletType.arbitrum); + await nodeSource.add(node); + await sharedPreferences.setInt(PreferencesKey.currentArbitrumNodeIdKey, node.key as int); + } + if (currentSolanaNodeServer == null) { final node = Node(uri: solanaDefaultNodeUri, type: WalletType.solana); await nodeSource.add(node); diff --git a/lib/entities/ens_record.dart b/lib/entities/ens_record.dart index b49afc0b3..35ed46995 100644 --- a/lib/entities/ens_record.dart +++ b/lib/entities/ens_record.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/polygon/polygon.dart'; @@ -26,6 +27,10 @@ class EnsRecord { _client = base!.getWeb3Client(wallet); } + if (wallet != null && wallet.type == WalletType.arbitrum) { + _client = arbitrum!.getWeb3Client(wallet); + } + if (_client == null) { late final client = ProxyWrapper().getHttpIOClient(); @@ -48,6 +53,7 @@ class EnsRecord { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: default: return (await ens.withName(name).getAddress()).hex; } diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index 90bb9ddcb..4f856db7d 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -52,6 +52,9 @@ Future> loadDefaultNodes(WalletType type) async { case WalletType.base: path = 'assets/base_node_list.yml'; break; + case WalletType.arbitrum: + path = 'assets/arbitrum_node_list.yml'; + break; case WalletType.banano: case WalletType.none: path = ''; @@ -104,6 +107,7 @@ Future resetToDefault(Box nodeSource) async { final zanoNodes = await loadDefaultNodes(WalletType.zano); final dogecoinElectrumServerList = await loadDefaultNodes(WalletType.dogecoin); final baseNodes = await loadDefaultNodes(WalletType.base); + final arbitrumNodes = await loadDefaultNodes(WalletType.arbitrum); final nodes = moneroNodes + bitcoinElectrumServerList + @@ -118,7 +122,8 @@ Future resetToDefault(Box nodeSource) async { zanoNodes + decredNodes + dogecoinElectrumServerList + - baseNodes; + baseNodes + + arbitrumNodes; await nodeSource.clear(); await nodeSource.addAll(nodes); diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index cbcf62dad..442810875 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -115,6 +115,7 @@ class AddressResolver { "pog", "polygon", "base", + "arbitrum" "press", "privacy", "pro", diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index eb678a07a..a9acc2a6b 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -9,6 +9,7 @@ class PreferencesKey { static const currentEthereumNodeIdKey = 'current_node_id_eth'; static const currentPolygonNodeIdKey = 'current_node_id_matic'; static const currentBaseNodeIdKey = 'current_node_id_base'; + static const currentArbitrumNodeIdKey = 'current_node_id_arbitrum'; static const currentNanoNodeIdKey = 'current_node_id_nano'; static const currentNanoPowNodeIdKey = 'current_node_id_nano_pow'; static const currentDecredNodeIdKey = 'current_node_id_decred'; @@ -80,6 +81,7 @@ class PreferencesKey { static const useEtherscan = 'use_etherscan'; static const usePolygonScan = 'use_polygonscan'; static const useBaseScan = 'use_base_scan'; + static const useArbiScan = 'use_arbitrum_scan'; 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 37ad2ab7e..4538da8e6 100644 --- a/lib/entities/priority_for_wallet_type.dart +++ b/lib/entities/priority_for_wallet_type.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; @@ -31,7 +32,7 @@ List priorityForWalletType(WalletType type) { return polygon!.getTransactionPriorities(); case WalletType.base: return base!.getTransactionPriorities(); - // no such thing for nano/banano/solana/tron: + case WalletType.arbitrum: case WalletType.nano: case WalletType.banano: case WalletType.solana: diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart index 4433a5494..26bb8cd19 100644 --- a/lib/ethereum/cw_ethereum.dart +++ b/lib/ethereum/cw_ethereum.dart @@ -189,8 +189,8 @@ class CWEthereum extends Ethereum { ((wallet as EVMChainWallet).evmChainPrivateKey as EvmBitboxCredentials) .setBitbox(service.manager, (await wallet.walletInfo.getDerivationInfo()).derivationPath); } else if (service is EVMChainTrezorService) { - ((wallet as EVMChainWallet).evmChainPrivateKey as EvmTrezorCredentials) - .setTrezorConnect(service.connect, (await wallet.walletInfo.getDerivationInfo()).derivationPath); + ((wallet as EVMChainWallet).evmChainPrivateKey as EvmTrezorCredentials).setTrezorConnect( + service.connect, (await wallet.walletInfo.getDerivationInfo()).derivationPath); } } @@ -211,15 +211,16 @@ class CWEthereum extends Ethereum { return DefaultEthereumErc20Tokens().initialErc20Tokens.map((e) => e.contractAddress).toList(); } - @override bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress) { final ethereumWallet = wallet as EthereumWallet; - return ethereumWallet.erc20Currencies.any((element) => element.contractAddress.toLowerCase() == contractAddress.toLowerCase()); + return ethereumWallet.erc20Currencies + .any((element) => element.contractAddress.toLowerCase() == contractAddress.toLowerCase()); } @override - Future isApprovalRequired(WalletBase wallet, String tokenContract,String spender, BigInt requiredAmount) => + Future isApprovalRequired( + WalletBase wallet, String tokenContract, String spender, BigInt requiredAmount) => (wallet as EVMChainWallet).isApprovalRequired(tokenContract, spender, requiredAmount); @override @@ -229,9 +230,10 @@ class CWEthereum extends Ethereum { amount, spender, token, priority as EVMChainTransactionPriority, "ETH"); @override - Future createRawCallDataTransaction(WalletBase wallet, String to, String dataHex, BigInt valueWei, - TransactionPriority priority) => - (wallet as EVMChainWallet).createCallDataTransaction(to, dataHex, valueWei, priority as EVMChainTransactionPriority); + Future createRawCallDataTransaction(WalletBase wallet, String to, + String dataHex, BigInt valueWei, TransactionPriority priority) => + (wallet as EVMChainWallet).createCallDataTransaction( + to, dataHex, valueWei, priority as EVMChainTransactionPriority); // Integrations @override @@ -270,4 +272,12 @@ class CWEthereum extends Ethereum { @override Future enableDEuroSaving(WalletBase wallet, TransactionPriority priority) => DEuro(wallet as EthereumWallet).enableSavings(priority as EVMChainTransactionPriority); + + @override + String? getEthereumNativeEstimatedFee(WalletBase wallet) => + (wallet as EVMChainWallet).nativeTxEstimatedFee; + + @override + String? getEthereumERC20EstimatedFee(WalletBase wallet) => + (wallet as EVMChainWallet).erc20TxEstimatedFee; } diff --git a/lib/exchange/provider/sideshift_exchange_provider.dart b/lib/exchange/provider/sideshift_exchange_provider.dart index 763064ec1..629597b34 100644 --- a/lib/exchange/provider/sideshift_exchange_provider.dart +++ b/lib/exchange/provider/sideshift_exchange_provider.dart @@ -466,6 +466,8 @@ class SideShiftExchangeProvider extends ExchangeProvider { return 'lightning'; case 'POL': return 'polygon'; + case 'ARB': + return 'arbitrum'; case 'ZEC': return 'zcash'; case 'AVAXC': @@ -481,6 +483,7 @@ class SideShiftExchangeProvider extends ExchangeProvider { 'tron' => 'TRX', 'lightning' => 'LN', 'polygon' => 'POL', + 'arbitrum' => 'ARB', 'zcash' => 'ZEC', 'avax' => 'AVAXC', _ => network, diff --git a/lib/main.dart b/lib/main.dart index a5b9be867..fe6e9477a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -260,7 +260,7 @@ Future initializeAppConfigs({bool loadWallet = true}) async { payjoinSessionSource: payjoinSessionSource, anonpayInvoiceInfo: anonpayInvoiceInfo, havenSeedStore: havenSeedStore, - initialMigrationVersion: 52, + initialMigrationVersion: 53, ); } diff --git a/lib/polygon/cw_polygon.dart b/lib/polygon/cw_polygon.dart index 8c8c2a78d..94396f83c 100644 --- a/lib/polygon/cw_polygon.dart +++ b/lib/polygon/cw_polygon.dart @@ -14,13 +14,14 @@ class CWPolygon extends Polygon { WalletInfo? walletInfo, String? password, String? passphrase, - }) => EVMChainNewWalletCredentials( - name: name, - walletInfo: walletInfo, - password: password, - mnemonic: mnemonic, - passphrase: passphrase, - ); + }) => + EVMChainNewWalletCredentials( + name: name, + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + passphrase: passphrase, + ); @override WalletCredentials createPolygonRestoreWalletFromSeedCredentials({ @@ -28,30 +29,33 @@ class CWPolygon extends Polygon { required String mnemonic, required String password, String? passphrase, - }) => EVMChainRestoreWalletFromSeedCredentials( - name: name, - password: password, - mnemonic: mnemonic, - passphrase: passphrase, - ); + }) => + EVMChainRestoreWalletFromSeedCredentials( + name: name, + password: password, + mnemonic: mnemonic, + passphrase: passphrase, + ); @override WalletCredentials createPolygonRestoreWalletFromPrivateKey({ required String name, required String privateKey, required String password, - }) => EVMChainRestoreWalletFromPrivateKey(name: name, password: password, privateKey: privateKey); + }) => + EVMChainRestoreWalletFromPrivateKey(name: name, password: password, privateKey: privateKey); @override WalletCredentials createPolygonHardwareWalletCredentials({ required String name, required HardwareAccountData hwAccountData, WalletInfo? walletInfo, - }) => EVMChainRestoreWalletFromHardware( - name: name, - hwAccountData: hwAccountData, - walletInfo: walletInfo, - ); + }) => + EVMChainRestoreWalletFromHardware( + name: name, + hwAccountData: hwAccountData, + walletInfo: walletInfo, + ); @override String getAddress(WalletBase wallet) => (wallet as PolygonWallet).walletAddresses.address; @@ -87,37 +91,39 @@ class CWPolygon extends Polygon { required TransactionPriority priority, required CryptoCurrency currency, int? feeRate, - }) => EVMChainTransactionCredentials( - outputs - .map( - (out) => OutputInfo( - fiatAmount: out.fiatAmount, - cryptoAmount: out.cryptoAmount, - address: out.address, - note: out.note, - sendAll: out.sendAll, - extractedAddress: out.extractedAddress, - isParsedAddress: out.isParsedAddress, - formattedCryptoAmount: out.formattedCryptoAmount, - ), - ) - .toList(), - priority: priority as EVMChainTransactionPriority, - currency: currency, - feeRate: feeRate, - ); + }) => + EVMChainTransactionCredentials( + outputs + .map( + (out) => OutputInfo( + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmount, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + formattedCryptoAmount: out.formattedCryptoAmount, + ), + ) + .toList(), + priority: priority as EVMChainTransactionPriority, + currency: currency, + feeRate: feeRate, + ); Object createPolygonTransactionCredentialsRaw( List outputs, { TransactionPriority? priority, required CryptoCurrency currency, required int feeRate, - }) => EVMChainTransactionCredentials( - outputs, - priority: priority as EVMChainTransactionPriority?, - currency: currency, - feeRate: feeRate, - ); + }) => + EVMChainTransactionCredentials( + outputs, + priority: priority as EVMChainTransactionPriority?, + currency: currency, + feeRate: feeRate, + ); @override int formatterPolygonParseAmount(String amount) => EVMChainFormatter.parseEVMChainAmount(amount); @@ -169,7 +175,8 @@ class CWPolygon extends Polygon { wallet as PolygonWallet; return wallet.erc20Currencies.firstWhere( - (element) => transaction.contractAddress?.toLowerCase() == element.contractAddress?.toLowerCase(), + (element) => + transaction.contractAddress?.toLowerCase() == element.contractAddress?.toLowerCase(), ); } @@ -183,12 +190,14 @@ class CWPolygon extends Polygon { @override String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress; - Future isApprovalRequired(WalletBase wallet, String tokenContract,String spender, BigInt requiredAmount) => + Future isApprovalRequired( + WalletBase wallet, String tokenContract, String spender, BigInt requiredAmount) => (wallet as PolygonWallet).isApprovalRequired(tokenContract, spender, requiredAmount); - Future createRawCallDataTransaction(WalletBase wallet, String to, String dataHex, BigInt valueWei, - TransactionPriority priority) => - (wallet as EVMChainWallet).createCallDataTransaction(to, dataHex, valueWei, priority as EVMChainTransactionPriority); + Future createRawCallDataTransaction(WalletBase wallet, String to, + String dataHex, BigInt valueWei, TransactionPriority priority) => + (wallet as EVMChainWallet).createCallDataTransaction( + to, dataHex, valueWei, priority as EVMChainTransactionPriority); @override Future createTokenApproval( @@ -197,13 +206,14 @@ class CWPolygon extends Polygon { String spender, CryptoCurrency token, TransactionPriority priority, - ) => (wallet as EVMChainWallet).createApprovalTransaction( - amount, - spender, - token, - priority as EVMChainTransactionPriority, - "POL", - ); + ) => + (wallet as EVMChainWallet).createApprovalTransaction( + amount, + spender, + token, + priority as EVMChainTransactionPriority, + "POL", + ); @override Future setHardwareWalletService(WalletBase wallet, HardwareWalletService service) async { @@ -214,8 +224,8 @@ class CWPolygon extends Polygon { ((wallet as EVMChainWallet).evmChainPrivateKey as EvmBitboxCredentials) .setBitbox(service.manager, (await wallet.walletInfo.getDerivationInfo()).derivationPath); } else if (service is EVMChainTrezorService) { - ((wallet as EVMChainWallet).evmChainPrivateKey as EvmTrezorCredentials) - .setTrezorConnect(service.connect, (await wallet.walletInfo.getDerivationInfo()).derivationPath); + ((wallet as EVMChainWallet).evmChainPrivateKey as EvmTrezorCredentials).setTrezorConnect( + service.connect, (await wallet.walletInfo.getDerivationInfo()).derivationPath); } } @@ -242,4 +252,12 @@ class CWPolygon extends Polygon { (element) => element.contractAddress.toLowerCase() == contractAddress.toLowerCase(), ); } + + @override + String? getPolygonNativeEstimatedFee(WalletBase wallet) => + (wallet as EVMChainWallet).nativeTxEstimatedFee; + + @override + String? getPolygonERC20EstimatedFee(WalletBase wallet) => + (wallet as EVMChainWallet).erc20TxEstimatedFee; } diff --git a/lib/reactions/fiat_rate_update.dart b/lib/reactions/fiat_rate_update.dart index abd8b097e..a821254c7 100644 --- a/lib/reactions/fiat_rate_update.dart +++ b/lib/reactions/fiat_rate_update.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/core/fiat_conversion_service.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; @@ -49,6 +50,10 @@ Future startFiatRateUpdate( currencies = base!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); } + if (appStore.wallet!.type == WalletType.arbitrum) { + currencies = + arbitrum!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); + } if (appStore.wallet!.type == WalletType.solana) { currencies = diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index 53da945c1..d6c872c05 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/di.dart'; @@ -133,6 +134,10 @@ void startCurrentWalletChangeReaction( currencies = base!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); } + if (wallet.type == WalletType.arbitrum) { + currencies = + arbitrum!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); + } if (wallet.type == WalletType.solana) { currencies = solana!.getSPLTokenCurrencies(appStore.wallet!).where((element) => element.enabled); diff --git a/lib/reactions/wallet_connect.dart b/lib/reactions/wallet_connect.dart index 7e969f344..0357b12c8 100644 --- a/lib/reactions/wallet_connect.dart +++ b/lib/reactions/wallet_connect.dart @@ -9,6 +9,7 @@ bool isEVMCompatibleChain(WalletType walletType) { case WalletType.polygon: case WalletType.ethereum: case WalletType.base: + case WalletType.arbitrum: return true; default: return false; @@ -21,6 +22,7 @@ bool isNFTACtivatedChain(WalletType walletType) { case WalletType.ethereum: case WalletType.base: case WalletType.solana: + case WalletType.arbitrum: return true; default: return false; @@ -33,6 +35,7 @@ bool isWalletConnectCompatibleChain(WalletType walletType) { case WalletType.ethereum: case WalletType.base: case WalletType.solana: + case WalletType.arbitrum: return true; default: return false; @@ -47,6 +50,8 @@ String getChainNameSpaceAndIdBasedOnWalletType(WalletType walletType) { return EVMChainId.polygon.chain(); case WalletType.base: return EVMChainId.base.chain(); + case WalletType.arbitrum: + return EVMChainId.arbitrum.chain(); case WalletType.solana: return SolanaChainId.mainnet.chain(); default: @@ -59,6 +64,7 @@ List getChainSupportedMethodsOnWalletType(WalletType walletType) { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: return EVMSupportedMethods.values.map((e) => e.name).toList(); case WalletType.solana: return SolanaSupportedMethods.values.map((e) => e.name).toList(); @@ -73,6 +79,8 @@ int getChainIdBasedOnWalletType(WalletType walletType) { return 137; case WalletType.base: return 8453; + case WalletType.arbitrum: + return 42161; // For now, we return eth chain Id as the default, we'll modify as we add more wallets case WalletType.ethereum: default: @@ -88,6 +96,8 @@ String getChainNameBasedOnWalletType(WalletType walletType) { return 'polygon'; case WalletType.base: return 'base'; + case WalletType.arbitrum: + return 'arbitrum'; case WalletType.solana: return 'mainnet'; default: @@ -103,6 +113,8 @@ String getTokenNameBasedOnWalletType(WalletType walletType) { return 'MATIC'; case WalletType.base: return 'BASE'; + case WalletType.arbitrum: + return 'ARB'; case WalletType.solana: return 'SOL'; default: diff --git a/lib/reactions/wallet_utils.dart b/lib/reactions/wallet_utils.dart index 8250dfd5b..2b48a49ea 100644 --- a/lib/reactions/wallet_utils.dart +++ b/lib/reactions/wallet_utils.dart @@ -5,6 +5,7 @@ bool isBIP39Wallet(WalletType walletType) { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.solana: case WalletType.tron: case WalletType.bitcoin: @@ -43,6 +44,7 @@ bool hasTokens(WalletType walletType) { case WalletType.tron: case WalletType.zano: case WalletType.base: + case WalletType.arbitrum: return true; default: return false; diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart index b77346a30..655abb5a6 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart @@ -39,6 +39,7 @@ class _DesktopWalletSelectionDropDownState extends State { this.bitcoinCashIcon = Image.asset('assets/images/crypto/bitcoin-cash.webp'), this.polygonIcon = Image.asset('assets/images/crypto/polygon.webp'), this.baseIcon = Image.asset('assets/images/crypto/base_icon.webp'), + this.arbitrumIcon = Image.asset('assets/images/crypto/arbitrum.webp'), this.solanaIcon = Image.asset('assets/images/crypto/solana.webp'), this.tronIcon = Image.asset('assets/images/crypto/tron.webp'), this.wowneroIcon = Image.asset('assets/images/crypto/wownero.webp'), @@ -62,6 +63,7 @@ class MenuWidgetState extends State { Image bananoIcon; Image polygonIcon; Image baseIcon; + Image arbitrumIcon; Image solanaIcon; Image tronIcon; Image wowneroIcon; @@ -251,6 +253,8 @@ class MenuWidgetState extends State { return solanaIcon; case WalletType.base: return baseIcon; + case WalletType.arbitrum: + return arbitrumIcon; case WalletType.tron: return tronIcon; case WalletType.wownero: diff --git a/lib/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart b/lib/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart index dabca54ed..85be26c92 100644 --- a/lib/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart +++ b/lib/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart @@ -60,7 +60,7 @@ class ExchangeTradeCardItemWidget extends StatelessWidget { FeeSelectionWidget( feesViewModel: feesViewModel, output: output, - onTap: () => pickTransactionPriority(context), + onTap: () => pickTransactionPriority(context, output), ), if (exchangeTradeViewModel.sendViewModel.hasCoinControl) CoinControlWidget( @@ -75,7 +75,7 @@ class ExchangeTradeCardItemWidget extends StatelessWidget { ); } - Future pickTransactionPriority(BuildContext context) async { + Future pickTransactionPriority(BuildContext context, Output output) async { final items = priorityForWalletType(feesViewModel.walletType); final selectedItem = items.indexOf(feesViewModel.transactionPriority); final customItemIndex = feesViewModel.getCustomPriorityIndex(items); @@ -102,10 +102,10 @@ class ExchangeTradeCardItemWidget extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, sliderValue: customFeeRate, onSliderChanged: (double newValue) => setState(() => customFeeRate = newValue), - onItemSelected: (TransactionPriority priority) { + onItemSelected: (TransactionPriority priority) async { feesViewModel.setTransactionPriority(priority); setState(() => selectedIdx = items.indexOf(priority)); - + await output.calculateEstimatedFee(); if (feesViewModel.isLowFee) { _showFeeAlert(context); } diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index fed3ebfd2..0b960d31f 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -542,7 +542,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin GestureDetector( key: ValueKey('send_page_select_fee_priority_button_key'), onTap: sendViewModel.feesViewModel.hasFeesPriority - ? () => pickTransactionPriority(context) + ? () => pickTransactionPriority(context, output) : () {}, child: Container( padding: EdgeInsets.only(top: 24), @@ -799,9 +799,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin pickTransactionPriority(BuildContext context) async { + Future pickTransactionPriority(BuildContext context, Output output) async { final items = priorityForWalletType(sendViewModel.walletType); final selectedItem = items.indexOf(sendViewModel.feesViewModel.transactionPriority); final customItemIndex = sendViewModel.feesViewModel.getCustomPriorityIndex(items); @@ -867,9 +865,10 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin setState(() => customFeeRate = newValue), - onItemSelected: (TransactionPriority priority) { + onItemSelected: (TransactionPriority priority) async { sendViewModel.feesViewModel.setTransactionPriority(priority); setState(() => selectedIdx = items.indexOf(priority)); + await output.calculateEstimatedFee(); }, ); }, diff --git a/lib/src/screens/settings/privacy_page.dart b/lib/src/screens/settings/privacy_page.dart index e753efc13..fa0a05b30 100644 --- a/lib/src/screens/settings/privacy_page.dart +++ b/lib/src/screens/settings/privacy_page.dart @@ -126,6 +126,14 @@ class PrivacyPage extends BasePage { _privacySettingsViewModel.setUseBaseScan(value); }, ), + if (_privacySettingsViewModel.canUseArbiScan) + SettingsSwitcherCell( + title: S.current.arbiscan_history, + value: _privacySettingsViewModel.useArbiScan, + onValueChange: (BuildContext _, bool value) { + _privacySettingsViewModel.setUseArbiScan(value); + }, + ), if (_privacySettingsViewModel.canUseTronGrid) SettingsSwitcherCell( title: S.current.trongrid_history, diff --git a/lib/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart b/lib/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart index 7e489288e..8672da0f5 100644 --- a/lib/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart +++ b/lib/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/polygon/polygon.dart'; @@ -26,6 +27,8 @@ class KeyServiceImpl implements WalletConnectKeyService { return polygon!.getPrivateKey(wallet); case WalletType.base: return base!.getPrivateKey(wallet); + case WalletType.arbitrum: + return arbitrum!.getPrivateKey(wallet); case WalletType.solana: return solana!.getPrivateKey(wallet); default: @@ -41,6 +44,8 @@ class KeyServiceImpl implements WalletConnectKeyService { return polygon!.getPublicKey(wallet); case WalletType.base: return base!.getPublicKey(wallet); + case WalletType.arbitrum: + return arbitrum!.getPublicKey(wallet); case WalletType.solana: return solana!.getPublicKey(wallet); default: diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 6b3dbc108..a825ee243 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -110,6 +110,7 @@ abstract class SettingsStoreBase with Store { required this.pinNativeTokenAtTop, required this.useEtherscan, required this.useBaseScan, + required this.useArbiScan, required this.usePolygonScan, required this.useTronGrid, required this.useMempoolFeeAPI, @@ -467,6 +468,11 @@ abstract class SettingsStoreBase with Store { (_) => useBaseScan, (bool useBaseScan) => _sharedPreferences.setBool(PreferencesKey.useBaseScan, useBaseScan)); + + reaction( + (_) => useArbiScan, + (bool useArbiScan) => + _sharedPreferences.setBool(PreferencesKey.useArbiScan, useArbiScan)); reaction((_) => useTronGrid, (bool useTronGrid) => _sharedPreferences.setBool(PreferencesKey.useTronGrid, useTronGrid)); @@ -826,6 +832,9 @@ abstract class SettingsStoreBase with Store { @observable bool useBaseScan; + @observable + bool useArbiScan; + @observable bool useTronGrid; @@ -1067,6 +1076,7 @@ abstract class SettingsStoreBase with Store { final useEtherscan = sharedPreferences.getBool(PreferencesKey.useEtherscan) ?? true; final usePolygonScan = sharedPreferences.getBool(PreferencesKey.usePolygonScan) ?? true; final useBaseScan = sharedPreferences.getBool(PreferencesKey.useBaseScan) ?? true; + final useArbiScan = sharedPreferences.getBool(PreferencesKey.useArbiScan) ?? true; final useTronGrid = sharedPreferences.getBool(PreferencesKey.useTronGrid) ?? true; final useMempoolFeeAPI = sharedPreferences.getBool(PreferencesKey.useMempoolFeeAPI) ?? true; final defaultNanoRep = sharedPreferences.getString(PreferencesKey.defaultNanoRep) ?? ""; @@ -1113,6 +1123,7 @@ abstract class SettingsStoreBase with Store { final ethereumNodeId = sharedPreferences.getInt(PreferencesKey.currentEthereumNodeIdKey); final polygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey); final baseNodeId = sharedPreferences.getInt(PreferencesKey.currentBaseNodeIdKey); + final arbitrumNodeId = sharedPreferences.getInt(PreferencesKey.currentArbitrumNodeIdKey); final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final nanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey); final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); @@ -1135,6 +1146,8 @@ abstract class SettingsStoreBase with Store { nodeSource.values.firstWhereOrNull((e) => e.uriRaw == polygonDefaultNodeUri); final baseNode = nodeSource.get(baseNodeId) ?? nodeSource.values.firstWhereOrNull((e) => e.uriRaw == baseDefaultNodeUri); + final arbitrumNode = nodeSource.get(arbitrumNodeId) ?? + nodeSource.values.firstWhereOrNull((e) => e.uriRaw == arbitrumDefaultNodeUri); final bitcoinCashElectrumServer = nodeSource.get(bitcoinCashElectrumServerId) ?? nodeSource.values.firstWhereOrNull((e) => e.uriRaw == cakeWalletBitcoinCashDefaultNodeUri); final nanoNode = nodeSource.get(nanoNodeId) ?? @@ -1212,6 +1225,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.base] = baseNode; } + if (arbitrumNode != null) { + nodes[WalletType.arbitrum] = arbitrumNode; + } + if (bitcoinCashElectrumServer != null) { nodes[WalletType.bitcoinCash] = bitcoinCashElectrumServer; } @@ -1391,6 +1408,7 @@ abstract class SettingsStoreBase with Store { useEtherscan: useEtherscan, usePolygonScan: usePolygonScan, useBaseScan: useBaseScan, + useArbiScan: useArbiScan, useTronGrid: useTronGrid, useMempoolFeeAPI: useMempoolFeeAPI, defaultNanoRep: defaultNanoRep, @@ -1588,6 +1606,7 @@ abstract class SettingsStoreBase with Store { useEtherscan = sharedPreferences.getBool(PreferencesKey.useEtherscan) ?? true; usePolygonScan = sharedPreferences.getBool(PreferencesKey.usePolygonScan) ?? true; useBaseScan = sharedPreferences.getBool(PreferencesKey.useBaseScan) ?? true; + useArbiScan = sharedPreferences.getBool(PreferencesKey.useArbiScan) ?? true; useTronGrid = sharedPreferences.getBool(PreferencesKey.useTronGrid) ?? true; useMempoolFeeAPI = sharedPreferences.getBool(PreferencesKey.useMempoolFeeAPI) ?? true; defaultNanoRep = sharedPreferences.getString(PreferencesKey.defaultNanoRep) ?? ""; @@ -1619,6 +1638,7 @@ abstract class SettingsStoreBase with Store { final ethereumNodeId = sharedPreferences.getInt(PreferencesKey.currentEthereumNodeIdKey); final polygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey); final baseNodeId = sharedPreferences.getInt(PreferencesKey.currentBaseNodeIdKey); + final arbitrumNodeId = sharedPreferences.getInt(PreferencesKey.currentArbitrumNodeIdKey); final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); final tronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); @@ -1633,6 +1653,7 @@ abstract class SettingsStoreBase with Store { final ethereumNode = nodeSource.get(ethereumNodeId); final polygonNode = nodeSource.get(polygonNodeId); final baseNode = nodeSource.get(baseNodeId); + final arbitrumNode = nodeSource.get(arbitrumNodeId); final bitcoinCashNode = nodeSource.get(bitcoinCashElectrumServerId); final nanoNode = nodeSource.get(nanoNodeId); final solanaNode = nodeSource.get(solanaNodeId); @@ -1670,6 +1691,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.base] = baseNode; } + if (arbitrumNode != null) { + nodes[WalletType.arbitrum] = arbitrumNode; + } + if (bitcoinCashNode != null) { nodes[WalletType.bitcoinCash] = bitcoinCashNode; } @@ -1833,6 +1858,9 @@ abstract class SettingsStoreBase with Store { case WalletType.base: await _sharedPreferences.setInt(PreferencesKey.currentBaseNodeIdKey, node.key as int); break; + case WalletType.arbitrum: + await _sharedPreferences.setInt(PreferencesKey.currentArbitrumNodeIdKey, node.key as int); + break; case WalletType.solana: await _sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, node.key as int); break; diff --git a/lib/utils/qr_util.dart b/lib/utils/qr_util.dart index 7e7aec066..3a9981bd6 100644 --- a/lib/utils/qr_util.dart +++ b/lib/utils/qr_util.dart @@ -30,6 +30,8 @@ String getQrImage(WalletType type) { return 'assets/images/doge_chain_qr.svg'; case WalletType.base: return 'assets/images/base_chain_QR.svg'; + case WalletType.arbitrum: + return 'assets/images/arbitrum_chain_QR.svg'; case WalletType.banano: case WalletType.haven: case WalletType.none: diff --git a/lib/utils/token_utilities.dart b/lib/utils/token_utilities.dart index 024c16853..184eb56c1 100644 --- a/lib/utils/token_utilities.dart +++ b/lib/utils/token_utilities.dart @@ -116,6 +116,7 @@ class TokenUtilities { WalletType.ethereum => '${walletKey}_${Erc20Token.ethereumBoxName}', WalletType.polygon => '${walletKey}_${Erc20Token.polygonBoxName}', WalletType.base => '${walletKey}_${Erc20Token.baseBoxName}', + WalletType.arbitrum => '${walletKey}_${Erc20Token.arbitrumBoxName}', _ => '${walletKey}_${Erc20Token.ethereumBoxName}', }; @@ -193,7 +194,7 @@ class TokenUtilities { } // Arbitrum One - if (title == 'arbitrum' || title == 'arb' || tag == 'arbitrum') { + if (title == 'arbitrum' || title == 'arb' || tag == 'arb') { return 42161; } diff --git a/lib/view_model/advanced_privacy_settings_view_model.dart b/lib/view_model/advanced_privacy_settings_view_model.dart index efc2270a7..429bdf0e1 100644 --- a/lib/view_model/advanced_privacy_settings_view_model.dart +++ b/lib/view_model/advanced_privacy_settings_view_model.dart @@ -40,6 +40,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { case WalletType.dogecoin: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.solana: case WalletType.tron: return true; @@ -81,6 +82,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { WalletType.ethereum, WalletType.polygon, WalletType.base, + WalletType.arbitrum, WalletType.tron, WalletType.solana, WalletType.monero, diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 78701b44c..552d318e4 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -167,6 +167,7 @@ abstract class BalanceViewModelBase with Store { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.solana: case WalletType.tron: return S.current.xmr_full_balance; diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 25a571f00..50e8d62b7 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -879,6 +879,7 @@ abstract class DashboardViewModelBase with Store { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: 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 c54d8b1b3..46c0248b4 100644 --- a/lib/view_model/dashboard/home_settings_view_model.dart +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/core/fiat_conversion_service.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; @@ -111,6 +112,17 @@ abstract class HomeSettingsViewModelBase with Store { ); await base!.addErc20Token(_balanceViewModel.wallet, baseToken); } + if (_balanceViewModel.wallet.type == WalletType.arbitrum) { + final arbitrumToken = Erc20Token( + name: token.name, + symbol: token.title, + decimal: token.decimals, + contractAddress: contractAddress.toLowerCase(), + iconPath: token.iconPath, + isPotentialScam: token.isPotentialScam, + ); + await arbitrum!.addErc20Token(_balanceViewModel.wallet, arbitrumToken); + } if (_balanceViewModel.wallet.type == WalletType.solana) { final splToken = token.copyWith(enabled: true); @@ -153,6 +165,10 @@ abstract class HomeSettingsViewModelBase with Store { return base!.isTokenAlreadyAdded(_balanceViewModel.wallet, contractAddress); } + if (_balanceViewModel.wallet.type == WalletType.arbitrum) { + return arbitrum!.isTokenAlreadyAdded(_balanceViewModel.wallet, contractAddress); + } + if (_balanceViewModel.wallet.type == WalletType.solana) { return solana!.isTokenAlreadyAdded(_balanceViewModel.wallet, contractAddress); } @@ -184,6 +200,10 @@ abstract class HomeSettingsViewModelBase with Store { await base!.deleteErc20Token(_balanceViewModel.wallet, token as Erc20Token); } + if (_balanceViewModel.wallet.type == WalletType.arbitrum) { + await arbitrum!.deleteErc20Token(_balanceViewModel.wallet, token as Erc20Token); + } + if (_balanceViewModel.wallet.type == WalletType.solana) { await solana!.deleteSPLToken(_balanceViewModel.wallet, token); } @@ -240,6 +260,8 @@ abstract class HomeSettingsViewModelBase with Store { break; case WalletType.base: defaultTokenAddresses = base!.getDefaultTokenContractAddresses(); + case WalletType.arbitrum: + defaultTokenAddresses = arbitrum!.getDefaultTokenContractAddresses(); break; case WalletType.solana: defaultTokenAddresses = solana!.getDefaultTokenContractAddresses(); @@ -380,6 +402,10 @@ abstract class HomeSettingsViewModelBase with Store { return await base!.getErc20Token(_balanceViewModel.wallet, contractAddress); } + if (_balanceViewModel.wallet.type == WalletType.arbitrum) { + return await arbitrum!.getErc20Token(_balanceViewModel.wallet, contractAddress); + } + if (_balanceViewModel.wallet.type == WalletType.solana) { return await solana!.getSPLToken(_balanceViewModel.wallet, contractAddress); } @@ -426,6 +452,11 @@ abstract class HomeSettingsViewModelBase with Store { if (!value) base!.removeTokenTransactionsInHistory(_balanceViewModel.wallet, token); } + if (_balanceViewModel.wallet.type == WalletType.arbitrum) { + arbitrum!.addErc20Token(_balanceViewModel.wallet, token as Erc20Token); + if (!value) arbitrum!.removeTokenTransactionsInHistory(_balanceViewModel.wallet, token); + } + if (_balanceViewModel.wallet.type == WalletType.solana) { final address = solana!.getTokenAddress(token); solana!.addSPLToken(_balanceViewModel.wallet, token, address); @@ -487,6 +518,14 @@ abstract class HomeSettingsViewModelBase with Store { ..sort(_sortFunc)); } + if (_balanceViewModel.wallet.type == WalletType.arbitrum) { + tokens.addAll(arbitrum! + .getERC20Currencies(_balanceViewModel.wallet) + .where((element) => _matchesSearchText(element)) + .toList() + ..sort(_sortFunc)); + } + if (_balanceViewModel.wallet.type == WalletType.solana) { tokens.addAll(solana! .getSPLTokenCurrencies(_balanceViewModel.wallet) @@ -528,7 +567,7 @@ abstract class HomeSettingsViewModelBase with Store { bool _matchesSearchText(CryptoCurrency asset) { final address = getTokenAddressBasedOnWallet(asset); - // The homes settings would only be displayed for either of Tron, Ethereum, Polygon, Base or Solana Wallets. + // The homes settings would only be displayed for either of Tron, EVM or Solana Wallets. if (address == null) return false; return searchText.isEmpty || @@ -558,11 +597,15 @@ abstract class HomeSettingsViewModelBase with Store { return base!.getTokenAddress(asset); } + if (_balanceViewModel.wallet.type == WalletType.arbitrum) { + return arbitrum!.getTokenAddress(asset); + } + if (_balanceViewModel.wallet.type == WalletType.zano) { return zano!.getZanoAssetAddress(asset); } - // We return null if it's neither Tron, Polygon, Base, Ethereum or Solana wallet (which is actually impossible because we only display home settings for either of these four wallets). + // We return null if it's neither Tron, EVM or Solana wallet (which is actually impossible because we only display home settings for either of these four wallets). return null; } } diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 874da36a6..33df3ea24 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; @@ -139,6 +140,10 @@ class TransactionListItem extends ActionListItem with Keyable { final asset = base!.assetOfTransaction(balanceViewModel.wallet, transaction); return asset; } + if (balanceViewModel.wallet.type == WalletType.arbitrum) { + final asset = arbitrum!.assetOfTransaction(balanceViewModel.wallet, transaction); + return asset; + } if (balanceViewModel.wallet.type == WalletType.solana) { final asset = solana!.assetOfTransaction(balanceViewModel.wallet, transaction); @@ -199,6 +204,13 @@ class TransactionListItem extends ActionListItem with Keyable { cryptoAmount: base!.formatterBaseAmountToDouble(transaction: transaction), price: price); break; + case WalletType.arbitrum: + final asset = arbitrum!.assetOfTransaction(balanceViewModel.wallet, transaction); + final price = balanceViewModel.fiatConvertationStore.prices[asset]; + amount = calculateFiatAmountRaw( + cryptoAmount: arbitrum!.formatterArbitrumAmountToDouble(transaction: transaction), + price: price); + break; case WalletType.nano: amount = calculateFiatAmountRaw( cryptoAmount: double.parse(nanoUtil!.getRawAsUsableString( diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 8fdf029b5..fec47f3f3 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -18,6 +18,7 @@ import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_item.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; @@ -109,14 +110,10 @@ abstract class ExchangeTradeViewModelBase with Store { @observable bool isSendable; - - bool get isSwapsXyzSendingEVMTokenSwap => (_provider is SwapsXyzExchangeProvider) && - (wallet.type == WalletType.ethereum && - wallet.currency != trade.from || - (wallet.type == WalletType.polygon && - wallet.currency != trade.from) || - (wallet.type == WalletType.base && - wallet.currency != trade.from)); + bool get isSwapsXyzSendingEVMTokenSwap => + (_provider is SwapsXyzExchangeProvider) && + isEVMCompatibleChain(wallet.type) && + wallet.currency != trade.from; String get extraInfo => trade.extraId != null && trade.extraId!.isNotEmpty ? '\n\n' + S.current.exchange_extra_info @@ -203,7 +200,8 @@ abstract class ExchangeTradeViewModelBase with Store { sendViewModel.selectedCryptoCurrency = selected; - final pendingTransaction = await sendViewModel.createTransaction(provider: _provider, trade: trade); + final pendingTransaction = + await sendViewModel.createTransaction(provider: _provider, trade: trade); if (_provider is SwapsXyzExchangeProvider) { final hash = pendingTransaction?.evmTxHashFromRawHex ?? pendingTransaction?.id ?? ''; @@ -336,6 +334,9 @@ abstract class ExchangeTradeViewModelBase with Store { bool _isBaseToken() => wallet.currency == CryptoCurrency.baseEth && tradeFrom?.tag == CryptoCurrency.baseEth.tag; + bool _isArbitrumToken() => + wallet.currency == CryptoCurrency.arbEth && tradeFrom?.tag == CryptoCurrency.arbEth.tag; + bool _isTronToken() => wallet.currency == CryptoCurrency.trx && tradeFrom?.tag == CryptoCurrency.trx.title; @@ -348,9 +349,10 @@ abstract class ExchangeTradeViewModelBase with Store { _isPolygonToken() || _isSplToken() || _isTronToken() || - _isBaseToken(); + _isBaseToken() || + _isArbitrumToken(); } - + Future registerSwapsXyzTransaction() async { try { if (!(_provider is SwapsXyzExchangeProvider)) return; @@ -366,7 +368,9 @@ abstract class ExchangeTradeViewModelBase with Store { return; } - final txHash = sendViewModel.pendingTransaction?.evmTxHashFromRawHex ?? sendViewModel.pendingTransaction?.id ?? ''; + final txHash = sendViewModel.pendingTransaction?.evmTxHashFromRawHex ?? + sendViewModel.pendingTransaction?.id ?? + ''; if (txHash.isEmpty) { printV('SwapsXyz: transaction register: skipped (txHash empty)'); @@ -379,7 +383,8 @@ abstract class ExchangeTradeViewModelBase with Store { return; } - printV('SwapsXyz: attempting to register transaction: tradeId = ${trade.id}, txHash = $txHash, chainId = $chainId, vmId = $vmId'); + printV( + 'SwapsXyz: attempting to register transaction: tradeId = ${trade.id}, txHash = $txHash, chainId = $chainId, vmId = $vmId'); final registered = await swaps.registerAltVmTx( txId: trade.id, @@ -423,6 +428,8 @@ abstract class ExchangeTradeViewModelBase with Store { return PolygonURI(amount: amount, address: inputAddress); case WalletType.base: return BaseURI(amount: amount, address: inputAddress); + case WalletType.arbitrum: + return ArbitrumURI(amount: amount, address: inputAddress); case WalletType.solana: return SolanaURI(amount: amount, address: inputAddress); case WalletType.tron: diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 29d992b75..8e471804b 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -878,6 +878,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with depositCurrency = CryptoCurrency.baseEth; receiveCurrency = CryptoCurrency.xmr; break; + case WalletType.arbitrum: + depositCurrency = CryptoCurrency.arbEth; + receiveCurrency = CryptoCurrency.xmr; + break; case WalletType.solana: depositCurrency = CryptoCurrency.sol; receiveCurrency = CryptoCurrency.xmr; 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 67a08872c..dffa690f8 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 @@ -81,6 +81,7 @@ abstract class NodeCreateOrEditViewModelBase with Store { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.solana: case WalletType.banano: case WalletType.nano: diff --git a/lib/view_model/payment/payment_view_model.dart b/lib/view_model/payment/payment_view_model.dart index 36bf4f193..5676a6b92 100644 --- a/lib/view_model/payment/payment_view_model.dart +++ b/lib/view_model/payment/payment_view_model.dart @@ -5,7 +5,6 @@ import 'package:cake_wallet/store/app_store.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; part 'payment_view_model.g.dart'; diff --git a/lib/view_model/restore/wallet_restore_from_qr_code.dart b/lib/view_model/restore/wallet_restore_from_qr_code.dart index c3364ffec..3b4709a2f 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -32,6 +32,7 @@ class WalletRestoreFromQRCode { 'ethereum-wallet': WalletType.ethereum, 'polygon-wallet': WalletType.polygon, 'base-wallet': WalletType.base, + 'arbitrum-wallet': WalletType.arbitrum, 'nano-wallet': WalletType.nano, 'nano_wallet': WalletType.nano, 'bitcoincash': WalletType.bitcoinCash, diff --git a/lib/view_model/send/fees_view_model.dart b/lib/view_model/send/fees_view_model.dart index ff878838c..49b2120f2 100644 --- a/lib/view_model/send/fees_view_model.dart +++ b/lib/view_model/send/fees_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/decred/decred.dart'; @@ -102,6 +103,7 @@ abstract class FeesViewModelBase extends WalletChangeListenerViewModel with Stor case WalletType.banano: case WalletType.solana: case WalletType.tron: + case WalletType.arbitrum: return false; } } @@ -119,7 +121,8 @@ abstract class FeesViewModelBase extends WalletChangeListenerViewModel with Stor wallet.type != WalletType.nano && wallet.type != WalletType.banano && wallet.type != WalletType.solana && - wallet.type != WalletType.tron; + wallet.type != WalletType.tron && + wallet.type != WalletType.arbitrum; @computed bool get isElectrumWallet => diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index a599ae836..1caaf050b 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -28,6 +28,7 @@ import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/entities/contact_base.dart'; @@ -49,8 +50,15 @@ abstract class OutputBase with Store { address = '', note = '', extractedAddress = '', + estimatedFee = 0.0, parsedAddress = ParsedAddress(addresses: []) { _setCryptoNumMaximumFractionDigits(); + autorun((_) { + final status = _wallet.syncStatus; + printV("Sync status changed to $status. Recalculating fees"); + + calculateEstimatedFee(); + }); } Key key; @@ -118,6 +126,9 @@ abstract class OutputBase with Store { case WalletType.base: _amount = base!.formatterBaseParseAmount(_cryptoAmount); break; + case WalletType.arbitrum: + _amount = arbitrum!.formatterArbitrumParseAmount(_cryptoAmount); + break; case WalletType.wownero: _amount = wownero!.formatterWowneroParseAmount(amount: _cryptoAmount); break; @@ -145,28 +156,74 @@ abstract class OutputBase with Store { return amount; } - @computed - double get estimatedFee { - try { - // forces mobx to rebuild the computed value - final _ = _wallet.syncStatus; + @observable + double estimatedFee; + @action + Future calculateEstimatedFee() async { + try { if (_wallet.type == WalletType.tron) { if (cryptoCurrencyHandler() == CryptoCurrency.trx) { - final nativeEstimatedFee = tron!.getTronNativeEstimatedFee(_wallet) ?? 0; - return double.parse(nativeEstimatedFee.toString()); + final nativeEstimatedFee = tron!.getTronNativeEstimatedFee(_wallet) ?? '0'; + estimatedFee = double.parse(nativeEstimatedFee); } else { - final trc20EstimatedFee = tron!.getTronTRC20EstimatedFee(_wallet) ?? 0; - return double.parse(trc20EstimatedFee.toString()); + final trc20EstimatedFee = tron!.getTronTRC20EstimatedFee(_wallet) ?? '0'; + estimatedFee = double.parse(trc20EstimatedFee); } } + if (isEVMCompatibleChain(_wallet.type)) { + if (_wallet.type == WalletType.arbitrum) { + String? fee = cryptoCurrencyHandler() == CryptoCurrency.arbEth + ? arbitrum!.getArbitrumNativeEstimatedFee(_wallet) + : arbitrum!.getArbitrumERC20EstimatedFee(_wallet); + + estimatedFee = arbitrum! + .formatterArbitrumAmountToDouble(amount: BigInt.from(double.parse(fee ?? '0.0'))); + return; + } + + await _wallet.updateEstimatedFeesParams(_settingsStore.priority[_wallet.type]!); + + double calculatedFee = 0.0; + + if (_wallet.type == WalletType.ethereum) { + String? fee = cryptoCurrencyHandler() == CryptoCurrency.eth + ? ethereum!.getEthereumNativeEstimatedFee(_wallet) + : ethereum!.getEthereumERC20EstimatedFee(_wallet); + + calculatedFee = ethereum! + .formatterEthereumAmountToDouble(amount: BigInt.from(double.parse(fee ?? '0.0'))); + } + + if (_wallet.type == WalletType.polygon) { + String? fee = cryptoCurrencyHandler() == CryptoCurrency.maticpoly + ? polygon!.getPolygonNativeEstimatedFee(_wallet) + : polygon!.getPolygonERC20EstimatedFee(_wallet); + + calculatedFee = polygon! + .formatterPolygonAmountToDouble(amount: BigInt.from(double.parse(fee ?? '0.0'))); + } + + if (_wallet.type == WalletType.base) { + String? fee = cryptoCurrencyHandler() == CryptoCurrency.baseEth + ? base!.getBaseNativeEstimatedFee(_wallet) + : base!.getBaseERC20EstimatedFee(_wallet); + calculatedFee = + base!.formatterBaseAmountToDouble(amount: BigInt.from(double.parse(fee ?? '0.0'))); + } + + estimatedFee = calculatedFee; + } + if (_wallet.type == WalletType.solana) { - return solana!.getEstimateFees(_wallet) ?? 0.0; + estimatedFee = solana!.getEstimateFees(_wallet) ?? 0.0; } int? fee = _wallet.calculateEstimatedFee( - _settingsStore.priority[_wallet.type]!, formattedCryptoAmount); + _settingsStore.priority[_wallet.type]!, + formattedCryptoAmount, + ); if (_wallet.type == WalletType.bitcoin) { if (_settingsStore.priority[_wallet.type] == @@ -175,48 +232,34 @@ abstract class OutputBase with Store { _wallet, _settingsStore.customBitcoinFeeRate, formattedCryptoAmount); } - return bitcoin!.formatterBitcoinAmountToDouble(amount: fee); + estimatedFee = bitcoin!.formatterBitcoinAmountToDouble(amount: fee); } if (_wallet.type == WalletType.litecoin || _wallet.type == WalletType.bitcoinCash || _wallet.type == WalletType.dogecoin) { - return bitcoin!.formatterBitcoinAmountToDouble(amount: fee); + estimatedFee = bitcoin!.formatterBitcoinAmountToDouble(amount: fee); } if (_wallet.type == WalletType.monero) { - return monero!.formatterMoneroAmountToDouble(amount: fee); + estimatedFee = monero!.formatterMoneroAmountToDouble(amount: fee); } if (_wallet.type == WalletType.wownero) { - return wownero!.formatterWowneroAmountToDouble(amount: fee); - } - - if (_wallet.type == WalletType.ethereum) { - return ethereum!.formatterEthereumAmountToDouble(amount: BigInt.from(fee)); - } - - if (_wallet.type == WalletType.polygon) { - return polygon!.formatterPolygonAmountToDouble(amount: BigInt.from(fee)); - } - - if (_wallet.type == WalletType.base) { - return base!.formatterBaseAmountToDouble(amount: BigInt.from(fee)); + estimatedFee = wownero!.formatterWowneroAmountToDouble(amount: fee); } if (_wallet.type == WalletType.zano) { - return zano!.formatterIntAmountToDouble( + estimatedFee = zano!.formatterIntAmountToDouble( amount: fee, currency: cryptoCurrencyHandler(), forFee: true); } if (_wallet.type == WalletType.decred) { - return decred!.formatterDecredAmountToDouble(amount: fee); + estimatedFee = decred!.formatterDecredAmountToDouble(amount: fee); } } catch (e) { printV(e.toString()); } - - return 0; } @computed @@ -337,6 +380,7 @@ abstract class OutputBase with Store { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.solana: case WalletType.tron: case WalletType.haven: diff --git a/lib/view_model/send/send_template_view_model.dart b/lib/view_model/send/send_template_view_model.dart index 43154868e..29f576d66 100644 --- a/lib/view_model/send/send_template_view_model.dart +++ b/lib/view_model/send/send_template_view_model.dart @@ -57,6 +57,7 @@ abstract class SendTemplateViewModelBase with Store { _wallet.type != WalletType.ethereum && _wallet.type != WalletType.polygon && _wallet.type != WalletType.base && + _wallet.type != WalletType.arbitrum && _wallet.type != WalletType.solana && _wallet.type != WalletType.tron; diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 24a575e48..2c038b036 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -29,6 +29,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/solana/solana.dart'; @@ -137,7 +138,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor bool get isMwebEnabled => balanceViewModel.mwebEnabled; bool get isEVMWallet => isEVMCompatibleChain(walletType); - @action void setShowAddressBookPopup(bool value) { _settingsStore.showAddressBookPopupEnabled = value; @@ -223,6 +223,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.tron: case WalletType.solana: return wallet.currency; @@ -603,6 +604,26 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor priority, ); + _isSwapsXYZCallDataTx = true; + state = ExecutedSuccessfullyState(); + return pendingTransaction; // do NOT fall back to regular flow + } + if (walletType == WalletType.arbitrum) { + _pendingApprovalTx = await buildApprovalIfNeeded( + spender: routerTo!, + tokenContract: tokenContract, + requiredAmount: requiredAmount, + sourceTokenDecimals: trade.sourceTokenDecimals, + ); + + // Build the callData tx + pendingTransaction = await arbitrum!.createRawCallDataTransaction( + wallet, + routerTo, + routerData, + routerValueWei, + ); + _isSwapsXYZCallDataTx = true; state = ExecutedSuccessfullyState(); return pendingTransaction; // do NOT fall back to regular flow @@ -637,7 +658,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor return pendingTransaction; } if (walletType == WalletType.base) { - final priority = _settingsStore.priority[WalletType.polygon]!; + final priority = _settingsStore.priority[WalletType.base]!; pendingTransaction = await base!.createRawCallDataTransaction( wallet, routerTo!, @@ -649,6 +670,17 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor state = ExecutedSuccessfullyState(); return pendingTransaction; } + if (walletType == WalletType.arbitrum) { + pendingTransaction = await arbitrum!.createRawCallDataTransaction( + wallet, + routerTo!, + routerData, + routerValueWei, + ); + _isSwapsXYZCallDataTx = true; + state = ExecutedSuccessfullyState(); + return pendingTransaction; + } } } @@ -847,7 +879,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor wallet.type != WalletType.nano && wallet.type != WalletType.banano && wallet.type != WalletType.solana && - wallet.type != WalletType.tron) { + wallet.type != WalletType.tron && + wallet.type != WalletType.arbitrum) { throw Exception('Priority is null for wallet type: ${wallet.type}'); } @@ -890,6 +923,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor case WalletType.base: return base!.createBaseTransactionCredentials(outputs, priority: priority!, currency: selectedCryptoCurrency); + case WalletType.arbitrum: + return arbitrum! + .createArbitrumTransactionCredentials(outputs, currency: selectedCryptoCurrency); case WalletType.solana: return solana! .createSolanaTransactionCredentials(outputs, currency: selectedCryptoCurrency); @@ -1016,6 +1052,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor if (walletType == WalletType.ethereum || walletType == WalletType.polygon || walletType == WalletType.base || + walletType == WalletType.arbitrum || walletType == WalletType.haven) { if (errorMessage.contains('gas required exceeds allowance')) { return S.current.gas_exceeds_allowance; @@ -1154,6 +1191,13 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor spender, requiredAmount, ); + } else if (walletType == WalletType.arbitrum) { + needsApproval = await arbitrum!.isApprovalRequired( + wallet, + tokenContract, + spender, + requiredAmount, + ); } if (!needsApproval) return null; @@ -1196,6 +1240,13 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor erc20Token, priority, ); + } else if (walletType == WalletType.arbitrum) { + return await arbitrum!.createTokenApproval( + wallet, + requiredAmount, + spender, + erc20Token, + ); } return null; diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart index 150ccdd3d..7d328c81f 100644 --- a/lib/view_model/settings/privacy_settings_view_model.dart +++ b/lib/view_model/settings/privacy_settings_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; @@ -79,6 +80,9 @@ abstract class PrivacySettingsViewModelBase with Store { @computed bool get useBaseScan => _settingsStore.useBaseScan; + @computed + bool get useArbiScan => _settingsStore.useArbiScan; + @computed bool get useTronGrid => _settingsStore.useTronGrid; @@ -118,6 +122,8 @@ abstract class PrivacySettingsViewModelBase with Store { bool get canUseBaseScan => _wallet.type == WalletType.base; + bool get canUseArbiScan => _wallet.type == WalletType.arbitrum; + bool get canUseTronGrid => _wallet.type == WalletType.tron; bool get canUseMempoolFeeAPI => _wallet.type == WalletType.bitcoin; @@ -195,6 +201,12 @@ abstract class PrivacySettingsViewModelBase with Store { tron!.updateTronGridUsageState(_wallet, value); } + @action + void setUseArbiScan(bool value) { + _settingsStore.useArbiScan = value; + arbitrum!.updateArbitrumScanUsageState(_wallet, value); + } + @action void setUseMempoolFeeAPI(bool value) => _settingsStore.useMempoolFeeAPI = value; diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index dde59058a..ffe22facf 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -77,6 +77,9 @@ abstract class TransactionDetailsViewModelBase with Store { case WalletType.base: _addBaseListItems(tx, dateFormat); break; + case WalletType.arbitrum: + _addArbitrumListItems(tx, dateFormat); + break; case WalletType.solana: _addSolanaListItems(tx, dateFormat); break; @@ -204,6 +207,8 @@ abstract class TransactionDetailsViewModelBase with Store { return 'https://blockchair.com/dogecoin/transaction/${txId}'; case WalletType.base: return 'https://basescan.org/tx/${txId}'; + case WalletType.arbitrum: + return 'https://arbiscan.io/tx/${txId}'; case WalletType.none: return ''; } @@ -241,6 +246,8 @@ abstract class TransactionDetailsViewModelBase with Store { return S.current.view_transaction_on + 'dcrdata.decred.org'; case WalletType.base: return S.current.view_transaction_on + 'basescan.org'; + case WalletType.arbitrum: + return S.current.view_transaction_on + 'arbiscan.io'; case WalletType.none: return ''; } @@ -606,6 +613,56 @@ abstract class TransactionDetailsViewModelBase with Store { items.addAll(_items); } + void _addArbitrumListItems(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 7f2a78b0d..259c0158f 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 @@ -13,6 +13,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/reactions/wallet_utils.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/decred/decred.dart'; @@ -146,6 +147,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo return DogeURI(amount: amount, address: address.address); case WalletType.base: return BaseURI(amount: amount, address: address.address); + case WalletType.arbitrum: + return ArbitrumURI(amount: amount, address: address.address); case WalletType.none: throw Exception('Unexpected type: ${type.toString()}'); } @@ -272,6 +275,12 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); } + if (wallet.type == WalletType.arbitrum) { + final primaryAddress = arbitrum!.getAddress(wallet); + + addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); + } + if (wallet.type == WalletType.solana) { final primaryAddress = solana!.getAddress(wallet); @@ -450,6 +459,12 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo 'assets/images/usdc_icon.svg', 'assets/images/more_tokens.svg', ]; + case WalletType.arbitrum: + return [ + 'assets/images/crypto/arbitrum.webp', + 'assets/images/usdc_icon.svg', + 'assets/images/more_tokens.svg', + ]; default: return []; } @@ -473,6 +488,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo return 'assets/images/zano_chain_mono.svg'; case WalletType.base: return 'assets/images/base_chain_mono.svg'; + case WalletType.arbitrum: + return 'assets/images/arbitrum_chain_mono.svg'; default: return 'assets/images/eth_chain_mono.svg'; } diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index f429cd958..7874ea677 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -127,6 +127,7 @@ abstract class WalletKeysViewModelBase with Store { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.solana: case WalletType.tron: items.addAll([ @@ -255,6 +256,8 @@ abstract class WalletKeysViewModelBase with Store { return 'polygon-wallet'; case WalletType.base: return 'base-wallet'; + case WalletType.arbitrum: + return 'arbitrum-wallet'; case WalletType.solana: return 'solana-wallet'; case WalletType.tron: diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index 64c5cb62b..79ecbdfb4 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/core/new_wallet_arguments.dart'; import 'package:cake_wallet/dogecoin/dogecoin.dart'; @@ -97,6 +98,13 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { mnemonic: newWalletArguments!.mnemonic, passphrase: passphrase, ); + case WalletType.arbitrum: + return arbitrum!.createArbitrumNewWalletCredentials( + name: name, + password: walletPassword, + mnemonic: newWalletArguments!.mnemonic, + passphrase: passphrase, + ); case WalletType.bitcoinCash: return bitcoinCash!.createBitcoinCashNewWalletCredentials( name: name, diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index ef16982c0..ce427b8a1 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; @@ -52,6 +53,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.decred: case WalletType.bitcoin: availableModes = [WalletRestoreMode.seed, WalletRestoreMode.keys]; @@ -92,6 +94,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { WalletType.ethereum, WalletType.polygon, WalletType.base, + WalletType.arbitrum, WalletType.nano, WalletType.banano, WalletType.solana, @@ -183,6 +186,13 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { password: password, passphrase: passphrase, ); + case WalletType.arbitrum: + return arbitrum!.createArbitrumRestoreWalletFromSeedCredentials( + name: name, + mnemonic: seed, + password: password, + passphrase: passphrase, + ); case WalletType.solana: return solana!.createSolanaRestoreWalletFromSeedCredentials( name: name, @@ -276,6 +286,12 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { password: password, privateKey: options['private_key'] as String, ); + case WalletType.arbitrum: + return arbitrum!.createArbitrumRestoreWalletFromPrivateKey( + name: name, + password: password, + privateKey: options['private_key'] as String, + ); case WalletType.solana: return solana!.createSolanaRestoreWalletFromPrivateKey( name: name, diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 510c3ad47..dda4bea3d 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -237,6 +237,7 @@ flutter: - assets/decred_node_list.yml - assets/dogecoin_electrum_server_list.yml - assets/base_node_list.yml + - assets/arbitrum_node_list.yml - assets/text/ - assets/faq/ - assets/animation/ diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index a450aaee9..e0d6b6da4 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -60,6 +60,7 @@ "approve": "ﺪﻤﺘﻌﻳ", "approve_request": "الموافقة على الطلب", "approve_tokens": "الموافقة على الرموز", + "arbiscan_history": "تاريخ أربيسكان", "arrive_in_this_address": "سيصل ${currency} ${tag}إلى هذا العنوان", "ascending": "تصاعدي", "ask_each_time": "اسأل في كل مرة", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index aef0b0db0..482a24069 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -60,6 +60,7 @@ "approve": "Одобряване", "approve_request": "Одобрете искане", "approve_tokens": "Одобрете жетоните", + "arbiscan_history": "Арбисканска история", "arrive_in_this_address": "${currency} ${tag}ще отидат на този адрес", "ascending": "Възходящ", "ask_each_time": "Питайте всеки път", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 7e6940b03..79331c22d 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -60,6 +60,7 @@ "approve": "Schvalovat", "approve_request": "Schválit žádost", "approve_tokens": "Schválit tokeny", + "arbiscan_history": "Arbiscan History", "arrive_in_this_address": "${currency} ${tag}přijde na tuto adresu", "ascending": "Vzestupné", "ask_each_time": "Zeptejte se pokaždé", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 267ae5632..49b4e2595 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -60,6 +60,7 @@ "approve": "Genehmigen", "approve_request": "Anfrage genehmigen", "approve_tokens": "Token genehmigen", + "arbiscan_history": "ArbiScan-Geschichte", "arrive_in_this_address": "${currency} ${tag} wird an dieser Adresse ankommen", "ascending": "Aufsteigend", "ask_each_time": "Jedes Mal fragen", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index cc9271dac..c2769eadf 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -60,6 +60,7 @@ "approve": "Approve", "approve_request": "Approve Request", "approve_tokens": "Approve tokens", + "arbiscan_history": "ArbiScan History", "arrive_in_this_address": "${currency} ${tag}will arrive in this address", "ascending": "Ascending", "ask_each_time": "Ask each time", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index a50b8ca90..f4b62918c 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -58,9 +58,10 @@ "apk_update": "Actualización de APK", "appearance": "Apariencia", "approve": "Aprobar", - "approve_request": "Aprobar Solicitud", - "approve_tokens": "Autorizar tokens", - "arrive_in_this_address": "${currency} ${tag} llegará a esta dirección", + "approve_request": "Aprobar la solicitud", + "approve_tokens": "Aprobar tokens", + "arbiscan_history": "Historia de ArbiScan", + "arrive_in_this_address": "${currency} ${tag}llegará a esta dirección", "ascending": "Ascendente", "ask_each_time": "Preguntar cada vez", "auth_store_ban_timeout": "tiempo de espera del bloqueo", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 13ca5e3f7..42d64b3a3 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -60,6 +60,7 @@ "approve": "Approuver", "approve_request": "Approuver la demande", "approve_tokens": "Approuver les jetons", + "arbiscan_history": "Historique d'ArbiScan", "arrive_in_this_address": "${currency} ${tag}arrivera à cette adresse", "ascending": "Ascendant", "ask_each_time": "Demander à chaque fois", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 7ff295654..ad20900cf 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -60,6 +60,7 @@ "approve": "Amincewa", "approve_request": "Amince da bukata", "approve_tokens": "Amince da Alamu", + "arbiscan_history": "Tarihin Arbiscan", "arrive_in_this_address": "${currency} ${tag} zai je wurin wannan adireshi", "ascending": "Hau", "ask_each_time": "Tambaya kowane lokaci", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index f45d89abd..967b2fe75 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -60,6 +60,7 @@ "approve": "मंज़ूरी देना", "approve_request": "अनुरोध को स्वीकृत करें", "approve_tokens": "टोकन को मंजूरी देना", + "arbiscan_history": "अरबीस्कैन इतिहास", "arrive_in_this_address": "${currency} ${tag}इस पते पर पहुंचेंगे", "ascending": "आरोही", "ask_each_time": "हर बार पूछें", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 1cdc4966c..70a6c2dbe 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -60,6 +60,7 @@ "approve": "Odobriti", "approve_request": "Odobriti zahtjev", "approve_tokens": "Odobriti tokene", + "arbiscan_history": "Arbiskanska povijest", "arrive_in_this_address": "${currency} ${tag}će stići na ovu adresu", "ascending": "Uzlazni", "ask_each_time": "Pitajte svaki put", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index bc85d346d..137262430 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -60,6 +60,7 @@ "approve": "Հաստատել", "approve_request": "Հաստատում է հայցը", "approve_tokens": "Հաստատում է նշանները", + "arbiscan_history": "Արբիսկանի պատմություն", "arrive_in_this_address": "${currency} ${tag}կժամանի այս հասցեում", "ascending": "Աճող", "ask_each_time": "Հարցնել ամեն անգամ", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 34bbf65f5..0fd485ab9 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -60,6 +60,7 @@ "approve": "Menyetujui", "approve_request": "Menyetujui permintaan", "approve_tokens": "Menyetujui token", + "arbiscan_history": "Sejarah ArbiScan", "arrive_in_this_address": "${currency} ${tag} akan tiba di alamat ini", "ascending": "Naik", "ask_each_time": "Tanyakan setiap kali", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 4b7819d34..5880b7ee4 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -60,6 +60,7 @@ "approve": "Approvare", "approve_request": "Approvare la richiesta", "approve_tokens": "Approvare i token", + "arbiscan_history": "Storia dell'ArbiScan", "arrive_in_this_address": "${currency} ${tag}arriverà a questo indirizzo", "ascending": "Ascendente", "ask_each_time": "Chiedi ogni volta", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 7c0c3381f..9fa6303dc 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -60,6 +60,7 @@ "approve": "承認する", "approve_request": "リクエストを承認します", "approve_tokens": "トークンを承認します", + "arbiscan_history": "ArbiScan の歴史", "arrive_in_this_address": "${currency} ${tag}はこの住所に到着します", "ascending": "上昇", "ask_each_time": "毎回尋ねてください", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index fce8f8fe2..a0445aad4 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -60,6 +60,7 @@ "approve": "승인", "approve_request": "요청 승인", "approve_tokens": "토큰을 승인합니다", + "arbiscan_history": "ArbiScan 역사", "arrive_in_this_address": "${currency} ${tag}이(가) 이 주소로 도착합니다", "ascending": "오름차순", "ask_each_time": "매번 묻기", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 213fbcb57..6f98ae808 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -60,6 +60,7 @@ "approve": "လက်မခံပါ။", "approve_request": "တောင်းဆိုမှုကိုအတည်ပြု", "approve_tokens": "တိုကင်အတည်ပြု", + "arbiscan_history": "arbiscan သမိုင်း", "arrive_in_this_address": "${currency} ${tag}ဤလိပ်စာသို့ ရောက်ရှိပါမည်။", "ascending": "တက်", "ask_each_time": "တစ်ခုချင်းစီကိုအချိန်မေးပါ", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 9c8025e06..4777ba9db 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -60,6 +60,7 @@ "approve": "Goedkeuren", "approve_request": "Het verzoek goedkeuren", "approve_tokens": "Tokens goedkeuren", + "arbiscan_history": "ArbiScan-geschiedenis", "arrive_in_this_address": "${currency} ${tag}komt aan op dit adres", "ascending": "Stijgend", "ask_each_time": "Vraag het elke keer", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 5d95b1ef6..6b0246a58 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -57,16 +57,17 @@ "anonpay_description": "Wygeneruj ${type}. Odbiorca może ${method} przy użyciu dowolnej obsługiwanej kryptowaluty, a otrzymasz środki w tym portfelu.", "apk_update": "Aktualizacja APK", "appearance": "Wygląd", - "approve": "Zatwierdź", - "approve_request": "Zatwierdź żądanie", - "approve_tokens": "Zatwierdź tokeny", - "arrive_in_this_address": "${currency} ${tag} dotrze na ten adres", - "ascending": "Rosnąco", - "ask_each_time": "Pytaj za każdym razem", - "auth_store_ban_timeout": "czas wygaśnięcia blokady", - "auth_store_banned_for": "Zbanowany na ", - "auth_store_banned_minutes": " minut", - "auth_store_incorrect_password": "Nieprawidłowy PIN", + "approve": "Zatwierdzić", + "approve_request": "Zatwierdzić żądanie", + "approve_tokens": "Zatwierdzić tokeny", + "arbiscan_history": "Historia ArbiScanu", + "arrive_in_this_address": "${currency} ${tag}dotrze na ten adres", + "ascending": "Wznoszący się", + "ask_each_time": "Zapytaj za każdym razem", + "auth_store_ban_timeout": "przekroczenie limitu czasu", + "auth_store_banned_for": "Zablokowany na ", + "auth_store_banned_minutes": " minuty", + "auth_store_incorrect_password": "Niepoprawny PIN", "authenticated": "Uwierzytelniony", "authentication": "Uwierzytelnianie", "auto_generate_addresses": "Automatyczne generowanie adresów", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index de4b1fbe6..334c33252 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -60,6 +60,7 @@ "approve": "Aprovar", "approve_request": "Aprovar solicitação", "approve_tokens": "Aprovar tokens", + "arbiscan_history": "História do ArbiScan", "arrive_in_this_address": "${currency} ${tag}chegará neste endereço", "ascending": "Ascendente", "ask_each_time": "Pergunte cada vez", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 93ba682d5..5e3825704 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -60,6 +60,7 @@ "approve": "Утвердить", "approve_request": "Утвердить запрос", "approve_tokens": "Одобрить токены", + "arbiscan_history": "История АрбиСкан", "arrive_in_this_address": "${currency} ${tag}придет на этот адрес", "ascending": "Восходящий", "ask_each_time": "Спросите каждый раз", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 0e32a1508..4c353932c 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -60,6 +60,7 @@ "approve": "อนุมัติ", "approve_request": "อนุมัติคำขอ", "approve_tokens": "อนุมัติโทเค็น", + "arbiscan_history": "ประวัติ ArbiScan", "arrive_in_this_address": "${currency} ${tag}จะมาถึงที่อยู่นี้", "ascending": "จากน้อยไปมาก", "ask_each_time": "ถามทุกครั้ง", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index a5a0e9d16..9719ae9cb 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -60,6 +60,7 @@ "approve": "Aprubahan", "approve_request": "Aprubahan ang kahilingan", "approve_tokens": "Aprubahan ang mga token", + "arbiscan_history": "Kasaysayan ng Arbiscan", "arrive_in_this_address": "Ang ${currency} ${tag} ay darating sa address na ito", "ascending": "Umakyat", "ask_each_time": "Magtanong sa tuwing", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index be16e17ac..9b10fecc2 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -60,6 +60,7 @@ "approve": "Onaylamak", "approve_request": "Talebi Onaylama", "approve_tokens": "Jetonları onaylayın", + "arbiscan_history": "ArbiScan Geçmişi", "arrive_in_this_address": "${currency} ${tag}bu adrese ulaşacak", "ascending": "Yükselen", "ask_each_time": "Her seferinde sor", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index ce4a0ee56..42c4dfb43 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -60,6 +60,7 @@ "approve": "Затвердити", "approve_request": "Запитайте запит", "approve_tokens": "Затвердити токени", + "arbiscan_history": "Арбісканська історія", "arrive_in_this_address": "${currency} ${tag}надійде на цю адресу", "ascending": "Висхід", "ask_each_time": "Запитайте кожен раз", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index bad4b8354..94189af3b 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -60,6 +60,7 @@ "approve": "ﻭﺮﮐ ﺭﻮﻈﻨﻣ", "approve_request": "درخواست کو منظور کریں", "approve_tokens": "ٹوکن کو منظور کریں", + "arbiscan_history": "اربیسکن تاریخ", "arrive_in_this_address": "${currency} ${tag}اس پتے پر پہنچے گا۔", "ascending": "چڑھنے", "ask_each_time": "ہر بار پوچھیں", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 5c5ac559d..086657785 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -60,6 +60,7 @@ "approve": "Phê duyệt", "approve_request": "Phê duyệt yêu cầu", "approve_tokens": "Phê duyệt mã thông báo", + "arbiscan_history": "Lịch sử quét Arbi", "arrive_in_this_address": "${currency} ${tag} sẽ đến địa chỉ này", "ascending": "Tăng dần", "ask_each_time": "Hỏi mỗi lần", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 76d0b3fa5..ae90c358e 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -60,6 +60,7 @@ "approve": "Fi ọwọ si", "approve_request": "IKILỌ RẸ", "approve_tokens": "Ṣe fọwọsi awọn àmi", + "arbiscan_history": "Itan Arbiscan", "arrive_in_this_address": "${currency} ${tag} máa dé sí àdírẹ́sì yìí", "ascending": "Goke", "ask_each_time": "Beere lọwọ kọọkan", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 077148c57..824f9949c 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -60,6 +60,7 @@ "approve": "批准", "approve_request": "批准请求", "approve_tokens": "批准令牌", + "arbiscan_history": "ArbiScan 历史", "arrive_in_this_address": "${currency} ${tag}将到达此地址", "ascending": "上升", "ask_each_time": "每次问", diff --git a/scripts/android/pubspec_gen.sh b/scripts/android/pubspec_gen.sh index 66cd4d615..1d4dcadc0 100755 --- a/scripts/android/pubspec_gen.sh +++ b/scripts/android/pubspec_gen.sh @@ -10,7 +10,7 @@ case $APP_ANDROID_TYPE in CONFIG_ARGS="--monero" ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano --decred --dogecoin --base" + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano --decred --dogecoin --base --arbitrum" ;; esac diff --git a/scripts/ios/app_config.sh b/scripts/ios/app_config.sh index f69c9f328..9a8ee840c 100755 --- a/scripts/ios/app_config.sh +++ b/scripts/ios/app_config.sh @@ -31,7 +31,7 @@ case $APP_IOS_TYPE in ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano --decred --dogecoin --base" + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano --decred --dogecoin --base --arbitrum" ;; esac diff --git a/scripts/linux/app_config.sh b/scripts/linux/app_config.sh index 70f35d9b7..78f01fa8d 100755 --- a/scripts/linux/app_config.sh +++ b/scripts/linux/app_config.sh @@ -13,7 +13,7 @@ CONFIG_ARGS="" case $APP_LINUX_TYPE in $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --dogecoin --base --excludeFlutterSecureStorage";; + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --dogecoin --base --arbitrum --excludeFlutterSecureStorage";; esac cp -rf pubspec_description.yaml pubspec.yaml diff --git a/scripts/macos/app_config.sh b/scripts/macos/app_config.sh index 0ae44f216..589b5ec1d 100755 --- a/scripts/macos/app_config.sh +++ b/scripts/macos/app_config.sh @@ -36,7 +36,7 @@ case $APP_MACOS_TYPE in $MONERO_COM) CONFIG_ARGS="--monero";; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --dogecoin --base";; + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --dogecoin --base --arbitrum";; esac cp -rf pubspec_description.yaml pubspec.yaml diff --git a/tool/configure.dart b/tool/configure.dart index e6b2267f1..47b337940 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -13,6 +13,7 @@ const zanoOutputPath = 'lib/zano/zano.dart'; const decredOutputPath = 'lib/decred/decred.dart'; const dogecoinOutputPath = 'lib/dogecoin/dogecoin.dart'; const baseOutputPath = 'lib/base/base.dart'; +const arbitrumOutputPath = 'lib/arbitrum/arbitrum.dart'; const walletTypesPath = 'lib/wallet_types.g.dart'; const secureStoragePath = 'lib/core/secure_storage.dart'; const pubspecDefaultPath = 'pubspec_default.yaml'; @@ -34,6 +35,7 @@ Future main(List args) async { final hasDecred = args.contains('${prefix}decred'); final hasDogecoin = args.contains('${prefix}dogecoin'); final hasBase = args.contains('${prefix}base'); + final hasArbitrum = args.contains('${prefix}arbitrum'); final excludeFlutterSecureStorage = args.contains('${prefix}excludeFlutterSecureStorage'); await generateBitcoin(hasBitcoin); @@ -50,6 +52,7 @@ Future main(List args) async { await generateDecred(hasDecred); await generateDogecoin(hasDogecoin); await generateBase(hasBase); + await generateArbitrum(hasArbitrum); await generatePubspec( hasMonero: hasMonero, @@ -67,6 +70,7 @@ Future main(List args) async { hasDecred: hasDecred, hasDogecoin: hasDogecoin, hasBase: hasBase, + hasArbitrum: hasArbitrum, ); await generateWalletTypes( hasMonero: hasMonero, @@ -83,6 +87,7 @@ Future main(List args) async { hasDecred: hasDecred, hasDogecoin: hasDogecoin, hasBase: hasBase, + hasArbitrum: hasArbitrum, ); await injectSecureStorage(!excludeFlutterSecureStorage); } @@ -795,6 +800,8 @@ abstract class Ethereum { HardwareWalletService getTrezorHardwareWalletService(trezor.TrezorConnect connect); List getDefaultTokenContractAddresses(); bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress); + String? getEthereumNativeEstimatedFee(WalletBase wallet); + String? getEthereumERC20EstimatedFee(WalletBase wallet); } """; @@ -916,6 +923,8 @@ abstract class Polygon { HardwareWalletService getTrezorHardwareWalletService(trezor.TrezorConnect connect); List getDefaultTokenContractAddresses(); bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress); + String? getPolygonNativeEstimatedFee(WalletBase wallet); + String? getPolygonERC20EstimatedFee(WalletBase wallet); } """; @@ -1491,9 +1500,7 @@ abstract class DogeCoin { final output = '$dogecoinCommonHeaders\n' + (hasImplementation ? '$dogecoinCWHeaders\n' : '\n') + (hasImplementation ? '$dogecoinCwPart\n\n' : '\n') + - (hasImplementation - ? dogecoinCWDefinition - : dogecoinEmptyDefinition) + + (hasImplementation ? dogecoinCWDefinition : dogecoinEmptyDefinition) + '\n' + dogecoinContent; @@ -1612,6 +1619,8 @@ abstract class Base { HardwareWalletService getBitboxHardwareWalletService(bitbox.BitboxManager manager); List getDefaultTokenContractAddresses(); bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress); + String? getBaseNativeEstimatedFee(WalletBase wallet); + String? getBaseERC20EstimatedFee(WalletBase wallet); } """; @@ -1632,6 +1641,131 @@ abstract class Base { await outputFile.writeAsString(output); } +Future generateArbitrum(bool hasImplementation) async { + final outputFile = File(arbitrumOutputPath); + const arbitrumCommonHeaders = """ +import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; +import 'package:cw_core/hardware/hardware_account_data.dart'; +import 'package:cw_core/hardware/hardware_wallet_service.dart'; +import 'package:cw_core/output_info.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:hive/hive.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; +import 'package:bitbox_flutter/bitbox_flutter.dart' as bitbox; +import 'package:web3dart/web3dart.dart'; + +"""; + const arbitrumCWHeaders = """ +import 'package:cw_evm/evm_chain_formatter.dart'; +import 'package:cw_evm/evm_chain_mnemonics.dart'; +import 'package:cw_evm/evm_chain_transaction_credentials.dart'; +import 'package:cw_evm/evm_chain_transaction_info.dart'; +import 'package:cw_evm/evm_chain_transaction_priority.dart'; +import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart'; +import 'package:cw_evm/hardware/evm_chain_ledger_credentials.dart'; +import 'package:cw_evm/hardware/evm_chain_bitbox_credentials.dart'; +import 'package:cw_evm/evm_chain_wallet.dart'; +import 'package:cw_evm/hardware/evm_chain_bitbox_service.dart'; +import 'package:cw_evm/hardware/evm_chain_ledger_service.dart'; + +import 'package:cw_arbitrum/arbitrum_client.dart'; +import 'package:cw_arbitrum/arbitrum_wallet.dart'; +import 'package:cw_arbitrum/arbitrum_wallet_service.dart'; +import 'package:cw_arbitrum/default_arbitrum_erc20_tokens.dart'; +import 'package:eth_sig_util/util/utils.dart'; + +"""; + const arbitrumCwPart = "part 'cw_arbitrum.dart';"; + const arbitrumContent = """ +abstract class Arbitrum { + List getArbitrumWordList(String language); + WalletService createArbitrumWalletService(bool isDirect); + WalletCredentials createArbitrumNewWalletCredentials( + {required String name, + WalletInfo? walletInfo, + String? password, + String? mnemonic, + String? passphrase}); + WalletCredentials createArbitrumRestoreWalletFromSeedCredentials( + {required String name, + required String mnemonic, + required String password, + String? passphrase}); + WalletCredentials createArbitrumRestoreWalletFromPrivateKey( + {required String name, required String privateKey, required String password}); + WalletCredentials createArbitrumHardwareWalletCredentials( + {required String name, required HardwareAccountData hwAccountData, WalletInfo? walletInfo}); + String getAddress(WalletBase wallet); + String getPrivateKey(WalletBase wallet); + String getPublicKey(WalletBase wallet); + + Object createArbitrumTransactionCredentials( + List outputs, { + required CryptoCurrency currency, + int? feeRate, + }); + + Object createArbitrumTransactionCredentialsRaw( + List outputs, { + required CryptoCurrency currency, + required int feeRate, + }); + + int formatterArbitrumParseAmount(String amount); + double formatterArbitrumAmountToDouble( + {TransactionInfo? transaction, BigInt? amount, int exponent = 18}); + List getERC20Currencies(WalletBase wallet); + Future addErc20Token(WalletBase wallet, CryptoCurrency token); + Future deleteErc20Token(WalletBase wallet, CryptoCurrency token); + Future removeTokenTransactionsInHistory(WalletBase wallet, CryptoCurrency token); + Future getErc20Token(WalletBase wallet, String contractAddress); + + Future createTokenApproval(WalletBase wallet, BigInt amount, String spender, + CryptoCurrency token); + + CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction); + void updateArbitrumScanUsageState(WalletBase wallet, bool isEnabled); + Web3Client? getWeb3Client(WalletBase wallet); + String getTokenAddress(CryptoCurrency asset); + + Future setHardwareWalletService(WalletBase wallet, HardwareWalletService service); + HardwareWalletService getLedgerHardwareWalletService(ledger.LedgerConnection connection); + HardwareWalletService getBitboxHardwareWalletService(bitbox.BitboxManager manager); + List getDefaultTokenContractAddresses(); + bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress); + Future isApprovalRequired(WalletBase wallet, String tokenContract, String spender, BigInt requiredAmount); + Future createRawCallDataTransaction(WalletBase wallet, String to, String dataHex, BigInt valueWei); + String? getArbitrumNativeEstimatedFee(WalletBase wallet); + String? getArbitrumERC20EstimatedFee(WalletBase wallet); +} + + """; + + const arbitrumEmptyDefinition = 'Arbitrum? arbitrum;\n'; + const arbitrumCWDefinition = 'Arbitrum? arbitrum = CWArbitrum();\n'; + + final output = '$arbitrumCommonHeaders\n' + + (hasImplementation ? '$arbitrumCWHeaders\n' : '\n') + + (hasImplementation ? '$arbitrumCwPart\n\n' : '\n') + + (hasImplementation ? arbitrumCWDefinition : arbitrumEmptyDefinition) + + '\n' + + arbitrumContent; + + if (outputFile.existsSync()) { + await outputFile.delete(); + } + + await outputFile.writeAsString(output); +} + Future generatePubspec({ required bool hasMonero, required bool hasBitcoin, @@ -1648,6 +1782,7 @@ Future generatePubspec({ required bool hasDecred, required bool hasDogecoin, required bool hasBase, + required bool hasArbitrum, }) async { const cwCore = """ cw_core: @@ -1720,7 +1855,10 @@ Future generatePubspec({ cw_base: path: ./cw_base """; - + const cwArbitrum = """ + cw_arbitrum: + path: ./cw_arbitrum + """; final inputFile = File(pubspecOutputPath); final inputText = await inputFile.readAsString(); final inputLines = inputText.split('\n'); @@ -1794,6 +1932,10 @@ Future generatePubspec({ output += '\n$cwBase'; } + if (hasArbitrum) { + output += '\n$cwArbitrum'; + } + final outputLines = output.split('\n'); inputLines.insertAll(dependenciesIndex + 1, outputLines); final outputContent = inputLines.join('\n'); @@ -1821,6 +1963,7 @@ Future generateWalletTypes({ required bool hasDecred, required bool hasDogecoin, required bool hasBase, + required bool hasArbitrum, }) async { final walletTypesFile = File(walletTypesPath); @@ -1856,6 +1999,10 @@ Future generateWalletTypes({ outputContent += '\tWalletType.base,\n'; } + if (hasArbitrum) { + outputContent += '\tWalletType.arbitrum,\n'; + } + if (hasBitcoinCash) { outputContent += '\tWalletType.bitcoinCash,\n'; } diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index f6e501877..c02c59176 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -56,6 +56,7 @@ class SecretKey { SecretKey('bitcoinCashTestWalletSeeds', () => ''), SecretKey('polygonTestWalletSeeds', () => ''), SecretKey('baseTestWalletSeeds', () => ''), + SecretKey('arbitrumTestWalletSeeds', () => ''), SecretKey('solanaTestWalletSeeds', () => ''), SecretKey('tronTestWalletSeeds', () => ''), SecretKey('nanoTestWalletSeeds', () => ''), @@ -67,6 +68,7 @@ class SecretKey { SecretKey('bitcoinCashTestWalletReceiveAddress', () => ''), SecretKey('polygonTestWalletReceiveAddress', () => ''), SecretKey('baseTestWalletReceiveAddress', () => ''), + SecretKey('arbitrumTestWalletReceiveAddress', () => ''), SecretKey('solanaTestWalletReceiveAddress', () => ''), SecretKey('tronTestWalletReceiveAddress', () => ''), SecretKey('nanoTestWalletReceiveAddress', () => ''),