merge main
33
.github/ISSUE_TEMPLATE/bug-report-🪲-.md
vendored
Normal file
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
name: "Bug Report \U0001FAB2 "
|
||||
about: 'Report a bug '
|
||||
title: ''
|
||||
labels: Bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Platform:**
|
||||
- OS: [e.g. iOS 15.1, Android 14]
|
||||
- Device: [e.g. iPhone 14, Galaxy S21]
|
||||
- Cake Wallet Version: [e.g. 4.12.1]
|
||||
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Not sure where to start?
|
||||
url: https://guides.cakewallet.com
|
||||
about: Start by reading checking out the guides!
|
||||
- name: Need help?
|
||||
url: https://cakewallet.com/#contact
|
||||
about: Use our live chat or send a support email!
|
20
.github/ISSUE_TEMPLATE/feature-or-enhancement-request-✨.md
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
name: Feature or Enhancement Request ✨
|
||||
about: Suggest an idea for Cake Wallet
|
||||
title: ''
|
||||
labels: Enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
10
.github/workflows/pr_test_build.yml
vendored
|
@ -105,21 +105,19 @@ jobs:
|
|||
run: |
|
||||
cd /opt/android/cake_wallet
|
||||
cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_evm && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_polygon && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
flutter packages pub run build_runner build --delete-conflicting-outputs
|
||||
|
||||
- name: Add secrets
|
||||
run: |
|
||||
cd /opt/android/cake_wallet
|
||||
touch lib/.secrets.g.dart
|
||||
touch cw_ethereum/lib/.secrets.g.dart
|
||||
touch cw_polygon/lib/.secrets.g.dart
|
||||
touch cw_evm/lib/.secrets.g.dart
|
||||
echo "const salt = '${{ secrets.SALT }}';" > lib/.secrets.g.dart
|
||||
echo "const keychainSalt = '${{ secrets.KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart
|
||||
echo "const key = '${{ secrets.KEY }}';" >> lib/.secrets.g.dart
|
||||
|
@ -146,14 +144,14 @@ jobs:
|
|||
echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart
|
||||
echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart
|
||||
echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart
|
||||
echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_ethereum/lib/.secrets.g.dart
|
||||
echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
|
||||
echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart
|
||||
echo "const exolixApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart
|
||||
echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart
|
||||
echo "const robinhoodCIdApiSecret = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart
|
||||
echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart
|
||||
echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart
|
||||
echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_ethereum/lib/.secrets.g.dart
|
||||
echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
|
||||
|
||||
- name: Rename app
|
||||
run: echo -e "id=com.cakewallet.test\nname=${{ env.BRANCH_NAME }}" > /opt/android/cake_wallet/android/app.properties
|
||||
|
|
12
.gitignore
vendored
|
@ -90,9 +90,10 @@ android/key.properties
|
|||
**/tool/.secrets-prod.json
|
||||
**/tool/.secrets-test.json
|
||||
**/tool/.secrets-config.json
|
||||
**/tool/.evm-secrets-config.json
|
||||
**/tool/.ethereum-secrets-config.json
|
||||
**/lib/.secrets.g.dart
|
||||
**/cw_ethereum/lib/.secrets.g.dart
|
||||
**/cw_evm/lib/.secrets.g.dart
|
||||
|
||||
vendor/
|
||||
|
||||
|
@ -147,3 +148,12 @@ assets/images/app_logo.png
|
|||
macos/Runner/Info.plist
|
||||
macos/Runner/DebugProfile.entitlements
|
||||
macos/Runner/Release.entitlements
|
||||
|
||||
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
|
||||
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
|
||||
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
|
||||
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
|
||||
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
|
||||
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
|
||||
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
|
||||
macos/Runner/Configs/AppInfo.xcconfig
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
Privacy Policy
|
||||
|
||||
Last modified: August 9, 2023
|
||||
Last modified: January 24, 2024
|
||||
|
||||
Introduction
|
||||
============
|
||||
|
@ -112,12 +112,12 @@ Data Security
|
|||
|
||||
In any situation, Cake Labs takes no responsibility for interception of personal data by any outside individual, group, corporation, or institution. You should understand this and take any and all appropriate actions to secure your own data.
|
||||
|
||||
Links to Other Websites
|
||||
-----------------------
|
||||
Other Websites and Third-Party Services
|
||||
---------------------------------------
|
||||
|
||||
The App may contain links to other websites that are not operated by us. If you click on a Third-Party Service link, you will be directed to that third party's site. We strongly advise you to review the Privacy Policy of every site you visit. We have no control over and assume no responsibility for the content, privacy policies or practices of any third-party sites or services.
|
||||
|
||||
The App includes several optional Third-Party Services, which may not be available to all users. If you use Third-Party Services, you must agree to their respective Privacy Policies.
|
||||
The App includes several optional Third-Party Services, which may not be available to all users. If you use Third-Party Services, you must agree to their respective Privacy Policies. When using certain optional features in the app such as buying and selling, you may be asked to provide information to a Third-Party Service. You will need to read and accept the privacy policy for that third party. This Third-Party Service may ask for your name, your photo ID, your social security number or other similar number, mailing address, cryptocurrency address, or other information. They may ask you to take a selfie image. Information shared with a Third-Party Service is subject to their respective Privacy Policies.
|
||||
|
||||
Changes to Our Privacy Policy
|
||||
-----------------------------
|
||||
|
|
|
@ -37,7 +37,7 @@ if (appPropertiesFile.exists()) {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
compileSdkVersion 34
|
||||
|
||||
lintOptions {
|
||||
disable 'InvalidPackage'
|
||||
|
|
|
@ -71,8 +71,8 @@
|
|||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
<provider
|
||||
android:name="com.pichillilorenzo.flutter_inappwebview.InAppWebViewFileProvider"
|
||||
android:authorities="${applicationId}.flutter_inappwebview.fileprovider"
|
||||
android:name="com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFileProvider"
|
||||
android:authorities="${applicationId}.flutter_inappwebview_android.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
|
|
BIN
assets/images/kaspa_icon.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 110 KiB |
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
Before Width: | Height: | Size: 678 B After Width: | Height: | Size: 678 B |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 117 KiB |
After Width: | Height: | Size: 5.7 KiB |
BIN
assets/images/macos_icons/monero_macos_icons/monero_macos_16.png
Normal file
After Width: | Height: | Size: 548 B |
After Width: | Height: | Size: 14 KiB |
BIN
assets/images/macos_icons/monero_macos_icons/monero_macos_32.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 40 KiB |
BIN
assets/images/macos_icons/monero_macos_icons/monero_macos_64.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
|
@ -1,4 +1,3 @@
|
|||
Polyseed enhancements
|
||||
New on-ramp provider DFX
|
||||
Security and Privacy enhancements
|
||||
Usability enhancements
|
||||
Bug fixes
|
|
@ -1,2 +1,4 @@
|
|||
Support multiple address types for Bitcoin Cash
|
||||
List previously used Bitcoin addresses
|
||||
Security and Privacy enhancements
|
||||
Usability enhancements
|
||||
Bug fixes
|
|
@ -1,14 +1,33 @@
|
|||
cd scripts/android
|
||||
IOS="ios"
|
||||
ANDROID="android"
|
||||
|
||||
PLATFORMS=($IOS $ANDROID)
|
||||
PLATFORM=$1
|
||||
|
||||
if ! [[ " ${PLATFORMS[*]} " =~ " ${PLATFORM} " ]]; then
|
||||
echo "specify platform: ./configure_cake_wallet.sh ios|android"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$PLATFORM" == "$IOS" ]; then
|
||||
echo "Configuring for iOS"
|
||||
cd scripts/ios
|
||||
fi
|
||||
|
||||
if [ "$PLATFORM" == "$ANDROID" ]; then
|
||||
echo "Configuring for Android"
|
||||
cd scripts/android
|
||||
fi
|
||||
|
||||
source ./app_env.sh cakewallet
|
||||
./app_config.sh
|
||||
cd ../.. && flutter pub get
|
||||
flutter packages pub run tool/generate_localization.dart
|
||||
cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_evm && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_polygon && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
flutter packages pub run build_runner build --delete-conflicting-outputs
|
|
@ -1,40 +1,70 @@
|
|||
import 'dart:convert';
|
||||
import 'package:bitbox/bitbox.dart' as bitbox;
|
||||
|
||||
class BitcoinAddressRecord {
|
||||
BitcoinAddressRecord(this.address,
|
||||
{required this.index, this.isHidden = false, bool isUsed = false})
|
||||
: _isUsed = isUsed;
|
||||
BitcoinAddressRecord(
|
||||
this.address, {
|
||||
required this.index,
|
||||
this.isHidden = false,
|
||||
int txCount = 0,
|
||||
int balance = 0,
|
||||
String name = '',
|
||||
bool isUsed = false,
|
||||
}) : _txCount = txCount,
|
||||
_balance = balance,
|
||||
_name = name,
|
||||
_isUsed = isUsed;
|
||||
|
||||
factory BitcoinAddressRecord.fromJSON(String jsonSource) {
|
||||
final decoded = json.decode(jsonSource) as Map;
|
||||
|
||||
return BitcoinAddressRecord(
|
||||
decoded['address'] as String,
|
||||
index: decoded['index'] as int,
|
||||
isHidden: decoded['isHidden'] as bool? ?? false,
|
||||
isUsed: decoded['isUsed'] as bool? ?? false);
|
||||
return BitcoinAddressRecord(decoded['address'] as String,
|
||||
index: decoded['index'] as int,
|
||||
isHidden: decoded['isHidden'] as bool? ?? false,
|
||||
isUsed: decoded['isUsed'] as bool? ?? false,
|
||||
txCount: decoded['txCount'] as int? ?? 0,
|
||||
name: decoded['name'] as String? ?? '',
|
||||
balance: decoded['balance'] as int? ?? 0);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object o) =>
|
||||
o is BitcoinAddressRecord && address == o.address;
|
||||
|
||||
final String address;
|
||||
final bool isHidden;
|
||||
final int index;
|
||||
int _txCount;
|
||||
int _balance;
|
||||
String _name;
|
||||
bool _isUsed;
|
||||
|
||||
int get txCount => _txCount;
|
||||
|
||||
String get name => _name;
|
||||
|
||||
int get balance => _balance;
|
||||
|
||||
set txCount(int value) => _txCount = value;
|
||||
|
||||
set balance(int value) => _balance = value;
|
||||
|
||||
bool get isUsed => _isUsed;
|
||||
|
||||
void setAsUsed() => _isUsed = true;
|
||||
void setNewName(String label) => _name = label;
|
||||
|
||||
@override
|
||||
bool operator ==(Object o) => o is BitcoinAddressRecord && address == o.address;
|
||||
|
||||
@override
|
||||
int get hashCode => address.hashCode;
|
||||
|
||||
bool _isUsed;
|
||||
String get cashAddr => bitbox.Address.toCashAddress(address);
|
||||
|
||||
void setAsUsed() => _isUsed = true;
|
||||
|
||||
String toJSON() =>
|
||||
json.encode({
|
||||
String toJSON() => json.encode({
|
||||
'address': address,
|
||||
'index': index,
|
||||
'isHidden': isHidden,
|
||||
'isUsed': isUsed});
|
||||
'txCount': txCount,
|
||||
'name': name,
|
||||
'isUsed': isUsed,
|
||||
'balance': balance,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -47,6 +47,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|||
sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType)
|
||||
.derivePath("m/0'/1"),
|
||||
networkType: networkType);
|
||||
autorun((_) {
|
||||
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
|
||||
});
|
||||
}
|
||||
|
||||
static Future<BitcoinWallet> create({
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
|
||||
import 'package:cw_core/balance.dart';
|
||||
|
||||
class ElectrumBalance extends Balance {
|
||||
const ElectrumBalance(
|
||||
{required this.confirmed,
|
||||
required this.unconfirmed,
|
||||
required this.frozen})
|
||||
const ElectrumBalance({required this.confirmed, required this.unconfirmed, required this.frozen})
|
||||
: super(confirmed, unconfirmed);
|
||||
|
||||
static ElectrumBalance? fromJSON(String? jsonSource) {
|
||||
|
@ -28,12 +24,10 @@ class ElectrumBalance extends Balance {
|
|||
final int frozen;
|
||||
|
||||
@override
|
||||
String get formattedAvailableBalance =>
|
||||
bitcoinAmountToString(amount: confirmed - unconfirmed.abs() - frozen);
|
||||
String get formattedAvailableBalance => bitcoinAmountToString(amount: confirmed - frozen);
|
||||
|
||||
@override
|
||||
String get formattedAdditionalBalance =>
|
||||
bitcoinAmountToString(amount: unconfirmed);
|
||||
String get formattedAdditionalBalance => bitcoinAmountToString(amount: unconfirmed);
|
||||
|
||||
@override
|
||||
String get formattedUnAvailableBalance {
|
||||
|
@ -41,6 +35,6 @@ class ElectrumBalance extends Balance {
|
|||
return frozenFormatted == '0.0' ? '' : frozenFormatted;
|
||||
}
|
||||
|
||||
String toJSON() => json.encode(
|
||||
{'confirmed': confirmed, 'unconfirmed': unconfirmed, 'frozen': frozen});
|
||||
String toJSON() =>
|
||||
json.encode({'confirmed': confirmed, 'unconfirmed': unconfirmed, 'frozen': frozen});
|
||||
}
|
||||
|
|
|
@ -63,6 +63,7 @@ abstract class ElectrumWalletBase
|
|||
_password = password,
|
||||
_feeRates = <int>[],
|
||||
_isTransactionUpdating = false,
|
||||
isEnabledAutoGenerateSubaddress = true,
|
||||
unspentCoins = [],
|
||||
_scripthashesUpdateSubject = {},
|
||||
balance = ObservableMap<CryptoCurrency, ElectrumBalance>.of(currency != null
|
||||
|
@ -87,6 +88,10 @@ abstract class ElectrumWalletBase
|
|||
final bitcoin.HDWallet hd;
|
||||
final String mnemonic;
|
||||
|
||||
@override
|
||||
@observable
|
||||
bool isEnabledAutoGenerateSubaddress;
|
||||
|
||||
late ElectrumClient electrumClient;
|
||||
Box<UnspentCoinsInfo> unspentCoinsInfo;
|
||||
|
||||
|
@ -583,38 +588,66 @@ abstract class ElectrumWalletBase
|
|||
Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
|
||||
final addressHashes = <String, BitcoinAddressRecord>{};
|
||||
final normalizedHistories = <Map<String, dynamic>>[];
|
||||
final newTxCounts = <String, int>{};
|
||||
|
||||
walletAddresses.addresses.forEach((addressRecord) {
|
||||
final sh = scriptHash(addressRecord.address, networkType: networkType);
|
||||
addressHashes[sh] = addressRecord;
|
||||
newTxCounts[sh] = 0;
|
||||
});
|
||||
final histories = addressHashes.keys.map((scriptHash) =>
|
||||
electrumClient.getHistory(scriptHash).then((history) => {scriptHash: history}));
|
||||
final historyResults = await Future.wait(histories);
|
||||
historyResults.forEach((history) {
|
||||
history.entries.forEach((historyItem) {
|
||||
if (historyItem.value.isNotEmpty) {
|
||||
final address = addressHashes[historyItem.key];
|
||||
address?.setAsUsed();
|
||||
normalizedHistories.addAll(historyItem.value);
|
||||
}
|
||||
|
||||
try {
|
||||
final histories = addressHashes.keys.map((scriptHash) =>
|
||||
electrumClient.getHistory(scriptHash).then((history) => {scriptHash: history}));
|
||||
final historyResults = await Future.wait(histories);
|
||||
|
||||
|
||||
|
||||
historyResults.forEach((history) {
|
||||
history.entries.forEach((historyItem) {
|
||||
if (historyItem.value.isNotEmpty) {
|
||||
final address = addressHashes[historyItem.key];
|
||||
address?.setAsUsed();
|
||||
newTxCounts[historyItem.key] = historyItem.value.length;
|
||||
normalizedHistories.addAll(historyItem.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
final historiesWithDetails = await Future.wait(normalizedHistories.map((transaction) {
|
||||
try {
|
||||
return fetchTransactionInfo(
|
||||
hash: transaction['tx_hash'] as String, height: transaction['height'] as int);
|
||||
} catch (_) {
|
||||
return Future.value(null);
|
||||
|
||||
for (var sh in addressHashes.keys) {
|
||||
var balanceData = await electrumClient.getBalance(sh);
|
||||
var addressRecord = addressHashes[sh];
|
||||
if (addressRecord != null) {
|
||||
addressRecord.balance = balanceData['confirmed'] as int? ?? 0;
|
||||
}
|
||||
}
|
||||
}));
|
||||
return historiesWithDetails
|
||||
.fold<Map<String, ElectrumTransactionInfo>>(<String, ElectrumTransactionInfo>{}, (acc, tx) {
|
||||
if (tx == null) {
|
||||
|
||||
|
||||
addressHashes.forEach((sh, addressRecord) {
|
||||
addressRecord.txCount = newTxCounts[sh] ?? 0;
|
||||
});
|
||||
|
||||
final historiesWithDetails = await Future.wait(normalizedHistories.map((transaction) {
|
||||
try {
|
||||
return fetchTransactionInfo(
|
||||
hash: transaction['tx_hash'] as String, height: transaction['height'] as int);
|
||||
} catch (_) {
|
||||
return Future.value(null);
|
||||
}
|
||||
}));
|
||||
|
||||
return historiesWithDetails.fold<Map<String, ElectrumTransactionInfo>>(
|
||||
<String, ElectrumTransactionInfo>{}, (acc, tx) {
|
||||
if (tx == null) {
|
||||
return acc;
|
||||
}
|
||||
acc[tx.id] = acc[tx.id]?.updated(tx) ?? tx;
|
||||
return acc;
|
||||
}
|
||||
acc[tx.id] = acc[tx.id]?.updated(tx) ?? tx;
|
||||
return acc;
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateTransactions() async {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
||||
import 'package:bitbox/bitbox.dart' as bitbox;
|
||||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||
import 'package:cw_bitcoin/electrum.dart';
|
||||
import 'package:cw_bitcoin/script_hash.dart';
|
||||
|
@ -10,8 +10,7 @@ import 'package:mobx/mobx.dart';
|
|||
|
||||
part 'electrum_wallet_addresses.g.dart';
|
||||
|
||||
class ElectrumWalletAddresses = ElectrumWalletAddressesBase
|
||||
with _$ElectrumWalletAddresses;
|
||||
class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses;
|
||||
|
||||
abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||
ElectrumWalletAddressesBase(WalletInfo walletInfo,
|
||||
|
@ -22,19 +21,16 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
List<BitcoinAddressRecord>? initialAddresses,
|
||||
int initialRegularAddressIndex = 0,
|
||||
int initialChangeAddressIndex = 0})
|
||||
: addresses = ObservableList<BitcoinAddressRecord>.of(
|
||||
(initialAddresses ?? []).toSet()),
|
||||
receiveAddresses = ObservableList<BitcoinAddressRecord>.of(
|
||||
(initialAddresses ?? [])
|
||||
: addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
|
||||
receiveAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
|
||||
.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed)
|
||||
.toSet()),
|
||||
changeAddresses = ObservableList<BitcoinAddressRecord>.of(
|
||||
(initialAddresses ?? [])
|
||||
.toSet()),
|
||||
changeAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
|
||||
.where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed)
|
||||
.toSet()),
|
||||
.toSet()),
|
||||
currentReceiveAddressIndex = initialRegularAddressIndex,
|
||||
currentChangeAddressIndex = initialChangeAddressIndex,
|
||||
super(walletInfo);
|
||||
super(walletInfo);
|
||||
|
||||
static const defaultReceiveAddressesCount = 22;
|
||||
static const defaultChangeAddressesCount = 17;
|
||||
|
@ -42,6 +38,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
|
||||
static String toCashAddr(String address) => bitbox.Address.toCashAddress(address);
|
||||
|
||||
static String toLegacy(String address) => bitbox.Address.toLegacyAddress(address);
|
||||
|
||||
final ObservableList<BitcoinAddressRecord> addresses;
|
||||
final ObservableList<BitcoinAddressRecord> receiveAddresses;
|
||||
final ObservableList<BitcoinAddressRecord> changeAddresses;
|
||||
|
@ -53,38 +51,67 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
@override
|
||||
@computed
|
||||
String get address {
|
||||
if (receiveAddresses.isEmpty) {
|
||||
final address = generateNewAddress().address;
|
||||
return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(address) : address;
|
||||
}
|
||||
final receiveAddress = receiveAddresses.first.address;
|
||||
if (isEnabledAutoGenerateSubaddress) {
|
||||
if (receiveAddresses.isEmpty) {
|
||||
final newAddress = generateNewAddress().address;
|
||||
return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(newAddress) : newAddress;
|
||||
}
|
||||
final receiveAddress = receiveAddresses.first.address;
|
||||
|
||||
return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(receiveAddress) : receiveAddress;
|
||||
return walletInfo.type == WalletType.bitcoinCash
|
||||
? toCashAddr(receiveAddress)
|
||||
: receiveAddress;
|
||||
} else {
|
||||
final receiveAddress = (receiveAddresses.first.address != addresses.first.address &&
|
||||
previousAddressRecord != null)
|
||||
? previousAddressRecord!.address
|
||||
: addresses.first.address;
|
||||
|
||||
return walletInfo.type == WalletType.bitcoinCash
|
||||
? toCashAddr(receiveAddress)
|
||||
: receiveAddress;
|
||||
}
|
||||
}
|
||||
|
||||
@observable
|
||||
bool isEnabledAutoGenerateSubaddress = true;
|
||||
|
||||
@override
|
||||
set address(String addr) {
|
||||
if (addr.startsWith('bitcoincash:')) {
|
||||
addr = toLegacy(addr);
|
||||
}
|
||||
final addressRecord = addresses.firstWhere((addressRecord) => addressRecord.address == addr);
|
||||
|
||||
previousAddressRecord = addressRecord;
|
||||
receiveAddresses.remove(addressRecord);
|
||||
receiveAddresses.insert(0, addressRecord);
|
||||
}
|
||||
|
||||
@override
|
||||
set address(String addr) => null;
|
||||
String get primaryAddress => getAddress(index: 0, hd: mainHd);
|
||||
|
||||
int currentReceiveAddressIndex;
|
||||
int currentChangeAddressIndex;
|
||||
|
||||
@computed
|
||||
int get totalCountOfReceiveAddresses =>
|
||||
addresses.fold(0, (acc, addressRecord) {
|
||||
if (!addressRecord.isHidden) {
|
||||
return acc + 1;
|
||||
}
|
||||
return acc;
|
||||
});
|
||||
@observable
|
||||
BitcoinAddressRecord? previousAddressRecord;
|
||||
|
||||
@computed
|
||||
int get totalCountOfChangeAddresses =>
|
||||
addresses.fold(0, (acc, addressRecord) {
|
||||
if (addressRecord.isHidden) {
|
||||
return acc + 1;
|
||||
}
|
||||
return acc;
|
||||
});
|
||||
int get totalCountOfReceiveAddresses => addresses.fold(0, (acc, addressRecord) {
|
||||
if (!addressRecord.isHidden) {
|
||||
return acc + 1;
|
||||
}
|
||||
return acc;
|
||||
});
|
||||
|
||||
@computed
|
||||
int get totalCountOfChangeAddresses => addresses.fold(0, (acc, addressRecord) {
|
||||
if (addressRecord.isHidden) {
|
||||
return acc + 1;
|
||||
}
|
||||
return acc;
|
||||
});
|
||||
|
||||
Future<void> discoverAddresses() async {
|
||||
await _discoverAddresses(mainHd, false);
|
||||
|
@ -114,11 +141,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
|
||||
if (changeAddresses.isEmpty) {
|
||||
final newAddresses = await _createNewAddresses(gap,
|
||||
hd: sideHd,
|
||||
startIndex: totalCountOfChangeAddresses > 0
|
||||
? totalCountOfChangeAddresses - 1
|
||||
: 0,
|
||||
isHidden: true);
|
||||
hd: sideHd,
|
||||
startIndex: totalCountOfChangeAddresses > 0 ? totalCountOfChangeAddresses - 1 : 0,
|
||||
isHidden: true);
|
||||
_addAddresses(newAddresses);
|
||||
}
|
||||
|
||||
|
@ -132,14 +157,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
return address;
|
||||
}
|
||||
|
||||
BitcoinAddressRecord generateNewAddress(
|
||||
{bitcoin.HDWallet? hd, bool isHidden = false}) {
|
||||
currentReceiveAddressIndex += 1;
|
||||
// FIX-ME: Check logic for whichi HD should be used here ???
|
||||
final address = BitcoinAddressRecord(
|
||||
getAddress(index: currentReceiveAddressIndex, hd: hd ?? sideHd),
|
||||
index: currentReceiveAddressIndex,
|
||||
isHidden: isHidden);
|
||||
BitcoinAddressRecord generateNewAddress({bitcoin.HDWallet? hd, String? label}) {
|
||||
final isHidden = hd == sideHd;
|
||||
|
||||
final newAddressIndex = addresses.fold(
|
||||
0, (int acc, addressRecord) => isHidden == addressRecord.isHidden ? acc + 1 : acc);
|
||||
|
||||
final address = BitcoinAddressRecord(getAddress(index: newAddressIndex, hd: hd ?? sideHd),
|
||||
index: newAddressIndex, isHidden: isHidden, name: label ?? '');
|
||||
addresses.add(address);
|
||||
return address;
|
||||
}
|
||||
|
@ -157,20 +182,32 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
}
|
||||
}
|
||||
|
||||
@action
|
||||
void updateAddress(String address, String label) {
|
||||
if (address.startsWith('bitcoincash:')) {
|
||||
address = toLegacy(address);
|
||||
}
|
||||
final addressRecord = addresses.firstWhere((addressRecord) => addressRecord.address == address);
|
||||
addressRecord.setNewName(label);
|
||||
final index = addresses.indexOf(addressRecord);
|
||||
addresses.remove(addressRecord);
|
||||
addresses.insert(index, addressRecord);
|
||||
}
|
||||
|
||||
@action
|
||||
void updateReceiveAddresses() {
|
||||
receiveAddresses.removeRange(0, receiveAddresses.length);
|
||||
final newAdresses = addresses
|
||||
.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed);
|
||||
receiveAddresses.addAll(newAdresses);
|
||||
final newAddresses =
|
||||
addresses.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed);
|
||||
receiveAddresses.addAll(newAddresses);
|
||||
}
|
||||
|
||||
@action
|
||||
void updateChangeAddresses() {
|
||||
changeAddresses.removeRange(0, changeAddresses.length);
|
||||
final newAdresses = addresses
|
||||
.where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed);
|
||||
changeAddresses.addAll(newAdresses);
|
||||
final newAddresses =
|
||||
addresses.where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed);
|
||||
changeAddresses.addAll(newAddresses);
|
||||
}
|
||||
|
||||
Future<void> _discoverAddresses(bitcoin.HDWallet hd, bool isHidden) async {
|
||||
|
@ -178,20 +215,16 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
List<BitcoinAddressRecord> addrs;
|
||||
|
||||
if (addresses.isNotEmpty) {
|
||||
addrs = addresses
|
||||
.where((addr) => addr.isHidden == isHidden)
|
||||
.toList();
|
||||
addrs = addresses.where((addr) => addr.isHidden == isHidden).toList();
|
||||
} else {
|
||||
addrs = await _createNewAddresses(
|
||||
isHidden
|
||||
? defaultChangeAddressesCount
|
||||
: defaultReceiveAddressesCount,
|
||||
isHidden ? defaultChangeAddressesCount : defaultReceiveAddressesCount,
|
||||
startIndex: 0,
|
||||
hd: hd,
|
||||
isHidden: isHidden);
|
||||
}
|
||||
|
||||
while(hasAddrUse) {
|
||||
while (hasAddrUse) {
|
||||
final addr = addrs.last.address;
|
||||
hasAddrUse = await _hasAddressUsed(addr);
|
||||
|
||||
|
@ -201,11 +234,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
|
||||
final start = addrs.length;
|
||||
final count = start + gap;
|
||||
final batch = await _createNewAddresses(
|
||||
count,
|
||||
startIndex: start,
|
||||
hd: hd,
|
||||
isHidden: isHidden);
|
||||
final batch = await _createNewAddresses(count, startIndex: start, hd: hd, isHidden: isHidden);
|
||||
addrs.addAll(batch);
|
||||
}
|
||||
|
||||
|
@ -229,21 +258,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
|
||||
if (countOfReceiveAddresses < defaultReceiveAddressesCount) {
|
||||
final addressesCount = defaultReceiveAddressesCount - countOfReceiveAddresses;
|
||||
final newAddresses = await _createNewAddresses(
|
||||
addressesCount,
|
||||
startIndex: countOfReceiveAddresses,
|
||||
hd: mainHd,
|
||||
isHidden: false);
|
||||
final newAddresses = await _createNewAddresses(addressesCount,
|
||||
startIndex: countOfReceiveAddresses, hd: mainHd, isHidden: false);
|
||||
addresses.addAll(newAddresses);
|
||||
}
|
||||
|
||||
if (countOfHiddenAddresses < defaultChangeAddressesCount) {
|
||||
final addressesCount = defaultChangeAddressesCount - countOfHiddenAddresses;
|
||||
final newAddresses = await _createNewAddresses(
|
||||
addressesCount,
|
||||
startIndex: countOfHiddenAddresses,
|
||||
hd: sideHd,
|
||||
isHidden: true);
|
||||
final newAddresses = await _createNewAddresses(addressesCount,
|
||||
startIndex: countOfHiddenAddresses, hd: sideHd, isHidden: true);
|
||||
addresses.addAll(newAddresses);
|
||||
}
|
||||
}
|
||||
|
@ -253,10 +276,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
final list = <BitcoinAddressRecord>[];
|
||||
|
||||
for (var i = startIndex; i < count + startIndex; i++) {
|
||||
final address = BitcoinAddressRecord(
|
||||
getAddress(index: i, hd: hd),
|
||||
index: i,
|
||||
isHidden: isHidden);
|
||||
final address =
|
||||
BitcoinAddressRecord(getAddress(index: i, hd: hd), index: i, isHidden: isHidden);
|
||||
list.add(address);
|
||||
}
|
||||
|
||||
|
@ -275,4 +296,4 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
|||
final transactionHistory = await electrumClient.getHistory(sh);
|
||||
return transactionHistory.isNotEmpty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,6 +51,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
|||
.fromSeed(seedBytes, network: networkType)
|
||||
.derivePath("m/0'/1"),
|
||||
networkType: networkType,);
|
||||
autorun((_) {
|
||||
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
|
||||
});
|
||||
}
|
||||
|
||||
static Future<LitecoinWallet> create({
|
||||
|
|
|
@ -57,6 +57,9 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
|
|||
sideHd: bitcoin.HDWallet.fromSeed(seedBytes)
|
||||
.derivePath("m/44'/145'/0'/1"),
|
||||
networkType: networkType);
|
||||
autorun((_) {
|
||||
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -81,6 +81,7 @@ class AmountConverter {
|
|||
return _moneroAmountToString(amount);
|
||||
case CryptoCurrency.btc:
|
||||
case CryptoCurrency.bch:
|
||||
case CryptoCurrency.ltc:
|
||||
return _bitcoinAmountToString(amount);
|
||||
case CryptoCurrency.xhv:
|
||||
case CryptoCurrency.xag:
|
||||
|
|
|
@ -95,6 +95,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
|||
CryptoCurrency.banano,
|
||||
CryptoCurrency.usdtPoly,
|
||||
CryptoCurrency.usdcEPoly,
|
||||
CryptoCurrency.kaspa,
|
||||
];
|
||||
|
||||
static const havenCurrencies = [
|
||||
|
@ -206,6 +207,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
|||
static const banano = CryptoCurrency(title: 'BAN', fullName: 'Banano', raw: 86, name: 'banano', iconPath: 'assets/images/nano_icon.png', decimals: 29);
|
||||
static const usdtPoly = CryptoCurrency(title: 'USDT', tag: 'POLY', fullName: 'Tether USD (PoS)', raw: 87, name: 'usdtpoly', iconPath: 'assets/images/usdt_icon.png', decimals: 6);
|
||||
static const usdcEPoly = CryptoCurrency(title: 'USDC.E', tag: 'POLY', fullName: 'USD Coin (PoS)', raw: 88, name: 'usdcepoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6);
|
||||
static const kaspa = CryptoCurrency(title: 'KAS', fullName: 'Kaspa', raw: 89, name: 'kaspa', iconPath: 'assets/images/kaspa_icon.png', decimals: 8);
|
||||
|
||||
|
||||
static final Map<int, CryptoCurrency> _rawCurrencyMap =
|
||||
|
|
|
@ -15,6 +15,7 @@ abstract class TransactionInfo extends Object with Keyable {
|
|||
String? feeFormatted();
|
||||
void changeFiatAmount(String amount);
|
||||
String? to;
|
||||
String? from;
|
||||
|
||||
@override
|
||||
dynamic get keyIndex => id;
|
||||
|
|
|
@ -10,6 +10,8 @@ abstract class WalletAddresses {
|
|||
|
||||
String get address;
|
||||
|
||||
String? get primaryAddress => null;
|
||||
|
||||
set address(String address);
|
||||
|
||||
Map<String, String> addressesMap;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/erc20_token.dart';
|
||||
|
||||
class DefaultErc20Tokens {
|
||||
class DefaultEthereumErc20Tokens {
|
||||
final List<Erc20Token> _defaultTokens = [
|
||||
Erc20Token(
|
||||
name: "USD Coin",
|
||||
|
|
|
@ -1,225 +1,22 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_ethereum/erc20_balance.dart';
|
||||
import 'package:cw_core/erc20_token.dart';
|
||||
import 'package:cw_ethereum/ethereum_transaction_model.dart';
|
||||
import 'package:cw_ethereum/pending_ethereum_transaction.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:cw_evm/evm_chain_client.dart';
|
||||
import 'package:cw_evm/.secrets.g.dart' as secrets;
|
||||
import 'package:cw_evm/evm_chain_transaction_model.dart';
|
||||
import 'package:web3dart/web3dart.dart';
|
||||
import 'package:erc20/erc20.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cw_ethereum/ethereum_transaction_priority.dart';
|
||||
import 'package:cw_ethereum/.secrets.g.dart' as secrets;
|
||||
|
||||
class EthereumClient {
|
||||
final httpClient = Client();
|
||||
Web3Client? _client;
|
||||
|
||||
bool connect(Node node) {
|
||||
try {
|
||||
_client = Web3Client(node.uri.toString(), httpClient);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void setListeners(EthereumAddress userAddress, Function() onNewTransaction) async {
|
||||
// _client?.pendingTransactions().listen((transactionHash) async {
|
||||
// final transaction = await _client!.getTransactionByHash(transactionHash);
|
||||
//
|
||||
// if (transaction.from.hex == userAddress || transaction.to?.hex == userAddress) {
|
||||
// onNewTransaction();
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
Future<EtherAmount> getBalance(EthereumAddress address) async {
|
||||
try {
|
||||
return await _client!.getBalance(address);
|
||||
} catch (_) {
|
||||
return EtherAmount.zero();
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> getGasUnitPrice() async {
|
||||
try {
|
||||
final gasPrice = await _client!.getGasPrice();
|
||||
return gasPrice.getInWei.toInt();
|
||||
} catch (_) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> getEstimatedGas() async {
|
||||
try {
|
||||
final estimatedGas = await _client!.estimateGas();
|
||||
return estimatedGas.toInt();
|
||||
} catch (_) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Future<PendingEthereumTransaction> signTransaction({
|
||||
required EthPrivateKey privateKey,
|
||||
required String toAddress,
|
||||
required String amount,
|
||||
required int gas,
|
||||
required EthereumTransactionPriority priority,
|
||||
required CryptoCurrency currency,
|
||||
required int exponent,
|
||||
String? contractAddress,
|
||||
}) async {
|
||||
assert(currency == CryptoCurrency.eth ||
|
||||
currency == CryptoCurrency.maticpoly ||
|
||||
contractAddress != null);
|
||||
|
||||
bool _isEVMCompatibleChain =
|
||||
currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly;
|
||||
|
||||
final price = _client!.getGasPrice();
|
||||
|
||||
final Transaction transaction = createTransaction(
|
||||
from: privateKey.address,
|
||||
to: EthereumAddress.fromHex(toAddress),
|
||||
maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip),
|
||||
amount: _isEVMCompatibleChain ? EtherAmount.inWei(BigInt.parse(amount)) : EtherAmount.zero(),
|
||||
);
|
||||
|
||||
final signedTransaction =
|
||||
await _client!.signTransaction(privateKey, transaction, chainId: chainId);
|
||||
|
||||
final Function _sendTransaction;
|
||||
|
||||
if (_isEVMCompatibleChain) {
|
||||
_sendTransaction = () async => await sendTransaction(signedTransaction);
|
||||
} else {
|
||||
final erc20 = ERC20(
|
||||
client: _client!,
|
||||
address: EthereumAddress.fromHex(contractAddress!),
|
||||
chainId: chainId,
|
||||
);
|
||||
|
||||
_sendTransaction = () async {
|
||||
await erc20.transfer(
|
||||
EthereumAddress.fromHex(toAddress),
|
||||
BigInt.parse(amount),
|
||||
credentials: privateKey,
|
||||
transaction: transaction,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
return PendingEthereumTransaction(
|
||||
signedTransaction: signedTransaction,
|
||||
amount: amount,
|
||||
fee: BigInt.from(gas) * (await price).getInWei,
|
||||
sendTransaction: _sendTransaction,
|
||||
exponent: exponent,
|
||||
);
|
||||
}
|
||||
|
||||
class EthereumClient extends EVMChainClient {
|
||||
@override
|
||||
int get chainId => 1;
|
||||
|
||||
Transaction createTransaction({
|
||||
required EthereumAddress from,
|
||||
required EthereumAddress to,
|
||||
required EtherAmount amount,
|
||||
EtherAmount? maxPriorityFeePerGas,
|
||||
}) {
|
||||
return Transaction(
|
||||
from: from,
|
||||
to: to,
|
||||
maxPriorityFeePerGas: maxPriorityFeePerGas,
|
||||
value: amount,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> sendTransaction(Uint8List signedTransaction) async =>
|
||||
await _client!.sendRawTransaction(prepareSignedTransactionForSending(signedTransaction));
|
||||
|
||||
@override
|
||||
Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction) =>
|
||||
prependTransactionType(0x02, signedTransaction);
|
||||
|
||||
Future getTransactionDetails(String transactionHash) async {
|
||||
// Wait for the transaction receipt to become available
|
||||
TransactionReceipt? receipt;
|
||||
while (receipt == null) {
|
||||
receipt = await _client!.getTransactionReceipt(transactionHash);
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
}
|
||||
|
||||
// Print the receipt information
|
||||
print('Transaction Hash: ${receipt.transactionHash}');
|
||||
print('Block Hash: ${receipt.blockHash}');
|
||||
print('Block Number: ${receipt.blockNumber}');
|
||||
print('Gas Used: ${receipt.gasUsed}');
|
||||
|
||||
/*
|
||||
Transaction Hash: [112, 244, 4, 238, 89, 199, 171, 191, 210, 236, 110, 42, 185, 202, 220, 21, 27, 132, 123, 221, 137, 90, 77, 13, 23, 43, 12, 230, 93, 63, 221, 116]
|
||||
I/flutter ( 4474): Block Hash: [149, 44, 250, 119, 111, 104, 82, 98, 17, 89, 30, 190, 25, 44, 218, 118, 127, 189, 241, 35, 213, 106, 25, 95, 195, 37, 55, 131, 185, 180, 246, 200]
|
||||
I/flutter ( 4474): Block Number: 17120242
|
||||
I/flutter ( 4474): Gas Used: 21000
|
||||
*/
|
||||
|
||||
// Wait for the transaction receipt to become available
|
||||
TransactionInformation? transactionInformation;
|
||||
while (transactionInformation == null) {
|
||||
print("********************************");
|
||||
transactionInformation = await _client!.getTransactionByHash(transactionHash);
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
}
|
||||
// Print the receipt information
|
||||
print('Transaction Hash: ${transactionInformation.hash}');
|
||||
print('Block Hash: ${transactionInformation.blockHash}');
|
||||
print('Block Number: ${transactionInformation.blockNumber}');
|
||||
print('Gas Used: ${transactionInformation.gas}');
|
||||
|
||||
/*
|
||||
Transaction Hash: 0x70f404ee59c7abbfd2ec6e2ab9cadc151b847bdd895a4d0d172b0ce65d3fdd74
|
||||
I/flutter ( 4474): Block Hash: 0x952cfa776f68526211591ebe192cda767fbdf123d56a195fc3253783b9b4f6c8
|
||||
I/flutter ( 4474): Block Number: 17120242
|
||||
I/flutter ( 4474): Gas Used: 53000
|
||||
*/
|
||||
}
|
||||
|
||||
Future<ERC20Balance> fetchERC20Balances(
|
||||
EthereumAddress userAddress, String contractAddress) async {
|
||||
final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!);
|
||||
final balance = await erc20.balanceOf(userAddress);
|
||||
|
||||
int exponent = (await erc20.decimals()).toInt();
|
||||
|
||||
return ERC20Balance(balance, exponent: exponent);
|
||||
}
|
||||
|
||||
Future<Erc20Token?> getErc20Token(String contractAddress) async {
|
||||
try {
|
||||
final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!);
|
||||
final name = await erc20.name();
|
||||
final symbol = await erc20.symbol();
|
||||
final decimal = await erc20.decimals();
|
||||
|
||||
return Erc20Token(
|
||||
name: name,
|
||||
symbol: symbol,
|
||||
contractAddress: contractAddress,
|
||||
decimal: decimal.toInt(),
|
||||
);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void stop() {
|
||||
_client?.dispose();
|
||||
}
|
||||
|
||||
Future<List<EthereumTransactionModel>> fetchTransactions(String address,
|
||||
@override
|
||||
Future<List<EVMChainTransactionModel>> fetchTransactions(String address,
|
||||
{String? contractAddress}) async {
|
||||
try {
|
||||
final response = await httpClient.get(Uri.https("api.etherscan.io", "/api", {
|
||||
|
@ -230,41 +27,18 @@ I/flutter ( 4474): Gas Used: 53000
|
|||
"apikey": secrets.etherScanApiKey,
|
||||
}));
|
||||
|
||||
final _jsonResponse = json.decode(response.body) as Map<String, dynamic>;
|
||||
final jsonResponse = json.decode(response.body) as Map<String, dynamic>;
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300 && _jsonResponse['status'] != 0) {
|
||||
return (_jsonResponse['result'] as List)
|
||||
.map((e) => EthereumTransactionModel.fromJson(e as Map<String, dynamic>))
|
||||
if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) {
|
||||
return (jsonResponse['result'] as List)
|
||||
.map((e) => EVMChainTransactionModel.fromJson(e as Map<String, dynamic>, 'ETH'))
|
||||
.toList();
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (e) {
|
||||
print(e);
|
||||
log(e.toString());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Web3Client? getWeb3Client() {
|
||||
return _client;
|
||||
}
|
||||
|
||||
// Future<int> _getDecimalPlacesForContract(DeployedContract contract) async {
|
||||
// final String abi = await rootBundle.loadString("assets/abi_json/erc20_abi.json");
|
||||
// final contractAbi = ContractAbi.fromJson(abi, "ERC20");
|
||||
//
|
||||
// final contract = DeployedContract(
|
||||
// contractAbi,
|
||||
// EthereumAddress.fromHex(_erc20Currencies[erc20Currency]!),
|
||||
// );
|
||||
// final decimalsFunction = contract.function('decimals');
|
||||
// final decimals = await _client!.call(
|
||||
// contract: contract,
|
||||
// function: decimalsFunction,
|
||||
// params: [],
|
||||
// );
|
||||
//
|
||||
// int exponent = int.parse(decimals.first.toString());
|
||||
// return exponent;
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
import 'package:cw_core/crypto_currency.dart';
|
||||
|
||||
class EthereumTransactionCreationException implements Exception {
|
||||
final String exceptionMessage;
|
||||
|
||||
EthereumTransactionCreationException(CryptoCurrency currency) :
|
||||
this.exceptionMessage = 'Wrong balance. Not enough ${currency.title} on your balance.';
|
||||
|
||||
@override
|
||||
String toString() => exceptionMessage;
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
import 'package:intl/intl.dart';
|
||||
|
||||
const ethereumAmountLength = 12;
|
||||
const ethereumAmountDivider = 1000000000000;
|
||||
final ethereumAmountFormat = NumberFormat()
|
||||
..maximumFractionDigits = ethereumAmountLength
|
||||
..minimumFractionDigits = 1;
|
||||
|
||||
class EthereumFormatter {
|
||||
static int parseEthereumAmount(String amount) {
|
||||
try {
|
||||
return (double.parse(amount) * ethereumAmountDivider).round();
|
||||
} catch (_) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static double parseEthereumAmountToDouble(int amount) {
|
||||
try {
|
||||
return amount / ethereumAmountDivider;
|
||||
} catch (_) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
5
cw_ethereum/lib/ethereum_mnemonics_exception.dart
Normal file
|
@ -0,0 +1,5 @@
|
|||
class EthereumMnemonicIsIncorrectException implements Exception {
|
||||
@override
|
||||
String toString() =>
|
||||
'Ethereum mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.';
|
||||
}
|
|
@ -1,77 +1,18 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:core';
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_ethereum/file.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:cw_core/transaction_history.dart';
|
||||
import 'package:cw_ethereum/ethereum_transaction_info.dart';
|
||||
import 'package:cw_evm/evm_chain_transaction_history.dart';
|
||||
import 'package:cw_evm/evm_chain_transaction_info.dart';
|
||||
|
||||
part 'ethereum_transaction_history.g.dart';
|
||||
|
||||
const transactionsHistoryFileName = 'transactions.json';
|
||||
|
||||
class EthereumTransactionHistory = EthereumTransactionHistoryBase with _$EthereumTransactionHistory;
|
||||
|
||||
abstract class EthereumTransactionHistoryBase
|
||||
extends TransactionHistoryBase<EthereumTransactionInfo> with Store {
|
||||
EthereumTransactionHistoryBase({required this.walletInfo, required String password})
|
||||
: _password = password {
|
||||
transactions = ObservableMap<String, EthereumTransactionInfo>();
|
||||
}
|
||||
|
||||
final WalletInfo walletInfo;
|
||||
String _password;
|
||||
|
||||
Future<void> init() async => await _load();
|
||||
class EthereumTransactionHistory extends EVMChainTransactionHistory {
|
||||
EthereumTransactionHistory({
|
||||
required super.walletInfo,
|
||||
required super.password,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<void> save() async {
|
||||
try {
|
||||
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
|
||||
final path = '$dirPath/$transactionsHistoryFileName';
|
||||
final data = json.encode({'transactions': transactions});
|
||||
await writeData(path: path, password: _password, data: data);
|
||||
} catch (e, s) {
|
||||
print('Error while save ethereum transaction history: ${e.toString()}');
|
||||
print(s);
|
||||
}
|
||||
}
|
||||
String getTransactionHistoryFileName() => 'transactions.json';
|
||||
|
||||
@override
|
||||
void addOne(EthereumTransactionInfo transaction) => transactions[transaction.id] = transaction;
|
||||
|
||||
@override
|
||||
void addMany(Map<String, EthereumTransactionInfo> transactions) =>
|
||||
this.transactions.addAll(transactions);
|
||||
|
||||
Future<Map<String, dynamic>> _read() async {
|
||||
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
|
||||
final path = '$dirPath/$transactionsHistoryFileName';
|
||||
final content = await read(path: path, password: _password);
|
||||
if (content.isEmpty) {
|
||||
return {};
|
||||
}
|
||||
return json.decode(content) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
try {
|
||||
final content = await _read();
|
||||
final txs = content['transactions'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
txs.entries.forEach((entry) {
|
||||
final val = entry.value;
|
||||
|
||||
if (val is Map<String, dynamic>) {
|
||||
final tx = EthereumTransactionInfo.fromJson(val);
|
||||
_update(tx);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
|
||||
void _update(EthereumTransactionInfo transaction) => transactions[transaction.id] = transaction;
|
||||
EVMChainTransactionInfo getTransactionInfo(Map<String, dynamic> val) =>
|
||||
EthereumTransactionInfo.fromJson(val);
|
||||
}
|
||||
|
|
|
@ -1,57 +1,21 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:cw_core/format_amount.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
import 'package:cw_core/transaction_info.dart';
|
||||
import 'package:cw_evm/evm_chain_transaction_info.dart';
|
||||
|
||||
class EthereumTransactionInfo extends TransactionInfo {
|
||||
class EthereumTransactionInfo extends EVMChainTransactionInfo {
|
||||
EthereumTransactionInfo({
|
||||
required this.id,
|
||||
required this.height,
|
||||
required this.ethAmount,
|
||||
required this.ethFee,
|
||||
this.tokenSymbol = "ETH",
|
||||
this.exponent = 18,
|
||||
required this.direction,
|
||||
required this.isPending,
|
||||
required this.date,
|
||||
required this.confirmations,
|
||||
required this.to,
|
||||
}) : this.amount = ethAmount.toInt(),
|
||||
this.fee = ethFee.toInt();
|
||||
|
||||
final String id;
|
||||
final int height;
|
||||
final int amount;
|
||||
final BigInt ethAmount;
|
||||
final int exponent;
|
||||
final TransactionDirection direction;
|
||||
final DateTime date;
|
||||
final bool isPending;
|
||||
final int fee;
|
||||
final BigInt ethFee;
|
||||
final int confirmations;
|
||||
final String tokenSymbol;
|
||||
String? _fiatAmount;
|
||||
final String? to;
|
||||
|
||||
@override
|
||||
String amountFormatted() {
|
||||
final amount = formatAmount((ethAmount / BigInt.from(10).pow(exponent)).toString());
|
||||
return '${amount.substring(0, min(10, amount.length))} $tokenSymbol';
|
||||
}
|
||||
|
||||
@override
|
||||
String fiatAmount() => _fiatAmount ?? '';
|
||||
|
||||
@override
|
||||
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
|
||||
|
||||
@override
|
||||
String feeFormatted() {
|
||||
final amount = (ethFee / BigInt.from(10).pow(18)).toString();
|
||||
return '${amount.substring(0, min(10, amount.length))} ETH';
|
||||
}
|
||||
required super.id,
|
||||
required super.height,
|
||||
required super.ethAmount,
|
||||
required super.ethFee,
|
||||
required super.tokenSymbol,
|
||||
required super.direction,
|
||||
required super.isPending,
|
||||
required super.date,
|
||||
required super.confirmations,
|
||||
required super.to,
|
||||
required super.from,
|
||||
super.exponent,
|
||||
});
|
||||
|
||||
factory EthereumTransactionInfo.fromJson(Map<String, dynamic> data) {
|
||||
return EthereumTransactionInfo(
|
||||
|
@ -66,20 +30,10 @@ class EthereumTransactionInfo extends TransactionInfo {
|
|||
confirmations: data['confirmations'] as int,
|
||||
tokenSymbol: data['tokenSymbol'] as String,
|
||||
to: data['to'],
|
||||
from: data['from'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'height': height,
|
||||
'amount': ethAmount.toString(),
|
||||
'exponent': exponent,
|
||||
'fee': ethFee.toString(),
|
||||
'direction': direction.index,
|
||||
'date': date.millisecondsSinceEpoch,
|
||||
'isPending': isPending,
|
||||
'confirmations': confirmations,
|
||||
'tokenSymbol': tokenSymbol,
|
||||
'to': to,
|
||||
};
|
||||
@override
|
||||
String get feeCurrency => 'ETH';
|
||||
}
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
import 'package:cw_core/transaction_priority.dart';
|
||||
|
||||
class EthereumTransactionPriority extends TransactionPriority {
|
||||
final int tip;
|
||||
|
||||
const EthereumTransactionPriority({required String title, required int raw, required this.tip})
|
||||
: super(title: title, raw: raw);
|
||||
|
||||
static const List<EthereumTransactionPriority> all = [fast, medium, slow];
|
||||
static const EthereumTransactionPriority slow =
|
||||
EthereumTransactionPriority(title: 'slow', raw: 0, tip: 1);
|
||||
static const EthereumTransactionPriority medium =
|
||||
EthereumTransactionPriority(title: 'Medium', raw: 1, tip: 2);
|
||||
static const EthereumTransactionPriority fast =
|
||||
EthereumTransactionPriority(title: 'Fast', raw: 2, tip: 4);
|
||||
|
||||
static EthereumTransactionPriority deserialize({required int raw}) {
|
||||
switch (raw) {
|
||||
case 0:
|
||||
return slow;
|
||||
case 1:
|
||||
return medium;
|
||||
case 2:
|
||||
return fast;
|
||||
default:
|
||||
throw Exception('Unexpected token: $raw for EthereumTransactionPriority deserialize');
|
||||
}
|
||||
}
|
||||
|
||||
String get units => 'gas';
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var label = '';
|
||||
|
||||
switch (this) {
|
||||
case EthereumTransactionPriority.slow:
|
||||
label = 'Slow';
|
||||
break;
|
||||
case EthereumTransactionPriority.medium:
|
||||
label = 'Medium';
|
||||
break;
|
||||
case EthereumTransactionPriority.fast:
|
||||
label = 'Fast';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
}
|
|
@ -1,126 +1,58 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/cake_hive.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/erc20_token.dart';
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
import 'package:cw_core/transaction_priority.dart';
|
||||
import 'package:cw_core/wallet_addresses.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_ethereum/default_ethereum_erc20_tokens.dart';
|
||||
import 'package:cw_ethereum/erc20_balance.dart';
|
||||
import 'package:cw_ethereum/ethereum_client.dart';
|
||||
import 'package:cw_ethereum/ethereum_exceptions.dart';
|
||||
import 'package:cw_ethereum/ethereum_formatter.dart';
|
||||
import 'package:cw_ethereum/ethereum_transaction_credentials.dart';
|
||||
import 'package:cw_ethereum/ethereum_transaction_history.dart';
|
||||
import 'package:cw_ethereum/ethereum_transaction_info.dart';
|
||||
import 'package:cw_ethereum/ethereum_transaction_model.dart';
|
||||
import 'package:cw_ethereum/ethereum_transaction_priority.dart';
|
||||
import 'package:cw_ethereum/ethereum_wallet_addresses.dart';
|
||||
import 'package:cw_ethereum/file.dart';
|
||||
import 'package:cw_core/erc20_token.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:hex/hex.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:web3dart/crypto.dart';
|
||||
import 'package:web3dart/web3dart.dart';
|
||||
import 'package:bip39/bip39.dart' as bip39;
|
||||
import 'package:bip32/bip32.dart' as bip32;
|
||||
import 'package:cw_evm/evm_chain_transaction_history.dart';
|
||||
import 'package:cw_evm/evm_chain_transaction_info.dart';
|
||||
import 'package:cw_evm/evm_chain_transaction_model.dart';
|
||||
import 'package:cw_evm/evm_chain_wallet.dart';
|
||||
import 'package:cw_evm/evm_erc20_balance.dart';
|
||||
import 'package:cw_evm/file.dart';
|
||||
|
||||
part 'ethereum_wallet.g.dart';
|
||||
class EthereumWallet extends EVMChainWallet {
|
||||
EthereumWallet({
|
||||
required super.client,
|
||||
required super.password,
|
||||
required super.walletInfo,
|
||||
super.mnemonic,
|
||||
super.initialBalance,
|
||||
super.privateKey,
|
||||
}) : super(nativeCurrency: CryptoCurrency.eth);
|
||||
|
||||
class EthereumWallet = EthereumWalletBase with _$EthereumWallet;
|
||||
@override
|
||||
void addInitialTokens() {
|
||||
final initialErc20Tokens = DefaultEthereumErc20Tokens().initialErc20Tokens;
|
||||
|
||||
abstract class EthereumWalletBase
|
||||
extends WalletBase<ERC20Balance, EthereumTransactionHistory, EthereumTransactionInfo>
|
||||
with Store {
|
||||
EthereumWalletBase({
|
||||
required WalletInfo walletInfo,
|
||||
String? mnemonic,
|
||||
String? privateKey,
|
||||
required String password,
|
||||
ERC20Balance? initialBalance,
|
||||
}) : syncStatus = NotConnectedSyncStatus(),
|
||||
_password = password,
|
||||
_mnemonic = mnemonic,
|
||||
_hexPrivateKey = privateKey,
|
||||
_isTransactionUpdating = false,
|
||||
_client = EthereumClient(),
|
||||
walletAddresses = EthereumWalletAddresses(walletInfo),
|
||||
balance = ObservableMap<CryptoCurrency, ERC20Balance>.of(
|
||||
{CryptoCurrency.eth: initialBalance ?? ERC20Balance(BigInt.zero)}),
|
||||
super(walletInfo) {
|
||||
this.walletInfo = walletInfo;
|
||||
transactionHistory = EthereumTransactionHistory(walletInfo: walletInfo, password: password);
|
||||
|
||||
if (!CakeHive.isAdapterRegistered(Erc20Token.typeId)) {
|
||||
CakeHive.registerAdapter(Erc20TokenAdapter());
|
||||
for (var token in initialErc20Tokens) {
|
||||
evmChainErc20TokensBox.put(token.contractAddress, token);
|
||||
}
|
||||
|
||||
_sharedPrefs.complete(SharedPreferences.getInstance());
|
||||
}
|
||||
|
||||
final String? _mnemonic;
|
||||
final String? _hexPrivateKey;
|
||||
final String _password;
|
||||
|
||||
late final Box<Erc20Token> erc20TokensBox;
|
||||
|
||||
late final Box<Erc20Token> ethereumErc20TokensBox;
|
||||
|
||||
late final EthPrivateKey _ethPrivateKey;
|
||||
|
||||
EthPrivateKey get ethPrivateKey => _ethPrivateKey;
|
||||
|
||||
late EthereumClient _client;
|
||||
|
||||
int? _gasPrice;
|
||||
int? _estimatedGas;
|
||||
bool _isTransactionUpdating;
|
||||
|
||||
// TODO: remove after integrating our own node and having eth_newPendingTransactionFilter
|
||||
Timer? _transactionsUpdateTimer;
|
||||
@override
|
||||
Future<bool> checkIfScanProviderIsEnabled() async {
|
||||
bool isEtherscanEnabled = (await sharedPrefs.future).getBool("use_etherscan") ?? true;
|
||||
return isEtherscanEnabled;
|
||||
}
|
||||
|
||||
@override
|
||||
WalletAddresses walletAddresses;
|
||||
|
||||
@override
|
||||
@observable
|
||||
SyncStatus syncStatus;
|
||||
|
||||
@override
|
||||
@observable
|
||||
late ObservableMap<CryptoCurrency, ERC20Balance> balance;
|
||||
|
||||
Completer<SharedPreferences> _sharedPrefs = Completer();
|
||||
|
||||
Future<void> init() async {
|
||||
Future<void> initErc20TokensBox() async {
|
||||
// This is for ethereum wallets,
|
||||
// Other wallets would override and initialize their respective boxes with their boxNames.
|
||||
await movePreviousErc20BoxConfigsToNewBox();
|
||||
|
||||
await walletAddresses.init();
|
||||
await transactionHistory.init();
|
||||
_ethPrivateKey = await getPrivateKey(
|
||||
mnemonic: _mnemonic,
|
||||
privateKey: _hexPrivateKey,
|
||||
password: _password,
|
||||
);
|
||||
walletAddresses.address = _ethPrivateKey.address.toString();
|
||||
await save();
|
||||
}
|
||||
|
||||
/// Majorly for backward compatibility for previous configs that have been set.
|
||||
Future<void> movePreviousErc20BoxConfigsToNewBox() async {
|
||||
// Opens a box specific to this wallet
|
||||
ethereumErc20TokensBox = await CakeHive.openBox<Erc20Token>(
|
||||
evmChainErc20TokensBox = await CakeHive.openBox<Erc20Token>(
|
||||
"${walletInfo.name.replaceAll(" ", "_")}_${Erc20Token.ethereumBoxName}");
|
||||
|
||||
//Open the previous token configs box
|
||||
|
@ -130,7 +62,7 @@ abstract class EthereumWalletBase
|
|||
if (erc20TokensBox.isEmpty) {
|
||||
// If it's empty, but the new wallet specific box is also empty,
|
||||
// we load the initial tokens to the new box.
|
||||
if (ethereumErc20TokensBox.isEmpty) addInitialTokens();
|
||||
if (evmChainErc20TokensBox.isEmpty) addInitialTokens();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -141,319 +73,37 @@ abstract class EthereumWalletBase
|
|||
await erc20TokensBox.deleteFromDisk();
|
||||
|
||||
// Add all the previous tokens with configs to the new box
|
||||
ethereumErc20TokensBox.addAll(allValues);
|
||||
evmChainErc20TokensBox.addAll(allValues);
|
||||
}
|
||||
|
||||
@override
|
||||
int calculateEstimatedFee(TransactionPriority priority, int? amount) {
|
||||
try {
|
||||
if (priority is EthereumTransactionPriority) {
|
||||
final priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt();
|
||||
return (_gasPrice! + priorityFee) * (_estimatedGas ?? 0);
|
||||
}
|
||||
|
||||
return 0;
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> changePassword(String password) {
|
||||
throw UnimplementedError("changePassword");
|
||||
}
|
||||
|
||||
@override
|
||||
void close() {
|
||||
_client.stop();
|
||||
_transactionsUpdateTimer?.cancel();
|
||||
}
|
||||
|
||||
@action
|
||||
@override
|
||||
Future<void> connectToNode({required Node node}) async {
|
||||
try {
|
||||
syncStatus = ConnectingSyncStatus();
|
||||
|
||||
final isConnected = _client.connect(node);
|
||||
|
||||
if (!isConnected) {
|
||||
throw Exception("Ethereum Node connection failed");
|
||||
}
|
||||
|
||||
_client.setListeners(_ethPrivateKey.address, _onNewTransaction);
|
||||
|
||||
_setTransactionUpdateTimer();
|
||||
|
||||
syncStatus = ConnectedSyncStatus();
|
||||
} catch (e) {
|
||||
syncStatus = FailedSyncStatus();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PendingTransaction> createTransaction(Object credentials) async {
|
||||
final _credentials = credentials as EthereumTransactionCredentials;
|
||||
final outputs = _credentials.outputs;
|
||||
final hasMultiDestination = outputs.length > 1;
|
||||
|
||||
final CryptoCurrency transactionCurrency =
|
||||
balance.keys.firstWhere((element) => element.title == _credentials.currency.title);
|
||||
|
||||
final _erc20Balance = balance[transactionCurrency]!;
|
||||
BigInt totalAmount = BigInt.zero;
|
||||
int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18;
|
||||
num amountToEthereumMultiplier = pow(10, exponent);
|
||||
|
||||
// so far this can not be made with Ethereum as Ethereum does not support multiple recipients
|
||||
if (hasMultiDestination) {
|
||||
if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
|
||||
throw EthereumTransactionCreationException(transactionCurrency);
|
||||
}
|
||||
|
||||
final totalOriginalAmount = EthereumFormatter.parseEthereumAmountToDouble(
|
||||
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)));
|
||||
totalAmount = BigInt.from(totalOriginalAmount * amountToEthereumMultiplier);
|
||||
|
||||
if (_erc20Balance.balance < totalAmount) {
|
||||
throw EthereumTransactionCreationException(transactionCurrency);
|
||||
}
|
||||
} else {
|
||||
final output = outputs.first;
|
||||
// since the fees are taken from Ethereum
|
||||
// then no need to subtract the fees from the amount if send all
|
||||
final BigInt allAmount;
|
||||
if (transactionCurrency is Erc20Token) {
|
||||
allAmount = _erc20Balance.balance;
|
||||
} else {
|
||||
allAmount = _erc20Balance.balance -
|
||||
BigInt.from(calculateEstimatedFee(_credentials.priority!, null));
|
||||
}
|
||||
final totalOriginalAmount =
|
||||
EthereumFormatter.parseEthereumAmountToDouble(output.formattedCryptoAmount ?? 0);
|
||||
totalAmount = output.sendAll
|
||||
? allAmount
|
||||
: BigInt.from(totalOriginalAmount * amountToEthereumMultiplier);
|
||||
|
||||
if (_erc20Balance.balance < totalAmount) {
|
||||
throw EthereumTransactionCreationException(transactionCurrency);
|
||||
}
|
||||
}
|
||||
|
||||
final pendingEthereumTransaction = await _client.signTransaction(
|
||||
privateKey: _ethPrivateKey,
|
||||
toAddress: _credentials.outputs.first.isParsedAddress
|
||||
? _credentials.outputs.first.extractedAddress!
|
||||
: _credentials.outputs.first.address,
|
||||
amount: totalAmount.toString(),
|
||||
gas: _estimatedGas!,
|
||||
priority: _credentials.priority!,
|
||||
currency: transactionCurrency,
|
||||
exponent: exponent,
|
||||
contractAddress:
|
||||
transactionCurrency is Erc20Token ? transactionCurrency.contractAddress : null,
|
||||
EVMChainTransactionInfo getTransactionInfo(
|
||||
EVMChainTransactionModel transactionModel, String address) {
|
||||
final model = EthereumTransactionInfo(
|
||||
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,
|
||||
);
|
||||
|
||||
return pendingEthereumTransaction;
|
||||
}
|
||||
|
||||
Future<void> _updateTransactions() async {
|
||||
try {
|
||||
if (_isTransactionUpdating) {
|
||||
return;
|
||||
}
|
||||
bool isEtherscanEnabled = (await _sharedPrefs.future).getBool("use_etherscan") ?? true;
|
||||
if (!isEtherscanEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isTransactionUpdating = true;
|
||||
final transactions = await fetchTransactions();
|
||||
transactionHistory.addMany(transactions);
|
||||
await transactionHistory.save();
|
||||
_isTransactionUpdating = false;
|
||||
} catch (_) {
|
||||
_isTransactionUpdating = false;
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, EthereumTransactionInfo>> fetchTransactions() async {
|
||||
final address = _ethPrivateKey.address.hex;
|
||||
final transactions = await _client.fetchTransactions(address);
|
||||
|
||||
final List<Future<List<EthereumTransactionModel>>> erc20TokensTransactions = [];
|
||||
|
||||
for (var token in balance.keys) {
|
||||
if (token is Erc20Token) {
|
||||
erc20TokensTransactions.add(_client.fetchTransactions(
|
||||
address,
|
||||
contractAddress: token.contractAddress,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
final tokensTransaction = await Future.wait(erc20TokensTransactions);
|
||||
transactions.addAll(tokensTransaction.expand((element) => element));
|
||||
|
||||
final Map<String, EthereumTransactionInfo> result = {};
|
||||
|
||||
for (var transactionModel in transactions) {
|
||||
if (transactionModel.isError) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result[transactionModel.hash] = EthereumTransactionInfo(
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
String getTransactionHistoryFileName() => 'transactions.json';
|
||||
|
||||
@override
|
||||
Object get keys => throw UnimplementedError("keys");
|
||||
|
||||
@override
|
||||
Future<void> rescan({required int height}) {
|
||||
throw UnimplementedError("rescan");
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save() async {
|
||||
await walletAddresses.updateAddressesInBox();
|
||||
final path = await makePath();
|
||||
await write(path: path, password: _password, data: toJSON());
|
||||
await transactionHistory.save();
|
||||
}
|
||||
|
||||
@override
|
||||
String? get seed => _mnemonic;
|
||||
|
||||
@override
|
||||
String get privateKey => HEX.encode(_ethPrivateKey.privateKey);
|
||||
|
||||
@action
|
||||
@override
|
||||
Future<void> startSync() async {
|
||||
try {
|
||||
syncStatus = AttemptingSyncStatus();
|
||||
await _updateBalance();
|
||||
await _updateTransactions();
|
||||
_gasPrice = await _client.getGasUnitPrice();
|
||||
_estimatedGas = await _client.getEstimatedGas();
|
||||
|
||||
Timer.periodic(
|
||||
const Duration(minutes: 1), (timer) async => _gasPrice = await _client.getGasUnitPrice());
|
||||
Timer.periodic(const Duration(seconds: 10),
|
||||
(timer) async => _estimatedGas = await _client.getEstimatedGas());
|
||||
|
||||
syncStatus = SyncedSyncStatus();
|
||||
} catch (e) {
|
||||
syncStatus = FailedSyncStatus();
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
|
||||
|
||||
String toJSON() => json.encode({
|
||||
'mnemonic': _mnemonic,
|
||||
'private_key': privateKey,
|
||||
'balance': balance[currency]!.toJSON(),
|
||||
});
|
||||
|
||||
static Future<EthereumWallet> open({
|
||||
required String name,
|
||||
required String password,
|
||||
required WalletInfo walletInfo,
|
||||
}) async {
|
||||
final path = await pathForWallet(name: name, type: walletInfo.type);
|
||||
final jsonSource = await read(path: path, password: password);
|
||||
final data = json.decode(jsonSource) as Map;
|
||||
final mnemonic = data['mnemonic'] as String?;
|
||||
final privateKey = data['private_key'] as String?;
|
||||
final balance = ERC20Balance.fromJSON(data['balance'] as String) ?? ERC20Balance(BigInt.zero);
|
||||
|
||||
return EthereumWallet(
|
||||
walletInfo: walletInfo,
|
||||
password: password,
|
||||
mnemonic: mnemonic,
|
||||
privateKey: privateKey,
|
||||
initialBalance: balance,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _updateBalance() async {
|
||||
balance[currency] = await _fetchEthBalance();
|
||||
|
||||
await _fetchErc20Balances();
|
||||
await save();
|
||||
}
|
||||
|
||||
Future<ERC20Balance> _fetchEthBalance() async {
|
||||
final balance = await _client.getBalance(_ethPrivateKey.address);
|
||||
return ERC20Balance(balance.getInWei);
|
||||
}
|
||||
|
||||
Future<void> _fetchErc20Balances() async {
|
||||
for (var token in ethereumErc20TokensBox.values) {
|
||||
try {
|
||||
if (token.enabled) {
|
||||
balance[token] = await _client.fetchERC20Balances(
|
||||
_ethPrivateKey.address,
|
||||
token.contractAddress,
|
||||
);
|
||||
} else {
|
||||
balance.remove(token);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
Future<EthPrivateKey> getPrivateKey(
|
||||
{String? mnemonic, String? privateKey, required String password}) async {
|
||||
assert(mnemonic != null || privateKey != null);
|
||||
|
||||
if (privateKey != null) {
|
||||
return EthPrivateKey.fromHex(privateKey);
|
||||
}
|
||||
|
||||
final seed = bip39.mnemonicToSeed(mnemonic!);
|
||||
|
||||
final root = bip32.BIP32.fromSeed(seed);
|
||||
|
||||
const _hdPathEthereum = "m/44'/60'/0'/0";
|
||||
const index = 0;
|
||||
final addressAtIndex = root.derivePath("$_hdPathEthereum/$index");
|
||||
|
||||
return EthPrivateKey.fromHex(HEX.encode(addressAtIndex.privateKey as List<int>));
|
||||
}
|
||||
|
||||
Future<void>? updateBalance() async => await _updateBalance();
|
||||
|
||||
List<Erc20Token> get erc20Currencies => ethereumErc20TokensBox.values.toList();
|
||||
|
||||
Future<void> addErc20Token(Erc20Token token) async {
|
||||
String? iconPath;
|
||||
try {
|
||||
iconPath = CryptoCurrency.all
|
||||
.firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase())
|
||||
.iconPath;
|
||||
} catch (_) {}
|
||||
|
||||
final _token = Erc20Token(
|
||||
Erc20Token createNewErc20TokenObject(Erc20Token token, String? iconPath) {
|
||||
return Erc20Token(
|
||||
name: token.name,
|
||||
symbol: token.symbol,
|
||||
contractAddress: token.contractAddress,
|
||||
|
@ -462,85 +112,30 @@ abstract class EthereumWalletBase
|
|||
tag: token.tag ?? "ETH",
|
||||
iconPath: iconPath,
|
||||
);
|
||||
|
||||
await ethereumErc20TokensBox.put(_token.contractAddress, _token);
|
||||
|
||||
if (_token.enabled) {
|
||||
balance[_token] = await _client.fetchERC20Balances(
|
||||
_ethPrivateKey.address,
|
||||
_token.contractAddress,
|
||||
);
|
||||
} else {
|
||||
balance.remove(_token);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteErc20Token(Erc20Token token) async {
|
||||
await token.delete();
|
||||
|
||||
balance.remove(token);
|
||||
_updateBalance();
|
||||
}
|
||||
|
||||
Future<Erc20Token?> getErc20Token(String contractAddress) async =>
|
||||
await _client.getErc20Token(contractAddress);
|
||||
|
||||
void _onNewTransaction() {
|
||||
_updateBalance();
|
||||
_updateTransactions();
|
||||
}
|
||||
|
||||
void addInitialTokens() {
|
||||
final initialErc20Tokens = DefaultErc20Tokens().initialErc20Tokens;
|
||||
|
||||
initialErc20Tokens.forEach((token) => ethereumErc20TokensBox.put(token.contractAddress, token));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> renameWalletFiles(String newWalletName) async {
|
||||
final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type);
|
||||
final currentWalletFile = File(currentWalletPath);
|
||||
|
||||
final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type);
|
||||
final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName');
|
||||
|
||||
// Copies current wallet files into new wallet name's dir and files
|
||||
if (currentWalletFile.existsSync()) {
|
||||
final newWalletPath = await pathForWallet(name: newWalletName, type: type);
|
||||
await currentWalletFile.copy(newWalletPath);
|
||||
}
|
||||
if (currentTransactionsFile.existsSync()) {
|
||||
final newDirPath = await pathForWalletDir(name: newWalletName, type: type);
|
||||
await currentTransactionsFile.copy('$newDirPath/$transactionsHistoryFileName');
|
||||
}
|
||||
|
||||
// Delete old name's dir and files
|
||||
await Directory(currentDirPath).delete(recursive: true);
|
||||
EVMChainTransactionHistory setUpTransactionHistory(WalletInfo walletInfo, String password) {
|
||||
return EthereumTransactionHistory(walletInfo: walletInfo, password: password);
|
||||
}
|
||||
|
||||
void _setTransactionUpdateTimer() {
|
||||
if (_transactionsUpdateTimer?.isActive ?? false) {
|
||||
_transactionsUpdateTimer!.cancel();
|
||||
}
|
||||
static Future<EthereumWallet> open(
|
||||
{required String name, required String password, required WalletInfo walletInfo}) async {
|
||||
final path = await pathForWallet(name: name, type: walletInfo.type);
|
||||
final jsonSource = await read(path: path, password: password);
|
||||
final data = json.decode(jsonSource) as Map;
|
||||
final mnemonic = data['mnemonic'] as String?;
|
||||
final privateKey = data['private_key'] as String?;
|
||||
final balance = EVMChainERC20Balance.fromJSON(data['balance'] as String) ??
|
||||
EVMChainERC20Balance(BigInt.zero);
|
||||
|
||||
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 10), (_) {
|
||||
_updateTransactions();
|
||||
_updateBalance();
|
||||
});
|
||||
return EthereumWallet(
|
||||
walletInfo: walletInfo,
|
||||
password: password,
|
||||
mnemonic: mnemonic,
|
||||
privateKey: privateKey,
|
||||
initialBalance: balance,
|
||||
client: EthereumClient(),
|
||||
);
|
||||
}
|
||||
|
||||
void updateEtherscanUsageState(bool isEnabled) {
|
||||
if (isEnabled) {
|
||||
_updateTransactions();
|
||||
_setTransactionUpdateTimer();
|
||||
} else {
|
||||
_transactionsUpdateTimer?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String signMessage(String message, {String? address}) =>
|
||||
bytesToHex(_ethPrivateKey.signPersonalMessageToUint8List(ascii.encode(message)));
|
||||
|
||||
Web3Client? getWeb3Client() => _client.getWeb3Client();
|
||||
}
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
import 'package:cw_core/wallet_credentials.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
|
||||
class EthereumNewWalletCredentials extends WalletCredentials {
|
||||
EthereumNewWalletCredentials({required String name, WalletInfo? walletInfo})
|
||||
: super(name: name, walletInfo: walletInfo);
|
||||
}
|
||||
|
||||
class EthereumRestoreWalletFromSeedCredentials extends WalletCredentials {
|
||||
EthereumRestoreWalletFromSeedCredentials(
|
||||
{required String name,
|
||||
required String password,
|
||||
required this.mnemonic,
|
||||
WalletInfo? walletInfo})
|
||||
: super(name: name, password: password, walletInfo: walletInfo);
|
||||
|
||||
final String mnemonic;
|
||||
}
|
||||
|
||||
class EthereumRestoreWalletFromPrivateKey extends WalletCredentials {
|
||||
EthereumRestoreWalletFromPrivateKey(
|
||||
{required String name,
|
||||
required String password,
|
||||
required this.privateKey,
|
||||
WalletInfo? walletInfo})
|
||||
: super(name: name, password: password, walletInfo: walletInfo);
|
||||
|
||||
final String privateKey;
|
||||
}
|
|
@ -1,32 +1,31 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_service.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cw_ethereum/ethereum_mnemonics.dart';
|
||||
import 'package:cw_ethereum/ethereum_client.dart';
|
||||
import 'package:cw_ethereum/ethereum_mnemonics_exception.dart';
|
||||
import 'package:cw_ethereum/ethereum_wallet.dart';
|
||||
import 'package:cw_ethereum/ethereum_wallet_creation_credentials.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart';
|
||||
import 'package:cw_evm/evm_chain_wallet_service.dart';
|
||||
import 'package:bip39/bip39.dart' as bip39;
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
class EthereumWalletService extends WalletService<EthereumNewWalletCredentials,
|
||||
EthereumRestoreWalletFromSeedCredentials, EthereumRestoreWalletFromPrivateKey> {
|
||||
EthereumWalletService(this.walletInfoSource);
|
||||
class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
|
||||
EthereumWalletService(super.walletInfoSource, {required this.client});
|
||||
|
||||
final Box<WalletInfo> walletInfoSource;
|
||||
late EthereumClient client;
|
||||
|
||||
@override
|
||||
Future<EthereumWallet> create(EthereumNewWalletCredentials credentials) async {
|
||||
WalletType getType() => WalletType.ethereum;
|
||||
|
||||
@override
|
||||
Future<EthereumWallet> create(EVMChainNewWalletCredentials credentials) async {
|
||||
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
|
||||
|
||||
final mnemonic = bip39.generateMnemonic(strength: strength);
|
||||
|
||||
final wallet = EthereumWallet(
|
||||
walletInfo: credentials.walletInfo!,
|
||||
mnemonic: mnemonic,
|
||||
password: credentials.password!,
|
||||
client: client,
|
||||
);
|
||||
|
||||
await wallet.init();
|
||||
|
@ -36,18 +35,11 @@ class EthereumWalletService extends WalletService<EthereumNewWalletCredentials,
|
|||
return wallet;
|
||||
}
|
||||
|
||||
@override
|
||||
WalletType getType() => WalletType.ethereum;
|
||||
|
||||
@override
|
||||
Future<bool> isWalletExit(String name) async =>
|
||||
File(await pathForWallet(name: name, type: getType())).existsSync();
|
||||
|
||||
@override
|
||||
Future<EthereumWallet> openWallet(String name, String password) async {
|
||||
final walletInfo =
|
||||
walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
|
||||
final wallet = await EthereumWalletBase.open(
|
||||
final wallet = await EthereumWallet.open(
|
||||
name: name,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
|
@ -60,19 +52,28 @@ class EthereumWalletService extends WalletService<EthereumNewWalletCredentials,
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> remove(String wallet) async {
|
||||
File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true);
|
||||
final walletInfo = walletInfoSource.values
|
||||
.firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!;
|
||||
await walletInfoSource.delete(walletInfo.key);
|
||||
Future<void> rename(String currentName, String password, String newName) async {
|
||||
final currentWalletInfo = walletInfoSource.values
|
||||
.firstWhere((info) => info.id == WalletBase.idFor(currentName, getType()));
|
||||
final currentWallet = await EthereumWallet.open(
|
||||
password: password, name: currentName, walletInfo: currentWalletInfo);
|
||||
|
||||
await currentWallet.renameWalletFiles(newName);
|
||||
|
||||
final newWalletInfo = currentWalletInfo;
|
||||
newWalletInfo.id = WalletBase.idFor(newName, getType());
|
||||
newWalletInfo.name = newName;
|
||||
|
||||
await walletInfoSource.put(currentWalletInfo.key, newWalletInfo);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<EthereumWallet> restoreFromKeys(EthereumRestoreWalletFromPrivateKey credentials) async {
|
||||
Future<EthereumWallet> restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials) async {
|
||||
final wallet = EthereumWallet(
|
||||
password: credentials.password!,
|
||||
privateKey: credentials.privateKey,
|
||||
walletInfo: credentials.walletInfo!,
|
||||
client: client,
|
||||
);
|
||||
|
||||
await wallet.init();
|
||||
|
@ -84,7 +85,7 @@ class EthereumWalletService extends WalletService<EthereumNewWalletCredentials,
|
|||
|
||||
@override
|
||||
Future<EthereumWallet> restoreFromSeed(
|
||||
EthereumRestoreWalletFromSeedCredentials credentials) async {
|
||||
EVMChainRestoreWalletFromSeedCredentials credentials) async {
|
||||
if (!bip39.validateMnemonic(credentials.mnemonic)) {
|
||||
throw EthereumMnemonicIsIncorrectException();
|
||||
}
|
||||
|
@ -93,6 +94,7 @@ class EthereumWalletService extends WalletService<EthereumNewWalletCredentials,
|
|||
password: credentials.password!,
|
||||
mnemonic: credentials.mnemonic,
|
||||
walletInfo: credentials.walletInfo!,
|
||||
client: client,
|
||||
);
|
||||
|
||||
await wallet.init();
|
||||
|
@ -101,20 +103,4 @@ class EthereumWalletService extends WalletService<EthereumNewWalletCredentials,
|
|||
|
||||
return wallet;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> rename(String currentName, String password, String newName) async {
|
||||
final currentWalletInfo = walletInfoSource.values
|
||||
.firstWhere((info) => info.id == WalletBase.idFor(currentName, getType()));
|
||||
final currentWallet = await EthereumWalletBase.open(
|
||||
password: password, name: currentName, walletInfo: currentWalletInfo);
|
||||
|
||||
await currentWallet.renameWalletFiles(newName);
|
||||
|
||||
final newWalletInfo = currentWalletInfo;
|
||||
newWalletInfo.id = WalletBase.idFor(newName, getType());
|
||||
newWalletInfo.name = newName;
|
||||
|
||||
await walletInfoSource.put(currentWalletInfo.key, newWalletInfo);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,56 +13,23 @@ dependencies:
|
|||
flutter:
|
||||
sdk: flutter
|
||||
web3dart: ^2.7.1
|
||||
erc20: ^1.0.1
|
||||
mobx: ^2.0.7+4
|
||||
bip39: ^1.0.6
|
||||
bip32: ^2.0.0
|
||||
hex: ^0.2.0
|
||||
http: ^1.1.0
|
||||
shared_preferences: ^2.0.15
|
||||
cw_core:
|
||||
path: ../cw_core
|
||||
cw_evm:
|
||||
path: ../cw_evm
|
||||
hive: ^2.2.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
build_runner: ^2.1.11
|
||||
mobx_codegen: ^2.0.7
|
||||
hive_generator: ^1.1.3
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter packages.
|
||||
flutter:
|
||||
|
||||
# To add assets to your package, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
#
|
||||
# For details regarding assets in packages, see
|
||||
# https://flutter.dev/assets-and-images/#from-packages
|
||||
#
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||
|
||||
# To add custom fonts to your package, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
# - asset: fonts/TrajanPro_Bold.ttf
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts in packages, see
|
||||
# https://flutter.dev/custom-fonts/#from-packages
|
||||
|
|
30
cw_evm/.gitignore
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
/pubspec.lock
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
.packages
|
||||
build/
|
10
cw_evm/.metadata
Normal file
|
@ -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: f468f3366c26a5092eb964a230ce7892fda8f2f8
|
||||
channel: stable
|
||||
|
||||
project_type: package
|
3
cw_evm/CHANGELOG.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
## 0.0.1
|
||||
|
||||
* TODO: Describe initial release.
|
1
cw_evm/LICENSE
Normal file
|
@ -0,0 +1 @@
|
|||
TODO: Add your license here.
|
39
cw_evm/README.md
Normal file
|
@ -0,0 +1,39 @@
|
|||
<!--
|
||||
This README describes the package. If you publish this package to pub.dev,
|
||||
this README's contents appear on the landing page for your package.
|
||||
|
||||
For information about how to write a good package README, see the guide for
|
||||
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
|
||||
|
||||
For general information about developing packages, see the Dart guide for
|
||||
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
|
||||
and the Flutter guide for
|
||||
[developing packages and plugins](https://flutter.dev/developing-packages).
|
||||
-->
|
||||
|
||||
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.
|
4
cw_evm/analysis_options.yaml
Normal file
|
@ -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
|
7
cw_evm/lib/cw_evm.dart
Normal file
|
@ -0,0 +1,7 @@
|
|||
library cw_evm;
|
||||
|
||||
/// A Calculator.
|
||||
class Calculator {
|
||||
/// Returns [value] plus 1.
|
||||
int addOne(int value) => value + 1;
|
||||
}
|
251
cw_evm/lib/evm_chain_client.dart
Normal file
|
@ -0,0 +1,251 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cw_core/erc20_token.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
|
||||
import 'package:cw_evm/evm_erc20_balance.dart';
|
||||
import 'package:cw_evm/evm_chain_transaction_model.dart';
|
||||
import 'package:cw_evm/pending_evm_chain_transaction.dart';
|
||||
import 'package:cw_evm/evm_chain_transaction_priority.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:erc20/erc20.dart';
|
||||
import 'package:web3dart/web3dart.dart';
|
||||
|
||||
abstract class EVMChainClient {
|
||||
final httpClient = Client();
|
||||
Web3Client? _client;
|
||||
|
||||
//! To be overridden by all child classes
|
||||
|
||||
int get chainId;
|
||||
|
||||
Future<List<EVMChainTransactionModel>> fetchTransactions(String address,
|
||||
{String? contractAddress});
|
||||
|
||||
Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction);
|
||||
|
||||
//! Common methods across all child classes
|
||||
|
||||
bool connect(Node node) {
|
||||
try {
|
||||
_client = Web3Client(node.uri.toString(), httpClient);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void setListeners(EthereumAddress userAddress, Function() onNewTransaction) async {
|
||||
// _client?.pendingTransactions().listen((transactionHash) async {
|
||||
// final transaction = await _client!.getTransactionByHash(transactionHash);
|
||||
//
|
||||
// if (transaction.from.hex == userAddress || transaction.to?.hex == userAddress) {
|
||||
// onNewTransaction();
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
Future<EtherAmount> getBalance(EthereumAddress address) async {
|
||||
try {
|
||||
return await _client!.getBalance(address);
|
||||
} catch (_) {
|
||||
return EtherAmount.zero();
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> getGasUnitPrice() async {
|
||||
try {
|
||||
final gasPrice = await _client!.getGasPrice();
|
||||
return gasPrice.getInWei.toInt();
|
||||
} catch (_) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> getEstimatedGas() async {
|
||||
try {
|
||||
final estimatedGas = await _client!.estimateGas();
|
||||
return estimatedGas.toInt();
|
||||
} catch (_) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Future<PendingEVMChainTransaction> signTransaction({
|
||||
required EthPrivateKey privateKey,
|
||||
required String toAddress,
|
||||
required String amount,
|
||||
required int gas,
|
||||
required EVMChainTransactionPriority priority,
|
||||
required CryptoCurrency currency,
|
||||
required int exponent,
|
||||
String? contractAddress,
|
||||
}) async {
|
||||
assert(currency == CryptoCurrency.eth ||
|
||||
currency == CryptoCurrency.maticpoly ||
|
||||
contractAddress != null);
|
||||
|
||||
bool isEVMCompatibleChain =
|
||||
currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly;
|
||||
|
||||
final price = _client!.getGasPrice();
|
||||
|
||||
final Transaction transaction = createTransaction(
|
||||
from: privateKey.address,
|
||||
to: EthereumAddress.fromHex(toAddress),
|
||||
maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip),
|
||||
amount: isEVMCompatibleChain ? EtherAmount.inWei(BigInt.parse(amount)) : EtherAmount.zero(),
|
||||
);
|
||||
|
||||
final signedTransaction =
|
||||
await _client!.signTransaction(privateKey, transaction, chainId: chainId);
|
||||
|
||||
final Function _sendTransaction;
|
||||
|
||||
if (isEVMCompatibleChain) {
|
||||
_sendTransaction = () async => await sendTransaction(signedTransaction);
|
||||
} else {
|
||||
final erc20 = ERC20(
|
||||
client: _client!,
|
||||
address: EthereumAddress.fromHex(contractAddress!),
|
||||
chainId: chainId,
|
||||
);
|
||||
|
||||
_sendTransaction = () async {
|
||||
await erc20.transfer(
|
||||
EthereumAddress.fromHex(toAddress),
|
||||
BigInt.parse(amount),
|
||||
credentials: privateKey,
|
||||
transaction: transaction,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
return PendingEVMChainTransaction(
|
||||
signedTransaction: signedTransaction,
|
||||
amount: amount,
|
||||
fee: BigInt.from(gas) * (await price).getInWei,
|
||||
sendTransaction: _sendTransaction,
|
||||
exponent: exponent,
|
||||
);
|
||||
}
|
||||
|
||||
Transaction createTransaction({
|
||||
required EthereumAddress from,
|
||||
required EthereumAddress to,
|
||||
required EtherAmount amount,
|
||||
EtherAmount? maxPriorityFeePerGas,
|
||||
}) {
|
||||
return Transaction(
|
||||
from: from,
|
||||
to: to,
|
||||
maxPriorityFeePerGas: maxPriorityFeePerGas,
|
||||
value: amount,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> sendTransaction(Uint8List signedTransaction) async =>
|
||||
await _client!.sendRawTransaction(prepareSignedTransactionForSending(signedTransaction));
|
||||
|
||||
Future getTransactionDetails(String transactionHash) async {
|
||||
// Wait for the transaction receipt to become available
|
||||
TransactionReceipt? receipt;
|
||||
while (receipt == null) {
|
||||
receipt = await _client!.getTransactionReceipt(transactionHash);
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
|
||||
// Print the receipt information
|
||||
log('Transaction Hash: ${receipt.transactionHash}');
|
||||
log('Block Hash: ${receipt.blockHash}');
|
||||
log('Block Number: ${receipt.blockNumber}');
|
||||
log('Gas Used: ${receipt.gasUsed}');
|
||||
|
||||
/*
|
||||
Transaction Hash: [112, 244, 4, 238, 89, 199, 171, 191, 210, 236, 110, 42, 185, 202, 220, 21, 27, 132, 123, 221, 137, 90, 77, 13, 23, 43, 12, 230, 93, 63, 221, 116]
|
||||
I/flutter ( 4474): Block Hash: [149, 44, 250, 119, 111, 104, 82, 98, 17, 89, 30, 190, 25, 44, 218, 118, 127, 189, 241, 35, 213, 106, 25, 95, 195, 37, 55, 131, 185, 180, 246, 200]
|
||||
I/flutter ( 4474): Block Number: 17120242
|
||||
I/flutter ( 4474): Gas Used: 21000
|
||||
*/
|
||||
|
||||
// Wait for the transaction receipt to become available
|
||||
TransactionInformation? transactionInformation;
|
||||
while (transactionInformation == null) {
|
||||
log("********************************");
|
||||
transactionInformation = await _client!.getTransactionByHash(transactionHash);
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
// Print the receipt information
|
||||
log('Transaction Hash: ${transactionInformation.hash}');
|
||||
log('Block Hash: ${transactionInformation.blockHash}');
|
||||
log('Block Number: ${transactionInformation.blockNumber}');
|
||||
log('Gas Used: ${transactionInformation.gas}');
|
||||
|
||||
/*
|
||||
Transaction Hash: 0x70f404ee59c7abbfd2ec6e2ab9cadc151b847bdd895a4d0d172b0ce65d3fdd74
|
||||
I/flutter ( 4474): Block Hash: 0x952cfa776f68526211591ebe192cda767fbdf123d56a195fc3253783b9b4f6c8
|
||||
I/flutter ( 4474): Block Number: 17120242
|
||||
I/flutter ( 4474): Gas Used: 53000
|
||||
*/
|
||||
}
|
||||
|
||||
Future<EVMChainERC20Balance> fetchERC20Balances(
|
||||
EthereumAddress userAddress, String contractAddress) async {
|
||||
final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!);
|
||||
final balance = await erc20.balanceOf(userAddress);
|
||||
|
||||
int exponent = (await erc20.decimals()).toInt();
|
||||
|
||||
return EVMChainERC20Balance(balance, exponent: exponent);
|
||||
}
|
||||
|
||||
Future<Erc20Token?> getErc20Token(String contractAddress) async {
|
||||
try {
|
||||
final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!);
|
||||
final name = await erc20.name();
|
||||
final symbol = await erc20.symbol();
|
||||
final decimal = await erc20.decimals();
|
||||
|
||||
return Erc20Token(
|
||||
name: name,
|
||||
symbol: symbol,
|
||||
contractAddress: contractAddress,
|
||||
decimal: decimal.toInt(),
|
||||
);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void stop() {
|
||||
_client?.dispose();
|
||||
}
|
||||
|
||||
Web3Client? getWeb3Client() {
|
||||
return _client;
|
||||
}
|
||||
|
||||
// Future<int> _getDecimalPlacesForContract(DeployedContract contract) async {
|
||||
// final String abi = await rootBundle.loadString("assets/abi_json/erc20_abi.json");
|
||||
// final contractAbi = ContractAbi.fromJson(abi, "ERC20");
|
||||
//
|
||||
// final contract = DeployedContract(
|
||||
// contractAbi,
|
||||
// EthereumAddress.fromHex(_erc20Currencies[erc20Currency]!),
|
||||
// );
|
||||
// final decimalsFunction = contract.function('decimals');
|
||||
// final decimals = await _client!.call(
|
||||
// contract: contract,
|
||||
// function: decimalsFunction,
|
||||
// params: [],
|
||||
// );
|
||||
//
|
||||
// int exponent = int.parse(decimals.first.toString());
|
||||
// return exponent;
|
||||
// }
|
||||
}
|
11
cw_evm/lib/evm_chain_exceptions.dart
Normal file
|
@ -0,0 +1,11 @@
|
|||
import 'package:cw_core/crypto_currency.dart';
|
||||
|
||||
class EVMChainTransactionCreationException implements Exception {
|
||||
final String exceptionMessage;
|
||||
|
||||
EVMChainTransactionCreationException(CryptoCurrency currency)
|
||||
: exceptionMessage = 'Wrong balance. Not enough ${currency.title} on your balance.';
|
||||
|
||||
@override
|
||||
String toString() => exceptionMessage;
|
||||
}
|
25
cw_evm/lib/evm_chain_formatter.dart
Normal file
|
@ -0,0 +1,25 @@
|
|||
import 'package:intl/intl.dart';
|
||||
|
||||
const evmChainAmountLength = 12;
|
||||
const evmChainAmountDivider = 1000000000000;
|
||||
final evmChainAmountFormat = NumberFormat()
|
||||
..maximumFractionDigits = evmChainAmountLength
|
||||
..minimumFractionDigits = 1;
|
||||
|
||||
class EVMChainFormatter {
|
||||
static int parseEVMChainAmount(String amount) {
|
||||
try {
|
||||
return (double.parse(amount) * evmChainAmountDivider).round();
|
||||
} catch (_) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static double parseEVMChainAmountToDouble(int amount) {
|
||||
try {
|
||||
return amount / evmChainAmountDivider;
|
||||
} catch (_) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,4 @@
|
|||
class EthereumMnemonicIsIncorrectException implements Exception {
|
||||
@override
|
||||
String toString() =>
|
||||
'Ethereum mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.';
|
||||
}
|
||||
|
||||
class EthereumMnemonics {
|
||||
class EVMChainMnemonics {
|
||||
static const englishWordlist = <String>[
|
||||
'abandon',
|
||||
'ability',
|
|
@ -1,9 +1,9 @@
|
|||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/output_info.dart';
|
||||
import 'package:cw_ethereum/ethereum_transaction_priority.dart';
|
||||
import 'package:cw_evm/evm_chain_transaction_priority.dart';
|
||||
|
||||
class EthereumTransactionCredentials {
|
||||
EthereumTransactionCredentials(
|
||||
class EVMChainTransactionCredentials {
|
||||
EVMChainTransactionCredentials(
|
||||
this.outputs, {
|
||||
required this.priority,
|
||||
required this.currency,
|
||||
|
@ -11,7 +11,7 @@ class EthereumTransactionCredentials {
|
|||
});
|
||||
|
||||
final List<OutputInfo> outputs;
|
||||
final EthereumTransactionPriority? priority;
|
||||
final EVMChainTransactionPriority? priority;
|
||||
final int? feeRate;
|
||||
final CryptoCurrency currency;
|
||||
}
|
88
cw_evm/lib/evm_chain_transaction_history.dart
Normal file
|
@ -0,0 +1,88 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:core';
|
||||
import 'dart:developer';
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_evm/evm_chain_transaction_info.dart';
|
||||
import 'package:cw_evm/file.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:cw_core/transaction_history.dart';
|
||||
|
||||
part 'evm_chain_transaction_history.g.dart';
|
||||
|
||||
abstract class EVMChainTransactionHistory = EVMChainTransactionHistoryBase
|
||||
with _$EVMChainTransactionHistory;
|
||||
|
||||
abstract class EVMChainTransactionHistoryBase
|
||||
extends TransactionHistoryBase<EVMChainTransactionInfo> with Store {
|
||||
EVMChainTransactionHistoryBase({required this.walletInfo, required String password})
|
||||
: _password = password {
|
||||
transactions = ObservableMap<String, EVMChainTransactionInfo>();
|
||||
}
|
||||
|
||||
String _password;
|
||||
|
||||
final WalletInfo walletInfo;
|
||||
|
||||
//! Method to be overridden by all child classes
|
||||
|
||||
String getTransactionHistoryFileName();
|
||||
|
||||
EVMChainTransactionInfo getTransactionInfo(Map<String, dynamic> val);
|
||||
|
||||
//! Common methods across all child classes
|
||||
|
||||
Future<void> init() async => await _load();
|
||||
|
||||
@override
|
||||
Future<void> save() async {
|
||||
final transactionsHistoryFileNameForWallet = getTransactionHistoryFileName();
|
||||
try {
|
||||
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
|
||||
String path = '$dirPath/$transactionsHistoryFileNameForWallet';
|
||||
final data = json.encode({'transactions': transactions});
|
||||
await writeData(path: path, password: _password, data: data);
|
||||
} catch (e, s) {
|
||||
log('Error while saving ${walletInfo.type.name} transaction history: ${e.toString()}');
|
||||
log(s.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void addOne(EVMChainTransactionInfo transaction) => transactions[transaction.id] = transaction;
|
||||
|
||||
@override
|
||||
void addMany(Map<String, EVMChainTransactionInfo> transactions) =>
|
||||
this.transactions.addAll(transactions);
|
||||
|
||||
Future<Map<String, dynamic>> _read() async {
|
||||
final transactionsHistoryFileNameForWallet = getTransactionHistoryFileName();
|
||||
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
|
||||
String path = '$dirPath/$transactionsHistoryFileNameForWallet';
|
||||
final content = await read(path: path, password: _password);
|
||||
if (content.isEmpty) {
|
||||
return {};
|
||||
}
|
||||
return json.decode(content) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
try {
|
||||
final content = await _read();
|
||||
final txs = content['transactions'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
for (var entry in txs.entries) {
|
||||
final val = entry.value;
|
||||
|
||||
if (val is Map<String, dynamic>) {
|
||||
final tx = getTransactionInfo(val);
|
||||
_update(tx);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
void _update(EVMChainTransactionInfo transaction) => transactions[transaction.id] = transaction;
|
||||
}
|
77
cw_evm/lib/evm_chain_transaction_info.dart
Normal file
|
@ -0,0 +1,77 @@
|
|||
// ignore_for_file: overridden_fields, annotate_overrides
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:cw_core/format_amount.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
import 'package:cw_core/transaction_info.dart';
|
||||
|
||||
abstract class EVMChainTransactionInfo extends TransactionInfo {
|
||||
EVMChainTransactionInfo({
|
||||
required this.id,
|
||||
required this.height,
|
||||
required this.ethAmount,
|
||||
required this.ethFee,
|
||||
required this.tokenSymbol,
|
||||
this.exponent = 18,
|
||||
required this.direction,
|
||||
required this.isPending,
|
||||
required this.date,
|
||||
required this.confirmations,
|
||||
required this.to,
|
||||
required this.from,
|
||||
}) : amount = ethAmount.toInt(),
|
||||
fee = ethFee.toInt();
|
||||
|
||||
final String id;
|
||||
final int height;
|
||||
final int amount;
|
||||
final BigInt ethAmount;
|
||||
final int exponent;
|
||||
final TransactionDirection direction;
|
||||
final DateTime date;
|
||||
final bool isPending;
|
||||
final int fee;
|
||||
final BigInt ethFee;
|
||||
final int confirmations;
|
||||
final String tokenSymbol;
|
||||
String? _fiatAmount;
|
||||
final String? to;
|
||||
final String? from;
|
||||
|
||||
//! Getter to be overridden in child classes
|
||||
String get feeCurrency;
|
||||
|
||||
@override
|
||||
String amountFormatted() {
|
||||
final amount = formatAmount((ethAmount / BigInt.from(10).pow(exponent)).toString());
|
||||
return '${amount.substring(0, min(10, amount.length))} $tokenSymbol';
|
||||
}
|
||||
|
||||
@override
|
||||
String fiatAmount() => _fiatAmount ?? '';
|
||||
|
||||
@override
|
||||
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
|
||||
|
||||
@override
|
||||
String feeFormatted() {
|
||||
final amount = (ethFee / BigInt.from(10).pow(18)).toString();
|
||||
return '${amount.substring(0, min(10, amount.length))} $feeCurrency';
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'height': height,
|
||||
'amount': ethAmount.toString(),
|
||||
'exponent': exponent,
|
||||
'fee': ethFee.toString(),
|
||||
'direction': direction.index,
|
||||
'date': date.millisecondsSinceEpoch,
|
||||
'isPending': isPending,
|
||||
'confirmations': confirmations,
|
||||
'tokenSymbol': tokenSymbol,
|
||||
'to': to,
|
||||
'from': from,
|
||||
};
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
//! Model used for in parsing transactions fetched using etherscan
|
||||
class EthereumTransactionModel {
|
||||
class EVMChainTransactionModel {
|
||||
final DateTime date;
|
||||
final String hash;
|
||||
final String from;
|
||||
|
@ -14,7 +13,7 @@ class EthereumTransactionModel {
|
|||
final int? tokenDecimal;
|
||||
final bool isError;
|
||||
|
||||
EthereumTransactionModel({
|
||||
EVMChainTransactionModel({
|
||||
required this.date,
|
||||
required this.hash,
|
||||
required this.from,
|
||||
|
@ -30,7 +29,8 @@ class EthereumTransactionModel {
|
|||
required this.isError,
|
||||
});
|
||||
|
||||
factory EthereumTransactionModel.fromJson(Map<String, dynamic> json) => EthereumTransactionModel(
|
||||
factory EVMChainTransactionModel.fromJson(Map<String, dynamic> json, String defaultSymbol) =>
|
||||
EVMChainTransactionModel(
|
||||
date: DateTime.fromMillisecondsSinceEpoch(int.parse(json["timeStamp"]) * 1000),
|
||||
hash: json["hash"],
|
||||
from: json["from"],
|
||||
|
@ -41,7 +41,7 @@ class EthereumTransactionModel {
|
|||
contractAddress: json["contractAddress"],
|
||||
confirmations: int.parse(json["confirmations"]),
|
||||
blockNumber: int.parse(json["blockNumber"]),
|
||||
tokenSymbol: json["tokenSymbol"] ?? "ETH",
|
||||
tokenSymbol: json["tokenSymbol"] ?? defaultSymbol,
|
||||
tokenDecimal: int.tryParse(json["tokenDecimal"] ?? ""),
|
||||
isError: json["isError"] == "1",
|
||||
);
|
52
cw_evm/lib/evm_chain_transaction_priority.dart
Normal file
|
@ -0,0 +1,52 @@
|
|||
import 'package:cw_core/transaction_priority.dart';
|
||||
|
||||
class EVMChainTransactionPriority extends TransactionPriority {
|
||||
final int tip;
|
||||
|
||||
const EVMChainTransactionPriority({required String title, required int raw, required this.tip})
|
||||
: super(title: title, raw: raw);
|
||||
|
||||
static const List<EVMChainTransactionPriority> all = [fast, medium, slow];
|
||||
static const EVMChainTransactionPriority slow =
|
||||
EVMChainTransactionPriority(title: 'slow', raw: 0, tip: 1);
|
||||
static const EVMChainTransactionPriority medium =
|
||||
EVMChainTransactionPriority(title: 'Medium', raw: 1, tip: 2);
|
||||
static const EVMChainTransactionPriority fast =
|
||||
EVMChainTransactionPriority(title: 'Fast', raw: 2, tip: 4);
|
||||
|
||||
static EVMChainTransactionPriority deserialize({required int raw}) {
|
||||
switch (raw) {
|
||||
case 0:
|
||||
return slow;
|
||||
case 1:
|
||||
return medium;
|
||||
case 2:
|
||||
return fast;
|
||||
default:
|
||||
throw Exception('Unexpected token: $raw for EVMChainTransactionPriority deserialize');
|
||||
}
|
||||
}
|
||||
|
||||
String get units => 'gas';
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var label = '';
|
||||
|
||||
switch (this) {
|
||||
case EVMChainTransactionPriority.slow:
|
||||
label = 'Slow';
|
||||
break;
|
||||
case EVMChainTransactionPriority.medium:
|
||||
label = 'Medium';
|
||||
break;
|
||||
case EVMChainTransactionPriority.fast:
|
||||
label = 'Fast';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
}
|
512
cw_evm/lib/evm_chain_wallet.dart
Normal file
|
@ -0,0 +1,512 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:bip32/bip32.dart' as bip32;
|
||||
import 'package:bip39/bip39.dart' as bip39;
|
||||
import 'package:cw_core/cake_hive.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/erc20_token.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
import 'package:cw_core/transaction_priority.dart';
|
||||
import 'package:cw_core/wallet_addresses.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_client.dart';
|
||||
import 'package:cw_evm/evm_chain_exceptions.dart';
|
||||
import 'package:cw_evm/evm_chain_formatter.dart';
|
||||
import 'package:cw_evm/evm_chain_transaction_credentials.dart';
|
||||
import 'package:cw_evm/evm_chain_transaction_history.dart';
|
||||
import 'package:cw_evm/evm_chain_transaction_model.dart';
|
||||
import 'package:cw_evm/evm_chain_transaction_priority.dart';
|
||||
import 'package:cw_evm/evm_chain_wallet_addresses.dart';
|
||||
import 'package:cw_evm/file.dart';
|
||||
import 'package:hex/hex.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:web3dart/crypto.dart';
|
||||
import 'package:web3dart/web3dart.dart';
|
||||
|
||||
import 'evm_chain_transaction_info.dart';
|
||||
import 'evm_erc20_balance.dart';
|
||||
|
||||
part 'evm_chain_wallet.g.dart';
|
||||
|
||||
abstract class EVMChainWallet = EVMChainWalletBase with _$EVMChainWallet;
|
||||
|
||||
abstract class EVMChainWalletBase
|
||||
extends WalletBase<EVMChainERC20Balance, EVMChainTransactionHistory, EVMChainTransactionInfo>
|
||||
with Store {
|
||||
EVMChainWalletBase({
|
||||
required WalletInfo walletInfo,
|
||||
required EVMChainClient client,
|
||||
required CryptoCurrency nativeCurrency,
|
||||
String? mnemonic,
|
||||
String? privateKey,
|
||||
required String password,
|
||||
EVMChainERC20Balance? initialBalance,
|
||||
}) : syncStatus = const NotConnectedSyncStatus(),
|
||||
_password = password,
|
||||
_mnemonic = mnemonic,
|
||||
_hexPrivateKey = privateKey,
|
||||
_isTransactionUpdating = false,
|
||||
_client = client,
|
||||
walletAddresses = EVMChainWalletAddresses(walletInfo),
|
||||
balance = ObservableMap<CryptoCurrency, EVMChainERC20Balance>.of(
|
||||
{
|
||||
// Not sure of this yet, will it work? will it not?
|
||||
nativeCurrency: initialBalance ?? EVMChainERC20Balance(BigInt.zero),
|
||||
},
|
||||
),
|
||||
super(walletInfo) {
|
||||
this.walletInfo = walletInfo;
|
||||
transactionHistory = setUpTransactionHistory(walletInfo, password);
|
||||
|
||||
if (!CakeHive.isAdapterRegistered(Erc20Token.typeId)) {
|
||||
CakeHive.registerAdapter(Erc20TokenAdapter());
|
||||
}
|
||||
|
||||
sharedPrefs.complete(SharedPreferences.getInstance());
|
||||
}
|
||||
|
||||
final String? _mnemonic;
|
||||
final String? _hexPrivateKey;
|
||||
final String _password;
|
||||
|
||||
late final Box<Erc20Token> erc20TokensBox;
|
||||
|
||||
late final Box<Erc20Token> evmChainErc20TokensBox;
|
||||
|
||||
late final EthPrivateKey _evmChainPrivateKey;
|
||||
|
||||
EthPrivateKey get evmChainPrivateKey => _evmChainPrivateKey;
|
||||
|
||||
late EVMChainClient _client;
|
||||
|
||||
int? _gasPrice;
|
||||
int? _estimatedGas;
|
||||
bool _isTransactionUpdating;
|
||||
|
||||
// TODO: remove after integrating our own node and having eth_newPendingTransactionFilter
|
||||
Timer? _transactionsUpdateTimer;
|
||||
|
||||
@override
|
||||
WalletAddresses walletAddresses;
|
||||
|
||||
@override
|
||||
@observable
|
||||
SyncStatus syncStatus;
|
||||
|
||||
@override
|
||||
@observable
|
||||
late ObservableMap<CryptoCurrency, EVMChainERC20Balance> balance;
|
||||
|
||||
Completer<SharedPreferences> sharedPrefs = Completer();
|
||||
|
||||
//! Methods to be overridden by every child
|
||||
|
||||
void addInitialTokens();
|
||||
|
||||
// Future<EVMChainWallet> open({
|
||||
// required String name,
|
||||
// required String password,
|
||||
// required WalletInfo walletInfo,
|
||||
// });
|
||||
|
||||
Future<void> initErc20TokensBox();
|
||||
|
||||
String getTransactionHistoryFileName();
|
||||
|
||||
Future<bool> checkIfScanProviderIsEnabled();
|
||||
|
||||
EVMChainTransactionInfo getTransactionInfo(
|
||||
EVMChainTransactionModel transactionModel, String address);
|
||||
|
||||
Erc20Token createNewErc20TokenObject(Erc20Token token, String? iconPath);
|
||||
|
||||
EVMChainTransactionHistory setUpTransactionHistory(WalletInfo walletInfo, String password);
|
||||
|
||||
//! Common Methods across child classes
|
||||
|
||||
String idFor(String name, WalletType type) => '${walletTypeToString(type).toLowerCase()}_$name';
|
||||
|
||||
Future<void> init() async {
|
||||
await initErc20TokensBox();
|
||||
|
||||
await walletAddresses.init();
|
||||
await transactionHistory.init();
|
||||
_evmChainPrivateKey = await getPrivateKey(
|
||||
mnemonic: _mnemonic,
|
||||
privateKey: _hexPrivateKey,
|
||||
password: _password,
|
||||
);
|
||||
walletAddresses.address = _evmChainPrivateKey.address.toString();
|
||||
await save();
|
||||
}
|
||||
|
||||
@override
|
||||
int calculateEstimatedFee(TransactionPriority priority, int? amount) {
|
||||
try {
|
||||
if (priority is EVMChainTransactionPriority) {
|
||||
final priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt();
|
||||
return (_gasPrice! + priorityFee) * (_estimatedGas ?? 0);
|
||||
}
|
||||
|
||||
return 0;
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> changePassword(String password) {
|
||||
throw UnimplementedError("changePassword");
|
||||
}
|
||||
|
||||
@override
|
||||
void close() {
|
||||
_client.stop();
|
||||
_transactionsUpdateTimer?.cancel();
|
||||
}
|
||||
|
||||
@action
|
||||
@override
|
||||
Future<void> connectToNode({required Node node}) async {
|
||||
try {
|
||||
syncStatus = ConnectingSyncStatus();
|
||||
|
||||
final isConnected = _client.connect(node);
|
||||
|
||||
if (!isConnected) {
|
||||
throw Exception("${walletInfo.type.name.toUpperCase()} Node connection failed");
|
||||
}
|
||||
|
||||
_client.setListeners(_evmChainPrivateKey.address, _onNewTransaction);
|
||||
|
||||
_setTransactionUpdateTimer();
|
||||
|
||||
syncStatus = ConnectedSyncStatus();
|
||||
} catch (e) {
|
||||
syncStatus = FailedSyncStatus();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
@override
|
||||
Future<void> startSync() async {
|
||||
try {
|
||||
syncStatus = AttemptingSyncStatus();
|
||||
await _updateBalance();
|
||||
await _updateTransactions();
|
||||
_gasPrice = await _client.getGasUnitPrice();
|
||||
_estimatedGas = await _client.getEstimatedGas();
|
||||
|
||||
Timer.periodic(
|
||||
const Duration(minutes: 1), (timer) async => _gasPrice = await _client.getGasUnitPrice());
|
||||
Timer.periodic(const Duration(seconds: 10),
|
||||
(timer) async => _estimatedGas = await _client.getEstimatedGas());
|
||||
|
||||
syncStatus = SyncedSyncStatus();
|
||||
} catch (e) {
|
||||
syncStatus = FailedSyncStatus();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PendingTransaction> createTransaction(Object credentials) async {
|
||||
final _credentials = credentials as EVMChainTransactionCredentials;
|
||||
final outputs = _credentials.outputs;
|
||||
final hasMultiDestination = outputs.length > 1;
|
||||
|
||||
final CryptoCurrency transactionCurrency =
|
||||
balance.keys.firstWhere((element) => element.title == _credentials.currency.title);
|
||||
|
||||
final _erc20Balance = balance[transactionCurrency]!;
|
||||
BigInt totalAmount = BigInt.zero;
|
||||
int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18;
|
||||
num amountToEVMChainMultiplier = pow(10, exponent);
|
||||
|
||||
// so far this can not be made with Ethereum as Ethereum does not support multiple recipients
|
||||
if (hasMultiDestination) {
|
||||
if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
|
||||
throw EVMChainTransactionCreationException(transactionCurrency);
|
||||
}
|
||||
|
||||
final totalOriginalAmount = EVMChainFormatter.parseEVMChainAmountToDouble(
|
||||
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)));
|
||||
totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
|
||||
|
||||
if (_erc20Balance.balance < totalAmount) {
|
||||
throw EVMChainTransactionCreationException(transactionCurrency);
|
||||
}
|
||||
} else {
|
||||
final output = outputs.first;
|
||||
// since the fees are taken from Ethereum
|
||||
// then no need to subtract the fees from the amount if send all
|
||||
final BigInt allAmount;
|
||||
if (transactionCurrency is Erc20Token) {
|
||||
allAmount = _erc20Balance.balance;
|
||||
} else {
|
||||
allAmount = _erc20Balance.balance -
|
||||
BigInt.from(calculateEstimatedFee(_credentials.priority!, null));
|
||||
}
|
||||
final totalOriginalAmount =
|
||||
EVMChainFormatter.parseEVMChainAmountToDouble(output.formattedCryptoAmount ?? 0);
|
||||
totalAmount = output.sendAll
|
||||
? allAmount
|
||||
: BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
|
||||
|
||||
if (_erc20Balance.balance < totalAmount) {
|
||||
throw EVMChainTransactionCreationException(transactionCurrency);
|
||||
}
|
||||
}
|
||||
|
||||
final pendingEVMChainTransaction = await _client.signTransaction(
|
||||
privateKey: _evmChainPrivateKey,
|
||||
toAddress: _credentials.outputs.first.isParsedAddress
|
||||
? _credentials.outputs.first.extractedAddress!
|
||||
: _credentials.outputs.first.address,
|
||||
amount: totalAmount.toString(),
|
||||
gas: _estimatedGas!,
|
||||
priority: _credentials.priority!,
|
||||
currency: transactionCurrency,
|
||||
exponent: exponent,
|
||||
contractAddress:
|
||||
transactionCurrency is Erc20Token ? transactionCurrency.contractAddress : null,
|
||||
);
|
||||
|
||||
return pendingEVMChainTransaction;
|
||||
}
|
||||
|
||||
Future<void> _updateTransactions() async {
|
||||
try {
|
||||
if (_isTransactionUpdating) {
|
||||
return;
|
||||
}
|
||||
|
||||
final isProviderEnabled = await checkIfScanProviderIsEnabled();
|
||||
|
||||
if (!isProviderEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isTransactionUpdating = true;
|
||||
final transactions = await fetchTransactions();
|
||||
transactionHistory.addMany(transactions);
|
||||
await transactionHistory.save();
|
||||
_isTransactionUpdating = false;
|
||||
} catch (_) {
|
||||
_isTransactionUpdating = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, EVMChainTransactionInfo>> fetchTransactions() async {
|
||||
final address = _evmChainPrivateKey.address.hex;
|
||||
final transactions = await _client.fetchTransactions(address);
|
||||
|
||||
final List<Future<List<EVMChainTransactionModel>>> erc20TokensTransactions = [];
|
||||
|
||||
for (var token in balance.keys) {
|
||||
if (token is Erc20Token) {
|
||||
erc20TokensTransactions.add(_client.fetchTransactions(
|
||||
address,
|
||||
contractAddress: token.contractAddress,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
final tokensTransaction = await Future.wait(erc20TokensTransactions);
|
||||
transactions.addAll(tokensTransaction.expand((element) => element));
|
||||
|
||||
final Map<String, EVMChainTransactionInfo> result = {};
|
||||
|
||||
for (var transactionModel in transactions) {
|
||||
if (transactionModel.isError) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result[transactionModel.hash] = getTransactionInfo(transactionModel, address);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Object get keys => throw UnimplementedError("keys");
|
||||
|
||||
@override
|
||||
Future<void> rescan({required int height}) {
|
||||
throw UnimplementedError("rescan");
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save() async {
|
||||
await walletAddresses.updateAddressesInBox();
|
||||
final path = await makePath();
|
||||
await write(path: path, password: _password, data: toJSON());
|
||||
await transactionHistory.save();
|
||||
}
|
||||
|
||||
@override
|
||||
String? get seed => _mnemonic;
|
||||
|
||||
@override
|
||||
String get privateKey => HEX.encode(_evmChainPrivateKey.privateKey);
|
||||
|
||||
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
|
||||
|
||||
String toJSON() => json.encode({
|
||||
'mnemonic': _mnemonic,
|
||||
'private_key': privateKey,
|
||||
'balance': balance[currency]!.toJSON(),
|
||||
});
|
||||
|
||||
Future<void> _updateBalance() async {
|
||||
balance[currency] = await _fetchEVMChainBalance();
|
||||
|
||||
await _fetchErc20Balances();
|
||||
await save();
|
||||
}
|
||||
|
||||
Future<EVMChainERC20Balance> _fetchEVMChainBalance() async {
|
||||
final balance = await _client.getBalance(_evmChainPrivateKey.address);
|
||||
return EVMChainERC20Balance(balance.getInWei);
|
||||
}
|
||||
|
||||
Future<void> _fetchErc20Balances() async {
|
||||
for (var token in evmChainErc20TokensBox.values) {
|
||||
try {
|
||||
if (token.enabled) {
|
||||
balance[token] = await _client.fetchERC20Balances(
|
||||
_evmChainPrivateKey.address,
|
||||
token.contractAddress,
|
||||
);
|
||||
} else {
|
||||
balance.remove(token);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
Future<EthPrivateKey> getPrivateKey(
|
||||
{String? mnemonic, String? privateKey, required String password}) async {
|
||||
assert(mnemonic != null || privateKey != null);
|
||||
|
||||
if (privateKey != null) {
|
||||
return EthPrivateKey.fromHex(privateKey);
|
||||
}
|
||||
|
||||
final seed = bip39.mnemonicToSeed(mnemonic!);
|
||||
|
||||
final root = bip32.BIP32.fromSeed(seed);
|
||||
|
||||
const hdPathEVMChain = "m/44'/60'/0'/0";
|
||||
const index = 0;
|
||||
final addressAtIndex = root.derivePath("$hdPathEVMChain/$index");
|
||||
|
||||
return EthPrivateKey.fromHex(HEX.encode(addressAtIndex.privateKey as List<int>));
|
||||
}
|
||||
|
||||
Future<void>? updateBalance() async => await _updateBalance();
|
||||
|
||||
List<Erc20Token> get erc20Currencies => evmChainErc20TokensBox.values.toList();
|
||||
|
||||
Future<void> addErc20Token(Erc20Token token) async {
|
||||
String? iconPath;
|
||||
try {
|
||||
iconPath = CryptoCurrency.all
|
||||
.firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase())
|
||||
.iconPath;
|
||||
} catch (_) {}
|
||||
|
||||
final newToken = createNewErc20TokenObject(token, iconPath);
|
||||
|
||||
await evmChainErc20TokensBox.put(newToken.contractAddress, newToken);
|
||||
|
||||
if (newToken.enabled) {
|
||||
balance[newToken] = await _client.fetchERC20Balances(
|
||||
_evmChainPrivateKey.address,
|
||||
newToken.contractAddress,
|
||||
);
|
||||
} else {
|
||||
balance.remove(newToken);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteErc20Token(Erc20Token token) async {
|
||||
await token.delete();
|
||||
|
||||
balance.remove(token);
|
||||
_updateBalance();
|
||||
}
|
||||
|
||||
Future<Erc20Token?> getErc20Token(String contractAddress) async =>
|
||||
await _client.getErc20Token(contractAddress);
|
||||
|
||||
void _onNewTransaction() {
|
||||
_updateBalance();
|
||||
_updateTransactions();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> renameWalletFiles(String newWalletName) async {
|
||||
final transactionHistoryFileNameForWallet = getTransactionHistoryFileName();
|
||||
|
||||
final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type);
|
||||
final currentWalletFile = File(currentWalletPath);
|
||||
|
||||
final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type);
|
||||
final currentTransactionsFile = File('$currentDirPath/$transactionHistoryFileNameForWallet');
|
||||
|
||||
// Copies current wallet files into new wallet name's dir and files
|
||||
if (currentWalletFile.existsSync()) {
|
||||
final newWalletPath = await pathForWallet(name: newWalletName, type: type);
|
||||
await currentWalletFile.copy(newWalletPath);
|
||||
}
|
||||
if (currentTransactionsFile.existsSync()) {
|
||||
final newDirPath = await pathForWalletDir(name: newWalletName, type: type);
|
||||
await currentTransactionsFile.copy('$newDirPath/$transactionHistoryFileNameForWallet');
|
||||
}
|
||||
|
||||
// Delete old name's dir and files
|
||||
await Directory(currentDirPath).delete(recursive: true);
|
||||
}
|
||||
|
||||
void _setTransactionUpdateTimer() {
|
||||
if (_transactionsUpdateTimer?.isActive ?? false) {
|
||||
_transactionsUpdateTimer!.cancel();
|
||||
}
|
||||
|
||||
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 10), (_) {
|
||||
_updateTransactions();
|
||||
_updateBalance();
|
||||
});
|
||||
}
|
||||
|
||||
/// Scan Providers:
|
||||
///
|
||||
/// EtherScan for Ethereum.
|
||||
///
|
||||
/// PolygonScan for Polygon.
|
||||
void updateScanProviderUsageState(bool isEnabled) {
|
||||
if (isEnabled) {
|
||||
_updateTransactions();
|
||||
_setTransactionUpdateTimer();
|
||||
} else {
|
||||
_transactionsUpdateTimer?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String signMessage(String message, {String? address}) =>
|
||||
bytesToHex(_evmChainPrivateKey.signPersonalMessageToUint8List(ascii.encode(message)));
|
||||
|
||||
Web3Client? getWeb3Client() => _client.getWeb3Client();
|
||||
}
|
|
@ -1,13 +1,15 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:cw_core/wallet_addresses.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
part 'ethereum_wallet_addresses.g.dart';
|
||||
part 'evm_chain_wallet_addresses.g.dart';
|
||||
|
||||
class EthereumWalletAddresses = EthereumWalletAddressesBase with _$EthereumWalletAddresses;
|
||||
class EVMChainWalletAddresses = EVMChainWalletAddressesBase with _$EVMChainWalletAddresses;
|
||||
|
||||
abstract class EthereumWalletAddressesBase extends WalletAddresses with Store {
|
||||
EthereumWalletAddressesBase(WalletInfo walletInfo)
|
||||
abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store {
|
||||
EVMChainWalletAddressesBase(WalletInfo walletInfo)
|
||||
: address = '',
|
||||
super(walletInfo);
|
||||
|
||||
|
@ -27,7 +29,7 @@ abstract class EthereumWalletAddressesBase extends WalletAddresses with Store {
|
|||
addressesMap[address] = '';
|
||||
await saveAddressesInBox();
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
29
cw_evm/lib/evm_chain_wallet_creation_credentials.dart
Normal file
|
@ -0,0 +1,29 @@
|
|||
import 'package:cw_core/wallet_credentials.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
|
||||
class EVMChainNewWalletCredentials extends WalletCredentials {
|
||||
EVMChainNewWalletCredentials({required String name, WalletInfo? walletInfo})
|
||||
: super(name: name, walletInfo: walletInfo);
|
||||
}
|
||||
|
||||
class EVMChainRestoreWalletFromSeedCredentials extends WalletCredentials {
|
||||
EVMChainRestoreWalletFromSeedCredentials({
|
||||
required String name,
|
||||
required String password,
|
||||
required this.mnemonic,
|
||||
WalletInfo? walletInfo,
|
||||
}) : super(name: name, password: password, walletInfo: walletInfo);
|
||||
|
||||
final String mnemonic;
|
||||
}
|
||||
|
||||
class EVMChainRestoreWalletFromPrivateKey extends WalletCredentials {
|
||||
EVMChainRestoreWalletFromPrivateKey({
|
||||
required String name,
|
||||
required String password,
|
||||
required this.privateKey,
|
||||
WalletInfo? walletInfo,
|
||||
}) : super(name: name, password: password, walletInfo: walletInfo);
|
||||
|
||||
final String privateKey;
|
||||
}
|
50
cw_evm/lib/evm_chain_wallet_service.dart
Normal file
|
@ -0,0 +1,50 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_service.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cw_evm/evm_chain_wallet.dart';
|
||||
import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
abstract class EVMChainWalletService<T extends EVMChainWallet> extends WalletService<
|
||||
EVMChainNewWalletCredentials,
|
||||
EVMChainRestoreWalletFromSeedCredentials,
|
||||
EVMChainRestoreWalletFromPrivateKey> {
|
||||
EVMChainWalletService(this.walletInfoSource);
|
||||
|
||||
final Box<WalletInfo> walletInfoSource;
|
||||
|
||||
@override
|
||||
WalletType getType();
|
||||
|
||||
@override
|
||||
Future<T> create(EVMChainNewWalletCredentials credentials);
|
||||
|
||||
@override
|
||||
Future<T> openWallet(String name, String password);
|
||||
|
||||
@override
|
||||
Future<void> rename(String currentName, String password, String newName);
|
||||
|
||||
@override
|
||||
Future<T> restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials);
|
||||
|
||||
@override
|
||||
Future<T> restoreFromSeed(EVMChainRestoreWalletFromSeedCredentials credentials);
|
||||
|
||||
@override
|
||||
Future<bool> isWalletExit(String name) async =>
|
||||
File(await pathForWallet(name: name, type: getType())).existsSync();
|
||||
|
||||
@override
|
||||
Future<void> remove(String wallet) async {
|
||||
File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true);
|
||||
final walletInfo = walletInfoSource.values
|
||||
.firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!;
|
||||
await walletInfoSource.delete(walletInfo.key);
|
||||
}
|
||||
}
|
|
@ -3,10 +3,9 @@ import 'dart:math';
|
|||
|
||||
import 'package:cw_core/balance.dart';
|
||||
|
||||
class ERC20Balance extends Balance {
|
||||
ERC20Balance(this.balance, {this.exponent = 18})
|
||||
: super(balance.toInt(),
|
||||
balance.toInt());
|
||||
class EVMChainERC20Balance extends Balance {
|
||||
EVMChainERC20Balance(this.balance, {this.exponent = 18})
|
||||
: super(balance.toInt(), balance.toInt());
|
||||
|
||||
final BigInt balance;
|
||||
final int exponent;
|
||||
|
@ -28,7 +27,7 @@ class ERC20Balance extends Balance {
|
|||
'exponent': exponent,
|
||||
});
|
||||
|
||||
static ERC20Balance? fromJSON(String? jsonSource) {
|
||||
static EVMChainERC20Balance? fromJSON(String? jsonSource) {
|
||||
if (jsonSource == null) {
|
||||
return null;
|
||||
}
|
||||
|
@ -36,12 +35,12 @@ class ERC20Balance extends Balance {
|
|||
final decoded = json.decode(jsonSource) as Map;
|
||||
|
||||
try {
|
||||
return ERC20Balance(
|
||||
return EVMChainERC20Balance(
|
||||
BigInt.parse(decoded['balanceInWei']),
|
||||
exponent: decoded['exponent'],
|
||||
);
|
||||
} catch (e) {
|
||||
return ERC20Balance(BigInt.zero);
|
||||
return EVMChainERC20Balance(BigInt.zero);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,14 +4,14 @@ import 'dart:typed_data';
|
|||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:web3dart/crypto.dart';
|
||||
|
||||
class PendingEthereumTransaction with PendingTransaction {
|
||||
class PendingEVMChainTransaction with PendingTransaction {
|
||||
final Function sendTransaction;
|
||||
final Uint8List signedTransaction;
|
||||
final BigInt fee;
|
||||
final String amount;
|
||||
final int exponent;
|
||||
|
||||
PendingEthereumTransaction({
|
||||
PendingEVMChainTransaction({
|
||||
required this.sendTransaction,
|
||||
required this.signedTransaction,
|
||||
required this.fee,
|
45
cw_evm/pubspec.yaml
Normal file
|
@ -0,0 +1,45 @@
|
|||
name: cw_evm
|
||||
description: A new Flutter package project.
|
||||
version: 0.0.1
|
||||
publish_to: none
|
||||
author: Cake Wallet
|
||||
homepage: https://cakewallet.com
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.6 <4.0.0'
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
web3dart: ^2.7.1
|
||||
erc20: ^1.0.1
|
||||
bip39: ^1.0.6
|
||||
bip32: ^2.0.0
|
||||
hex: ^0.2.0
|
||||
http: ^1.1.0
|
||||
hive: ^2.2.3
|
||||
collection: ^1.17.1
|
||||
shared_preferences: ^2.0.15
|
||||
cw_core:
|
||||
path: ../cw_core
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
build_runner: ^2.1.11
|
||||
mobx_codegen: ^2.0.7
|
||||
hive_generator: ^1.1.3
|
||||
flutter_lints: ^2.0.0
|
||||
|
||||
flutter:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
#
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
12
cw_evm/test/cw_evm_test.dart
Normal file
|
@ -0,0 +1,12 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:cw_evm/cw_evm.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);
|
||||
});
|
||||
}
|
|
@ -1,20 +1,19 @@
|
|||
import 'package:cw_core/balance.dart';
|
||||
import 'package:cw_nano/nano_util.dart';
|
||||
import 'package:nanoutil/nanoutil.dart';
|
||||
|
||||
class BananoBalance extends Balance {
|
||||
final BigInt currentBalance;
|
||||
final BigInt receivableBalance;
|
||||
|
||||
BananoBalance({required this.currentBalance, required this.receivableBalance}) : super(0, 0) {
|
||||
}
|
||||
BananoBalance({required this.currentBalance, required this.receivableBalance}) : super(0, 0);
|
||||
|
||||
@override
|
||||
String get formattedAvailableBalance {
|
||||
return NanoUtil.getRawAsUsableString(currentBalance.toString(), NanoUtil.rawPerBanano);
|
||||
return NanoAmounts.getRawAsUsableString(currentBalance.toString(), NanoAmounts.rawPerBanano);
|
||||
}
|
||||
|
||||
@override
|
||||
String get formattedAdditionalBalance {
|
||||
return NanoUtil.getRawAsUsableString(receivableBalance.toString(), NanoUtil.rawPerBanano);
|
||||
return NanoAmounts.getRawAsUsableString(receivableBalance.toString(), NanoAmounts.rawPerBanano);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,34 +1,35 @@
|
|||
import 'package:cw_core/balance.dart';
|
||||
import 'package:cw_nano/nano_util.dart';
|
||||
import 'package:nanoutil/nanoutil.dart';
|
||||
|
||||
BigInt stringAmountToBigInt(String amount) {
|
||||
return BigInt.parse(NanoUtil.getAmountAsRaw(amount, NanoUtil.rawPerNano));
|
||||
return BigInt.parse(NanoAmounts.getAmountAsRaw(amount, NanoAmounts.rawPerNano));
|
||||
}
|
||||
|
||||
class NanoBalance extends Balance {
|
||||
final BigInt currentBalance;
|
||||
final BigInt receivableBalance;
|
||||
late String formattedCurrentBalance;
|
||||
late String formattedReceivableBalance;
|
||||
|
||||
NanoBalance({required this.currentBalance, required this.receivableBalance}) : super(0, 0) {
|
||||
this.formattedCurrentBalance = "";
|
||||
this.formattedReceivableBalance = "";
|
||||
}
|
||||
NanoBalance({required this.currentBalance, required this.receivableBalance}) : super(0, 0);
|
||||
|
||||
NanoBalance.fromString(
|
||||
{required this.formattedCurrentBalance, required this.formattedReceivableBalance})
|
||||
NanoBalance.fromFormattedString(
|
||||
{required String formattedCurrentBalance, required String formattedReceivableBalance})
|
||||
: currentBalance = stringAmountToBigInt(formattedCurrentBalance),
|
||||
receivableBalance = stringAmountToBigInt(formattedReceivableBalance),
|
||||
super(0, 0);
|
||||
|
||||
NanoBalance.fromRawString(
|
||||
{required String currentBalance, required String receivableBalance})
|
||||
: currentBalance = BigInt.parse(currentBalance),
|
||||
receivableBalance = BigInt.parse(receivableBalance),
|
||||
super(0, 0);
|
||||
|
||||
@override
|
||||
String get formattedAvailableBalance {
|
||||
return NanoUtil.getRawAsUsableString(currentBalance.toString(), NanoUtil.rawPerNano);
|
||||
return NanoAmounts.getRawAsUsableString(currentBalance.toString(), NanoAmounts.rawPerNano);
|
||||
}
|
||||
|
||||
@override
|
||||
String get formattedAdditionalBalance {
|
||||
return NanoUtil.getRawAsUsableString(receivableBalance.toString(), NanoUtil.rawPerNano);
|
||||
return NanoAmounts.getRawAsUsableString(receivableBalance.toString(), NanoAmounts.rawPerNano);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,10 +4,10 @@ import 'dart:convert';
|
|||
import 'package:cw_core/nano_account_info_response.dart';
|
||||
import 'package:cw_nano/nano_balance.dart';
|
||||
import 'package:cw_nano/nano_transaction_model.dart';
|
||||
import 'package:cw_nano/nano_util.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:nanodart/nanodart.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:nanoutil/nanoutil.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class NanoClient {
|
||||
|
@ -61,6 +61,13 @@ class NanoClient {
|
|||
),
|
||||
);
|
||||
final data = await jsonDecode(response.body);
|
||||
if (response.statusCode != 200 ||
|
||||
data["error"] != null ||
|
||||
data["balance"] == null ||
|
||||
data["receivable"] == null) {
|
||||
throw Exception(
|
||||
"Error while trying to get balance! ${data["error"] != null ? data["error"] : ""}");
|
||||
}
|
||||
final String currentBalance = data["balance"] as String;
|
||||
final String receivableBalance = data["receivable"] as String;
|
||||
final BigInt cur = BigInt.parse(currentBalance);
|
||||
|
@ -203,7 +210,7 @@ class NanoClient {
|
|||
String? previousHash,
|
||||
}) async {
|
||||
// our address:
|
||||
final String publicAddress = NanoUtil.privateKeyToAddress(privateKey);
|
||||
final String publicAddress = NanoDerivations.privateKeyToAddress(privateKey);
|
||||
|
||||
// first get the current account balance:
|
||||
if (balanceAfterTx == null) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:cw_core/format_amount.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
import 'package:cw_core/transaction_info.dart';
|
||||
import 'package:cw_nano/nano_util.dart';
|
||||
import 'package:nanoutil/nanoutil.dart';
|
||||
|
||||
class NanoTransactionInfo extends TransactionInfo {
|
||||
NanoTransactionInfo({
|
||||
|
@ -13,6 +13,8 @@ class NanoTransactionInfo extends TransactionInfo {
|
|||
required this.confirmed,
|
||||
required this.date,
|
||||
required this.confirmations,
|
||||
required this.to,
|
||||
required this.from,
|
||||
}) : this.amount = amountRaw.toInt();
|
||||
|
||||
final String id;
|
||||
|
@ -24,14 +26,17 @@ class NanoTransactionInfo extends TransactionInfo {
|
|||
final bool confirmed;
|
||||
final int confirmations;
|
||||
final String tokenSymbol;
|
||||
final String? to;
|
||||
final String? from;
|
||||
String? _fiatAmount;
|
||||
|
||||
bool get isPending => !this.confirmed;
|
||||
|
||||
@override
|
||||
String amountFormatted() {
|
||||
final String amt = NanoUtil.getRawAsUsableString(amountRaw.toString(), NanoUtil.rawPerNano);
|
||||
final String acc = NanoUtil.getRawAccuracy(amountRaw.toString(), NanoUtil.rawPerNano);
|
||||
final String amt =
|
||||
NanoAmounts.getRawAsUsableString(amountRaw.toString(), NanoAmounts.rawPerNano);
|
||||
final String acc = NanoAmounts.getRawAccuracy(amountRaw.toString(), NanoAmounts.rawPerNano);
|
||||
return "$acc$amt $tokenSymbol";
|
||||
}
|
||||
|
||||
|
@ -54,6 +59,8 @@ class NanoTransactionInfo extends TransactionInfo {
|
|||
confirmed: data['confirmed'] as bool,
|
||||
confirmations: data['confirmations'] as int,
|
||||
tokenSymbol: data['tokenSymbol'] as String,
|
||||
to: data['to'] as String,
|
||||
from: data['from'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -66,5 +73,7 @@ class NanoTransactionInfo extends TransactionInfo {
|
|||
'confirmed': confirmed,
|
||||
'confirmations': confirmations,
|
||||
'tokenSymbol': tokenSymbol,
|
||||
'to': to,
|
||||
'from': from,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,193 +0,0 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:convert/convert.dart';
|
||||
import "package:ed25519_hd_key/ed25519_hd_key.dart";
|
||||
import 'package:libcrypto/libcrypto.dart';
|
||||
import 'package:nanodart/nanodart.dart';
|
||||
import 'package:decimal/decimal.dart';
|
||||
|
||||
class NanoUtil {
|
||||
// standard:
|
||||
static String seedToPrivate(String seed, int index) {
|
||||
return NanoKeys.seedToPrivate(seed, index);
|
||||
}
|
||||
|
||||
static String seedToAddress(String seed, int index) {
|
||||
return NanoAccounts.createAccount(
|
||||
NanoAccountType.NANO, privateKeyToPublic(seedToPrivate(seed, index)));
|
||||
}
|
||||
|
||||
static String seedToMnemonic(String seed) {
|
||||
return NanoMnemomics.seedToMnemonic(seed).join(" ");
|
||||
}
|
||||
|
||||
static Future<String> mnemonicToSeed(String mnemonic) async {
|
||||
return NanoMnemomics.mnemonicListToSeed(mnemonic.split(' '));
|
||||
}
|
||||
|
||||
static String privateKeyToPublic(String privateKey) {
|
||||
// return NanoHelpers.byteToHex(Ed25519Blake2b.getPubkey(NanoHelpers.hexToBytes(privateKey))!);
|
||||
return NanoKeys.createPublicKey(privateKey);
|
||||
}
|
||||
|
||||
static String addressToPublicKey(String publicAddress) {
|
||||
return NanoAccounts.extractPublicKey(publicAddress);
|
||||
}
|
||||
|
||||
// universal:
|
||||
static String privateKeyToAddress(String privateKey) {
|
||||
return NanoAccounts.createAccount(NanoAccountType.NANO, privateKeyToPublic(privateKey));
|
||||
}
|
||||
|
||||
static String publicKeyToAddress(String publicKey) {
|
||||
return NanoAccounts.createAccount(NanoAccountType.NANO, publicKey);
|
||||
}
|
||||
|
||||
// standard + hd:
|
||||
static bool isValidSeed(String seed) {
|
||||
// Ensure seed is 64 or 128 characters long
|
||||
if (seed == null || (seed.length != 64 && seed.length != 128)) {
|
||||
return false;
|
||||
}
|
||||
// Ensure seed only contains hex characters, 0-9;A-F
|
||||
return NanoHelpers.isHexString(seed);
|
||||
}
|
||||
|
||||
// // hd:
|
||||
static Future<String> hdMnemonicListToSeed(List<String> words) async {
|
||||
// if (words.length != 24) {
|
||||
// throw Exception('Expected a 24-word list, got a ${words.length} list');
|
||||
// }
|
||||
final Uint8List salt = Uint8List.fromList(utf8.encode('mnemonic'));
|
||||
final Pbkdf2 hasher = Pbkdf2(iterations: 2048);
|
||||
final String seed = await hasher.sha512(words.join(' '), salt);
|
||||
return seed;
|
||||
}
|
||||
|
||||
static Future<String> hdSeedToPrivate(String seed, int index) async {
|
||||
List<int> seedBytes = hex.decode(seed);
|
||||
KeyData data = await ED25519_HD_KEY.derivePath("m/44'/165'/$index'", seedBytes);
|
||||
return hex.encode(data.key);
|
||||
}
|
||||
|
||||
static Future<String> hdSeedToAddress(String seed, int index) async {
|
||||
return NanoAccounts.createAccount(
|
||||
NanoAccountType.NANO, privateKeyToPublic(await hdSeedToPrivate(seed, index)));
|
||||
}
|
||||
|
||||
static Future<String> uniSeedToAddress(String seed, int index, String type) {
|
||||
if (type == "standard") {
|
||||
return Future<String>.value(seedToAddress(seed, index));
|
||||
} else if (type == "hd") {
|
||||
return hdSeedToAddress(seed, index);
|
||||
} else {
|
||||
throw Exception('Unknown seed type');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<String> uniSeedToPrivate(String seed, int index, String type) {
|
||||
if (type == "standard") {
|
||||
return Future<String>.value(seedToPrivate(seed, index));
|
||||
} else if (type == "hd") {
|
||||
return hdSeedToPrivate(seed, index);
|
||||
} else {
|
||||
throw Exception('Unknown seed type');
|
||||
}
|
||||
}
|
||||
|
||||
static bool isValidBip39Seed(String seed) {
|
||||
// Ensure seed is 128 characters long
|
||||
if (seed.length != 128) {
|
||||
return false;
|
||||
}
|
||||
// Ensure seed only contains hex characters, 0-9;A-F
|
||||
return NanoHelpers.isHexString(seed);
|
||||
}
|
||||
|
||||
// number util:
|
||||
|
||||
static const int maxDecimalDigits = 6; // Max digits after decimal
|
||||
static BigInt rawPerNano = BigInt.parse("1000000000000000000000000000000");
|
||||
static BigInt rawPerNyano = BigInt.parse("1000000000000000000000000");
|
||||
static BigInt rawPerBanano = BigInt.parse("100000000000000000000000000000");
|
||||
static BigInt rawPerXMR = BigInt.parse("1000000000000");
|
||||
static BigInt convertXMRtoNano = BigInt.parse("1000000000000000000");
|
||||
// static BigInt convertXMRtoNano = BigInt.parse("1000000000000000000000000000");
|
||||
|
||||
/// Convert raw to ban and return as BigDecimal
|
||||
///
|
||||
/// @param raw 100000000000000000000000000000
|
||||
/// @return Decimal value 1.000000000000000000000000000000
|
||||
///
|
||||
static Decimal getRawAsDecimal(String? raw, BigInt? rawPerCur) {
|
||||
rawPerCur ??= rawPerNano;
|
||||
final Decimal amount = Decimal.parse(raw.toString());
|
||||
final Decimal result = (amount / Decimal.parse(rawPerCur.toString())).toDecimal();
|
||||
return result;
|
||||
}
|
||||
|
||||
static String truncateDecimal(Decimal input, {int digits = maxDecimalDigits}) {
|
||||
Decimal bigger = input.shift(digits);
|
||||
bigger = bigger.floor(); // chop off the decimal: 1.059 -> 1.05
|
||||
bigger = bigger.shift(-digits);
|
||||
return bigger.toString();
|
||||
}
|
||||
|
||||
/// Return raw as a NANO amount.
|
||||
///
|
||||
/// @param raw 100000000000000000000000000000
|
||||
/// @returns 1
|
||||
///
|
||||
static String getRawAsUsableString(String? raw, BigInt rawPerCur) {
|
||||
final String res =
|
||||
truncateDecimal(getRawAsDecimal(raw, rawPerCur), digits: maxDecimalDigits + 9);
|
||||
|
||||
if (raw == null || raw == "0" || raw == "00000000000000000000000000000000") {
|
||||
return "0";
|
||||
}
|
||||
|
||||
if (!res.contains(".")) {
|
||||
return res;
|
||||
}
|
||||
|
||||
final String numAmount = res.split(".")[0];
|
||||
String decAmount = res.split(".")[1];
|
||||
|
||||
// truncate:
|
||||
if (decAmount.length > maxDecimalDigits) {
|
||||
decAmount = decAmount.substring(0, maxDecimalDigits);
|
||||
// remove trailing zeros:
|
||||
decAmount = decAmount.replaceAllMapped(RegExp(r'0+$'), (Match match) => '');
|
||||
if (decAmount.isEmpty) {
|
||||
return numAmount;
|
||||
}
|
||||
}
|
||||
|
||||
return "$numAmount.$decAmount";
|
||||
}
|
||||
|
||||
static String getRawAccuracy(String? raw, BigInt rawPerCur) {
|
||||
final String rawString = getRawAsUsableString(raw, rawPerCur);
|
||||
final String rawDecimalString = getRawAsDecimal(raw, rawPerCur).toString();
|
||||
|
||||
if (raw == null || raw.isEmpty || raw == "0") {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (rawString != rawDecimalString) {
|
||||
return "~";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/// Return readable string amount as raw string
|
||||
/// @param amount 1.01
|
||||
/// @returns 101000000000000000000000000000
|
||||
///
|
||||
static String getAmountAsRaw(String amount, BigInt rawPerCur) {
|
||||
final Decimal asDecimal = Decimal.parse(amount);
|
||||
final Decimal rawDecimal = Decimal.parse(rawPerCur.toString());
|
||||
return (asDecimal * rawDecimal).toString();
|
||||
}
|
||||
}
|
|
@ -18,7 +18,6 @@ import 'package:cw_nano/nano_client.dart';
|
|||
import 'package:cw_nano/nano_transaction_credentials.dart';
|
||||
import 'package:cw_nano/nano_transaction_history.dart';
|
||||
import 'package:cw_nano/nano_transaction_info.dart';
|
||||
import 'package:cw_nano/nano_util.dart';
|
||||
import 'package:cw_nano/nano_wallet_keys.dart';
|
||||
import 'package:cw_nano/pending_nano_transaction.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
@ -27,6 +26,7 @@ import 'package:cw_nano/nano_wallet_addresses.dart';
|
|||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:nanodart/nanodart.dart';
|
||||
import 'package:bip39/bip39.dart' as bip39;
|
||||
import 'package:nanoutil/nanoutil.dart';
|
||||
|
||||
part 'nano_wallet.g.dart';
|
||||
|
||||
|
@ -83,6 +83,8 @@ abstract class NanoWalletBase
|
|||
@observable
|
||||
late ObservableMap<CryptoCurrency, NanoBalance> balance;
|
||||
|
||||
static const int POLL_INTERVAL_SECONDS = 10;
|
||||
|
||||
// initialize the different forms of private / public key we'll need:
|
||||
Future<void> init() async {
|
||||
if (_derivationType == DerivationType.unknown) {
|
||||
|
@ -100,11 +102,21 @@ abstract class NanoWalletBase
|
|||
if (_derivationType == DerivationType.nano) {
|
||||
_hexSeed = bip39.mnemonicToEntropy(_mnemonic).toUpperCase();
|
||||
} else {
|
||||
_hexSeed = await NanoUtil.hdMnemonicListToSeed(_mnemonic.split(' '));
|
||||
_hexSeed = await NanoDerivations.hdMnemonicListToSeed(_mnemonic.split(' '));
|
||||
}
|
||||
}
|
||||
_privateKey = await NanoUtil.uniSeedToPrivate(_hexSeed!, 0, type);
|
||||
_publicAddress = await NanoUtil.uniSeedToAddress(_hexSeed!, 0, type);
|
||||
NanoDerivationType derivationType =
|
||||
type == "standard" ? NanoDerivationType.STANDARD : NanoDerivationType.HD;
|
||||
_privateKey = await NanoDerivations.universalSeedToPrivate(
|
||||
_hexSeed!,
|
||||
index: 0,
|
||||
type: derivationType,
|
||||
);
|
||||
_publicAddress = await NanoDerivations.universalSeedToAddress(
|
||||
_hexSeed!,
|
||||
index: 0,
|
||||
type: derivationType,
|
||||
);
|
||||
this.walletInfo.address = _publicAddress!;
|
||||
|
||||
await walletAddresses.init();
|
||||
|
@ -125,6 +137,7 @@ abstract class NanoWalletBase
|
|||
@override
|
||||
void close() {
|
||||
_client.stop();
|
||||
_receiveTimer?.cancel();
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -139,6 +152,7 @@ abstract class NanoWalletBase
|
|||
|
||||
try {
|
||||
await _updateBalance();
|
||||
await updateTransactions();
|
||||
await _updateRep();
|
||||
await _receiveAll();
|
||||
} catch (e) {
|
||||
|
@ -173,8 +187,8 @@ abstract class NanoWalletBase
|
|||
if (txOut.sendAll) {
|
||||
amt = balance[currency]?.currentBalance ?? BigInt.zero;
|
||||
} else {
|
||||
amt = BigInt.tryParse(NanoUtil.getAmountAsRaw(
|
||||
txOut.cryptoAmount?.replaceAll(',', '.') ?? "0", NanoUtil.rawPerNano)) ??
|
||||
amt = BigInt.tryParse(NanoAmounts.getAmountAsRaw(
|
||||
txOut.cryptoAmount?.replaceAll(',', '.') ?? "0", NanoAmounts.rawPerNano)) ??
|
||||
BigInt.zero;
|
||||
}
|
||||
|
||||
|
@ -186,9 +200,7 @@ abstract class NanoWalletBase
|
|||
|
||||
final block = await _client.constructSendBlock(
|
||||
amountRaw: amt.toString(),
|
||||
destinationAddress: txOut.isParsedAddress
|
||||
? txOut.extractedAddress!
|
||||
: txOut.address,
|
||||
destinationAddress: txOut.isParsedAddress ? txOut.extractedAddress! : txOut.address,
|
||||
privateKey: _privateKey!,
|
||||
balanceAfterTx: runningBalance,
|
||||
previousHash: previousHash,
|
||||
|
@ -236,10 +248,10 @@ abstract class NanoWalletBase
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> updateTransactions() async {
|
||||
Future<bool> updateTransactions() async {
|
||||
try {
|
||||
if (_isTransactionUpdating) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
_isTransactionUpdating = true;
|
||||
|
@ -247,8 +259,10 @@ abstract class NanoWalletBase
|
|||
transactionHistory.addMany(transactions);
|
||||
await transactionHistory.save();
|
||||
_isTransactionUpdating = false;
|
||||
return true;
|
||||
} catch (_) {
|
||||
_isTransactionUpdating = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -261,16 +275,17 @@ abstract class NanoWalletBase
|
|||
final Map<String, NanoTransactionInfo> result = {};
|
||||
|
||||
for (var transactionModel in transactions) {
|
||||
final bool isSend = transactionModel.type == "send";
|
||||
result[transactionModel.hash] = NanoTransactionInfo(
|
||||
id: transactionModel.hash,
|
||||
amountRaw: transactionModel.amount,
|
||||
height: transactionModel.height,
|
||||
direction: transactionModel.type == "send"
|
||||
? TransactionDirection.outgoing
|
||||
: TransactionDirection.incoming,
|
||||
direction: isSend ? TransactionDirection.outgoing : TransactionDirection.incoming,
|
||||
confirmed: transactionModel.confirmed,
|
||||
date: transactionModel.date ?? DateTime.now(),
|
||||
confirmations: transactionModel.confirmed ? 1 : 0,
|
||||
to: isSend ? transactionModel.account : address,
|
||||
from: isSend ? address : transactionModel.account,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -312,11 +327,10 @@ abstract class NanoWalletBase
|
|||
Future<void> startSync() async {
|
||||
try {
|
||||
syncStatus = AttemptingSyncStatus();
|
||||
await _updateBalance();
|
||||
await updateTransactions();
|
||||
|
||||
// setup a timer to receive transactions periodically:
|
||||
_receiveTimer?.cancel();
|
||||
_receiveTimer = Timer.periodic(const Duration(seconds: 15), (timer) async {
|
||||
_receiveTimer = Timer.periodic(const Duration(seconds: POLL_INTERVAL_SECONDS), (timer) async {
|
||||
// get our balance:
|
||||
await _updateBalance();
|
||||
// if we have anything to receive, process it:
|
||||
|
@ -325,6 +339,14 @@ abstract class NanoWalletBase
|
|||
}
|
||||
});
|
||||
|
||||
// also run once, immediately:
|
||||
await _updateBalance();
|
||||
bool updateSuccess = await updateTransactions();
|
||||
if (!updateSuccess) {
|
||||
syncStatus = FailedSyncStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
syncStatus = SyncedSyncStatus();
|
||||
} catch (e) {
|
||||
print(e);
|
||||
|
@ -353,9 +375,11 @@ abstract class NanoWalletBase
|
|||
|
||||
final data = json.decode(jsonSource) as Map;
|
||||
final mnemonic = data['mnemonic'] as String;
|
||||
final balance = NanoBalance.fromString(
|
||||
formattedCurrentBalance: data['currentBalance'] as String? ?? "0",
|
||||
formattedReceivableBalance: data['receivableBalance'] as String? ?? "0");
|
||||
|
||||
final balance = NanoBalance.fromRawString(
|
||||
currentBalance: data['currentBalance'] as String? ?? "0",
|
||||
receivableBalance: data['receivableBalance'] as String? ?? "0",
|
||||
);
|
||||
|
||||
DerivationType derivationType = DerivationType.nano;
|
||||
if (data['derivationType'] == "DerivationType.bip39") {
|
||||
|
@ -374,12 +398,26 @@ abstract class NanoWalletBase
|
|||
}
|
||||
|
||||
Future<void> _updateBalance() async {
|
||||
var oldBalance = balance[currency];
|
||||
try {
|
||||
balance[currency] = await _client.getBalance(_publicAddress!);
|
||||
} catch (e) {
|
||||
print("Failed to get balance $e");
|
||||
// if we don't have a balance, we should at least create one, since it's a late binding
|
||||
// otherwise, it's better to just leave it as whatever it was before:
|
||||
if (balance[currency] == null) {
|
||||
balance[currency] =
|
||||
NanoBalance(currentBalance: BigInt.zero, receivableBalance: BigInt.zero);
|
||||
}
|
||||
}
|
||||
// don't save unnecessarily:
|
||||
// trying to save too frequently can cause problems with the file system
|
||||
// since nano is updated frequently this can be a problem, so we only save if there is a change:
|
||||
if (oldBalance == null ||
|
||||
balance[currency]!.currentBalance != oldBalance.currentBalance ||
|
||||
balance[currency]!.receivableBalance != oldBalance.receivableBalance) {
|
||||
await save();
|
||||
}
|
||||
await save();
|
||||
}
|
||||
|
||||
Future<void> _updateRep() async {
|
||||
|
@ -394,11 +432,19 @@ abstract class NanoWalletBase
|
|||
}
|
||||
|
||||
Future<void> regenerateAddress() async {
|
||||
final String type = (_derivationType == DerivationType.nano) ? "standard" : "hd";
|
||||
_privateKey =
|
||||
await NanoUtil.uniSeedToPrivate(_hexSeed!, this.walletAddresses.account!.id, type);
|
||||
_publicAddress =
|
||||
await NanoUtil.uniSeedToAddress(_hexSeed!, this.walletAddresses.account!.id, type);
|
||||
final NanoDerivationType type = (_derivationType == DerivationType.nano)
|
||||
? NanoDerivationType.STANDARD
|
||||
: NanoDerivationType.HD;
|
||||
_privateKey = await NanoDerivations.universalSeedToPrivate(
|
||||
_hexSeed!,
|
||||
index: this.walletAddresses.account!.id,
|
||||
type: type,
|
||||
);
|
||||
_publicAddress = await NanoDerivations.universalSeedToAddress(
|
||||
_hexSeed!,
|
||||
index: this.walletAddresses.account!.id,
|
||||
type: type,
|
||||
);
|
||||
|
||||
this.walletInfo.address = _publicAddress!;
|
||||
this.walletAddresses.address = _publicAddress!;
|
||||
|
|
|
@ -6,12 +6,12 @@ import 'package:cw_core/wallet_info.dart';
|
|||
import 'package:cw_core/wallet_service.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cw_nano/nano_mnemonic.dart' as nm;
|
||||
import 'package:cw_nano/nano_util.dart';
|
||||
import 'package:cw_nano/nano_wallet.dart';
|
||||
import 'package:cw_nano/nano_wallet_creation_credentials.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:bip39/bip39.dart' as bip39;
|
||||
import 'package:nanodart/nanodart.dart';
|
||||
import 'package:nanoutil/nanoutil.dart';
|
||||
|
||||
class NanoWalletService extends WalletService<NanoNewWalletCredentials,
|
||||
NanoRestoreWalletFromSeedCredentials, NanoRestoreWalletFromKeysCredentials> {
|
||||
|
@ -30,7 +30,7 @@ class NanoWalletService extends WalletService<NanoNewWalletCredentials,
|
|||
// nano standard:
|
||||
DerivationType derivationType = DerivationType.nano;
|
||||
String seedKey = NanoSeeds.generateSeed();
|
||||
String mnemonic = NanoUtil.seedToMnemonic(seedKey);
|
||||
String mnemonic = NanoDerivations.standardSeedToMnemonic(seedKey);
|
||||
|
||||
credentials.walletInfo!.derivationType = derivationType;
|
||||
|
||||
|
@ -95,7 +95,7 @@ class NanoWalletService extends WalletService<NanoNewWalletCredentials,
|
|||
// we can't derive the mnemonic from the key in all cases, only if it's a "nano" seed
|
||||
if (credentials.seedKey.length == 64) {
|
||||
try {
|
||||
mnemonic = NanoUtil.seedToMnemonic(credentials.seedKey);
|
||||
mnemonic = NanoDerivations.standardSeedToMnemonic(credentials.seedKey);
|
||||
} catch (e) {
|
||||
throw Exception("Wasn't a valid nano style seed!");
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:cw_nano/nano_client.dart';
|
||||
import 'package:cw_nano/nano_util.dart';
|
||||
import 'package:nanoutil/nanoutil.dart';
|
||||
|
||||
class PendingNanoTransaction with PendingTransaction {
|
||||
PendingNanoTransaction({
|
||||
|
@ -18,13 +18,13 @@ class PendingNanoTransaction with PendingTransaction {
|
|||
|
||||
@override
|
||||
String get amountFormatted {
|
||||
final String amt = NanoUtil.getRawAsUsableString(amount.toString(), NanoUtil.rawPerNano);
|
||||
final String amt = NanoAmounts.getRawAsUsableString(amount.toString(), NanoAmounts.rawPerNano);
|
||||
return amt;
|
||||
}
|
||||
|
||||
String get accurateAmountFormatted {
|
||||
final String amt = NanoUtil.getRawAsUsableString(amount.toString(), NanoUtil.rawPerNano);
|
||||
final String acc = NanoUtil.getRawAccuracy(amount.toString(), NanoUtil.rawPerNano);
|
||||
final String amt = NanoAmounts.getRawAsUsableString(amount.toString(), NanoAmounts.rawPerNano);
|
||||
final String acc = NanoAmounts.getRawAccuracy(amount.toString(), NanoAmounts.rawPerNano);
|
||||
return "$acc$amt";
|
||||
}
|
||||
|
||||
|
|
|
@ -471,6 +471,15 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
nanoutil:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: c37e72817cf0a28162f43124f79661d6c8e0098f
|
||||
resolved-ref: c37e72817cf0a28162f43124f79661d6c8e0098f
|
||||
url: "https://github.com/perishllc/nanoutil.git"
|
||||
source: git
|
||||
version: "1.0.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -764,15 +773,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
tor:
|
||||
dependency: transitive
|
||||
description:
|
||||
path: "."
|
||||
ref: main
|
||||
resolved-ref: "09ba92cb11d4e3cacf97256e57863b805f79f2e5"
|
||||
url: "https://github.com/cake-tech/tor.git"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -22,6 +22,10 @@ dependencies:
|
|||
hex: ^0.2.0
|
||||
http: ^1.1.0
|
||||
shared_preferences: ^2.0.15
|
||||
nanoutil:
|
||||
git:
|
||||
url: https://github.com/perishllc/nanoutil.git
|
||||
ref: c37e72817cf0a28162f43124f79661d6c8e0098f
|
||||
cw_core:
|
||||
path: ../cw_core
|
||||
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:cw_ethereum/pending_ethereum_transaction.dart';
|
||||
|
||||
class PendingPolygonTransaction extends PendingEthereumTransaction {
|
||||
PendingPolygonTransaction({
|
||||
required Function sendTransaction,
|
||||
required Uint8List signedTransaction,
|
||||
required BigInt fee,
|
||||
required String amount,
|
||||
required int exponent,
|
||||
}) : super(
|
||||
amount: amount,
|
||||
sendTransaction: sendTransaction,
|
||||
signedTransaction: signedTransaction,
|
||||
fee: fee,
|
||||
exponent: exponent,
|
||||
);
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:cw_ethereum/ethereum_client.dart';
|
||||
import 'package:cw_polygon/polygon_transaction_model.dart';
|
||||
import 'package:cw_ethereum/.secrets.g.dart' as secrets;
|
||||
import 'package:cw_evm/evm_chain_client.dart';
|
||||
import 'package:cw_evm/.secrets.g.dart' as secrets;
|
||||
import 'package:cw_evm/evm_chain_transaction_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:web3dart/web3dart.dart';
|
||||
|
||||
class PolygonClient extends EthereumClient {
|
||||
class PolygonClient extends EVMChainClient {
|
||||
@override
|
||||
Transaction createTransaction({
|
||||
required EthereumAddress from,
|
||||
|
@ -28,7 +28,7 @@ class PolygonClient extends EthereumClient {
|
|||
int get chainId => 137;
|
||||
|
||||
@override
|
||||
Future<List<PolygonTransactionModel>> fetchTransactions(String address,
|
||||
Future<List<EVMChainTransactionModel>> fetchTransactions(String address,
|
||||
{String? contractAddress}) async {
|
||||
try {
|
||||
final response = await httpClient.get(Uri.https("api.polygonscan.com", "/api", {
|
||||
|
@ -43,7 +43,9 @@ class PolygonClient extends EthereumClient {
|
|||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) {
|
||||
return (jsonResponse['result'] as List)
|
||||
.map((e) => PolygonTransactionModel.fromJson(e as Map<String, dynamic>))
|
||||
.map(
|
||||
(e) => EVMChainTransactionModel.fromJson(e as Map<String, dynamic>, 'MATIC'),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_ethereum/ethereum_exceptions.dart';
|
||||
|
||||
class PolygonTransactionCreationException extends EthereumTransactionCreationException {
|
||||
PolygonTransactionCreationException(CryptoCurrency currency) : super(currency);
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
import 'package:intl/intl.dart';
|
||||
|
||||
const polygonAmountLength = 12;
|
||||
const polygonAmountDivider = 1000000000000;
|
||||
final polygonAmountFormat = NumberFormat()
|
||||
..maximumFractionDigits = polygonAmountLength
|
||||
..minimumFractionDigits = 1;
|
||||
|
||||
class PolygonFormatter {
|
||||
static int parsePolygonAmount(String amount) {
|
||||
try {
|
||||
return (double.parse(amount) * polygonAmountDivider).round();
|
||||
} catch (_) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static double parsePolygonAmountToDouble(int amount) {
|
||||
try {
|
||||
return amount / polygonAmountDivider;
|
||||
} catch (_) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/output_info.dart';
|
||||
import 'package:cw_ethereum/ethereum_transaction_credentials.dart';
|
||||
import 'package:cw_polygon/polygon_transaction_priority.dart';
|
||||
|
||||
class PolygonTransactionCredentials extends EthereumTransactionCredentials {
|
||||
PolygonTransactionCredentials(
|
||||
List<OutputInfo> outputs, {
|
||||
required PolygonTransactionPriority? priority,
|
||||
required CryptoCurrency currency,
|
||||
final int? feeRate,
|
||||
}) : super(
|
||||
outputs,
|
||||
currency: currency,
|
||||
priority: priority,
|
||||
feeRate: feeRate,
|
||||
);
|
||||
}
|
|
@ -1,77 +1,19 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:core';
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_ethereum/file.dart';
|
||||
|
||||
import 'package:cw_evm/evm_chain_transaction_history.dart';
|
||||
import 'package:cw_evm/evm_chain_transaction_info.dart';
|
||||
import 'package:cw_polygon/polygon_transaction_info.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:cw_core/transaction_history.dart';
|
||||
|
||||
part 'polygon_transaction_history.g.dart';
|
||||
|
||||
const transactionsHistoryFileName = 'polygon_transactions.json';
|
||||
|
||||
class PolygonTransactionHistory = PolygonTransactionHistoryBase with _$PolygonTransactionHistory;
|
||||
|
||||
abstract class PolygonTransactionHistoryBase extends TransactionHistoryBase<PolygonTransactionInfo>
|
||||
with Store {
|
||||
PolygonTransactionHistoryBase({required this.walletInfo, required String password})
|
||||
: _password = password {
|
||||
transactions = ObservableMap<String, PolygonTransactionInfo>();
|
||||
}
|
||||
|
||||
final WalletInfo walletInfo;
|
||||
String _password;
|
||||
|
||||
Future<void> init() async => await _load();
|
||||
class PolygonTransactionHistory extends EVMChainTransactionHistory {
|
||||
PolygonTransactionHistory({
|
||||
required super.walletInfo,
|
||||
required super.password,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<void> save() async {
|
||||
try {
|
||||
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
|
||||
final path = '$dirPath/$transactionsHistoryFileName';
|
||||
final data = json.encode({'transactions': transactions});
|
||||
await writeData(path: path, password: _password, data: data);
|
||||
} catch (e, s) {
|
||||
print('Error while saving polygon transaction history: ${e.toString()}');
|
||||
print(s);
|
||||
}
|
||||
}
|
||||
String getTransactionHistoryFileName() => 'polygon_transactions.json';
|
||||
|
||||
@override
|
||||
void addOne(PolygonTransactionInfo transaction) => transactions[transaction.id] = transaction;
|
||||
|
||||
@override
|
||||
void addMany(Map<String, PolygonTransactionInfo> transactions) =>
|
||||
this.transactions.addAll(transactions);
|
||||
|
||||
Future<Map<String, dynamic>> _read() async {
|
||||
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
|
||||
final path = '$dirPath/$transactionsHistoryFileName';
|
||||
final content = await read(path: path, password: _password);
|
||||
if (content.isEmpty) {
|
||||
return {};
|
||||
}
|
||||
return json.decode(content) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
try {
|
||||
final content = await _read();
|
||||
final txs = content['transactions'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
txs.entries.forEach((entry) {
|
||||
final val = entry.value;
|
||||
|
||||
if (val is Map<String, dynamic>) {
|
||||
final tx = PolygonTransactionInfo.fromJson(val);
|
||||
_update(tx);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
|
||||
void _update(PolygonTransactionInfo transaction) => transactions[transaction.id] = transaction;
|
||||
EVMChainTransactionInfo getTransactionInfo(Map<String, dynamic> val) =>
|
||||
PolygonTransactionInfo.fromJson(val);
|
||||
}
|
||||
|
|
|
@ -1,32 +1,21 @@
|
|||
import 'package:cw_core/transaction_direction.dart';
|
||||
import 'package:cw_ethereum/ethereum_transaction_info.dart';
|
||||
import 'package:cw_evm/evm_chain_transaction_info.dart';
|
||||
|
||||
class PolygonTransactionInfo extends EthereumTransactionInfo {
|
||||
class PolygonTransactionInfo extends EVMChainTransactionInfo {
|
||||
PolygonTransactionInfo({
|
||||
required String id,
|
||||
required int height,
|
||||
required BigInt ethAmount,
|
||||
int exponent = 18,
|
||||
required TransactionDirection direction,
|
||||
required DateTime date,
|
||||
required bool isPending,
|
||||
required BigInt ethFee,
|
||||
required int confirmations,
|
||||
String tokenSymbol = "MATIC",
|
||||
required String? to,
|
||||
}) : super(
|
||||
confirmations: confirmations,
|
||||
id: id,
|
||||
height: height,
|
||||
ethAmount: ethAmount,
|
||||
exponent: exponent,
|
||||
direction: direction,
|
||||
date: date,
|
||||
isPending: isPending,
|
||||
ethFee: ethFee,
|
||||
to: to,
|
||||
tokenSymbol: tokenSymbol,
|
||||
);
|
||||
required super.id,
|
||||
required super.height,
|
||||
required super.ethAmount,
|
||||
required super.ethFee,
|
||||
required super.tokenSymbol,
|
||||
required super.direction,
|
||||
required super.isPending,
|
||||
required super.date,
|
||||
required super.confirmations,
|
||||
required super.to,
|
||||
required super.from,
|
||||
super.exponent,
|
||||
});
|
||||
|
||||
factory PolygonTransactionInfo.fromJson(Map<String, dynamic> data) {
|
||||
return PolygonTransactionInfo(
|
||||
|
@ -41,9 +30,10 @@ class PolygonTransactionInfo extends EthereumTransactionInfo {
|
|||
confirmations: data['confirmations'] as int,
|
||||
tokenSymbol: data['tokenSymbol'] as String,
|
||||
to: data['to'],
|
||||
from: data['from'],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String feeFormatted() => '${(ethFee / BigInt.from(10).pow(18)).toString()} MATIC';
|
||||
String get feeCurrency => 'MATIC';
|
||||
}
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
import 'package:cw_ethereum/ethereum_transaction_model.dart';
|
||||
|
||||
class PolygonTransactionModel extends EthereumTransactionModel {
|
||||
PolygonTransactionModel({
|
||||
required DateTime date,
|
||||
required String hash,
|
||||
required String from,
|
||||
required String to,
|
||||
required BigInt amount,
|
||||
required int gasUsed,
|
||||
required BigInt gasPrice,
|
||||
required String contractAddress,
|
||||
required int confirmations,
|
||||
required int blockNumber,
|
||||
required String? tokenSymbol,
|
||||
required int? tokenDecimal,
|
||||
required bool isError,
|
||||
}) : super(
|
||||
amount: amount,
|
||||
date: date,
|
||||
hash: hash,
|
||||
from: from,
|
||||
to: to,
|
||||
gasPrice: gasPrice,
|
||||
gasUsed: gasUsed,
|
||||
confirmations: confirmations,
|
||||
contractAddress: contractAddress,
|
||||
blockNumber: blockNumber,
|
||||
tokenDecimal: tokenDecimal,
|
||||
tokenSymbol: tokenSymbol,
|
||||
isError: isError,
|
||||
);
|
||||
|
||||
factory PolygonTransactionModel.fromJson(Map<String, dynamic> json) => PolygonTransactionModel(
|
||||
date: DateTime.fromMillisecondsSinceEpoch(int.parse(json["timeStamp"]) * 1000),
|
||||
hash: json["hash"],
|
||||
from: json["from"],
|
||||
to: json["to"],
|
||||
amount: BigInt.parse(json["value"]),
|
||||
gasUsed: int.parse(json["gasUsed"]),
|
||||
gasPrice: BigInt.parse(json["gasPrice"]),
|
||||
contractAddress: json["contractAddress"],
|
||||
confirmations: int.parse(json["confirmations"]),
|
||||
blockNumber: int.parse(json["blockNumber"]),
|
||||
tokenSymbol: json["tokenSymbol"] ?? "MATIC",
|
||||
tokenDecimal: int.tryParse(json["tokenDecimal"] ?? ""),
|
||||
isError: json["isError"] == "1",
|
||||
);
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
import 'package:cw_ethereum/ethereum_transaction_priority.dart';
|
||||
|
||||
class PolygonTransactionPriority extends EthereumTransactionPriority {
|
||||
const PolygonTransactionPriority({required String title, required int raw, required int tip})
|
||||
: super(title: title, raw: raw, tip: tip);
|
||||
|
||||
static const List<PolygonTransactionPriority> all = [fast, medium, slow];
|
||||
static const PolygonTransactionPriority slow =
|
||||
PolygonTransactionPriority(title: 'slow', raw: 0, tip: 1);
|
||||
static const PolygonTransactionPriority medium =
|
||||
PolygonTransactionPriority(title: 'Medium', raw: 1, tip: 2);
|
||||
static const PolygonTransactionPriority fast =
|
||||
PolygonTransactionPriority(title: 'Fast', raw: 2, tip: 4);
|
||||
|
||||
static PolygonTransactionPriority deserialize({required int raw}) {
|
||||
switch (raw) {
|
||||
case 0:
|
||||
return slow;
|
||||
case 1:
|
||||
return medium;
|
||||
case 2:
|
||||
return fast;
|
||||
default:
|
||||
throw Exception('Unexpected token: $raw for PolygonTransactionPriority deserialize');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String get units => 'gas';
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var label = '';
|
||||
|
||||
switch (this) {
|
||||
case PolygonTransactionPriority.slow:
|
||||
label = 'Slow';
|
||||
break;
|
||||
case PolygonTransactionPriority.medium:
|
||||
label = 'Medium';
|
||||
break;
|
||||
case PolygonTransactionPriority.fast:
|
||||
label = 'Fast';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
}
|
|
@ -1,362 +1,109 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/cake_hive.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
import 'package:cw_core/transaction_priority.dart';
|
||||
import 'package:cw_core/wallet_addresses.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_ethereum/erc20_balance.dart';
|
||||
import 'package:cw_ethereum/ethereum_formatter.dart';
|
||||
import 'package:cw_ethereum/file.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_evm/evm_chain_transaction_history.dart';
|
||||
import 'package:cw_evm/evm_chain_transaction_info.dart';
|
||||
import 'package:cw_evm/evm_chain_transaction_model.dart';
|
||||
import 'package:cw_evm/evm_chain_wallet.dart';
|
||||
import 'package:cw_evm/evm_erc20_balance.dart';
|
||||
import 'package:cw_evm/file.dart';
|
||||
import 'package:cw_polygon/default_polygon_erc20_tokens.dart';
|
||||
import 'package:cw_polygon/polygon_client.dart';
|
||||
import 'package:cw_polygon/polygon_exceptions.dart';
|
||||
import 'package:cw_polygon/polygon_formatter.dart';
|
||||
import 'package:cw_polygon/polygon_transaction_credentials.dart';
|
||||
import 'package:cw_polygon/polygon_transaction_history.dart';
|
||||
import 'package:cw_polygon/polygon_transaction_info.dart';
|
||||
import 'package:cw_polygon/polygon_transaction_model.dart';
|
||||
import 'package:cw_polygon/polygon_transaction_priority.dart';
|
||||
import 'package:cw_polygon/polygon_wallet_addresses.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:hex/hex.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:web3dart/crypto.dart';
|
||||
import 'package:web3dart/web3dart.dart';
|
||||
import 'package:bip39/bip39.dart' as bip39;
|
||||
import 'package:bip32/bip32.dart' as bip32;
|
||||
import 'package:cw_polygon/polygon_client.dart';
|
||||
import 'package:cw_polygon/polygon_transaction_history.dart';
|
||||
|
||||
part 'polygon_wallet.g.dart';
|
||||
|
||||
class PolygonWallet = PolygonWalletBase with _$PolygonWallet;
|
||||
|
||||
abstract class PolygonWalletBase
|
||||
extends WalletBase<ERC20Balance, PolygonTransactionHistory, PolygonTransactionInfo> with Store {
|
||||
PolygonWalletBase({
|
||||
required WalletInfo walletInfo,
|
||||
String? mnemonic,
|
||||
String? privateKey,
|
||||
required String password,
|
||||
ERC20Balance? initialBalance,
|
||||
}) : syncStatus = const NotConnectedSyncStatus(),
|
||||
_password = password,
|
||||
_mnemonic = mnemonic,
|
||||
_hexPrivateKey = privateKey,
|
||||
_isTransactionUpdating = false,
|
||||
_client = PolygonClient(),
|
||||
walletAddresses = PolygonWalletAddresses(walletInfo),
|
||||
balance = ObservableMap<CryptoCurrency, ERC20Balance>.of(
|
||||
{CryptoCurrency.maticpoly: initialBalance ?? ERC20Balance(BigInt.zero)}),
|
||||
super(walletInfo) {
|
||||
this.walletInfo = walletInfo;
|
||||
transactionHistory = PolygonTransactionHistory(walletInfo: walletInfo, password: password);
|
||||
|
||||
if (!CakeHive.isAdapterRegistered(Erc20Token.typeId)) {
|
||||
CakeHive.registerAdapter(Erc20TokenAdapter());
|
||||
}
|
||||
|
||||
_sharedPrefs.complete(SharedPreferences.getInstance());
|
||||
}
|
||||
|
||||
final String? _mnemonic;
|
||||
final String? _hexPrivateKey;
|
||||
final String _password;
|
||||
|
||||
late final Box<Erc20Token> polygonErc20TokensBox;
|
||||
|
||||
late final EthPrivateKey _polygonPrivateKey;
|
||||
|
||||
late final PolygonClient _client;
|
||||
|
||||
EthPrivateKey get polygonPrivateKey => _polygonPrivateKey;
|
||||
|
||||
int? _gasPrice;
|
||||
int? _estimatedGas;
|
||||
bool _isTransactionUpdating;
|
||||
|
||||
// TODO: remove after integrating our own node and having eth_newPendingTransactionFilter
|
||||
Timer? _transactionsUpdateTimer;
|
||||
class PolygonWallet extends EVMChainWallet {
|
||||
PolygonWallet({
|
||||
required super.walletInfo,
|
||||
required super.password,
|
||||
super.mnemonic,
|
||||
super.initialBalance,
|
||||
super.privateKey,
|
||||
required super.client,
|
||||
}) : super(nativeCurrency: CryptoCurrency.maticpoly);
|
||||
|
||||
@override
|
||||
WalletAddresses walletAddresses;
|
||||
|
||||
@override
|
||||
@observable
|
||||
SyncStatus syncStatus;
|
||||
|
||||
@override
|
||||
@observable
|
||||
late ObservableMap<CryptoCurrency, ERC20Balance> balance;
|
||||
|
||||
final Completer<SharedPreferences> _sharedPrefs = Completer();
|
||||
|
||||
Future<void> init() async {
|
||||
polygonErc20TokensBox = await CakeHive.openBox<Erc20Token>(
|
||||
"${walletInfo.name.replaceAll(" ", "_")}_${Erc20Token.polygonBoxName}");
|
||||
await walletAddresses.init();
|
||||
await transactionHistory.init();
|
||||
_polygonPrivateKey = await getPrivateKey(
|
||||
mnemonic: _mnemonic,
|
||||
privateKey: _hexPrivateKey,
|
||||
password: _password,
|
||||
);
|
||||
walletAddresses.address = _polygonPrivateKey.address.toString();
|
||||
await save();
|
||||
}
|
||||
|
||||
@override
|
||||
int calculateEstimatedFee(TransactionPriority priority, int? amount) {
|
||||
try {
|
||||
if (priority is PolygonTransactionPriority) {
|
||||
final priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt();
|
||||
return (_gasPrice! + priorityFee) * (_estimatedGas ?? 0);
|
||||
}
|
||||
|
||||
return 0;
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> changePassword(String password) {
|
||||
throw UnimplementedError("changePassword");
|
||||
}
|
||||
|
||||
@override
|
||||
void close() {
|
||||
_client.stop();
|
||||
_transactionsUpdateTimer?.cancel();
|
||||
}
|
||||
|
||||
@action
|
||||
@override
|
||||
Future<void> connectToNode({required Node node}) async {
|
||||
try {
|
||||
syncStatus = ConnectingSyncStatus();
|
||||
|
||||
final isConnected = _client.connect(node);
|
||||
|
||||
if (!isConnected) {
|
||||
throw Exception("Polygon Node connection failed");
|
||||
}
|
||||
|
||||
_client.setListeners(_polygonPrivateKey.address, _onNewTransaction);
|
||||
|
||||
_setTransactionUpdateTimer();
|
||||
|
||||
syncStatus = ConnectedSyncStatus();
|
||||
} catch (e) {
|
||||
syncStatus = FailedSyncStatus();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PendingTransaction> createTransaction(Object credentials) async {
|
||||
final credentials0 = credentials as PolygonTransactionCredentials;
|
||||
final outputs = credentials0.outputs;
|
||||
final hasMultiDestination = outputs.length > 1;
|
||||
|
||||
final CryptoCurrency transactionCurrency =
|
||||
balance.keys.firstWhere((element) => element.title == credentials0.currency.title);
|
||||
|
||||
final erc20Balance = balance[transactionCurrency]!;
|
||||
BigInt totalAmount = BigInt.zero;
|
||||
int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18;
|
||||
num amountToPolygonMultiplier = pow(10, exponent);
|
||||
|
||||
// so far this can not be made with Polygon as Polygon does not support multiple recipients
|
||||
if (hasMultiDestination) {
|
||||
if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
|
||||
throw PolygonTransactionCreationException(transactionCurrency);
|
||||
}
|
||||
|
||||
final totalOriginalAmount = PolygonFormatter.parsePolygonAmountToDouble(
|
||||
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)));
|
||||
totalAmount = BigInt.from(totalOriginalAmount * amountToPolygonMultiplier);
|
||||
|
||||
if (erc20Balance.balance < totalAmount) {
|
||||
throw PolygonTransactionCreationException(transactionCurrency);
|
||||
}
|
||||
Future<void> initErc20TokensBox() async {
|
||||
final boxName = "${walletInfo.name.replaceAll(" ", "_")}_ ${Erc20Token.polygonBoxName}";
|
||||
if (await CakeHive.boxExists(boxName)) {
|
||||
evmChainErc20TokensBox = await CakeHive.openBox<Erc20Token>(boxName);
|
||||
} else {
|
||||
final output = outputs.first;
|
||||
// since the fees are taken from Ethereum
|
||||
// then no need to subtract the fees from the amount if send all
|
||||
final BigInt allAmount;
|
||||
if (transactionCurrency is Erc20Token) {
|
||||
allAmount = erc20Balance.balance;
|
||||
} else {
|
||||
allAmount =
|
||||
erc20Balance.balance - BigInt.from(calculateEstimatedFee(credentials0.priority!, null));
|
||||
}
|
||||
final totalOriginalAmount =
|
||||
EthereumFormatter.parseEthereumAmountToDouble(output.formattedCryptoAmount ?? 0);
|
||||
totalAmount =
|
||||
output.sendAll ? allAmount : BigInt.from(totalOriginalAmount * amountToPolygonMultiplier);
|
||||
|
||||
if (erc20Balance.balance < totalAmount) {
|
||||
throw PolygonTransactionCreationException(transactionCurrency);
|
||||
}
|
||||
evmChainErc20TokensBox = await CakeHive.openBox<Erc20Token>(boxName.replaceAll(" ", ""));
|
||||
}
|
||||
}
|
||||
|
||||
final pendingPolygonTransaction = await _client.signTransaction(
|
||||
privateKey: _polygonPrivateKey,
|
||||
toAddress: credentials0.outputs.first.isParsedAddress
|
||||
? credentials0.outputs.first.extractedAddress!
|
||||
: credentials0.outputs.first.address,
|
||||
amount: totalAmount.toString(),
|
||||
gas: _estimatedGas!,
|
||||
priority: credentials0.priority!,
|
||||
currency: transactionCurrency,
|
||||
exponent: exponent,
|
||||
contractAddress:
|
||||
transactionCurrency is Erc20Token ? transactionCurrency.contractAddress : null,
|
||||
@override
|
||||
void addInitialTokens() {
|
||||
final initialErc20Tokens = DefaultPolygonErc20Tokens().initialPolygonErc20Tokens;
|
||||
|
||||
for (var token in initialErc20Tokens) {
|
||||
evmChainErc20TokensBox.put(token.contractAddress, token);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> checkIfScanProviderIsEnabled() async {
|
||||
bool isPolygonScanEnabled = (await sharedPrefs.future).getBool("use_polygonscan") ?? true;
|
||||
return isPolygonScanEnabled;
|
||||
}
|
||||
|
||||
@override
|
||||
String getTransactionHistoryFileName() => 'polygon_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 ?? "MATIC",
|
||||
iconPath: iconPath,
|
||||
);
|
||||
|
||||
return pendingPolygonTransaction;
|
||||
}
|
||||
|
||||
Future<void> _updateTransactions() async {
|
||||
try {
|
||||
if (_isTransactionUpdating) {
|
||||
return;
|
||||
}
|
||||
bool isPolygonScanEnabled = (await _sharedPrefs.future).getBool("use_polygonscan") ?? true;
|
||||
if (!isPolygonScanEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isTransactionUpdating = true;
|
||||
final transactions = await fetchTransactions();
|
||||
transactionHistory.addMany(transactions);
|
||||
await transactionHistory.save();
|
||||
_isTransactionUpdating = false;
|
||||
} catch (_) {
|
||||
_isTransactionUpdating = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, PolygonTransactionInfo>> fetchTransactions() async {
|
||||
final address = _polygonPrivateKey.address.hex;
|
||||
final transactions = await _client.fetchTransactions(address);
|
||||
|
||||
final List<Future<List<PolygonTransactionModel>>> polygonErc20TokensTransactions = [];
|
||||
|
||||
for (var token in balance.keys) {
|
||||
if (token is Erc20Token) {
|
||||
polygonErc20TokensTransactions.add(
|
||||
_client.fetchTransactions(
|
||||
address,
|
||||
contractAddress: token.contractAddress,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final tokensTransaction = await Future.wait(polygonErc20TokensTransactions);
|
||||
transactions.addAll(tokensTransaction.expand((element) => element));
|
||||
|
||||
final Map<String, PolygonTransactionInfo> result = {};
|
||||
|
||||
for (var transactionModel in transactions) {
|
||||
if (transactionModel.isError) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result[transactionModel.hash] = PolygonTransactionInfo(
|
||||
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 ?? "MATIC",
|
||||
to: transactionModel.to,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
EVMChainTransactionInfo getTransactionInfo(
|
||||
EVMChainTransactionModel transactionModel, String address) {
|
||||
final model = PolygonTransactionInfo(
|
||||
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 ?? "MATIC",
|
||||
to: transactionModel.to,
|
||||
from: transactionModel.from,
|
||||
);
|
||||
return model;
|
||||
}
|
||||
|
||||
@override
|
||||
Object get keys => throw UnimplementedError("keys");
|
||||
|
||||
@override
|
||||
Future<void> rescan({required int height}) {
|
||||
throw UnimplementedError("rescan");
|
||||
EVMChainTransactionHistory setUpTransactionHistory(WalletInfo walletInfo, String password) {
|
||||
return PolygonTransactionHistory(walletInfo: walletInfo, password: password);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save() async {
|
||||
await walletAddresses.updateAddressesInBox();
|
||||
final path = await makePath();
|
||||
await write(path: path, password: _password, data: toJSON());
|
||||
await transactionHistory.save();
|
||||
}
|
||||
|
||||
@override
|
||||
String? get seed => _mnemonic;
|
||||
|
||||
@override
|
||||
String get privateKey => HEX.encode(_polygonPrivateKey.privateKey);
|
||||
|
||||
@action
|
||||
@override
|
||||
Future<void> startSync() async {
|
||||
try {
|
||||
syncStatus = AttemptingSyncStatus();
|
||||
await _updateBalance();
|
||||
await _updateTransactions();
|
||||
_gasPrice = await _client.getGasUnitPrice();
|
||||
_estimatedGas = await _client.getEstimatedGas();
|
||||
|
||||
Timer.periodic(
|
||||
const Duration(minutes: 1), (timer) async => _gasPrice = await _client.getGasUnitPrice());
|
||||
Timer.periodic(const Duration(seconds: 10),
|
||||
(timer) async => _estimatedGas = await _client.getEstimatedGas());
|
||||
|
||||
syncStatus = SyncedSyncStatus();
|
||||
} catch (e) {
|
||||
syncStatus = FailedSyncStatus();
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
|
||||
|
||||
String toJSON() => json.encode({
|
||||
'mnemonic': _mnemonic,
|
||||
'private_key': privateKey,
|
||||
'balance': balance[currency]!.toJSON(),
|
||||
});
|
||||
|
||||
static Future<PolygonWallet> open({
|
||||
required String name,
|
||||
required String password,
|
||||
required WalletInfo walletInfo,
|
||||
}) async {
|
||||
static Future<PolygonWallet> open(
|
||||
{required String name, required String password, required WalletInfo walletInfo}) async {
|
||||
final path = await pathForWallet(name: name, type: walletInfo.type);
|
||||
final jsonSource = await read(path: path, password: password);
|
||||
final data = json.decode(jsonSource) as Map;
|
||||
final mnemonic = data['mnemonic'] as String?;
|
||||
final privateKey = data['private_key'] as String?;
|
||||
final balance = ERC20Balance.fromJSON(data['balance'] as String) ?? ERC20Balance(BigInt.zero);
|
||||
final balance = EVMChainERC20Balance.fromJSON(data['balance'] as String) ??
|
||||
EVMChainERC20Balance(BigInt.zero);
|
||||
|
||||
return PolygonWallet(
|
||||
walletInfo: walletInfo,
|
||||
|
@ -364,158 +111,7 @@ abstract class PolygonWalletBase
|
|||
mnemonic: mnemonic,
|
||||
privateKey: privateKey,
|
||||
initialBalance: balance,
|
||||
client: PolygonClient(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _updateBalance() async {
|
||||
balance[currency] = await _fetchMaticBalance();
|
||||
|
||||
await _fetchErc20Balances();
|
||||
await save();
|
||||
}
|
||||
|
||||
Future<ERC20Balance> _fetchMaticBalance() async {
|
||||
final balance = await _client.getBalance(_polygonPrivateKey.address);
|
||||
return ERC20Balance(balance.getInWei);
|
||||
}
|
||||
|
||||
Future<void> _fetchErc20Balances() async {
|
||||
for (var token in polygonErc20TokensBox.values) {
|
||||
try {
|
||||
if (token.enabled) {
|
||||
balance[token] = await _client.fetchERC20Balances(
|
||||
_polygonPrivateKey.address,
|
||||
token.contractAddress,
|
||||
);
|
||||
} else {
|
||||
balance.remove(token);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
Future<EthPrivateKey> getPrivateKey(
|
||||
{String? mnemonic, String? privateKey, required String password}) async {
|
||||
assert(mnemonic != null || privateKey != null);
|
||||
|
||||
if (privateKey != null) {
|
||||
return EthPrivateKey.fromHex(privateKey);
|
||||
}
|
||||
|
||||
final seed = bip39.mnemonicToSeed(mnemonic!);
|
||||
|
||||
final root = bip32.BIP32.fromSeed(seed);
|
||||
|
||||
const hdPathPolygon = "m/44'/60'/0'/0";
|
||||
const index = 0;
|
||||
final addressAtIndex = root.derivePath("$hdPathPolygon/$index");
|
||||
|
||||
return EthPrivateKey.fromHex(HEX.encode(addressAtIndex.privateKey as List<int>));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void>? updateBalance() async => await _updateBalance();
|
||||
|
||||
List<Erc20Token> get erc20Currencies => polygonErc20TokensBox.values.toList();
|
||||
|
||||
Future<void> addErc20Token(Erc20Token token) async {
|
||||
String? iconPath;
|
||||
try {
|
||||
iconPath = CryptoCurrency.all
|
||||
.firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase())
|
||||
.iconPath;
|
||||
} catch (_) {}
|
||||
|
||||
final token0 = Erc20Token(
|
||||
name: token.name,
|
||||
symbol: token.symbol,
|
||||
contractAddress: token.contractAddress,
|
||||
decimal: token.decimal,
|
||||
enabled: token.enabled,
|
||||
tag: token.tag ?? "POLY",
|
||||
iconPath: iconPath,
|
||||
);
|
||||
|
||||
await polygonErc20TokensBox.put(token0.contractAddress, token0);
|
||||
|
||||
if (token0.enabled) {
|
||||
balance[token0] = await _client.fetchERC20Balances(
|
||||
_polygonPrivateKey.address,
|
||||
token0.contractAddress,
|
||||
);
|
||||
} else {
|
||||
balance.remove(token0);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteErc20Token(Erc20Token token) async {
|
||||
await token.delete();
|
||||
|
||||
balance.remove(token);
|
||||
_updateBalance();
|
||||
}
|
||||
|
||||
Future<Erc20Token?> getErc20Token(String contractAddress) async =>
|
||||
await _client.getErc20Token(contractAddress);
|
||||
|
||||
void _onNewTransaction() {
|
||||
_updateBalance();
|
||||
_updateTransactions();
|
||||
}
|
||||
|
||||
void addInitialTokens() {
|
||||
final initialErc20Tokens = DefaultPolygonErc20Tokens().initialPolygonErc20Tokens;
|
||||
|
||||
for (var token in initialErc20Tokens) {
|
||||
polygonErc20TokensBox.put(token.contractAddress, token);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> renameWalletFiles(String newWalletName) async {
|
||||
final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type);
|
||||
final currentWalletFile = File(currentWalletPath);
|
||||
|
||||
final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type);
|
||||
final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName');
|
||||
|
||||
// Copies current wallet files into new wallet name's dir and files
|
||||
if (currentWalletFile.existsSync()) {
|
||||
final newWalletPath = await pathForWallet(name: newWalletName, type: type);
|
||||
await currentWalletFile.copy(newWalletPath);
|
||||
}
|
||||
if (currentTransactionsFile.existsSync()) {
|
||||
final newDirPath = await pathForWalletDir(name: newWalletName, type: type);
|
||||
await currentTransactionsFile.copy('$newDirPath/$transactionsHistoryFileName');
|
||||
}
|
||||
|
||||
// Delete old name's dir and files
|
||||
await Directory(currentDirPath).delete(recursive: true);
|
||||
}
|
||||
|
||||
void _setTransactionUpdateTimer() {
|
||||
if (_transactionsUpdateTimer?.isActive ?? false) {
|
||||
_transactionsUpdateTimer!.cancel();
|
||||
}
|
||||
|
||||
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 10), (_) {
|
||||
_updateTransactions();
|
||||
_updateBalance();
|
||||
});
|
||||
}
|
||||
|
||||
void updatePolygonScanUsageState(bool isEnabled) {
|
||||
if (isEnabled) {
|
||||
_updateTransactions();
|
||||
_setTransactionUpdateTimer();
|
||||
} else {
|
||||
_transactionsUpdateTimer?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String signMessage(String message, {String? address}) =>
|
||||
bytesToHex(_polygonPrivateKey.signPersonalMessageToUint8List(ascii.encode(message)));
|
||||
|
||||
Web3Client? getWeb3Client() => _client.getWeb3Client();
|
||||
}
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import 'package:cw_ethereum/ethereum_wallet_addresses.dart';
|
||||
|
||||
class PolygonWalletAddresses extends EthereumWalletAddresses {
|
||||
PolygonWalletAddresses(super.walletInfo);
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
import 'package:cw_core/wallet_credentials.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
class PolygonNewWalletCredentials extends WalletCredentials {
|
||||
PolygonNewWalletCredentials({required String name, WalletInfo? walletInfo})
|
||||
: super(name: name, walletInfo: walletInfo);
|
||||
}
|
||||
|
||||
class PolygonRestoreWalletFromSeedCredentials extends WalletCredentials {
|
||||
PolygonRestoreWalletFromSeedCredentials(
|
||||
{required String name,
|
||||
required String password,
|
||||
required this.mnemonic,
|
||||
WalletInfo? walletInfo})
|
||||
: super(name: name, password: password, walletInfo: walletInfo);
|
||||
|
||||
final String mnemonic;
|
||||
}
|
||||
|
||||
class PolygonRestoreWalletFromPrivateKey extends WalletCredentials {
|
||||
PolygonRestoreWalletFromPrivateKey(
|
||||
{required String name,
|
||||
required String password,
|
||||
required this.privateKey,
|
||||
WalletInfo? walletInfo})
|
||||
: super(name: name, password: password, walletInfo: walletInfo);
|
||||
|
||||
final String privateKey;
|
||||
}
|
|
@ -1,32 +1,34 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_service.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cw_ethereum/ethereum_mnemonics.dart';
|
||||
import 'package:cw_polygon/polygon_wallet.dart';
|
||||
import 'package:bip39/bip39.dart' as bip39;
|
||||
import 'package:hive/hive.dart';
|
||||
import 'polygon_wallet_creation_credentials.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:cw_core/wallet_base.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_polygon/polygon_client.dart';
|
||||
import 'package:cw_polygon/polygon_mnemonics_exception.dart';
|
||||
import 'package:cw_polygon/polygon_wallet.dart';
|
||||
|
||||
class PolygonWalletService extends WalletService<PolygonNewWalletCredentials,
|
||||
PolygonRestoreWalletFromSeedCredentials, PolygonRestoreWalletFromPrivateKey> {
|
||||
PolygonWalletService(this.walletInfoSource);
|
||||
class PolygonWalletService extends EVMChainWalletService<PolygonWallet> {
|
||||
PolygonWalletService(
|
||||
super.walletInfoSource, {
|
||||
required this.client,
|
||||
});
|
||||
|
||||
final Box<WalletInfo> walletInfoSource;
|
||||
late PolygonClient client;
|
||||
|
||||
@override
|
||||
Future<PolygonWallet> create(PolygonNewWalletCredentials credentials) async {
|
||||
WalletType getType() => WalletType.polygon;
|
||||
|
||||
@override
|
||||
Future<PolygonWallet> create(EVMChainNewWalletCredentials credentials) async {
|
||||
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
|
||||
|
||||
final mnemonic = bip39.generateMnemonic(strength: strength);
|
||||
|
||||
final wallet = PolygonWallet(
|
||||
walletInfo: credentials.walletInfo!,
|
||||
mnemonic: mnemonic,
|
||||
password: credentials.password!,
|
||||
client: client,
|
||||
);
|
||||
|
||||
await wallet.init();
|
||||
|
@ -36,18 +38,11 @@ class PolygonWalletService extends WalletService<PolygonNewWalletCredentials,
|
|||
return wallet;
|
||||
}
|
||||
|
||||
@override
|
||||
WalletType getType() => WalletType.polygon;
|
||||
|
||||
@override
|
||||
Future<bool> isWalletExit(String name) async =>
|
||||
File(await pathForWallet(name: name, type: getType())).existsSync();
|
||||
|
||||
@override
|
||||
Future<PolygonWallet> openWallet(String name, String password) async {
|
||||
final walletInfo =
|
||||
walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
|
||||
final wallet = await PolygonWalletBase.open(
|
||||
final wallet = await PolygonWallet.open(
|
||||
name: name,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
|
@ -60,19 +55,13 @@ class PolygonWalletService extends WalletService<PolygonNewWalletCredentials,
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> remove(String wallet) async {
|
||||
File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true);
|
||||
final walletInfo = walletInfoSource.values
|
||||
.firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!;
|
||||
await walletInfoSource.delete(walletInfo.key);
|
||||
}
|
||||
Future<PolygonWallet> restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials) async {
|
||||
|
||||
@override
|
||||
Future<PolygonWallet> restoreFromKeys(PolygonRestoreWalletFromPrivateKey credentials) async {
|
||||
final wallet = PolygonWallet(
|
||||
password: credentials.password!,
|
||||
privateKey: credentials.privateKey,
|
||||
walletInfo: credentials.walletInfo!,
|
||||
client: client,
|
||||
);
|
||||
|
||||
await wallet.init();
|
||||
|
@ -83,15 +72,17 @@ class PolygonWalletService extends WalletService<PolygonNewWalletCredentials,
|
|||
}
|
||||
|
||||
@override
|
||||
Future<PolygonWallet> restoreFromSeed(PolygonRestoreWalletFromSeedCredentials credentials) async {
|
||||
Future<PolygonWallet> restoreFromSeed(
|
||||
EVMChainRestoreWalletFromSeedCredentials credentials) async {
|
||||
if (!bip39.validateMnemonic(credentials.mnemonic)) {
|
||||
throw EthereumMnemonicIsIncorrectException();
|
||||
throw PolygonMnemonicIsIncorrectException();
|
||||
}
|
||||
|
||||
final wallet = PolygonWallet(
|
||||
password: credentials.password!,
|
||||
mnemonic: credentials.mnemonic,
|
||||
walletInfo: credentials.walletInfo!,
|
||||
client: client,
|
||||
);
|
||||
|
||||
await wallet.init();
|
||||
|
@ -105,7 +96,7 @@ class PolygonWalletService extends WalletService<PolygonNewWalletCredentials,
|
|||
Future<void> rename(String currentName, String password, String newName) async {
|
||||
final currentWalletInfo = walletInfoSource.values
|
||||
.firstWhere((info) => info.id == WalletBase.idFor(currentName, getType()));
|
||||
final currentWallet = await PolygonWalletBase.open(
|
||||
final currentWallet = await PolygonWallet.open(
|
||||
password: password, name: currentName, walletInfo: currentWalletInfo);
|
||||
|
||||
await currentWallet.renameWalletFiles(newName);
|
||||
|
|
|
@ -16,15 +16,12 @@ dependencies:
|
|||
path: ../cw_core
|
||||
cw_ethereum:
|
||||
path: ../cw_ethereum
|
||||
mobx: ^2.0.7+4
|
||||
intl: ^0.18.0
|
||||
bip39: ^1.0.6
|
||||
hive: ^2.2.3
|
||||
collection: ^1.17.1
|
||||
cw_evm:
|
||||
path: ../cw_evm
|
||||
web3dart: ^2.7.1
|
||||
bip32: ^2.0.0
|
||||
hex: ^0.2.0
|
||||
shared_preferences: ^2.0.15
|
||||
hive: ^2.2.3
|
||||
bip39: ^1.0.6
|
||||
collection: ^1.17.1
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
|
@ -32,8 +29,6 @@ dev_dependencies:
|
|||
sdk: flutter
|
||||
flutter_lints: ^2.0.0
|
||||
build_runner: ^2.1.11
|
||||
mobx_codegen: ^2.0.7
|
||||
hive_generator: ^1.1.3
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
|
|
@ -102,11 +102,11 @@ PODS:
|
|||
- DKImagePickerController/PhotoGallery
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
- flutter_inappwebview (0.0.1):
|
||||
- flutter_inappwebview_ios (0.0.1):
|
||||
- Flutter
|
||||
- flutter_inappwebview/Core (= 0.0.1)
|
||||
- flutter_inappwebview_ios/Core (= 0.0.1)
|
||||
- OrderedSet (~> 5.0)
|
||||
- flutter_inappwebview/Core (0.0.1):
|
||||
- flutter_inappwebview_ios/Core (0.0.1):
|
||||
- Flutter
|
||||
- OrderedSet (~> 5.0)
|
||||
- flutter_mailer (0.0.1):
|
||||
|
@ -169,7 +169,7 @@ DEPENDENCIES:
|
|||
- devicelocale (from `.symlinks/plugins/devicelocale/ios`)
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`)
|
||||
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
|
||||
- flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||
|
@ -224,8 +224,8 @@ EXTERNAL SOURCES:
|
|||
:path: ".symlinks/plugins/file_picker/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_inappwebview:
|
||||
:path: ".symlinks/plugins/flutter_inappwebview/ios"
|
||||
flutter_inappwebview_ios:
|
||||
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
|
||||
flutter_mailer:
|
||||
:path: ".symlinks/plugins/flutter_mailer/ios"
|
||||
flutter_secure_storage:
|
||||
|
@ -274,29 +274,29 @@ SPEC CHECKSUMS:
|
|||
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
||||
file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_inappwebview: 3d32228f1304635e7c028b0d4252937730bbc6cf
|
||||
flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
|
||||
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
|
||||
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
|
||||
fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0
|
||||
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
|
||||
local_auth_ios: c6cf091ded637a88f24f86a8875d8b0f526e2605
|
||||
local_auth_ios: 1ba1475238daa33a6ffa2a29242558437be435ac
|
||||
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
|
||||
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
|
||||
package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62
|
||||
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
|
||||
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
|
||||
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
||||
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
SDWebImage: 2aea163b50bfcb569a2726b6a754c54a4506fcf6
|
||||
sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986
|
||||
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
|
||||
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
|
||||
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
|
||||
SwiftProtobuf: 40bd808372cb8706108f22d28f8ab4a6b9bc6989
|
||||
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
|
||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||
uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
|
||||
UnstoppableDomainsResolution: c3c67f4d0a5e2437cb00d4bd50c2e00d6e743841
|
||||
url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b
|
||||
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
|
||||
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
|
||||
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
||||
|
||||
|
|
|
@ -63,9 +63,17 @@ class CWBitcoin extends Bitcoin {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> generateNewAddress(Object wallet) async {
|
||||
Future<void> generateNewAddress(Object wallet, String label) async {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
await bitcoinWallet.walletAddresses.generateNewAddress();
|
||||
await bitcoinWallet.walletAddresses.generateNewAddress(label: label);
|
||||
await wallet.save();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateAddress(Object wallet,String address, String label) async {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
bitcoinWallet.walletAddresses.updateAddress(address, label);
|
||||
await wallet.save();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -99,6 +107,21 @@ class CWBitcoin extends Bitcoin {
|
|||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
@computed
|
||||
List<ElectrumSubAddress> getSubAddresses(Object wallet) {
|
||||
final electrumWallet = wallet as ElectrumWallet;
|
||||
return electrumWallet.walletAddresses.addresses
|
||||
.map((BitcoinAddressRecord addr) => ElectrumSubAddress(
|
||||
id: addr.index,
|
||||
name: addr.name,
|
||||
address: electrumWallet.type == WalletType.bitcoinCash ? addr.cashAddr : addr.address,
|
||||
txCount: addr.txCount,
|
||||
balance: addr.balance,
|
||||
isChange: addr.isHidden))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
String getAddress(Object wallet) {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
|
@ -127,7 +150,7 @@ class CWBitcoin extends Bitcoin {
|
|||
return bitcoinWallet.unspentCoins;
|
||||
}
|
||||
|
||||
void updateUnspents(Object wallet) async {
|
||||
Future<void> updateUnspents(Object wallet) async {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
await bitcoinWallet.updateUnspent();
|
||||
}
|
||||
|
|