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