diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index dc231df42..acaa12fe0 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -42,7 +42,7 @@ jobs: - name: Flutter action uses: subosito/flutter-action@v1 with: - flutter-version: "3.10.x" + flutter-version: "3.19.5" channel: stable - name: Install package dependencies @@ -113,6 +113,7 @@ jobs: touch lib/.secrets.g.dart touch cw_evm/lib/.secrets.g.dart touch cw_solana/lib/.secrets.g.dart + touch cw_tron/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 @@ -150,6 +151,9 @@ jobs: echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart + echo "const quantexExchangeMarkup = '${{ secrets.QUANTEX_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart + echo "const nano2ApiKey = '${{ secrets.NANO2_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart + echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart - name: Rename app run: | diff --git a/.gitignore b/.gitignore index 6f2d0a182..d0952ca98 100644 --- a/.gitignore +++ b/.gitignore @@ -94,9 +94,12 @@ android/app/key.jks **/tool/.evm-secrets-config.json **/tool/.ethereum-secrets-config.json **/tool/.solana-secrets-config.json +**/tool/.nano-secrets-config.json +**/tool/.tron-secrets-config.json **/lib/.secrets.g.dart **/cw_evm/lib/.secrets.g.dart **/cw_solana/lib/.secrets.g.dart +**/cw_tron/lib/.secrets.g.dart vendor/ @@ -132,6 +135,7 @@ lib/bitcoin_cash/bitcoin_cash.dart lib/nano/nano.dart lib/polygon/polygon.dart lib/solana/solana.dart +lib/tron/tron.dart ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png @@ -152,6 +156,7 @@ assets/images/app_logo.png macos/Runner/Info.plist macos/Runner/DebugProfile.entitlements macos/Runner/Release.entitlements +lib/core/secure_storage.dart macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index eea9b5521..57462099c 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -9,6 +9,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + > getAvailableAccounts({int index = 0, int limit = 5}) async { + final bitcoinLedgerApp = BitcoinLedgerApp(ledger); + + final masterFp = await bitcoinLedgerApp.getMasterFingerprint(device); + print(masterFp); + + final accounts = []; + final indexRange = List.generate(limit, (i) => i + index); + + for (final i in indexRange) { + final derivationPath = "m/84'/0'/$i'"; + final xpub = await bitcoinLedgerApp.getXPubKey(device, derivationPath: derivationPath); + HDWallet hd = HDWallet.fromBase58(xpub).derive(0); + + final address = generateP2WPKHAddress(hd: hd, index: 0, network: BitcoinNetwork.mainnet); + + accounts.add(HardwareAccountData( + address: address, + accountIndex: i, + derivationPath: derivationPath, + masterFingerprint: masterFp, + xpub: xpub, + )); + } + + return accounts; + } +} diff --git a/cw_bitcoin/lib/bitcoin_mnemonic.dart b/cw_bitcoin/lib/bitcoin_mnemonic.dart index 9163fcb11..4a01d6ddc 100644 --- a/cw_bitcoin/lib/bitcoin_mnemonic.dart +++ b/cw_bitcoin/lib/bitcoin_mnemonic.dart @@ -90,8 +90,7 @@ List prefixMatches(String source, List prefixes) { return prefixes.map((prefix) => hx.startsWith(prefix.toLowerCase())).toList(); } -Future generateMnemonic( - {int strength = 264, String prefix = segwit}) async { +Future generateElectrumMnemonic({int strength = 264, String prefix = segwit}) async { final wordBitlen = logBase(wordlist.length, 2).ceil(); final wordCount = strength / wordBitlen; final byteCount = ((wordCount * wordBitlen).ceil() / 8).ceil(); @@ -106,22 +105,29 @@ Future generateMnemonic( return result; } +Future checkIfMnemonicIsElectrum2(String mnemonic) async { + return prefixMatches(mnemonic, [segwit]).first; +} + +Future getMnemonicHash(String mnemonic) async { + final hmacSha512 = Hmac(sha512, utf8.encode('Seed version')); + final digest = hmacSha512.convert(utf8.encode(normalizeText(mnemonic))); + final hx = digest.toString(); + return hx; +} + Future mnemonicToSeedBytes(String mnemonic, {String prefix = segwit}) async { - final pbkdf2 = cryptography.Pbkdf2( - macAlgorithm: cryptography.Hmac.sha512(), - iterations: 2048, - bits: 512); + final pbkdf2 = + cryptography.Pbkdf2(macAlgorithm: cryptography.Hmac.sha512(), iterations: 2048, bits: 512); final text = normalizeText(mnemonic); // pbkdf2.deriveKey(secretKey: secretKey, nonce: nonce) final key = await pbkdf2.deriveKey( - secretKey: cryptography.SecretKey(text.codeUnits), - nonce: 'electrum'.codeUnits); + secretKey: cryptography.SecretKey(text.codeUnits), nonce: 'electrum'.codeUnits); final bytes = await key.extractBytes(); return Uint8List.fromList(bytes); } -bool matchesAnyPrefix(String mnemonic) => - prefixMatches(mnemonic, [segwit]).any((el) => el); +bool matchesAnyPrefix(String mnemonic) => prefixMatches(mnemonic, [segwit]).any((el) => el); bool validateMnemonic(String mnemonic, {String prefix = segwit}) { try { @@ -208,10 +214,8 @@ String removeCJKSpaces(String source) { } String normalizeText(String source) { - final res = removeCombiningCharacters(unorm.nfkd(source).toLowerCase()) - .trim() - .split('/\s+/') - .join(' '); + final res = + removeCombiningCharacters(unorm.nfkd(source).toLowerCase()).trim().split('/\s+/').join(' '); return removeCJKSpaces(res); } diff --git a/cw_bitcoin/lib/bitcoin_transaction_priority.dart b/cw_bitcoin/lib/bitcoin_transaction_priority.dart index d51775368..7c4dcfd5f 100644 --- a/cw_bitcoin/lib/bitcoin_transaction_priority.dart +++ b/cw_bitcoin/lib/bitcoin_transaction_priority.dart @@ -37,7 +37,7 @@ class BitcoinTransactionPriority extends TransactionPriority { switch (this) { case BitcoinTransactionPriority.slow: - label = 'Slow ~24hrs'; // '${S.current.transaction_priority_slow} ~24hrs'; + label = 'Slow ~24hrs+'; // '${S.current.transaction_priority_slow} ~24hrs'; break; case BitcoinTransactionPriority.medium: label = 'Medium'; // S.current.transaction_priority_medium; diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index bf59e8637..f96b0e4da 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -1,8 +1,13 @@ import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:convert/convert.dart'; + import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; +import 'package:cw_bitcoin/psbt_transaction_builder.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:hive/hive.dart'; +import 'package:ledger_bitcoin/ledger_bitcoin.dart'; +import 'package:ledger_flutter/ledger_flutter.dart'; import 'package:mobx/mobx.dart'; import 'package:flutter/foundation.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; @@ -12,6 +17,7 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; +import 'package:bip39/bip39.dart' as bip39; part 'bitcoin_wallet.g.dart'; @@ -19,31 +25,40 @@ class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet; abstract class BitcoinWalletBase extends ElectrumWallet with Store { BitcoinWalletBase({ - required String mnemonic, required String password, required WalletInfo walletInfo, required Box unspentCoinsInfo, - required Uint8List seedBytes, + Uint8List? seedBytes, + String? mnemonic, + String? xpub, String? addressPageType, BasedUtxoNetwork? networkParam, List? initialAddresses, ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, + String? passphrase, }) : super( - mnemonic: mnemonic, - password: password, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfo, - networkType: networkParam == null - ? bitcoin.bitcoin - : networkParam == BitcoinNetwork.mainnet - ? bitcoin.bitcoin - : bitcoin.testnet, - initialAddresses: initialAddresses, - initialBalance: initialBalance, - seedBytes: seedBytes, - currency: CryptoCurrency.btc) { + mnemonic: mnemonic, + passphrase: passphrase, + xpub: xpub, + password: password, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + networkType: networkParam == null + ? bitcoin.bitcoin + : networkParam == BitcoinNetwork.mainnet + ? bitcoin.bitcoin + : bitcoin.testnet, + initialAddresses: initialAddresses, + initialBalance: initialBalance, + seedBytes: seedBytes, + currency: CryptoCurrency.btc) { + // in a standard BIP44 wallet, mainHd derivation path = m/84'/0'/0'/0 (account 0, index unspecified here) + // the sideHd derivation path = m/84'/0'/0'/1 (account 1, index unspecified here) + // String derivationPath = walletInfo.derivationInfo!.derivationPath!; + // String sideDerivationPath = derivationPath.substring(0, derivationPath.length - 1) + "1"; + // final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType); walletAddresses = BitcoinWalletAddresses( walletInfo, electrumClient: electrumClient, @@ -51,7 +66,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, mainHd: hd, - sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/1"), + sideHd: accountHD.derive(1), network: networkParam ?? network, ); autorun((_) { @@ -64,6 +79,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { required String password, required WalletInfo walletInfo, required Box unspentCoinsInfo, + String? passphrase, String? addressPageType, BasedUtxoNetwork? network, List? initialAddresses, @@ -71,14 +87,29 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, }) async { + late Uint8List seedBytes; + + switch (walletInfo.derivationInfo?.derivationType) { + case DerivationType.bip39: + seedBytes = await bip39.mnemonicToSeed( + mnemonic, + passphrase: passphrase ?? "", + ); + break; + case DerivationType.electrum: + default: + seedBytes = await mnemonicToSeedBytes(mnemonic); + break; + } return BitcoinWallet( mnemonic: mnemonic, + passphrase: passphrase ?? "", password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, initialAddresses: initialAddresses, initialBalance: initialBalance, - seedBytes: await mnemonicToSeedBytes(mnemonic), + seedBytes: seedBytes, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: addressPageType, @@ -97,18 +128,91 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { : BitcoinNetwork.mainnet; final snp = await ElectrumWalletSnapshot.load(name, walletInfo.type, password, network); + walletInfo.derivationInfo ??= DerivationInfo( + derivationType: snp.derivationType ?? DerivationType.electrum, + derivationPath: snp.derivationPath, + ); + + // set the default if not present: + walletInfo.derivationInfo!.derivationPath = snp.derivationPath ?? "m/0'/0"; + walletInfo.derivationInfo!.derivationType = snp.derivationType ?? DerivationType.electrum; + + Uint8List? seedBytes = null; + + if (snp.mnemonic != null) { + switch (walletInfo.derivationInfo!.derivationType) { + case DerivationType.electrum: + seedBytes = await mnemonicToSeedBytes(snp.mnemonic!); + break; + case DerivationType.bip39: + default: + seedBytes = await bip39.mnemonicToSeed( + snp.mnemonic!, + passphrase: snp.passphrase ?? '', + ); + break; + } + } + return BitcoinWallet( mnemonic: snp.mnemonic, + xpub: snp.xpub, password: password, + passphrase: snp.passphrase, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, initialAddresses: snp.addresses, initialBalance: snp.balance, - seedBytes: await mnemonicToSeedBytes(snp.mnemonic), + seedBytes: seedBytes, initialRegularAddressIndex: snp.regularAddressIndex, initialChangeAddressIndex: snp.changeAddressIndex, addressPageType: snp.addressPageType, networkParam: network, ); } + + Ledger? _ledger; + LedgerDevice? _ledgerDevice; + BitcoinLedgerApp? _bitcoinLedgerApp; + + void setLedger(Ledger setLedger, LedgerDevice setLedgerDevice) { + _ledger = setLedger; + _ledgerDevice = setLedgerDevice; + _bitcoinLedgerApp = BitcoinLedgerApp(_ledger!, derivationPath: walletInfo.derivationInfo!.derivationPath!); + } + + @override + Future buildHardwareWalletTransaction({ + required List outputs, + required BigInt fee, + required BasedUtxoNetwork network, + required List utxos, + required Map publicKeys, + String? memo, + bool enableRBF = false, + BitcoinOrdering inputOrdering = BitcoinOrdering.bip69, + BitcoinOrdering outputOrdering = BitcoinOrdering.bip69, + }) async { + final masterFingerprint = await _bitcoinLedgerApp!.getMasterFingerprint(_ledgerDevice!); + + final psbtReadyInputs = []; + for (final utxo in utxos) { + final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); + final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; + + psbtReadyInputs.add(PSBTReadyUtxoWithAddress( + utxo: utxo.utxo, + rawTx: rawTx, + ownerDetails: utxo.ownerDetails, + ownerDerivationPath: publicKeyAndDerivationPath.derivationPath, + ownerMasterFingerprint: masterFingerprint, + ownerPublicKey: publicKeyAndDerivationPath.publicKey, + )); + } + + final psbt = PSBTTransactionBuild(inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF); + + final rawHex = await _bitcoinLedgerApp!.signPsbt(_ledgerDevice!, psbt: psbt.psbt); + return BtcTransaction.fromRaw(hex.encode(rawHex)); + } } diff --git a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart index 37b272a1b..915d7cc10 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart @@ -1,23 +1,58 @@ +import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; class BitcoinNewWalletCredentials extends WalletCredentials { - BitcoinNewWalletCredentials({required String name, WalletInfo? walletInfo}) - : super(name: name, walletInfo: walletInfo); + BitcoinNewWalletCredentials( + {required String name, + WalletInfo? walletInfo, + DerivationType? derivationType, + String? derivationPath}) + : super( + name: name, + walletInfo: walletInfo, + ); } class BitcoinRestoreWalletFromSeedCredentials extends WalletCredentials { - BitcoinRestoreWalletFromSeedCredentials( - {required String name, required String password, required this.mnemonic, WalletInfo? walletInfo}) - : super(name: name, password: password, walletInfo: walletInfo); + BitcoinRestoreWalletFromSeedCredentials({ + required String name, + required String password, + required this.mnemonic, + WalletInfo? walletInfo, + required DerivationType derivationType, + required String derivationPath, + String? passphrase, + }) : super( + name: name, + password: password, + passphrase: passphrase, + walletInfo: walletInfo, + derivationInfo: DerivationInfo( + derivationType: derivationType, + derivationPath: derivationPath, + )); final String mnemonic; } class BitcoinRestoreWalletFromWIFCredentials extends WalletCredentials { - BitcoinRestoreWalletFromWIFCredentials( - {required String name, required String password, required this.wif, WalletInfo? walletInfo}) - : super(name: name, password: password, walletInfo: walletInfo); + BitcoinRestoreWalletFromWIFCredentials({ + required String name, + required String password, + required this.wif, + WalletInfo? walletInfo, + }) : super(name: name, password: password, walletInfo: walletInfo); final String wif; -} \ No newline at end of file +} + +class BitcoinRestoreWalletFromHardware extends WalletCredentials { + BitcoinRestoreWalletFromHardware({ + required String name, + required this.hwAccountData, + WalletInfo? walletInfo, + }) : super(name: name, walletInfo: walletInfo); + + final HardwareAccountData hwAccountData; +} diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index 38e769d15..cf99324da 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -12,9 +12,13 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:hive/hive.dart'; import 'package:collection/collection.dart'; +import 'package:bip39/bip39.dart' as bip39; -class BitcoinWalletService extends WalletService { +class BitcoinWalletService extends WalletService< + BitcoinNewWalletCredentials, + BitcoinRestoreWalletFromSeedCredentials, + BitcoinRestoreWalletFromWIFCredentials, + BitcoinRestoreWalletFromHardware> { BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); final Box walletInfoSource; @@ -29,8 +33,9 @@ class BitcoinWalletService extends WalletService restoreFromHardwareWallet(BitcoinRestoreWalletFromHardware credentials, + {bool? isTestnet}) async { + + final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet; + credentials.walletInfo?.network = network.value; + credentials.walletInfo?.derivationInfo?.derivationPath = credentials.hwAccountData.derivationPath; + + final wallet = await BitcoinWallet(password: credentials.password!, + xpub: credentials.hwAccountData.xpub, + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + networkParam: network, + ); + await wallet.save(); + await wallet.init(); + return wallet; + } + @override Future restoreFromKeys(BitcoinRestoreWalletFromWIFCredentials credentials, - {bool? isTestnet}) async => + {bool? isTestnet}) async => throw UnimplementedError(); @override Future restoreFromSeed(BitcoinRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { - if (!validateMnemonic(credentials.mnemonic)) { + if (!validateMnemonic(credentials.mnemonic) && !bip39.validateMnemonic(credentials.mnemonic)) { throw BitcoinMnemonicIsIncorrectException(); } @@ -114,6 +138,7 @@ class BitcoinWalletService extends WalletService> electrum_derivations = { + DerivationType.electrum: [ + DerivationInfo( + derivationType: DerivationType.electrum, + derivationPath: "m/0'", + description: "Electrum", + scriptType: "p2wpkh", + ), + ], + DerivationType.bip39: [ + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/44'/0'/0'", + description: "Standard BIP44", + scriptType: "p2pkh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/49'/0'/0'", + description: "Standard BIP49 compatibility segwit", + scriptType: "p2wpkh-p2sh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/84'/0'/0'", + description: "Standard BIP84 native segwit", + scriptType: "p2wpkh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/0'", + description: "Non-standard legacy", + scriptType: "p2pkh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/0'", + description: "Non-standard compatibility segwit", + scriptType: "p2wpkh-p2sh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/0'", + description: "Non-standard native segwit", + scriptType: "p2wpkh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/44'/0'/0'", + description: "Samourai Deposit", + scriptType: "p2wpkh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/49'/0'/0'", + description: "Samourai Deposit", + scriptType: "p2wpkh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/84'/0'/2147483644'", + description: "Samourai Bad Bank (toxic change)", + scriptType: "p2wpkh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/84'/0'/2147483645'", + description: "Samourai Whirlpool Pre Mix", + scriptType: "p2wpkh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/84'/0'/2147483646'", + description: "Samourai Whirlpool Post Mix", + scriptType: "p2wpkh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/44'/0'/2147483647'", + description: "Samourai Ricochet legacy", + scriptType: "p2pkh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/49'/0'/2147483647'", + description: "Samourai Ricochet compatibility segwit", + scriptType: "p2wpkh-p2sh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/84'/0'/2147483647'", + description: "Samourai Ricochet native segwit", + scriptType: "p2wpkh", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/84'/2'/0'", + description: "Default Litecoin", + scriptType: "p2wpkh", + ), + ], +}; diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 5bed6a449..783eb10d7 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -4,8 +4,8 @@ import 'dart:io'; import 'dart:math'; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin_base; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:collection/collection.dart'; import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; @@ -37,9 +37,9 @@ import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; import 'package:mobx/mobx.dart'; import 'package:rxdart/subjects.dart'; -import 'package:http/http.dart' as http; part 'electrum_wallet.g.dart'; @@ -53,15 +53,16 @@ abstract class ElectrumWalletBase required WalletInfo walletInfo, required Box unspentCoinsInfo, required this.networkType, - required this.mnemonic, - required Uint8List seedBytes, + String? xpub, + String? mnemonic, + Uint8List? seedBytes, + this.passphrase, List? initialAddresses, ElectrumClient? electrumClient, ElectrumBalance? initialBalance, CryptoCurrency? currency}) - : hd = currency == CryptoCurrency.bch - ? bitcoinCashHDWallet(seedBytes) - : bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/0"), + : accountHD = + getAccountHDWallet(currency, networkType, seedBytes, xpub, walletInfo.derivationInfo), syncStatus = NotConnectedSyncStatus(), _password = password, _feeRates = [], @@ -78,20 +79,45 @@ abstract class ElectrumWalletBase this.unspentCoinsInfo = unspentCoinsInfo, this.network = _getNetwork(networkType, currency), this.isTestnet = networkType == bitcoin.testnet, + this._mnemonic = mnemonic, super(walletInfo) { this.electrumClient = electrumClient ?? ElectrumClient(); this.walletInfo = walletInfo; transactionHistory = ElectrumTransactionHistory(walletInfo: walletInfo, password: password); } + static bitcoin.HDWallet getAccountHDWallet( + CryptoCurrency? currency, + bitcoin.NetworkType networkType, + Uint8List? seedBytes, + String? xpub, + DerivationInfo? derivationInfo) { + if (seedBytes == null && xpub == null) { + throw Exception( + "To create a Wallet you need either a seed or an xpub. This should not happen"); + } + + if (seedBytes != null) { + return currency == CryptoCurrency.bch + ? bitcoinCashHDWallet(seedBytes) + : bitcoin.HDWallet.fromSeed(seedBytes, network: networkType) + .derivePath(_hardenedDerivationPath(derivationInfo?.derivationPath ?? "m/0'")); + } + + return bitcoin.HDWallet.fromBase58(xpub!); + } + static bitcoin.HDWallet bitcoinCashHDWallet(Uint8List seedBytes) => - bitcoin.HDWallet.fromSeed(seedBytes).derivePath("m/44'/145'/0'/0"); + bitcoin.HDWallet.fromSeed(seedBytes).derivePath("m/44'/145'/0'"); static int estimatedTransactionSize(int inputsCount, int outputsCounts) => inputsCount * 68 + outputsCounts * 34 + 10; - final bitcoin.HDWallet hd; - final String mnemonic; + final bitcoin.HDWallet accountHD; + final String? _mnemonic; + + bitcoin.HDWallet get hd => accountHD.derive(0); + final String? passphrase; @override @observable @@ -120,10 +146,10 @@ abstract class ElectrumWalletBase .map((addr) => scriptHash(addr.address, network: network)) .toList(); - String get xpub => hd.base58!; + String get xpub => accountHD.base58!; @override - String get seed => mnemonic; + String? get seed => _mnemonic; bitcoin.NetworkType networkType; BasedUtxoNetwork network; @@ -200,22 +226,38 @@ abstract class ElectrumWalletBase int credentialsAmount = 0, }) async { final utxos = []; - List privateKeys = []; + final privateKeys = []; + final publicKeys = {}; + int allInputsAmount = 0; + bool spendsUnconfirmedTX = false; + for (int i = 0; i < unspentCoins.length; i++) { final utx = unspentCoins[i]; - if (utx.isSending) { + if (utx.isSending && !utx.isFrozen) { + if (!spendsUnconfirmedTX) spendsUnconfirmedTX = utx.confirmations == 0; + allInputsAmount += utx.value; final address = addressTypeFromStr(utx.address, network); - final privkey = generateECPrivate( - hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, - index: utx.bitcoinAddressRecord.index, - network: network); + final hd = + utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd; + final derivationPath = + "${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? "m/0'")}" + "/${utx.bitcoinAddressRecord.isHidden ? "1" : "0"}" + "/${utx.bitcoinAddressRecord.index}"; + final pubKeyHex = hd.derive(utx.bitcoinAddressRecord.index).pubKey!; - privateKeys.add(privkey); + publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath); + + if (!walletInfo.isHardwareWallet) { + final privkey = + generateECPrivate(hd: hd, index: utx.bitcoinAddressRecord.index, network: network); + + privateKeys.add(privkey); + } utxos.add( UtxoWithAddress( @@ -226,7 +268,7 @@ abstract class ElectrumWalletBase scriptType: _getScriptType(address), ), ownerDetails: UtxoAddressDetails( - publicKey: privkey.getPublic().toHex(), + publicKey: pubKeyHex, address: address, ), ), @@ -264,6 +306,10 @@ abstract class ElectrumWalletBase // Here, when sending all, the output amount equals to the input value - fee to fully spend every input on the transaction and have no amount left for change int amount = allInputsAmount - fee; + if (amount <= 0) { + throw BitcoinTransactionWrongBalanceException(); + } + // Attempting to send less than the dust limit if (_isBelowDust(amount)) { throw BitcoinTransactionNoDustException(); @@ -283,11 +329,13 @@ abstract class ElectrumWalletBase return EstimatedTxResult( utxos: utxos, privateKeys: privateKeys, + publicKeys: publicKeys, fee: fee, amount: amount, isSendAll: true, hasChange: false, memo: memo, + spendsUnconfirmedTX: spendsUnconfirmedTX, ); } @@ -297,27 +345,48 @@ abstract class ElectrumWalletBase int feeRate, { int? inputsCount, String? memo, + bool? useUnconfirmed, }) async { final utxos = []; - List privateKeys = []; + final privateKeys = []; + final publicKeys = {}; + int allInputsAmount = 0; + bool spendsUnconfirmedTX = false; int leftAmount = credentialsAmount; - final sendingCoins = unspentCoins.where((utx) => utx.isSending).toList(); + final sendingCoins = unspentCoins.where((utx) => utx.isSending && !utx.isFrozen).toList(); + final unconfirmedCoins = sendingCoins.where((utx) => utx.confirmations == 0).toList(); for (int i = 0; i < sendingCoins.length; i++) { final utx = sendingCoins[i]; + final isUncormirmed = utx.confirmations == 0; + if (useUnconfirmed != true && isUncormirmed) continue; + + if (!spendsUnconfirmedTX) spendsUnconfirmedTX = isUncormirmed; + allInputsAmount += utx.value; leftAmount = leftAmount - utx.value; final address = addressTypeFromStr(utx.address, network); - final privkey = generateECPrivate( - hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, - index: utx.bitcoinAddressRecord.index, - network: network); - privateKeys.add(privkey); + final hd = + utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd; + final derivationPath = + "${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? "m/0'")}" + "/${utx.bitcoinAddressRecord.isHidden ? "1" : "0"}" + "/${utx.bitcoinAddressRecord.index}"; + final pubKeyHex = hd.derive(utx.bitcoinAddressRecord.index).pubKey!; + + publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath); + + if (!walletInfo.isHardwareWallet) { + final privkey = + generateECPrivate(hd: hd, index: utx.bitcoinAddressRecord.index, network: network); + + privateKeys.add(privkey); + } utxos.add( UtxoWithAddress( @@ -328,7 +397,7 @@ abstract class ElectrumWalletBase scriptType: _getScriptType(address), ), ownerDetails: UtxoAddressDetails( - publicKey: privkey.getPublic().toHex(), + publicKey: pubKeyHex, address: address, ), ), @@ -345,11 +414,23 @@ abstract class ElectrumWalletBase } final spendingAllCoins = sendingCoins.length == utxos.length; + final spendingAllConfirmedCoins = + !spendsUnconfirmedTX && utxos.length == sendingCoins.length - unconfirmedCoins.length; // How much is being spent - how much is being sent int amountLeftForChangeAndFee = allInputsAmount - credentialsAmount; if (amountLeftForChangeAndFee <= 0) { + if (!spendingAllCoins) { + return estimateTxForAmount( + credentialsAmount, + outputs, + feeRate, + inputsCount: utxos.length + 1, + memo: memo, + useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, + ); + } throw BitcoinTransactionWrongBalanceException(); } @@ -403,6 +484,7 @@ abstract class ElectrumWalletBase feeRate, inputsCount: utxos.length + 1, memo: memo, + useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, ); } @@ -449,6 +531,7 @@ abstract class ElectrumWalletBase feeRate, inputsCount: utxos.length + 1, memo: memo, + useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, ); } } @@ -456,11 +539,13 @@ abstract class ElectrumWalletBase return EstimatedTxResult( utxos: utxos, privateKeys: privateKeys, + publicKeys: publicKeys, fee: fee, amount: amount, hasChange: true, isSendAll: false, memo: memo, + spendsUnconfirmedTX: spendsUnconfirmedTX, ); } @@ -522,6 +607,35 @@ abstract class ElectrumWalletBase ); } + if (walletInfo.isHardwareWallet) { + final transaction = await buildHardwareWalletTransaction( + utxos: estimatedTx.utxos, + outputs: outputs, + publicKeys: estimatedTx.publicKeys, + fee: BigInt.from(estimatedTx.fee), + network: network, + memo: estimatedTx.memo, + outputOrdering: BitcoinOrdering.none, + enableRBF: true, + ); + + return PendingBitcoinTransaction( + transaction, + type, + electrumClient: electrumClient, + amount: estimatedTx.amount, + fee: estimatedTx.fee, + feeRate: feeRateInt.toString(), + network: network, + hasChange: estimatedTx.hasChange, + isSendAll: estimatedTx.isSendAll, + hasTaprootInputs: false, // ToDo: (Konsti) Support Taproot + )..addListener((transaction) async { + transactionHistory.addOne(transaction); + await updateBalance(); + }); + } + BasedBitcoinTransacationBuilder txb; if (network is BitcoinCashNetwork) { txb = ForkedTransactionBuilder( @@ -531,7 +645,7 @@ abstract class ElectrumWalletBase network: network, memo: estimatedTx.memo, outputOrdering: BitcoinOrdering.none, - enableRBF: true, + enableRBF: !estimatedTx.spendsUnconfirmedTX, ); } else { txb = BitcoinTransactionBuilder( @@ -541,7 +655,7 @@ abstract class ElectrumWalletBase network: network, memo: estimatedTx.memo, outputOrdering: BitcoinOrdering.none, - enableRBF: true, + enableRBF: !estimatedTx.spendsUnconfirmedTX, ); } @@ -583,8 +697,23 @@ abstract class ElectrumWalletBase } } + Future buildHardwareWalletTransaction({ + required List outputs, + required BigInt fee, + required BasedUtxoNetwork network, + required List utxos, + required Map publicKeys, + String? memo, + bool enableRBF = false, + BitcoinOrdering inputOrdering = BitcoinOrdering.bip69, + BitcoinOrdering outputOrdering = BitcoinOrdering.bip69, + }) async => + throw UnimplementedError(); + String toJSON() => json.encode({ - 'mnemonic': mnemonic, + 'mnemonic': _mnemonic, + 'xpub': xpub, + 'passphrase': passphrase ?? '', 'account_index': walletAddresses.currentReceiveAddressIndexByType, 'change_address_index': walletAddresses.currentChangeAddressIndexByType, 'addresses': walletAddresses.allAddresses.map((addr) => addr.toJSON()).toList(), @@ -592,6 +721,8 @@ abstract class ElectrumWalletBase ? SegwitAddresType.p2wpkh.toString() : walletInfo.addressPageType.toString(), 'balance': balance[currency]?.toJSON(), + 'derivationTypeIndex': walletInfo.derivationInfo?.derivationType?.index, + 'derivationPath': walletInfo.derivationInfo?.derivationPath, }); int feeRate(TransactionPriority priority) { @@ -721,6 +852,7 @@ abstract class ElectrumWalletBase final tx = await fetchTransactionInfo( hash: coin.hash, height: 0, myAddresses: addressesSet); coin.isChange = tx?.direction == TransactionDirection.outgoing; + coin.confirmations = tx?.confirmations; updatedUnspentCoins.add(coin); } catch (_) {} })))); @@ -745,6 +877,7 @@ abstract class ElectrumWalletBase coin.isFrozen = coinInfo.isFrozen; coin.isSending = coinInfo.isSending; coin.note = coinInfo.note; + coin.bitcoinAddressRecord.balance += coinInfo.value; } else { _addCoinInfo(coin); } @@ -1037,9 +1170,11 @@ abstract class ElectrumWalletBase return Future.wait(addressesByType.map((addressRecord) async { final history = await _fetchAddressHistory(addressRecord, addressesSet, currentHeight); + final balance = await electrumClient.getBalance(addressRecord.scriptHash!); if (history.isNotEmpty) { addressRecord.txCount = history.length; + addressRecord.balance = balance['confirmed'] as int? ?? 0; historiesWithDetails.addAll(history); final matchedAddresses = @@ -1221,7 +1356,7 @@ abstract class ElectrumWalletBase void setExceptionHandler(void Function(FlutterErrorDetails) onError) => _onError = onError; @override - String signMessage(String message, {String? address = null}) { + Future signMessage(String message, {String? address = null}) async { final index = address != null ? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index : null; @@ -1244,6 +1379,9 @@ abstract class ElectrumWalletBase return BitcoinNetwork.mainnet; } + + static String _hardenedDerivationPath(String derivationPath) => + derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1); } class EstimateTxParams { @@ -1265,20 +1403,31 @@ class EstimatedTxResult { EstimatedTxResult({ required this.utxos, required this.privateKeys, + required this.publicKeys, required this.fee, required this.amount, required this.hasChange, required this.isSendAll, this.memo, + required this.spendsUnconfirmedTX, }); final List utxos; final List privateKeys; + final Map publicKeys; // PubKey to derivationPath final int fee; final int amount; final bool hasChange; final bool isSendAll; final String? memo; + final bool spendsUnconfirmedTX; +} + +class PublicKeyWithDerivationPath { + const PublicKeyWithDerivationPath(this.publicKey, this.derivationPath); + + final String derivationPath; + final String publicKey; } BitcoinBaseAddress addressTypeFromStr(String address, BasedUtxoNetwork network) { diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 6f76ab312..340b17cfb 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -3,6 +3,7 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/utils/file.dart'; import 'package:cw_core/wallet_type.dart'; @@ -12,11 +13,15 @@ class ElectrumWalletSnapshot { required this.type, required this.password, required this.mnemonic, + required this.xpub, required this.addresses, required this.balance, required this.regularAddressIndex, required this.changeAddressIndex, required this.addressPageType, + this.passphrase, + this.derivationType, + this.derivationPath, }); final String name; @@ -24,11 +29,15 @@ class ElectrumWalletSnapshot { final WalletType type; final String? addressPageType; - String mnemonic; + String? mnemonic; + String? xpub; List addresses; ElectrumBalance balance; Map regularAddressIndex; Map changeAddressIndex; + String? passphrase; + DerivationType? derivationType; + String? derivationPath; static Future load( String name, WalletType type, String password, BasedUtxoNetwork network) async { @@ -36,7 +45,9 @@ class ElectrumWalletSnapshot { final jsonSource = await read(path: path, password: password); final data = json.decode(jsonSource) as Map; final addressesTmp = data['addresses'] as List? ?? []; - final mnemonic = data['mnemonic'] as String; + final mnemonic = data['mnemonic'] as String?; + final xpub = data['xpub'] as String?; + final passphrase = data['passphrase'] as String? ?? ''; final addresses = addressesTmp .whereType() .map((addr) => BitcoinAddressRecord.fromJSON(addr, network)) @@ -46,6 +57,10 @@ class ElectrumWalletSnapshot { var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; var changeAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; + final derivationType = + DerivationType.values[(data['derivationTypeIndex'] as int?) ?? DerivationType.electrum.index]; + final derivationPath = data['derivationPath'] as String? ?? "m/0'/0"; + try { regularAddressIndexByType = { SegwitAddresType.p2wpkh.toString(): int.parse(data['account_index'] as String? ?? '0') @@ -65,12 +80,16 @@ class ElectrumWalletSnapshot { name: name, type: type, password: password, + passphrase: passphrase, mnemonic: mnemonic, + xpub: xpub, addresses: addresses, balance: balance, regularAddressIndex: regularAddressIndexByType, changeAddressIndex: changeAddressIndexByType, addressPageType: data['address_page_type'] as String?, + derivationType: derivationType, + derivationPath: derivationPath, ); } } diff --git a/cw_bitcoin/lib/exceptions.dart b/cw_bitcoin/lib/exceptions.dart index 4b03eb922..979c1a433 100644 --- a/cw_bitcoin/lib/exceptions.dart +++ b/cw_bitcoin/lib/exceptions.dart @@ -15,7 +15,9 @@ class BitcoinTransactionNoDustOnChangeException extends TransactionNoDustOnChang BitcoinTransactionNoDustOnChangeException(super.max, super.min); } -class BitcoinTransactionCommitFailed extends TransactionCommitFailed {} +class BitcoinTransactionCommitFailed extends TransactionCommitFailed { + BitcoinTransactionCommitFailed({super.errorMessage}); +} class BitcoinTransactionCommitFailedDustChange extends TransactionCommitFailedDustChange {} diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index d2379d5a5..2ffb99405 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -15,6 +15,7 @@ import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/litecoin_network.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:bip39/bip39.dart' as bip39; part 'litecoin_wallet.g.dart'; @@ -49,7 +50,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, mainHd: hd, - sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/1"), + sideHd: accountHD.derive(1), network: network, ); autorun((_) { @@ -62,11 +63,26 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { required String password, required WalletInfo walletInfo, required Box unspentCoinsInfo, + String? passphrase, String? addressPageType, List? initialAddresses, ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex}) async { + late Uint8List seedBytes; + + switch (walletInfo.derivationInfo?.derivationType) { + case DerivationType.bip39: + seedBytes = await bip39.mnemonicToSeed( + mnemonic, + passphrase: passphrase ?? "", + ); + break; + case DerivationType.electrum: + default: + seedBytes = await mnemonicToSeedBytes(mnemonic); + break; + } return LitecoinWallet( mnemonic: mnemonic, password: password, @@ -74,7 +90,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { unspentCoinsInfo: unspentCoinsInfo, initialAddresses: initialAddresses, initialBalance: initialBalance, - seedBytes: await mnemonicToSeedBytes(mnemonic), + seedBytes: seedBytes, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: addressPageType, @@ -90,13 +106,13 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final snp = await ElectrumWalletSnapshot.load(name, walletInfo.type, password, LitecoinNetwork.mainnet); return LitecoinWallet( - mnemonic: snp.mnemonic, + mnemonic: snp.mnemonic!, password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, initialAddresses: snp.addresses, initialBalance: snp.balance, - seedBytes: await mnemonicToSeedBytes(snp.mnemonic), + seedBytes: await mnemonicToSeedBytes(snp.mnemonic!), initialRegularAddressIndex: snp.regularAddressIndex, initialChangeAddressIndex: snp.changeAddressIndex, addressPageType: snp.addressPageType, diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index ee3b0e628..bb51a4eaa 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -11,11 +11,12 @@ import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:collection/collection.dart'; +import 'package:bip39/bip39.dart' as bip39; class LitecoinWalletService extends WalletService< BitcoinNewWalletCredentials, BitcoinRestoreWalletFromSeedCredentials, - BitcoinRestoreWalletFromWIFCredentials> { + BitcoinRestoreWalletFromWIFCredentials,BitcoinNewWalletCredentials> { LitecoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); final Box walletInfoSource; @@ -27,8 +28,9 @@ class LitecoinWalletService extends WalletService< @override Future create(BitcoinNewWalletCredentials credentials, {bool? isTestnet}) async { final wallet = await LitecoinWalletBase.create( - mnemonic: await generateMnemonic(), + mnemonic: await generateElectrumMnemonic(), password: credentials.password!, + passphrase: credentials.passphrase, walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); await wallet.save(); @@ -92,6 +94,11 @@ class LitecoinWalletService extends WalletService< await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); } + @override + Future restoreFromHardwareWallet(BitcoinNewWalletCredentials credentials) { + throw UnimplementedError("Restoring a Litecoin wallet from a hardware wallet is not yet supported!"); + } + @override Future restoreFromKeys( BitcoinRestoreWalletFromWIFCredentials credentials, {bool? isTestnet}) async => @@ -100,12 +107,13 @@ class LitecoinWalletService extends WalletService< @override Future restoreFromSeed( BitcoinRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { - if (!validateMnemonic(credentials.mnemonic)) { + if (!validateMnemonic(credentials.mnemonic) && !bip39.validateMnemonic(credentials.mnemonic)) { throw LitecoinMnemonicIsIncorrectException(); } final wallet = await LitecoinWalletBase.create( password: credentials.password!, + passphrase: credentials.passphrase, mnemonic: credentials.mnemonic, walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 529ac61da..a59b4f429 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -73,7 +73,9 @@ class PendingBitcoinTransaction with PendingTransaction { if (error.contains("bad-txns-vout-negative")) { throw BitcoinTransactionCommitFailedVoutNegative(); } + throw BitcoinTransactionCommitFailed(errorMessage: error); } + throw BitcoinTransactionCommitFailed(); } diff --git a/cw_bitcoin/lib/psbt_transaction_builder.dart b/cw_bitcoin/lib/psbt_transaction_builder.dart new file mode 100644 index 000000000..d8d2c9fac --- /dev/null +++ b/cw_bitcoin/lib/psbt_transaction_builder.dart @@ -0,0 +1,96 @@ +import 'dart:typed_data'; + +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:convert/convert.dart'; +import 'package:ledger_bitcoin/psbt.dart'; + +class PSBTTransactionBuild { + final PsbtV2 psbt = PsbtV2(); + + PSBTTransactionBuild( + {required List inputs, required List outputs, bool enableRBF = true}) { + psbt.setGlobalTxVersion(2); + psbt.setGlobalInputCount(inputs.length); + psbt.setGlobalOutputCount(outputs.length); + + for (var i = 0; i < inputs.length; i++) { + final input = inputs[i]; + + print(input.utxo.isP2tr()); + print(input.utxo.isSegwit()); + print(input.utxo.isP2shSegwit()); + + psbt.setInputPreviousTxId(i, Uint8List.fromList(hex.decode(input.utxo.txHash).reversed.toList())); + psbt.setInputOutputIndex(i, input.utxo.vout); + psbt.setInputSequence(i, enableRBF ? 0x1 : 0xffffffff); + + + if (input.utxo.isSegwit()) { + setInputSegwit(i, input); + } else if (input.utxo.isP2shSegwit()) { + setInputP2shSegwit(i, input); + } else if (input.utxo.isP2tr()) { + // ToDo: (Konsti) Handle Taproot Inputs + } else { + setInputP2pkh(i, input); + } + } + + for (var i = 0; i < outputs.length; i++) { + final output = outputs[i]; + + if (output is BitcoinOutput) { + psbt.setOutputScript(i, Uint8List.fromList(output.address.toScriptPubKey().toBytes())); + psbt.setOutputAmount(i, output.value.toInt()); + } + } + } + + void setInputP2pkh(int i, PSBTReadyUtxoWithAddress input) { + psbt.setInputNonWitnessUtxo(i, Uint8List.fromList(hex.decode(input.rawTx))); + psbt.setInputBip32Derivation( + i, + Uint8List.fromList(hex.decode(input.ownerPublicKey)), + input.ownerMasterFingerprint, + BIPPath.fromString(input.ownerDerivationPath).toPathArray()); + } + + void setInputSegwit(int i, PSBTReadyUtxoWithAddress input) { + psbt.setInputNonWitnessUtxo(i, Uint8List.fromList(hex.decode(input.rawTx))); + psbt.setInputBip32Derivation( + i, + Uint8List.fromList(hex.decode(input.ownerPublicKey)), + input.ownerMasterFingerprint, + BIPPath.fromString(input.ownerDerivationPath).toPathArray()); + + psbt.setInputWitnessUtxo(i, Uint8List.fromList(bigIntToUint64LE(input.utxo.value)), + Uint8List.fromList(input.ownerDetails.address.toScriptPubKey().toBytes())); + } + + void setInputP2shSegwit(int i, PSBTReadyUtxoWithAddress input) { + psbt.setInputNonWitnessUtxo(i, Uint8List.fromList(hex.decode(input.rawTx))); + psbt.setInputBip32Derivation(i, Uint8List.fromList(hex.decode(input.ownerPublicKey)), + input.ownerMasterFingerprint, BIPPath.fromString(input.ownerDerivationPath).toPathArray()); + + psbt.setInputRedeemScript( + i, Uint8List.fromList(input.ownerDetails.address.toScriptPubKey().toBytes())); + psbt.setInputWitnessUtxo(i, Uint8List.fromList(bigIntToUint64LE(input.utxo.value)), + Uint8List.fromList(input.ownerDetails.address.toScriptPubKey().toBytes())); + } +} + +class PSBTReadyUtxoWithAddress extends UtxoWithAddress { + final String rawTx; + final String ownerDerivationPath; + final Uint8List ownerMasterFingerprint; + final String ownerPublicKey; + + PSBTReadyUtxoWithAddress({ + required super.utxo, + required this.rawTx, + required super.ownerDetails, + required this.ownerDerivationPath, + required this.ownerMasterFingerprint, + required this.ownerPublicKey, + }); +} diff --git a/cw_bitcoin/lib/utils.dart b/cw_bitcoin/lib/utils.dart index b156ccba3..b3707e764 100644 --- a/cw_bitcoin/lib/utils.dart +++ b/cw_bitcoin/lib/utils.dart @@ -5,29 +5,58 @@ import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; import 'package:hex/hex.dart'; -bitcoin.PaymentData generatePaymentData({required bitcoin.HDWallet hd, required int index}) => - PaymentData(pubkey: Uint8List.fromList(HEX.decode(hd.derive(index).pubKey!))); +bitcoin.PaymentData generatePaymentData({required bitcoin.HDWallet hd, int? index}) { + final pubKey = index != null ? hd.derive(index).pubKey! : hd.pubKey!; + return PaymentData(pubkey: Uint8List.fromList(HEX.decode(pubKey))); +} ECPrivate generateECPrivate( - {required bitcoin.HDWallet hd, required int index, required BasedUtxoNetwork network}) => - ECPrivate.fromWif(hd.derive(index).wif!, netVersion: network.wifNetVer); + {required bitcoin.HDWallet hd, required BasedUtxoNetwork network, int? index}) { + final wif = index != null ? hd.derive(index).wif! : hd.wif!; + return ECPrivate.fromWif(wif, netVersion: network.wifNetVer); +} -String generateP2WPKHAddress( - {required bitcoin.HDWallet hd, required int index, required BasedUtxoNetwork network}) => - ECPublic.fromHex(hd.derive(index).pubKey!).toP2wpkhAddress().toAddress(network); +String generateP2WPKHAddress({ + required bitcoin.HDWallet hd, + required BasedUtxoNetwork network, + int? index, +}) { + final pubKey = index != null ? hd.derive(index).pubKey! : hd.pubKey!; + return ECPublic.fromHex(pubKey).toP2wpkhAddress().toAddress(network); +} -String generateP2SHAddress( - {required bitcoin.HDWallet hd, required int index, required BasedUtxoNetwork network}) => - ECPublic.fromHex(hd.derive(index).pubKey!).toP2wpkhInP2sh().toAddress(network); +String generateP2SHAddress({ + required bitcoin.HDWallet hd, + required BasedUtxoNetwork network, + int? index, +}) { + final pubKey = index != null ? hd.derive(index).pubKey! : hd.pubKey!; + return ECPublic.fromHex(pubKey).toP2wpkhInP2sh().toAddress(network); +} -String generateP2WSHAddress( - {required bitcoin.HDWallet hd, required int index, required BasedUtxoNetwork network}) => - ECPublic.fromHex(hd.derive(index).pubKey!).toP2wshAddress().toAddress(network); +String generateP2WSHAddress({ + required bitcoin.HDWallet hd, + required BasedUtxoNetwork network, + int? index, +}) { + final pubKey = index != null ? hd.derive(index).pubKey! : hd.pubKey!; + return ECPublic.fromHex(pubKey).toP2wshAddress().toAddress(network); +} -String generateP2PKHAddress( - {required bitcoin.HDWallet hd, required int index, required BasedUtxoNetwork network}) => - ECPublic.fromHex(hd.derive(index).pubKey!).toP2pkhAddress().toAddress(network); +String generateP2PKHAddress({ + required bitcoin.HDWallet hd, + required BasedUtxoNetwork network, + int? index, +}) { + final pubKey = index != null ? hd.derive(index).pubKey! : hd.pubKey!; + return ECPublic.fromHex(pubKey).toP2pkhAddress().toAddress(network); +} -String generateP2TRAddress( - {required bitcoin.HDWallet hd, required int index, required BasedUtxoNetwork network}) => - ECPublic.fromHex(hd.derive(index).pubKey!).toTaprootAddress().toAddress(network); +String generateP2TRAddress({ + required bitcoin.HDWallet hd, + required BasedUtxoNetwork network, + int? index, +}) { + final pubKey = index != null ? hd.derive(index).pubKey! : hd.pubKey!; + return ECPublic.fromHex(pubKey).toTaprootAddress().toAddress(network); +} diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 3d828243c..e6f0b34dd 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.0" asn1lib: dependency: transitive description: @@ -80,7 +80,7 @@ packages: description: path: "." ref: cake-update-v2 - resolved-ref: "3fd81d238b990bb767fc7a4fdd5053a22a142e2e" + resolved-ref: "01d844a5f5a520a31df5254e34169af4664aa769" url: "https://github.com/cake-tech/bitcoin_base.git" source: git version: "4.2.0" @@ -153,10 +153,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.9" build_runner_core: dependency: transitive description: @@ -177,10 +177,10 @@ packages: dependency: transitive description: name: built_value - sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6 + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.9.0" + version: "8.9.2" characters: dependency: transitive description: @@ -217,10 +217,10 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.18.0" convert: dependency: transitive description: @@ -260,6 +260,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.4" + dart_varuint_bitcoin: + dependency: transitive + description: + name: dart_varuint_bitcoin + sha256: "4f0ccc9733fb54148b9d3688eea822b7aaabf5cc00025998f8c09a1d45b31b4b" + url: "https://pub.dev" + source: hosted + version: "1.0.3" encrypt: dependency: transitive description: @@ -309,10 +317,18 @@ packages: dependency: "direct main" description: name: flutter_mobx - sha256: "4a5d062ff85ed3759f4aac6410ff0ffae32e324b2e71ca722ae1b37b32e865f4" + sha256: "859fbf452fa9c2519d2700b125dd7fb14c508bbdd7fb65e26ca8ff6c92280e2e" url: "https://pub.dev" source: hosted - version: "2.2.0+2" + version: "2.2.1+1" + flutter_reactive_ble: + dependency: transitive + description: + name: flutter_reactive_ble + sha256: "247e2efa76de203d1ba11335c13754b5b9d0504b5423e5b0c93a600f016b24e0" + url: "https://pub.dev" + source: hosted + version: "5.3.1" flutter_test: dependency: "direct dev" description: flutter @@ -322,10 +338,18 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" + functional_data: + dependency: transitive + description: + name: functional_data + sha256: aefdec4365452283b2a7cf420a3169654d51d3e9553069a22d76680d7a9d7c3d + url: "https://pub.dev" + source: hosted + version: "1.1.1" glob: dependency: transitive description: @@ -422,6 +446,55 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + ledger_bitcoin: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: b6ed573cbeb57d5f0d39dfe4254bf9d15b620ab6 + url: "https://github.com/cake-tech/ledger-bitcoin.git" + source: git + version: "0.0.1" + ledger_flutter: + dependency: "direct main" + description: + name: ledger_flutter + sha256: f1680060ed6ff78f275837e0024ccaf667715a59ba7aa29fa7354bc7752e71c8 + url: "https://pub.dev" + source: hosted + version: "1.0.1" + ledger_usb: + dependency: transitive + description: + name: ledger_usb + sha256: "52c92d03a4cffe06c82921c8e2f79f3cdad6e1cf78e1e9ca35444196ff8f14c2" + url: "https://pub.dev" + source: hosted + version: "1.0.0" logging: dependency: transitive description: @@ -434,26 +507,26 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.11.0" mime: dependency: transitive description: @@ -466,10 +539,10 @@ packages: dependency: "direct main" description: name: mobx - sha256: "74ee54012dc7c1b3276eaa960a600a7418ef5f9997565deb8fca1fd88fb36b78" + sha256: "63920b27b32ad1910adfe767ab1750e4c212e8923232a1f891597b362074ea5e" url: "https://pub.dev" source: hosted - version: "2.3.0+1" + version: "2.3.3+2" mobx_codegen: dependency: "direct dev" description: @@ -498,10 +571,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_provider: dependency: "direct main" description: @@ -570,10 +643,10 @@ packages: dependency: transitive description: name: pointycastle - sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" + sha256: "70fe966348fe08c34bf929582f1d8247d9d9408130723206472b4687227e4333" url: "https://pub.dev" source: hosted - version: "3.7.4" + version: "3.8.0" pool: dependency: transitive description: @@ -582,14 +655,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "01dd9bd0fa02548bf2ceee13545d4a0ec6046459d847b6b061d8a27237108a08" + url: "https://pub.dev" + source: hosted + version: "2.1.0" provider: dependency: transitive description: name: provider - sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c url: "https://pub.dev" source: hosted - version: "6.1.1" + version: "6.1.2" pub_semver: dependency: transitive description: @@ -606,6 +687,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.3" + reactive_ble_mobile: + dependency: transitive + description: + name: reactive_ble_mobile + sha256: "9ec2b4c9c725e439950838d551579750060258fbccd5536d0543b4d07d225798" + url: "https://pub.dev" + source: hosted + version: "5.3.1" + reactive_ble_platform_interface: + dependency: transitive + description: + name: reactive_ble_platform_interface + sha256: "632c92401a2d69c9b94bd48f8fd47488a7013f3d1f9b291884350291a4a81813" + url: "https://pub.dev" + source: hosted + version: "5.3.1" rxdart: dependency: "direct main" description: @@ -663,26 +760,26 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -711,10 +808,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.1" timing: dependency: transitive description: @@ -747,8 +844,16 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - watcher: + vm_service: dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" + watcher: + dependency: "direct overridden" description: name: watcher sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" @@ -788,5 +893,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.0.0 <4.0.0" + dart: ">=3.2.0-0 <4.0.0" flutter: ">=3.10.0" diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 632a3140a..84254b5b5 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -35,15 +35,22 @@ dependencies: url: https://github.com/cake-tech/bitcoin_base.git ref: cake-update-v2 blockchain_utils: ^2.1.1 + ledger_flutter: ^1.0.1 + ledger_bitcoin: + git: + url: https://github.com/cake-tech/ledger-bitcoin.git dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.1.11 + build_runner: ^2.4.7 build_resolvers: ^2.0.9 mobx_codegen: ^2.0.7 hive_generator: ^1.1.3 +dependency_overrides: + watcher: ^1.1.0 + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index 1f04e5624..d58144f1e 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -51,7 +51,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, mainHd: hd, - sideHd: bitcoin.HDWallet.fromSeed(seedBytes).derivePath("m/44'/145'/0'/1"), + sideHd: accountHD.derive(1), network: network, initialAddressPageType: addressPageType, ); @@ -93,7 +93,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { final snp = await ElectrumWalletSnapshot.load( name, walletInfo.type, password, BitcoinCashNetwork.mainnet); return BitcoinCashWallet( - mnemonic: snp.mnemonic, + mnemonic: snp.mnemonic!, password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, @@ -118,7 +118,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { } }).toList(), initialBalance: snp.balance, - seedBytes: await Mnemonic.toSeed(snp.mnemonic), + seedBytes: await Mnemonic.toSeed(snp.mnemonic!), initialRegularAddressIndex: snp.regularAddressIndex, initialChangeAddressIndex: snp.changeAddressIndex, addressPageType: P2pkhAddressType.p2pkh, @@ -166,7 +166,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { } @override - String signMessage(String message, {String? address = null}) { + Future signMessage(String message, {String? address = null}) async { final index = address != null ? walletAddresses.allAddresses .firstWhere((element) => element.address == AddressUtils.toLegacyAddress(address)) diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart index df8e841f8..e6c0cad07 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart @@ -12,7 +12,7 @@ import 'package:collection/collection.dart'; import 'package:hive/hive.dart'; class BitcoinCashWalletService extends WalletService { + BitcoinCashRestoreWalletFromSeedCredentials, BitcoinCashRestoreWalletFromWIFCredentials, BitcoinCashNewWalletCredentials> { BitcoinCashWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); final Box walletInfoSource; @@ -93,6 +93,11 @@ class BitcoinCashWalletService extends WalletService restoreFromHardwareWallet(BitcoinCashNewWalletCredentials credentials) { + throw UnimplementedError("Restoring a Bitcoin Cash wallet from a hardware wallet is not yet supported!"); + } + @override Future restoreFromKeys(credentials, {bool? isTestnet}) { // TODO: implement restoreFromKeys diff --git a/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart b/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart index da4710a8b..6d2ab4696 100644 --- a/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart +++ b/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart @@ -62,7 +62,9 @@ class PendingBitcoinCashTransaction with PendingTransaction { if (error.contains("bad-txns-vout-negative")) { throw BitcoinTransactionCommitFailedVoutNegative(); } + throw BitcoinTransactionCommitFailed(errorMessage: error); } + throw BitcoinTransactionCommitFailed(); } diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index 37827f1ba..ceef539c3 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -39,10 +39,13 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.1.11 + build_runner: ^2.4.7 mobx_codegen: ^2.0.7 hive_generator: ^1.1.3 +dependency_overrides: + watcher: ^1.1.0 + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index f1c1cd8ae..fabc84051 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -103,6 +103,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen CryptoCurrency.kaspa, CryptoCurrency.digibyte, CryptoCurrency.usdtSol, + CryptoCurrency.usdcTrc20, ]; static const havenCurrencies = [ @@ -217,6 +218,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const kaspa = CryptoCurrency(title: 'KAS', fullName: 'Kaspa', raw: 89, name: 'kas', iconPath: 'assets/images/kaspa_icon.png', decimals: 8); static const digibyte = CryptoCurrency(title: 'DGB', fullName: 'DigiByte', raw: 90, name: 'dgb', iconPath: 'assets/images/digibyte.png', decimals: 8); static const usdtSol = CryptoCurrency(title: 'USDT', tag: 'SOL', fullName: 'USDT Tether', raw: 91, name: 'usdtsol', iconPath: 'assets/images/usdt_icon.png', decimals: 6); + static const usdcTrc20 = CryptoCurrency(title: 'USDC', tag: 'TRX', fullName: 'USDC Coin', raw: 92, name: 'usdctrc20', iconPath: 'assets/images/usdc_icon.png', decimals: 6); static final Map _rawCurrencyMap = @@ -257,10 +259,16 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen element.tag == walletCurrency?.tag)); } catch (_) {} + // search by fullName if not found by title: + try { + return CryptoCurrency.all.firstWhere((element) => element.fullName?.toLowerCase() == name); + } catch (_) {} + if (CryptoCurrency._nameCurrencyMap[name.toLowerCase()] == null) { final s = 'Unexpected token: $name for CryptoCurrency fromString'; throw ArgumentError.value(name, 'name', s); } + return CryptoCurrency._nameCurrencyMap[name.toLowerCase()]!; } diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart index 58ee37669..92e78b2e6 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -23,7 +23,10 @@ CryptoCurrency currencyForWalletType(WalletType type) { return CryptoCurrency.maticpoly; case WalletType.solana: return CryptoCurrency.sol; + case WalletType.tron: + return CryptoCurrency.trx; default: - throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType'); + throw Exception( + 'Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType'); } } diff --git a/cw_core/lib/exceptions.dart b/cw_core/lib/exceptions.dart index 848ac40e6..d07da8109 100644 --- a/cw_core/lib/exceptions.dart +++ b/cw_core/lib/exceptions.dart @@ -19,7 +19,11 @@ class TransactionNoDustOnChangeException implements Exception { final String min; } -class TransactionCommitFailed implements Exception {} +class TransactionCommitFailed implements Exception { + final String? errorMessage; + + TransactionCommitFailed({this.errorMessage}); +} class TransactionCommitFailedDustChange implements Exception {} diff --git a/cw_core/lib/hardware/device_connection_type.dart b/cw_core/lib/hardware/device_connection_type.dart new file mode 100644 index 000000000..99fd5b1f0 --- /dev/null +++ b/cw_core/lib/hardware/device_connection_type.dart @@ -0,0 +1,28 @@ +import 'package:cw_core/wallet_type.dart'; + +enum DeviceConnectionType { + usb, + ble; + + static List supportedConnectionTypes(WalletType walletType, + [bool isIOS = false]) { + switch (walletType) { + case WalletType.bitcoin: + case WalletType.ethereum: + case WalletType.polygon: + if (isIOS) return [DeviceConnectionType.ble]; + return [DeviceConnectionType.ble, DeviceConnectionType.usb]; + default: + return []; + } + } + + String get iconString { + switch (this) { + case ble: + return 'assets/images/bluetooth.png'; + case usb: + return 'assets/images/usb.png'; + } + } +} diff --git a/cw_core/lib/hardware/device_not_connected_exception.dart b/cw_core/lib/hardware/device_not_connected_exception.dart new file mode 100644 index 000000000..bc2a1b095 --- /dev/null +++ b/cw_core/lib/hardware/device_not_connected_exception.dart @@ -0,0 +1,7 @@ +class DeviceNotConnectedException implements Exception { + final String message; + + DeviceNotConnectedException({ + this.message = '', + }); +} diff --git a/cw_core/lib/hardware/hardware_account_data.dart b/cw_core/lib/hardware/hardware_account_data.dart new file mode 100644 index 000000000..a9562f8be --- /dev/null +++ b/cw_core/lib/hardware/hardware_account_data.dart @@ -0,0 +1,19 @@ +import 'dart:typed_data'; + +class HardwareAccountData { + HardwareAccountData({ + required this.address, + required this.accountIndex, + required this.derivationPath, + this.xpub, + this.masterFingerprint, + }); + + final String address; + final int accountIndex; + final String derivationPath; + + // Bitcoin Specific + final Uint8List? masterFingerprint; + final String? xpub; +} diff --git a/cw_core/lib/hive_type_ids.dart b/cw_core/lib/hive_type_ids.dart index 3fa2eb647..4da616a79 100644 --- a/cw_core/lib/hive_type_ids.dart +++ b/cw_core/lib/hive_type_ids.dart @@ -14,4 +14,7 @@ const ERC20_TOKEN_TYPE_ID = 12; const NANO_ACCOUNT_TYPE_ID = 13; const POW_NODE_TYPE_ID = 14; const DERIVATION_TYPE_TYPE_ID = 15; -const SPL_TOKEN_TYPE_ID = 16; +const SPL_TOKEN_TYPE_ID = 16; +const DERIVATION_INFO_TYPE_ID = 17; +const TRON_TOKEN_TYPE_ID = 18; +const HARDWARE_WALLET_TYPE_TYPE_ID = 19; diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index d7e91d692..1195b6819 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -10,7 +10,7 @@ import 'package:http/io_client.dart' as ioc; part 'node.g.dart'; -Uri createUriFromElectrumAddress(String address) => Uri.tryParse('tcp://$address')!; +Uri createUriFromElectrumAddress(String address, String path) => Uri.tryParse('tcp://$address$path')!; @HiveType(typeId: Node.typeId) class Node extends HiveObject with Keyable { @@ -83,7 +83,7 @@ class Node extends HiveObject with Keyable { case WalletType.bitcoin: case WalletType.litecoin: case WalletType.bitcoinCash: - return createUriFromElectrumAddress(uriRaw); + return createUriFromElectrumAddress(uriRaw, path ?? ''); case WalletType.nano: case WalletType.banano: if (isSSL) { @@ -94,7 +94,8 @@ class Node extends HiveObject with Keyable { case WalletType.ethereum: case WalletType.polygon: case WalletType.solana: - return Uri.https(uriRaw, ''); + case WalletType.tron: + return Uri.https(uriRaw, path ?? ''); default: throw Exception('Unexpected type ${type.toString()} for Node uri'); } @@ -152,6 +153,7 @@ class Node extends HiveObject with Keyable { case WalletType.ethereum: case WalletType.polygon: case WalletType.solana: + case WalletType.tron: return requestElectrumServer(); default: return false; diff --git a/cw_core/lib/unspent_transaction_output.dart b/cw_core/lib/unspent_transaction_output.dart index b52daf43c..595df18f4 100644 --- a/cw_core/lib/unspent_transaction_output.dart +++ b/cw_core/lib/unspent_transaction_output.dart @@ -14,6 +14,7 @@ class Unspent { bool isChange; bool isSending; bool isFrozen; + int? confirmations; String note; bool get isP2wpkh => address.startsWith('bc') || address.startsWith('ltc'); diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index 037a26d38..709462fa1 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -56,6 +56,8 @@ abstract class WalletBase false; + bool get isHardwareWallet => walletInfo.isHardwareWallet; + Future connectToNode({required Node node}); // there is a default definition here because only coins with a pow node (nano based) need to override this @@ -88,7 +90,7 @@ abstract class WalletBase renameWalletFiles(String newWalletName); - String signMessage(String message, {String? address = null}) => throw UnimplementedError(); + Future signMessage(String message, {String? address = null}) => throw UnimplementedError(); bool? isTestnet; } diff --git a/cw_core/lib/wallet_credentials.dart b/cw_core/lib/wallet_credentials.dart index 4d5f331c9..30ae2546c 100644 --- a/cw_core/lib/wallet_credentials.dart +++ b/cw_core/lib/wallet_credentials.dart @@ -7,15 +7,21 @@ abstract class WalletCredentials { this.seedPhraseLength, this.walletInfo, this.password, - this.derivationType, - this.derivationPath, - }); + this.passphrase, + this.derivationInfo, + this.hardwareWalletType, + }) { + if (this.walletInfo != null && derivationInfo != null) { + this.walletInfo!.derivationInfo = derivationInfo; + } + } final String name; final int? height; int? seedPhraseLength; String? password; - DerivationType? derivationType; - String? derivationPath; + String? passphrase; WalletInfo? walletInfo; + DerivationInfo? derivationInfo; + HardwareWalletType? hardwareWalletType; } diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index 2a44175a7..57cdad81b 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -1,4 +1,5 @@ import 'dart:async'; + import 'package:cw_core/address_info.dart'; import 'package:cw_core/hive_type_ids.dart'; import 'package:cw_core/wallet_type.dart'; @@ -17,28 +18,48 @@ enum DerivationType { @HiveField(3) bip39, @HiveField(4) - electrum1, - @HiveField(5) - electrum2, + electrum, } -class DerivationInfo { +@HiveType(typeId: HARDWARE_WALLET_TYPE_TYPE_ID) +enum HardwareWalletType { + @HiveField(0) + ledger, +} + +@HiveType(typeId: DerivationInfo.typeId) +class DerivationInfo extends HiveObject { DerivationInfo({ - required this.derivationType, + this.derivationType, this.derivationPath, this.balance = "", this.address = "", - this.height = 0, - this.script_type, + this.transactionsCount = 0, + this.scriptType, this.description, }); - String balance; + static const typeId = DERIVATION_INFO_TYPE_ID; + + @HiveField(0, defaultValue: '') String address; - int height; - final DerivationType derivationType; - final String? derivationPath; - final String? script_type; + + @HiveField(1, defaultValue: '') + String balance; + + @HiveField(2, defaultValue: 0) + int transactionsCount; + + @HiveField(3) + DerivationType? derivationType; + + @HiveField(4) + String? derivationPath; + + @HiveField(5) + final String? scriptType; + + @HiveField(6) final String? description; } @@ -57,9 +78,9 @@ class WalletInfo extends HiveObject { this.yatEid, this.yatLastUsedAddressRaw, this.showIntroCakePayCard, - this.derivationType, - this.derivationPath) - : _yatLastUsedAddressController = StreamController.broadcast(); + this.derivationInfo, + this.hardwareWalletType, + ): _yatLastUsedAddressController = StreamController.broadcast(); factory WalletInfo.external({ required String id, @@ -74,24 +95,25 @@ class WalletInfo extends HiveObject { bool? showIntroCakePayCard, String yatEid = '', String yatLastUsedAddressRaw = '', - DerivationType? derivationType, - String? derivationPath, + DerivationInfo? derivationInfo, + HardwareWalletType? hardwareWalletType, }) { return WalletInfo( - id, - name, - type, - isRecovery, - restoreHeight, - date.millisecondsSinceEpoch, - dirPath, - path, - address, - yatEid, - yatLastUsedAddressRaw, - showIntroCakePayCard, - derivationType, - derivationPath); + id, + name, + type, + isRecovery, + restoreHeight, + date.millisecondsSinceEpoch, + dirPath, + path, + address, + yatEid, + yatLastUsedAddressRaw, + showIntroCakePayCard, + derivationInfo, + hardwareWalletType, + ); } static const typeId = WALLET_INFO_TYPE_ID; @@ -142,11 +164,13 @@ class WalletInfo extends HiveObject { @HiveField(15) List? usedAddresses; + @deprecated @HiveField(16) - DerivationType? derivationType; + DerivationType? derivationType; // no longer used + @deprecated @HiveField(17) - String? derivationPath; + String? derivationPath; // no longer used @HiveField(18) String? addressPageType; @@ -154,6 +178,12 @@ class WalletInfo extends HiveObject { @HiveField(19) String? network; + @HiveField(20) + DerivationInfo? derivationInfo; + + @HiveField(21) + HardwareWalletType? hardwareWalletType; + String get yatLastUsedAddress => yatLastUsedAddressRaw ?? ''; set yatLastUsedAddress(String address) { @@ -170,6 +200,8 @@ class WalletInfo extends HiveObject { return showIntroCakePayCard!; } + bool get isHardwareWallet => hardwareWalletType != null; + DateTime get date => DateTime.fromMillisecondsSinceEpoch(timestamp); Stream get yatLastUsedAddressStream => _yatLastUsedAddressController.stream; diff --git a/cw_core/lib/wallet_service.dart b/cw_core/lib/wallet_service.dart index 22981b9db..fcbd59ff3 100644 --- a/cw_core/lib/wallet_service.dart +++ b/cw_core/lib/wallet_service.dart @@ -6,11 +6,13 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_type.dart'; abstract class WalletService { + RFK extends WalletCredentials, RFH extends WalletCredentials> { WalletType getType(); Future create(N credentials, {bool? isTestnet}); + Future restoreFromHardwareWallet(RFH credentials); + Future restoreFromSeed(RFS credentials, {bool? isTestnet}); Future restoreFromKeys(RFK credentials, {bool? isTestnet}); diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index a63ddf37c..e846093d0 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -15,6 +15,7 @@ const walletTypes = [ WalletType.banano, WalletType.polygon, WalletType.solana, + WalletType.tron, ]; @HiveType(typeId: WALLET_TYPE_TYPE_ID) @@ -50,7 +51,10 @@ enum WalletType { polygon, @HiveField(10) - solana + solana, + + @HiveField(11) + tron } int serializeToInt(WalletType type) { @@ -75,6 +79,8 @@ int serializeToInt(WalletType type) { return 8; case WalletType.solana: return 9; + case WalletType.tron: + return 10; default: return -1; } @@ -102,6 +108,8 @@ WalletType deserializeFromInt(int raw) { return WalletType.polygon; case 9: return WalletType.solana; + case 10: + return WalletType.tron; default: throw Exception('Unexpected token: $raw for WalletType deserializeFromInt'); } @@ -129,6 +137,8 @@ String walletTypeToString(WalletType type) { return 'Polygon'; case WalletType.solana: return 'Solana'; + case WalletType.tron: + return 'Tron'; default: return ''; } @@ -156,6 +166,8 @@ String walletTypeToDisplayName(WalletType type) { return 'Polygon (MATIC)'; case WalletType.solana: return 'Solana (SOL)'; + case WalletType.tron: + return 'Tron (TRX)'; default: return ''; } @@ -183,6 +195,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type) { return CryptoCurrency.maticpoly; case WalletType.solana: return CryptoCurrency.sol; + case WalletType.tron: + return CryptoCurrency.trx; default: throw Exception( 'Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency'); diff --git a/cw_core/lib/window_size.dart b/cw_core/lib/window_size.dart new file mode 100644 index 000000000..a0f192f66 --- /dev/null +++ b/cw_core/lib/window_size.dart @@ -0,0 +1,22 @@ +import 'dart:io'; + +import 'package:flutter/services.dart'; + +const MethodChannel _channel = MethodChannel('com.cake_wallet/native_utils'); + +Future setDefaultMinimumWindowSize() async { + if (!Platform.isMacOS) return; + + try { + final result = await _channel.invokeMethod( + 'setMinWindowSize', + {'width': 500, 'height': 700}, + ) as bool; + + if (!result) { + print("Failed to set minimum window size."); + } + } on PlatformException catch (e) { + print("Failed to set minimum window size: '${e.message}'."); + } +} diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index 678e57b54..abfdbfc58 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.18.0" convert: dependency: transitive description: @@ -331,6 +331,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" logging: dependency: transitive description: @@ -343,26 +367,26 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.11.0" mime: dependency: transitive description: @@ -407,10 +431,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_provider: dependency: "direct main" description: @@ -564,26 +588,26 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -612,10 +636,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.1" timing: dependency: transitive description: @@ -640,8 +664,16 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - watcher: + vm_service: dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" + watcher: + dependency: "direct overridden" description: name: watcher sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" @@ -681,5 +713,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.0.0 <4.0.0" + dart: ">=3.2.0-0 <4.0.0" flutter: ">=3.10.0" diff --git a/cw_core/pubspec.yaml b/cw_core/pubspec.yaml index 04a840d4e..51d671dc7 100644 --- a/cw_core/pubspec.yaml +++ b/cw_core/pubspec.yaml @@ -28,11 +28,14 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.1.11 + build_runner: ^2.4.7 build_resolvers: ^2.0.9 mobx_codegen: ^2.0.7 hive_generator: ^2.0.1 +dependency_overrides: + watcher: ^1.1.0 + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_ethereum/lib/ethereum_wallet_service.dart b/cw_ethereum/lib/ethereum_wallet_service.dart index 53c8bfea9..c0d3df2d6 100644 --- a/cw_ethereum/lib/ethereum_wallet_service.dart +++ b/cw_ethereum/lib/ethereum_wallet_service.dart @@ -1,11 +1,12 @@ +import 'package:bip39/bip39.dart' as bip39; import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.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_evm/evm_chain_wallet_creation_credentials.dart'; import 'package:cw_evm/evm_chain_wallet_service.dart'; -import 'package:bip39/bip39.dart' as bip39; class EthereumWalletService extends EVMChainWalletService { EthereumWalletService(super.walletInfoSource, {required this.client}); @@ -82,6 +83,29 @@ class EthereumWalletService extends EVMChainWalletService { await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); } + @override + Future restoreFromHardwareWallet( + EVMChainRestoreWalletFromHardware credentials) async { + credentials.walletInfo!.derivationInfo = DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/44'/60'/${credentials.hwAccountData.accountIndex}'/0/0" + ); + credentials.walletInfo!.hardwareWalletType = credentials.hardwareWalletType; + credentials.walletInfo!.address = credentials.hwAccountData.address; + + final wallet = EthereumWallet( + walletInfo: credentials.walletInfo!, + password: credentials.password!, + client: client, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + @override Future restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials, {bool? isTestnet}) async { diff --git a/cw_ethereum/pubspec.yaml b/cw_ethereum/pubspec.yaml index 649ec574b..462e1d77e 100644 --- a/cw_ethereum/pubspec.yaml +++ b/cw_ethereum/pubspec.yaml @@ -19,10 +19,18 @@ dependencies: path: ../cw_evm hive: ^2.2.3 +dependency_overrides: + web3dart: + git: + url: https://github.com/cake-tech/web3dart.git + ref: cake + watcher: ^1.1.0 + dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.1.11 + build_runner: ^2.4.7 + flutter: # assets: # - images/a_dot_burr.jpeg diff --git a/cw_evm/lib/contract/erc20.dart b/cw_evm/lib/contract/erc20.dart new file mode 100644 index 000000000..297b77e71 --- /dev/null +++ b/cw_evm/lib/contract/erc20.dart @@ -0,0 +1,209 @@ +import 'dart:typed_data'; + +import 'package:web3dart/web3dart.dart' as web3; + +final _contractAbi = web3.ContractAbi.fromJson( + '[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}]', + 'Erc20'); + +/// Interface of the ERC20 standard as defined in the EIP. +class ERC20 extends web3.GeneratedContract { + /// Constructor. + ERC20({ + required web3.EthereumAddress address, + required web3.Web3Client client, + int? chainId, + }) : super(web3.DeployedContract(_contractAbi, address), client, chainId); + + /// Returns the remaining number of tokens that [spender] will be allowed to spend on behalf of [owner] through [transferFrom]. This is zero by default. This value changes when [approve] or [transferFrom] are called. + /// + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future allowance( + web3.EthereumAddress owner, + web3.EthereumAddress spender, { + web3.BlockNum? atBlock, + }) async { + final function = self.abi.functions[0]; + assert(checkSignature(function, 'dd62ed3e')); + final params = [owner, spender]; + final response = await read(function, params, atBlock); + return (response[0] as BigInt); + } + + /// Sets [amount] as the allowance of [spender] over the caller's tokens. Returns a boolean value indicating whether the operation succeeded. IMPORTANT: Beware that changing an allowance with this method brings the risk that someone may use both the old and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 Emits an [Approval] event. + /// + /// The optional [transaction] parameter can be used to override parameters + /// like the gas price, nonce and max gas. The `data` and `to` fields will be + /// set by the contract. + Future approve( + web3.EthereumAddress spender, + BigInt amount, { + required web3.Credentials credentials, + web3.Transaction? transaction, + }) async { + final function = self.abi.functions[1]; + assert(checkSignature(function, '095ea7b3')); + final params = [spender, amount]; + return writeRaw(credentials, transaction, function, params); + } + + /// Returns the amount of tokens owned by [account]. + /// + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future balanceOf( + web3.EthereumAddress account, { + web3.BlockNum? atBlock, + }) async { + final function = self.abi.functions[2]; + assert(checkSignature(function, '70a08231')); + final params = [account]; + final response = await read(function, params, atBlock); + return (response[0] as BigInt); + } + + /// Returns the decimal precision of the token. + /// + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future decimals({web3.BlockNum? atBlock}) async { + final function = self.abi.functions[3]; + assert(checkSignature(function, '313ce567')); + final params = []; + final response = await read(function, params, atBlock); + return (response[0] as BigInt); + } + + /// Returns the name of the token. + /// + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future name({web3.BlockNum? atBlock}) async { + final function = self.abi.functions[4]; + assert(checkSignature(function, '06fdde03')); + final params = []; + final response = await read(function, params, atBlock); + return (response[0] as String); + } + + /// Returns the symbol of the token. + /// + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future symbol({web3.BlockNum? atBlock}) async { + final function = self.abi.functions[5]; + assert(checkSignature(function, '95d89b41')); + final params = []; + final response = await read(function, params, atBlock); + return (response[0] as String); + } + + /// Returns the amount of tokens in existence. + /// + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future totalSupply({web3.BlockNum? atBlock}) async { + final function = self.abi.functions[6]; + assert(checkSignature(function, '18160ddd')); + final params = []; + final response = await read(function, params, atBlock); + return (response[0] as BigInt); + } + + /// Moves [amount] tokens from the caller's account to [recipient]. Returns a boolean value indicating whether the operation succeeded. Emits a [Transfer] event. + /// + /// The optional [transaction] parameter can be used to override parameters + /// like the gas price, nonce and max gas. The `data` and `to` fields will be + /// set by the contract. + Future transfer( + web3.EthereumAddress recipient, + BigInt amount, { + required web3.Credentials credentials, + web3.Transaction? transaction, + }) async { + final function = self.abi.functions[7]; + assert(checkSignature(function, 'a9059cbb')); + final params = [recipient, amount]; + return writeRaw(credentials, transaction, function, params); + } + + /// Moves [amount] tokens from [sender] to [recipient] using the allowance mechanism. [amount] is then deducted from the caller's allowance. Returns a boolean value indicating whether the operation succeeded. Emits a [Transfer] event. + /// + /// The optional [transaction] parameter can be used to override parameters + /// like the gas price, nonce and max gas. The `data` and `to` fields will be + /// set by the contract. + Future transferFrom(web3.EthereumAddress sender, + web3.EthereumAddress recipient, BigInt amount, + {required web3.Credentials credentials, + web3.Transaction? transaction}) async { + final function = self.abi.functions[8]; + assert(checkSignature(function, '23b872dd')); + final params = [sender, recipient, amount]; + return writeRaw(credentials, transaction, function, params); + } + + /// Returns a live stream of all Approval events emitted by this contract. + Stream approvalEvents( + {web3.BlockNum? fromBlock, web3.BlockNum? toBlock}) { + final event = self.event('Approval'); + final filter = web3.FilterOptions.events( + contract: self, event: event, fromBlock: fromBlock, toBlock: toBlock); + return client.events(filter).map((web3.FilterEvent result) { + final decoded = event.decodeResults(result.topics!, result.data!); + return Approval._(decoded); + }); + } + + /// Returns a live stream of all Transfer events emitted by this contract. + Stream transferEvents( + {web3.BlockNum? fromBlock, web3.BlockNum? toBlock}) { + final event = self.event('Transfer'); + final filter = web3.FilterOptions.events( + contract: self, event: event, fromBlock: fromBlock, toBlock: toBlock); + return client.events(filter).map((web3.FilterEvent result) { + final decoded = event.decodeResults(result.topics!, result.data!); + return Transfer._(decoded); + }); + } +} + +/// Emitted when the allowance of a [spender] for an [owner] is set by a call to [ERC20.approve]. [value] is the new allowance. +class Approval { + Approval._(List response) + : owner = (response[0] as web3.EthereumAddress), + spender = (response[1] as web3.EthereumAddress), + value = (response[2] as BigInt); + + /// The owner address. + final web3.EthereumAddress owner; + + /// The spender address. + final web3.EthereumAddress spender; + + /// Value. + final BigInt value; +} + +/// Emitted when [value] tokens are moved from one account ([from]) to another ([to]). Note that [value] may be zero. +class Transfer { + Transfer._(List response) + : from = (response[0] as web3.EthereumAddress), + to = (response[1] as web3.EthereumAddress), + value = (response[2] as BigInt); + + /// From address. + final web3.EthereumAddress from; + + /// To address. + final web3.EthereumAddress to; + + /// Value. + final BigInt value; +} diff --git a/cw_evm/lib/evm_chain_client.dart b/cw_evm/lib/evm_chain_client.dart index eebbe4f4f..2185936ea 100644 --- a/cw_evm/lib/evm_chain_client.dart +++ b/cw_evm/lib/evm_chain_client.dart @@ -2,21 +2,20 @@ import 'dart:async'; import 'dart:convert'; 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_core/erc20_token.dart'; +import 'package:cw_core/node.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:cw_evm/evm_erc20_balance.dart'; +import 'package:cw_evm/pending_evm_chain_transaction.dart'; import 'package:cw_evm/.secrets.g.dart' as secrets; import 'package:flutter/services.dart'; - -import 'package:http/http.dart'; -import 'package:erc20/erc20.dart'; -import 'package:web3dart/web3dart.dart'; import 'package:hex/hex.dart' as hex; +import 'package:http/http.dart'; +import 'package:web3dart/web3dart.dart'; + +import 'contract/erc20.dart'; abstract class EVMChainClient { final httpClient = Client(); @@ -82,7 +81,7 @@ abstract class EVMChainClient { } Future signTransaction({ - required EthPrivateKey privateKey, + required Credentials privateKey, required String toAddress, required BigInt amount, required int gas, @@ -96,8 +95,7 @@ abstract class EVMChainClient { currency == CryptoCurrency.maticpoly || contractAddress != null); - bool isEVMCompatibleChain = - currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly; + bool isNativeToken = currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly; final price = _client!.getGasPrice(); @@ -105,17 +103,16 @@ abstract class EVMChainClient { from: privateKey.address, to: EthereumAddress.fromHex(toAddress), maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip), - amount: isEVMCompatibleChain ? EtherAmount.inWei(amount) : EtherAmount.zero(), + amount: isNativeToken ? EtherAmount.inWei(amount) : EtherAmount.zero(), data: data != null ? hexToBytes(data) : null, ); - final signedTransaction = - await _client!.signTransaction(privateKey, transaction, chainId: chainId); + Uint8List signedTransaction; final Function _sendTransaction; - if (isEVMCompatibleChain) { - _sendTransaction = () async => await sendTransaction(signedTransaction); + if (isNativeToken) { + signedTransaction = await _client!.signTransaction(privateKey, transaction, chainId: chainId); } else { final erc20 = ERC20( client: _client!, @@ -123,16 +120,17 @@ abstract class EVMChainClient { chainId: chainId, ); - _sendTransaction = () async { - await erc20.transfer( - EthereumAddress.fromHex(toAddress), - amount, - credentials: privateKey, - transaction: transaction, - ); - }; + signedTransaction = await erc20.transfer( + EthereumAddress.fromHex(toAddress), + amount, + credentials: privateKey, + transaction: transaction, + ); } + _sendTransaction = () async => await sendTransaction(signedTransaction); + + return PendingEVMChainTransaction( signedTransaction: signedTransaction, amount: amount.toString(), @@ -158,8 +156,9 @@ abstract class EVMChainClient { ); } - Future sendTransaction(Uint8List signedTransaction) async => - await _client!.sendRawTransaction(prepareSignedTransactionForSending(signedTransaction)); + Future sendTransaction(Uint8List signedTransaction) async { + return await _client!.sendRawTransaction(prepareSignedTransactionForSending(signedTransaction)); + } Future getTransactionDetails(String transactionHash) async { // Wait for the transaction receipt to become available @@ -234,14 +233,17 @@ abstract class EVMChainClient { final decodedResponse = jsonDecode(response.body)[0] as Map; + + final symbol = (decodedResponse['symbol'] ?? '') as String; + String filteredSymbol = symbol.replaceFirst(RegExp('^\\\$'), ''); + final name = decodedResponse['name'] ?? ''; - final symbol = decodedResponse['symbol'] ?? ''; final decimal = decodedResponse['decimals'] ?? '0'; final iconPath = decodedResponse['logo'] ?? ''; return Erc20Token( name: name, - symbol: symbol, + symbol: filteredSymbol, contractAddress: contractAddress, decimal: int.tryParse(decimal) ?? 0, iconPath: iconPath, diff --git a/cw_evm/lib/evm_chain_exceptions.dart b/cw_evm/lib/evm_chain_exceptions.dart index 8aa371b19..d0f300270 100644 --- a/cw_evm/lib/evm_chain_exceptions.dart +++ b/cw_evm/lib/evm_chain_exceptions.dart @@ -6,6 +6,8 @@ class EVMChainTransactionCreationException implements Exception { EVMChainTransactionCreationException(CryptoCurrency currency) : exceptionMessage = 'Wrong balance. Not enough ${currency.title} on your balance.'; + EVMChainTransactionCreationException.fromMessage(this.exceptionMessage); + @override String toString() => exceptionMessage; } diff --git a/cw_evm/lib/evm_chain_hardware_wallet_service.dart b/cw_evm/lib/evm_chain_hardware_wallet_service.dart new file mode 100644 index 000000000..6f0d11f2e --- /dev/null +++ b/cw_evm/lib/evm_chain_hardware_wallet_service.dart @@ -0,0 +1,35 @@ +import 'dart:async'; + +import 'package:cw_core/hardware/hardware_account_data.dart'; +import 'package:ledger_ethereum/ledger_ethereum.dart'; +import 'package:ledger_flutter/ledger_flutter.dart'; + +class EVMChainHardwareWalletService { + EVMChainHardwareWalletService(this.ledger, this.device); + + final Ledger ledger; + final LedgerDevice device; + + Future> getAvailableAccounts({int index = 0, int limit = 5}) async { + final ethereumLedgerApp = EthereumLedgerApp(ledger); + + final version = await ethereumLedgerApp.getVersion(device); + + final accounts = []; + final indexRange = List.generate(limit, (i) => i + index); + + for (final i in indexRange) { + final derivationPath = "m/44'/60'/$i'/0/0"; + final address = + await ethereumLedgerApp.getAccounts(device, accountsDerivationPath: derivationPath); + + accounts.add(HardwareAccountData( + address: address.first, + accountIndex: i, + derivationPath: derivationPath, + )); + } + + return accounts; + } +} diff --git a/cw_evm/lib/evm_chain_transaction_credentials.dart b/cw_evm/lib/evm_chain_transaction_credentials.dart index 5b5bdf170..02927cb4d 100644 --- a/cw_evm/lib/evm_chain_transaction_credentials.dart +++ b/cw_evm/lib/evm_chain_transaction_credentials.dart @@ -1,6 +1,7 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/output_info.dart'; import 'package:cw_evm/evm_chain_transaction_priority.dart'; +import 'package:ledger_flutter/ledger_flutter.dart'; class EVMChainTransactionCredentials { EVMChainTransactionCredentials( diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index 4193e590a..56b58d400 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -25,6 +25,7 @@ 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/evm_ledger_credentials.dart'; import 'package:cw_evm/file.dart'; import 'package:hex/hex.dart'; import 'package:hive/hive.dart'; @@ -83,9 +84,9 @@ abstract class EVMChainWalletBase late final Box evmChainErc20TokensBox; - late final EthPrivateKey _evmChainPrivateKey; + late final Credentials _evmChainPrivateKey; - EthPrivateKey get evmChainPrivateKey => _evmChainPrivateKey; + Credentials get evmChainPrivateKey => _evmChainPrivateKey; late EVMChainClient _client; @@ -141,12 +142,18 @@ abstract class EVMChainWalletBase await walletAddresses.init(); await transactionHistory.init(); - _evmChainPrivateKey = await getPrivateKey( - mnemonic: _mnemonic, - privateKey: _hexPrivateKey, - password: _password, - ); - walletAddresses.address = _evmChainPrivateKey.address.hexEip55; + + if (walletInfo.isHardwareWallet) { + _evmChainPrivateKey = EvmLedgerCredentials(walletInfo.address); + walletAddresses.address = walletInfo.address; + } else { + _evmChainPrivateKey = await getPrivateKey( + mnemonic: _mnemonic, + privateKey: _hexPrivateKey, + password: _password, + ); + walletAddresses.address = _evmChainPrivateKey.address.hexEip55; + } await save(); } @@ -283,6 +290,11 @@ abstract class EVMChainWalletBase } } + if (transactionCurrency is Erc20Token && isHardwareWallet) { + await (_evmChainPrivateKey as EvmLedgerCredentials) + .provideERC20Info(transactionCurrency.contractAddress, _client.chainId); + } + final pendingEVMChainTransaction = await _client.signTransaction( privateKey: _evmChainPrivateKey, toAddress: _credentials.outputs.first.isParsedAddress @@ -377,7 +389,9 @@ abstract class EVMChainWalletBase String? get seed => _mnemonic; @override - String get privateKey => HEX.encode(_evmChainPrivateKey.privateKey); + String? get privateKey => evmChainPrivateKey is EthPrivateKey + ? HEX.encode((evmChainPrivateKey as EthPrivateKey).privateKey) + : null; Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); @@ -468,9 +482,15 @@ abstract class EVMChainWalletBase await token.delete(); balance.remove(token); + await _removeTokenTransactionsInHistory(token); _updateBalance(); } + Future _removeTokenTransactionsInHistory(Erc20Token token) async { + transactionHistory.transactions.removeWhere((key, value) => value.tokenSymbol == token.title); + await transactionHistory.save(); + } + Future getErc20Token(String contractAddress, String chainName) async => await _client.getErc20Token(contractAddress, chainName); @@ -529,8 +549,8 @@ abstract class EVMChainWalletBase } @override - String signMessage(String message, {String? address}) => - bytesToHex(_evmChainPrivateKey.signPersonalMessageToUint8List(ascii.encode(message))); + Future signMessage(String message, {String? address}) async => + bytesToHex(await _evmChainPrivateKey.signPersonalMessage(ascii.encode(message))); Web3Client? getWeb3Client() => _client.getWeb3Client(); } diff --git a/cw_evm/lib/evm_chain_wallet_creation_credentials.dart b/cw_evm/lib/evm_chain_wallet_creation_credentials.dart index 7c3271daf..be763bac7 100644 --- a/cw_evm/lib/evm_chain_wallet_creation_credentials.dart +++ b/cw_evm/lib/evm_chain_wallet_creation_credentials.dart @@ -1,3 +1,4 @@ +import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; @@ -27,3 +28,13 @@ class EVMChainRestoreWalletFromPrivateKey extends WalletCredentials { final String privateKey; } + +class EVMChainRestoreWalletFromHardware extends WalletCredentials { + EVMChainRestoreWalletFromHardware({ + required String name, + required this.hwAccountData, + WalletInfo? walletInfo, + }) : super(name: name, walletInfo: walletInfo); + + final HardwareAccountData hwAccountData; +} diff --git a/cw_evm/lib/evm_chain_wallet_service.dart b/cw_evm/lib/evm_chain_wallet_service.dart index d77a3a81a..2bbe6bd47 100644 --- a/cw_evm/lib/evm_chain_wallet_service.dart +++ b/cw_evm/lib/evm_chain_wallet_service.dart @@ -13,7 +13,8 @@ import 'package:hive/hive.dart'; abstract class EVMChainWalletService extends WalletService< EVMChainNewWalletCredentials, EVMChainRestoreWalletFromSeedCredentials, - EVMChainRestoreWalletFromPrivateKey> { + EVMChainRestoreWalletFromPrivateKey, + EVMChainRestoreWalletFromHardware> { EVMChainWalletService(this.walletInfoSource); final Box walletInfoSource; @@ -24,6 +25,9 @@ abstract class EVMChainWalletService extends WalletSer @override Future create(EVMChainNewWalletCredentials credentials, {bool? isTestnet}); + @override + Future restoreFromHardwareWallet(EVMChainRestoreWalletFromHardware credentials); + @override Future openWallet(String name, String password); diff --git a/cw_evm/lib/evm_ledger_credentials.dart b/cw_evm/lib/evm_ledger_credentials.dart new file mode 100644 index 000000000..0d8de1736 --- /dev/null +++ b/cw_evm/lib/evm_ledger_credentials.dart @@ -0,0 +1,103 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:cw_core/hardware/device_not_connected_exception.dart'; +import 'package:ledger_ethereum/ledger_ethereum.dart'; +import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:web3dart/crypto.dart'; +import 'package:web3dart/web3dart.dart'; + +class EvmLedgerCredentials extends CredentialsWithKnownAddress { + final String _address; + + Ledger? ledger; + LedgerDevice? ledgerDevice; + EthereumLedgerApp? ethereumLedgerApp; + + EvmLedgerCredentials(this._address); + + @override + EthereumAddress get address => EthereumAddress.fromHex(_address); + + void setLedger(Ledger setLedger, [LedgerDevice? setLedgerDevice, String? derivationPath]) { + ledger = setLedger; + ledgerDevice = setLedgerDevice; + ethereumLedgerApp = + EthereumLedgerApp(ledger!, derivationPath: derivationPath ?? "m/44'/60'/0'/0/0"); + } + + @override + MsgSignature signToEcSignature(Uint8List payload, {int? chainId, bool isEIP1559 = false}) => + throw UnimplementedError("EvmLedgerCredentials.signToEcSignature"); + + @override + Future signToSignature(Uint8List payload, + {int? chainId, bool isEIP1559 = false}) async { + if (ledgerDevice == null && ledger?.devices.isNotEmpty != true) { + throw DeviceNotConnectedException(); + } + + final sig = await ethereumLedgerApp!.signTransaction(device, payload); + + final v = sig[0].toInt(); + final r = bytesToHex(sig.sublist(1, 1 + 32)); + final s = bytesToHex(sig.sublist(1 + 32, 1 + 32 + 32)); + + var truncChainId = chainId ?? 1; + while (truncChainId.bitLength > 32) { + truncChainId >>= 8; + } + + final truncTarget = truncChainId * 2 + 35; + + int parity = v; + if (truncTarget & 0xff == v) { + parity = 0; + } else if ((truncTarget + 1) & 0xff == v) { + parity = 1; + } + + // https://github.com/ethereumjs/ethereumjs-util/blob/8ffe697fafb33cefc7b7ec01c11e3a7da787fe0e/src/signature.ts#L26 + int chainIdV; + if (isEIP1559) { + chainIdV = v; + } else { + chainIdV = chainId != null ? (parity + (chainId * 2 + 35)) : parity; + } + + return MsgSignature(BigInt.parse(r, radix: 16), BigInt.parse(s, radix: 16), chainIdV); + } + + @override + Future signPersonalMessage(Uint8List payload, {int? chainId}) async { + if (isNotConnected) throw DeviceNotConnectedException(); + + final sig = await ethereumLedgerApp!.signMessage(device, payload); + + final r = sig.sublist(1, 1 + 32); + final s = sig.sublist(1 + 32, 1 + 32 + 32); + final v = [sig[0]]; + + // https://github.com/ethereumjs/ethereumjs-util/blob/8ffe697fafb33cefc7b7ec01c11e3a7da787fe0e/src/signature.ts#L63 + return Uint8List.fromList(r + s + v); + } + + @override + Uint8List signPersonalMessageToUint8List(Uint8List payload, {int? chainId}) => + throw UnimplementedError("EvmLedgerCredentials.signPersonalMessageToUint8List"); + + Future provideERC20Info(String erc20ContractAddress, int chainId) async { + if (isNotConnected) throw DeviceNotConnectedException(); + + try { + await ethereumLedgerApp!.getAndProvideERC20TokenInformation(device, + erc20ContractAddress: erc20ContractAddress, chainId: chainId); + } on LedgerException catch (e) { + if (e.errorCode != -28672) rethrow; + } + } + + bool get isNotConnected => (ledgerDevice ?? ledger?.devices.firstOrNull) == null; + + LedgerDevice get device => ledgerDevice ?? ledger!.devices.first; +} diff --git a/cw_evm/pubspec.yaml b/cw_evm/pubspec.yaml index c202cc72a..e4b29b676 100644 --- a/cw_evm/pubspec.yaml +++ b/cw_evm/pubspec.yaml @@ -13,7 +13,6 @@ dependencies: flutter: sdk: flutter web3dart: ^2.7.1 - erc20: ^1.0.1 bip39: ^1.0.6 bip32: ^2.0.0 hex: ^0.2.0 @@ -23,11 +22,26 @@ dependencies: shared_preferences: ^2.0.15 cw_core: path: ../cw_core + ledger_flutter: ^1.0.1 + ledger_ethereum: + git: + url: https://github.com/cake-tech/ledger-ethereum.git + +dependency_overrides: + web3dart: + git: + url: https://github.com/cake-tech/web3dart.git + ref: cake + ledger_flutter: + git: + url: https://github.com/cake-tech/ledger-flutter.git + ref: cake + watcher: ^1.1.0 dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.1.11 + build_runner: ^2.4.7 mobx_codegen: ^2.0.7 hive_generator: ^1.1.3 flutter_lints: ^2.0.0 diff --git a/cw_haven/lib/haven_wallet_service.dart b/cw_haven/lib/haven_wallet_service.dart index d4808c2d6..14bc520da 100644 --- a/cw_haven/lib/haven_wallet_service.dart +++ b/cw_haven/lib/haven_wallet_service.dart @@ -56,7 +56,8 @@ class HavenRestoreWalletFromKeysCredentials extends WalletCredentials { class HavenWalletService extends WalletService< HavenNewWalletCredentials, HavenRestoreWalletFromSeedCredentials, - HavenRestoreWalletFromKeysCredentials> { + HavenRestoreWalletFromKeysCredentials, + HavenNewWalletCredentials> { HavenWalletService(this.walletInfoSource); final Box walletInfoSource; @@ -172,6 +173,11 @@ class HavenWalletService extends WalletService< await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); } + @override + Future restoreFromHardwareWallet(HavenNewWalletCredentials credentials) { + throw UnimplementedError("Restoring a Haven wallet from a hardware wallet is not yet supported!"); + } + @override Future restoreFromKeys( HavenRestoreWalletFromKeysCredentials credentials, {bool? isTestnet}) async { diff --git a/cw_haven/pubspec.lock b/cw_haven/pubspec.lock index b0a350cc7..8aeb70a97 100644 --- a/cw_haven/pubspec.lock +++ b/cw_haven/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf" + sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.0.0" build_resolvers: dependency: "direct dev" description: @@ -85,10 +85,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.9" build_runner_core: dependency: transitive description: @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.18.0" convert: dependency: transitive description: @@ -338,6 +338,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" logging: dependency: transitive description: @@ -350,26 +374,26 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.11.0" mime: dependency: transitive description: @@ -406,10 +430,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_provider: dependency: "direct main" description: @@ -563,26 +587,26 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -611,10 +635,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.1" timing: dependency: transitive description: @@ -639,14 +663,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - watcher: + vm_service: dependency: transitive description: - name: watcher - sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "13.0.0" + watcher: + dependency: "direct overridden" + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" web_socket_channel: dependency: transitive description: @@ -680,5 +712,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=3.0.0 <4.0.0" + dart: ">=3.2.0-0 <4.0.0" flutter: ">=3.7.0" diff --git a/cw_haven/pubspec.yaml b/cw_haven/pubspec.yaml index c215ab779..d868c986d 100644 --- a/cw_haven/pubspec.yaml +++ b/cw_haven/pubspec.yaml @@ -24,11 +24,14 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.1.11 + build_runner: ^2.4.7 mobx_codegen: ^2.0.7 build_resolvers: ^2.0.9 hive_generator: ^1.1.3 +dependency_overrides: + watcher: ^1.1.0 + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_monero/example/pubspec.lock b/cw_monero/example/pubspec.lock index c9ca8d92b..7816a7fad 100644 --- a/cw_monero/example/pubspec.lock +++ b/cw_monero/example/pubspec.lock @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.18.0" convert: dependency: transitive description: @@ -201,6 +201,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.7" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" lints: dependency: transitive description: @@ -213,26 +237,26 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.11.0" mobx: dependency: transitive description: @@ -245,10 +269,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_provider: dependency: transitive description: @@ -354,26 +378,26 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" string_scanner: dependency: transitive description: @@ -394,10 +418,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.1" typed_data: dependency: transitive description: @@ -414,6 +438,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" win32: dependency: transitive description: @@ -431,5 +463,5 @@ packages: source: hosted version: "0.2.0+3" sdks: - dart: ">=3.0.6 <4.0.0" + dart: ">=3.2.0-0 <4.0.0" flutter: ">=3.7.0" diff --git a/cw_monero/lib/api/signatures.dart b/cw_monero/lib/api/signatures.dart index bc4fc9d38..bba7b5528 100644 --- a/cw_monero/lib/api/signatures.dart +++ b/cw_monero/lib/api/signatures.dart @@ -17,6 +17,9 @@ typedef restore_wallet_from_keys = Int8 Function(Pointer, Pointer, P typedef restore_wallet_from_spend_key = Int8 Function(Pointer, Pointer, Pointer, Pointer, Pointer, Int32, Int64, Pointer); +// typedef restore_wallet_from_device = Int8 Function(Pointer, Pointer, Pointer, +// Int32, Int64, Pointer); + typedef is_wallet_exist = Int8 Function(Pointer); typedef load_wallet = Int8 Function(Pointer, Pointer, Int8); diff --git a/cw_monero/lib/api/types.dart b/cw_monero/lib/api/types.dart index 40a1e0321..87d8a153e 100644 --- a/cw_monero/lib/api/types.dart +++ b/cw_monero/lib/api/types.dart @@ -17,6 +17,9 @@ typedef RestoreWalletFromKeys = int Function(Pointer, Pointer, typedef RestoreWalletFromSpendKey = int Function(Pointer, Pointer, Pointer, Pointer, Pointer, int, int, Pointer); +typedef RestoreWalletFromDevice = int Function(Pointer, Pointer, Pointer, + int, int, Pointer); + typedef IsWalletExist = int Function(Pointer); typedef LoadWallet = int Function(Pointer, Pointer, int); diff --git a/cw_monero/lib/api/wallet_manager.dart b/cw_monero/lib/api/wallet_manager.dart index 0aa694e9a..ae88f76ab 100644 --- a/cw_monero/lib/api/wallet_manager.dart +++ b/cw_monero/lib/api/wallet_manager.dart @@ -31,6 +31,11 @@ final restoreWalletFromSpendKeyNative = moneroApi 'restore_wallet_from_spend_key') .asFunction(); +// final restoreWalletFromDeviceNative = moneroApi +// .lookup>( +// 'restore_wallet_from_device') +// .asFunction(); + final isWalletExistNative = moneroApi .lookup>('is_wallet_exist') .asFunction(); @@ -185,6 +190,38 @@ void restoreWalletFromSpendKeySync( } } +// void restoreMoneroWalletFromDevice( +// {required String path, +// required String password, +// required String deviceName, +// int nettype = 0, +// int restoreHeight = 0}) { +// +// final pathPointer = path.toNativeUtf8(); +// final passwordPointer = password.toNativeUtf8(); +// final deviceNamePointer = deviceName.toNativeUtf8(); +// final errorMessagePointer = ''.toNativeUtf8(); +// +// final isWalletRestored = restoreWalletFromDeviceNative( +// pathPointer, +// passwordPointer, +// deviceNamePointer, +// nettype, +// restoreHeight, +// errorMessagePointer) != 0; +// +// calloc.free(pathPointer); +// calloc.free(passwordPointer); +// +// storeSync(); +// +// if (!isWalletRestored) { +// throw WalletRestoreFromKeysException( +// message: convertUTF8ToString(pointer: errorMessagePointer)); +// } +// } + + void loadWallet({ required String path, required String password, diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index d00a54c8f..c270bb113 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -686,7 +686,7 @@ abstract class MoneroWalletBase void setExceptionHandler(void Function(FlutterErrorDetails) e) => onError = e; @override - String signMessage(String message, {String? address}) { + Future signMessage(String message, {String? address}) async { final useAddress = address ?? ""; return monero_wallet.signMessage(message, address: useAddress); } diff --git a/cw_monero/lib/monero_wallet_service.dart b/cw_monero/lib/monero_wallet_service.dart index 1f33dbb3d..bc59499f9 100644 --- a/cw_monero/lib/monero_wallet_service.dart +++ b/cw_monero/lib/monero_wallet_service.dart @@ -55,7 +55,7 @@ class MoneroRestoreWalletFromKeysCredentials extends WalletCredentials { } class MoneroWalletService extends WalletService { + MoneroRestoreWalletFromSeedCredentials, MoneroRestoreWalletFromKeysCredentials, MoneroNewWalletCredentials> { MoneroWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); final Box walletInfoSource; @@ -227,6 +227,11 @@ class MoneroWalletService extends WalletService restoreFromHardwareWallet(MoneroNewWalletCredentials credentials) { + throw UnimplementedError("Restoring a Monero wallet from a hardware wallet is not yet supported!"); + } + @override Future restoreFromSeed(MoneroRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 0f8f2c90e..adb50bd02 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf" + sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.0.0" build_resolvers: dependency: "direct dev" description: @@ -85,10 +85,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.9" build_runner_core: dependency: transitive description: @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.18.0" convert: dependency: transitive description: @@ -354,6 +354,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" logging: dependency: transitive description: @@ -366,26 +390,26 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.11.0" mime: dependency: transitive description: @@ -422,10 +446,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_provider: dependency: "direct main" description: @@ -587,26 +611,26 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -635,10 +659,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.1" timing: dependency: transitive description: @@ -663,14 +687,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - watcher: + vm_service: dependency: transitive description: - name: watcher - sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "13.0.0" + watcher: + dependency: "direct overridden" + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" web_socket_channel: dependency: transitive description: @@ -704,5 +736,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=3.0.6 <4.0.0" + dart: ">=3.2.0-0 <4.0.0" flutter: ">=3.7.0" diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml index a6fe7f967..c49a541ab 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -26,11 +26,14 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.1.11 + build_runner: ^2.4.7 build_resolvers: ^2.0.9 mobx_codegen: ^2.0.7 hive_generator: ^1.1.3 +dependency_overrides: + watcher: ^1.1.0 + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_nano/lib/banano_balance.dart b/cw_nano/lib/banano_balance.dart index b904a35cb..d766077fc 100644 --- a/cw_nano/lib/banano_balance.dart +++ b/cw_nano/lib/banano_balance.dart @@ -1,12 +1,28 @@ import 'package:cw_core/balance.dart'; import 'package:nanoutil/nanoutil.dart'; +BigInt stringAmountToBigIntBanano(String amount) { + return BigInt.parse(NanoAmounts.getAmountAsRaw(amount, NanoAmounts.rawPerBanano)); +} + class BananoBalance extends Balance { final BigInt currentBalance; final BigInt receivableBalance; BananoBalance({required this.currentBalance, required this.receivableBalance}) : super(0, 0); + BananoBalance.fromFormattedString( + {required String formattedCurrentBalance, required String formattedReceivableBalance}) + : currentBalance = stringAmountToBigIntBanano(formattedCurrentBalance), + receivableBalance = stringAmountToBigIntBanano(formattedReceivableBalance), + super(0, 0); + + BananoBalance.fromRawString( + {required String currentBalance, required String receivableBalance}) + : currentBalance = BigInt.parse(currentBalance), + receivableBalance = BigInt.parse(receivableBalance), + super(0, 0); + @override String get formattedAvailableBalance { return NanoAmounts.getRawAsUsableString(currentBalance.toString(), NanoAmounts.rawPerBanano); diff --git a/cw_nano/lib/nano_balance.dart b/cw_nano/lib/nano_balance.dart index 8b8c93b33..691b3a32d 100644 --- a/cw_nano/lib/nano_balance.dart +++ b/cw_nano/lib/nano_balance.dart @@ -1,7 +1,7 @@ import 'package:cw_core/balance.dart'; import 'package:nanoutil/nanoutil.dart'; -BigInt stringAmountToBigInt(String amount) { +BigInt stringAmountToBigIntNano(String amount) { return BigInt.parse(NanoAmounts.getAmountAsRaw(amount, NanoAmounts.rawPerNano)); } @@ -13,8 +13,8 @@ class NanoBalance extends Balance { NanoBalance.fromFormattedString( {required String formattedCurrentBalance, required String formattedReceivableBalance}) - : currentBalance = stringAmountToBigInt(formattedCurrentBalance), - receivableBalance = stringAmountToBigInt(formattedReceivableBalance), + : currentBalance = stringAmountToBigIntNano(formattedCurrentBalance), + receivableBalance = stringAmountToBigIntNano(formattedReceivableBalance), super(0, 0); NanoBalance.fromRawString( diff --git a/cw_nano/lib/nano_client.dart b/cw_nano/lib/nano_client.dart index 064a0bdee..3b388e5e8 100644 --- a/cw_nano/lib/nano_client.dart +++ b/cw_nano/lib/nano_client.dart @@ -10,6 +10,7 @@ import 'package:nanodart/nanodart.dart'; import 'package:cw_core/node.dart'; import 'package:nanoutil/nanoutil.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:cw_nano/.secrets.g.dart' as secrets; class NanoClient { static const Map CAKE_HEADERS = { @@ -52,10 +53,19 @@ class NanoClient { } } + Map getHeaders() { + if (_node!.uri == "https://rpc.nano.to") { + return CAKE_HEADERS..addAll({ + "key": secrets.nano2ApiKey, + }); + } + return CAKE_HEADERS; + } + Future getBalance(String address) async { final response = await http.post( _node!.uri, - headers: CAKE_HEADERS, + headers: getHeaders(), body: jsonEncode( { "action": "account_balance", @@ -82,7 +92,7 @@ class NanoClient { try { final response = await http.post( _node!.uri, - headers: CAKE_HEADERS, + headers: getHeaders(), body: jsonEncode( { "action": "account_info", @@ -94,7 +104,7 @@ class NanoClient { final data = await jsonDecode(response.body); return AccountInfoResponse.fromJson(data as Map); } catch (e) { - print("error while getting account info"); + print("error while getting account info $e"); return null; } } @@ -149,7 +159,7 @@ class NanoClient { Future requestWork(String hash) async { final response = await http.post( _powNode!.uri, - headers: CAKE_HEADERS, + headers: getHeaders(), body: json.encode( { "action": "work_generate", @@ -192,7 +202,7 @@ class NanoClient { final processResponse = await http.post( _node!.uri, - headers: CAKE_HEADERS, + headers: getHeaders(), body: processBody, ); @@ -351,7 +361,7 @@ class NanoClient { }); final processResponse = await http.post( _node!.uri, - headers: CAKE_HEADERS, + headers: getHeaders(), body: processBody, ); @@ -367,7 +377,7 @@ class NanoClient { required String privateKey, }) async { final receivableResponse = await http.post(_node!.uri, - headers: CAKE_HEADERS, + headers: getHeaders(), body: jsonEncode({ "action": "receivable", "account": destinationAddress, @@ -417,7 +427,7 @@ class NanoClient { Future> fetchTransactions(String address) async { try { final response = await http.post(_node!.uri, - headers: CAKE_HEADERS, + headers: getHeaders(), body: jsonEncode({ "action": "account_history", "account": address, diff --git a/cw_nano/lib/nano_wallet.dart b/cw_nano/lib/nano_wallet.dart index 265f78eb7..5efe3006d 100644 --- a/cw_nano/lib/nano_wallet.dart +++ b/cw_nano/lib/nano_wallet.dart @@ -43,7 +43,7 @@ abstract class NanoWalletBase }) : syncStatus = NotConnectedSyncStatus(), _password = password, _mnemonic = mnemonic, - _derivationType = walletInfo.derivationType!, + _derivationType = walletInfo.derivationInfo!.derivationType!, _isTransactionUpdating = false, _client = NanoClient(), walletAddresses = NanoWalletAddresses(walletInfo), @@ -389,7 +389,10 @@ abstract class NanoWalletBase derivationType = DerivationType.bip39; } - walletInfo.derivationType = derivationType; + walletInfo.derivationInfo ??= DerivationInfo(derivationType: derivationType); + if (walletInfo.derivationInfo!.derivationType == null) { + walletInfo.derivationInfo!.derivationType = derivationType; + } return NanoWallet( walletInfo: walletInfo, diff --git a/cw_nano/lib/nano_wallet_creation_credentials.dart b/cw_nano/lib/nano_wallet_creation_credentials.dart index 3616fcf44..4ee79ce48 100644 --- a/cw_nano/lib/nano_wallet_creation_credentials.dart +++ b/cw_nano/lib/nano_wallet_creation_credentials.dart @@ -2,8 +2,15 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; class NanoNewWalletCredentials extends WalletCredentials { - NanoNewWalletCredentials({required String name, String? password}) - : super(name: name, password: password); + NanoNewWalletCredentials({ + required String name, + String? password, + DerivationType? derivationType, + }) : super( + name: name, + password: password, + derivationInfo: DerivationInfo(derivationType: derivationType), + ); } class NanoRestoreWalletFromSeedCredentials extends WalletCredentials { @@ -11,11 +18,11 @@ class NanoRestoreWalletFromSeedCredentials extends WalletCredentials { required String name, required this.mnemonic, String? password, - DerivationType? derivationType, + required DerivationType derivationType, }) : super( name: name, password: password, - derivationType: derivationType, + derivationInfo: DerivationInfo(derivationType: derivationType), ); final String mnemonic; @@ -30,12 +37,12 @@ class NanoRestoreWalletFromKeysCredentials extends WalletCredentials { NanoRestoreWalletFromKeysCredentials({ required String name, required String password, + required DerivationType derivationType, required this.seedKey, - DerivationType? derivationType, }) : super( name: name, password: password, - derivationType: derivationType, + derivationInfo: DerivationInfo(derivationType: derivationType), ); final String seedKey; diff --git a/cw_nano/lib/nano_wallet_service.dart b/cw_nano/lib/nano_wallet_service.dart index 7ab502d49..a1af3c872 100644 --- a/cw_nano/lib/nano_wallet_service.dart +++ b/cw_nano/lib/nano_wallet_service.dart @@ -14,7 +14,7 @@ import 'package:nanodart/nanodart.dart'; import 'package:nanoutil/nanoutil.dart'; class NanoWalletService extends WalletService { + NanoRestoreWalletFromSeedCredentials, NanoRestoreWalletFromKeysCredentials, NanoNewWalletCredentials> { NanoWalletService(this.walletInfoSource); final Box walletInfoSource; @@ -28,11 +28,11 @@ class NanoWalletService extends WalletService create(NanoNewWalletCredentials credentials, {bool? isTestnet}) async { // nano standard: - DerivationType derivationType = DerivationType.nano; String seedKey = NanoSeeds.generateSeed(); String mnemonic = NanoDerivations.standardSeedToMnemonic(seedKey); - credentials.walletInfo!.derivationType = derivationType; + // ensure default if not present: + credentials.walletInfo!.derivationInfo ??= DerivationInfo(derivationType: DerivationType.nano); final wallet = NanoWallet( walletInfo: credentials.walletInfo!, @@ -88,9 +88,6 @@ class NanoWalletService extends WalletService restoreFromHardwareWallet(NanoNewWalletCredentials credentials) { + throw UnimplementedError("Restoring a Nano wallet from a hardware wallet is not yet supported!"); + } + @override Future restoreFromSeed(NanoRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { if (credentials.mnemonic.contains(' ')) { @@ -128,9 +130,10 @@ class NanoWalletService extends WalletService=3.0.0 <4.0.0" + dart: ">=3.2.0-0 <4.0.0" flutter: ">=3.7.0" diff --git a/cw_nano/pubspec.yaml b/cw_nano/pubspec.yaml index a4b8732fd..768c1bb4e 100644 --- a/cw_nano/pubspec.yaml +++ b/cw_nano/pubspec.yaml @@ -32,10 +32,13 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.1.11 + build_runner: ^2.4.7 mobx_codegen: ^2.0.7 hive_generator: ^1.1.3 +dependency_overrides: + watcher: ^1.1.0 + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_polygon/lib/polygon_wallet_service.dart b/cw_polygon/lib/polygon_wallet_service.dart index 59e14abbf..ee84a014e 100644 --- a/cw_polygon/lib/polygon_wallet_service.dart +++ b/cw_polygon/lib/polygon_wallet_service.dart @@ -1,5 +1,6 @@ import 'package:bip39/bip39.dart' as bip39; import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart'; import 'package:cw_evm/evm_chain_wallet_service.dart'; @@ -86,6 +87,29 @@ class PolygonWalletService extends EVMChainWalletService { return wallet; } + @override + Future restoreFromHardwareWallet( + EVMChainRestoreWalletFromHardware credentials) async { + credentials.walletInfo!.derivationInfo = DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/44'/60'/${credentials.hwAccountData.accountIndex}'/0/0" + ); + credentials.walletInfo!.hardwareWalletType = credentials.hardwareWalletType; + credentials.walletInfo!.address = credentials.hwAccountData.address; + + final wallet = PolygonWallet( + walletInfo: credentials.walletInfo!, + password: credentials.password!, + client: client, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + @override Future restoreFromSeed(EVMChainRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { diff --git a/cw_polygon/pubspec.yaml b/cw_polygon/pubspec.yaml index 505838d7c..8421562b4 100644 --- a/cw_polygon/pubspec.yaml +++ b/cw_polygon/pubspec.yaml @@ -23,12 +23,19 @@ dependencies: bip39: ^1.0.6 collection: ^1.17.1 +dependency_overrides: + web3dart: + git: + url: https://github.com/cake-tech/web3dart.git + ref: cake + watcher: ^1.1.0 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 - build_runner: ^2.1.11 + build_runner: ^2.4.7 + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_solana/lib/solana_transaction_info.dart b/cw_solana/lib/solana_transaction_info.dart index 1b7610e34..7a0844e52 100644 --- a/cw_solana/lib/solana_transaction_info.dart +++ b/cw_solana/lib/solana_transaction_info.dart @@ -34,10 +34,7 @@ class SolanaTransactionInfo extends TransactionInfo { @override String amountFormatted() { String stringBalance = solAmount.toString(); - - if (stringBalance.toString().length >= 6) { - stringBalance = stringBalance.substring(0, 6); - } + return '$stringBalance $tokenSymbol'; } diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index ad58c4293..401968698 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -27,6 +27,7 @@ import 'package:hex/hex.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:solana/base58.dart'; import 'package:solana/metaplex.dart' as metaplex; import 'package:solana/solana.dart'; @@ -108,7 +109,17 @@ abstract class SolanaWalletBase String? get seed => _mnemonic; @override - String get privateKey => HEX.encode(_keyPairData!.bytes); + String get privateKey { + final privateKeyBytes = _keyPairData!.bytes; + + final publicKeyBytes = _keyPairData!.publicKey.bytes; + + final encodedBytes = privateKeyBytes + publicKeyBytes; + + final privateKey = base58encode(encodedBytes); + + return privateKey; + } Future init() async { final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${SPLToken.boxName}"; @@ -134,12 +145,17 @@ abstract class SolanaWalletBase Future getWalletPair({String? mnemonic, String? privateKey}) async { assert(mnemonic != null || privateKey != null); - if (privateKey != null) { - final privateKeyBytes = HEX.decode(privateKey); - return await Wallet.fromPrivateKeyBytes(privateKey: privateKeyBytes); + if (mnemonic != null) { + return Wallet.fromMnemonic(mnemonic, account: 0, change: 0); } - return Wallet.fromMnemonic(mnemonic!, account: 0, change: 0); + try { + final privateKeyBytes = base58decode(privateKey!); + return await Wallet.fromPrivateKeyBytes(privateKey: privateKeyBytes.take(32).toList()); + } catch (_) { + final privateKeyBytes = HEX.decode(privateKey!); + return await Wallet.fromPrivateKeyBytes(privateKey: privateKeyBytes); + } } @override @@ -262,32 +278,12 @@ abstract class SolanaWalletBase final transactions = await _client.fetchTransactions(address); - final Map result = {}; - - for (var transactionModel in transactions) { - result[transactionModel.id] = SolanaTransactionInfo( - id: transactionModel.id, - to: transactionModel.to, - from: transactionModel.from, - blockTime: transactionModel.blockTime, - direction: transactionModel.isOutgoingTx - ? TransactionDirection.outgoing - : TransactionDirection.incoming, - solAmount: transactionModel.amount, - isPending: false, - txFee: transactionModel.fee, - tokenSymbol: transactionModel.tokenSymbol, - ); - } - - transactionHistory.addMany(result); - - await transactionHistory.save(); + await _addTransactionsToTransactionHistory(transactions); } /// Fetches the SPL Tokens transactions linked to the token account Public Key Future _updateSPLTokenTransactions() async { - List splTokenTransactions = []; + // List splTokenTransactions = []; // Make a copy of keys to avoid concurrent modification var tokenKeys = List.from(balance.keys); @@ -301,13 +297,20 @@ abstract class SolanaWalletBase _walletKeyPair!, ); - splTokenTransactions.addAll(tokenTxs); + // splTokenTransactions.addAll(tokenTxs); + await _addTransactionsToTransactionHistory(tokenTxs); } } + // await _addTransactionsToTransactionHistory(splTokenTransactions); + } + + Future _addTransactionsToTransactionHistory( + List transactions, + ) async { final Map result = {}; - for (var transactionModel in splTokenTransactions) { + for (var transactionModel in transactions) { result[transactionModel.id] = SolanaTransactionInfo( id: transactionModel.id, to: transactionModel.to, @@ -362,7 +365,7 @@ abstract class SolanaWalletBase String toJSON() => json.encode({ 'mnemonic': _mnemonic, - 'private_key': privateKey, + 'private_key': _hexPrivateKey, 'balance': balance[currency]!.toJSON(), }); @@ -449,12 +452,23 @@ abstract class SolanaWalletBase await token.delete(); balance.remove(token); + await _removeTokenTransactionsInHistory(token); _updateBalance(); } + Future _removeTokenTransactionsInHistory(SPLToken token) async { + transactionHistory.transactions.removeWhere((key, value) => value.tokenSymbol == token.title); + await transactionHistory.save(); + } + Future getSPLToken(String mintAddress) async { // Convert SPL token mint address to public key - final mintPublicKey = Ed25519HDPublicKey.fromBase58(mintAddress); + final Ed25519HDPublicKey mintPublicKey; + try { + mintPublicKey = Ed25519HDPublicKey.fromBase58(mintAddress); + } catch (_) { + return null; + } // Fetch token's metadata account try { @@ -469,10 +483,12 @@ abstract class SolanaWalletBase iconPath = await _client.getIconImageFromTokenUri(token.uri); } catch (_) {} + String filteredTokenSymbol = token.symbol.replaceFirst(RegExp('^\\\$'), ''); + return SPLToken.fromMetadata( name: token.name, mint: token.mint, - symbol: token.symbol, + symbol: filteredTokenSymbol, mintAddress: mintAddress, iconPath: iconPath, ); @@ -508,7 +524,7 @@ abstract class SolanaWalletBase _transactionsUpdateTimer!.cancel(); } - _transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 20), (_) { + _transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 30), (_) { _updateBalance(); _updateNativeSOLTransactions(); _updateSPLTokenTransactions(); diff --git a/cw_solana/lib/solana_wallet_service.dart b/cw_solana/lib/solana_wallet_service.dart index 83370ff73..4afb2f7f4 100644 --- a/cw_solana/lib/solana_wallet_service.dart +++ b/cw_solana/lib/solana_wallet_service.dart @@ -2,7 +2,10 @@ import 'dart:io'; import 'package:bip39/bip39.dart' as bip39; import 'package:collection/collection.dart'; +import 'package:cw_core/balance.dart'; import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; @@ -13,7 +16,7 @@ import 'package:cw_solana/solana_wallet_creation_credentials.dart'; import 'package:hive/hive.dart'; class SolanaWalletService extends WalletService { + SolanaRestoreWalletFromSeedCredentials, SolanaRestoreWalletFromPrivateKey, SolanaNewWalletCredentials> { SolanaWalletService(this.walletInfoSource); final Box walletInfoSource; @@ -134,4 +137,10 @@ class SolanaWalletService extends WalletService, TransactionInfo>> restoreFromHardwareWallet(SolanaNewWalletCredentials credentials) { + // TODO: implement restoreFromHardwareWallet + throw UnimplementedError(); + } } diff --git a/cw_solana/lib/spl_token.dart b/cw_solana/lib/spl_token.dart index 0b3b8b372..a40eb0b86 100644 --- a/cw_solana/lib/spl_token.dart +++ b/cw_solana/lib/spl_token.dart @@ -19,7 +19,7 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin { @HiveField(3) final int decimal; - @HiveField(4, defaultValue: false) + @HiveField(4, defaultValue: true) bool _enabled; @HiveField(5) @@ -39,7 +39,7 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin { required this.mint, this.iconPath, this.tag = 'SOL', - bool enabled = false, + bool enabled = true, }) : _enabled = enabled, super( name: mint.toLowerCase(), diff --git a/cw_solana/pubspec.yaml b/cw_solana/pubspec.yaml index 7e24983bf..6b59282b4 100644 --- a/cw_solana/pubspec.yaml +++ b/cw_solana/pubspec.yaml @@ -26,10 +26,13 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 - build_runner: ^2.1.11 + build_runner: ^2.4.7 mobx_codegen: ^2.0.7 hive_generator: ^1.1.3 +dependency_overrides: + watcher: ^1.1.0 + flutter: # assets: # - images/a_dot_burr.jpeg diff --git a/cw_tron/.gitignore b/cw_tron/.gitignore new file mode 100644 index 000000000..96486fd93 --- /dev/null +++ b/cw_tron/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/cw_tron/.metadata b/cw_tron/.metadata new file mode 100644 index 000000000..fa347fc6a --- /dev/null +++ b/cw_tron/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + channel: stable + +project_type: package diff --git a/cw_tron/CHANGELOG.md b/cw_tron/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/cw_tron/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/cw_tron/LICENSE b/cw_tron/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_tron/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_tron/README.md b/cw_tron/README.md new file mode 100644 index 000000000..02fe8ecab --- /dev/null +++ b/cw_tron/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/cw_tron/analysis_options.yaml b/cw_tron/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_tron/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/cw_tron/lib/cw_tron.dart b/cw_tron/lib/cw_tron.dart new file mode 100644 index 000000000..6981fccba --- /dev/null +++ b/cw_tron/lib/cw_tron.dart @@ -0,0 +1,7 @@ +library cw_tron; + +/// A Calculator. +class Calculator { + /// Returns [value] plus 1. + int addOne(int value) => value + 1; +} diff --git a/cw_tron/lib/default_tron_tokens.dart b/cw_tron/lib/default_tron_tokens.dart new file mode 100644 index 000000000..6aa6357e6 --- /dev/null +++ b/cw_tron/lib/default_tron_tokens.dart @@ -0,0 +1,103 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_tron/tron_token.dart'; + +class DefaultTronTokens { + final List _defaultTokens = [ + TronToken( + name: "Tether USD", + symbol: "USDT", + contractAddress: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", + decimal: 6, + enabled: true, + ), + TronToken( + name: "USD Coin", + symbol: "USDC", + contractAddress: "TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8", + decimal: 6, + enabled: true, + ), + TronToken( + name: "Bitcoin", + symbol: "BTC", + contractAddress: "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9", + decimal: 8, + enabled: false, + ), + TronToken( + name: "Ethereum", + symbol: "ETH", + contractAddress: "TRFe3hT5oYhjSZ6f3ji5FJ7YCfrkWnHRvh", + decimal: 18, + enabled: false, + ), + TronToken( + name: "Wrapped BTC", + symbol: "WBTC", + contractAddress: "TXpw8XeWYeTUd4quDskoUqeQPowRh4jY65", + decimal: 8, + enabled: true, + ), + TronToken( + name: "Dogecoin", + symbol: "DOGE", + contractAddress: "THbVQp8kMjStKNnf2iCY6NEzThKMK5aBHg", + decimal: 8, + enabled: true, + ), + TronToken( + name: "JUST Stablecoin", + symbol: "USDJ", + contractAddress: "TMwFHYXLJaRUPeW6421aqXL4ZEzPRFGkGT", + decimal: 18, + enabled: false, + ), + TronToken( + name: "SUN", + symbol: "SUN", + contractAddress: "TSSMHYeV2uE9qYH95DqyoCuNCzEL1NvU3S", + decimal: 18, + enabled: false, + ), + TronToken( + name: "Wrapped TRX", + symbol: "WTRX", + contractAddress: "TNUC9Qb1rRpS5CbWLmNMxXBjyFoydXjWFR", + decimal: 6, + enabled: false, + ), + TronToken( + name: "BitTorent", + symbol: "BTT", + contractAddress: "TAFjULxiVgT4qWk6UZwjqwZXTSaGaqnVp4", + decimal: 18, + enabled: false, + ), + TronToken( + name: "BUSD Token", + symbol: "BUSD", + contractAddress: "TMz2SWatiAtZVVcH2ebpsbVtYwUPT9EdjH", + decimal: 18, + enabled: false, + ), + TronToken( + name: "HTX", + symbol: "HTX", + contractAddress: "TUPM7K8REVzD2UdV4R5fe5M8XbnR2DdoJ6", + decimal: 18, + enabled: false, + ), + ]; + + List get initialTronTokens => _defaultTokens.map((token) { + String? iconPath; + try { + iconPath = CryptoCurrency.all + .firstWhere((element) => + element.title.toUpperCase() == token.symbol.split(".").first.toUpperCase()) + .iconPath; + } catch (_) {} + + return TronToken.copyWith(token, iconPath, 'TRX'); + }).toList(); +} diff --git a/cw_tron/lib/file.dart b/cw_tron/lib/file.dart new file mode 100644 index 000000000..8fd236ec3 --- /dev/null +++ b/cw_tron/lib/file.dart @@ -0,0 +1,39 @@ +import 'dart:io'; +import 'package:cw_core/key.dart'; +import 'package:encrypt/encrypt.dart' as encrypt; + +Future write( + {required String path, + required String password, + required String data}) async { + final keys = extractKeys(password); + final key = encrypt.Key.fromBase64(keys.first); + final iv = encrypt.IV.fromBase64(keys.last); + final encrypted = await encode(key: key, iv: iv, data: data); + final f = File(path); + f.writeAsStringSync(encrypted); +} + +Future writeData( + {required String path, + required String password, + required String data}) async { + final keys = extractKeys(password); + final key = encrypt.Key.fromBase64(keys.first); + final iv = encrypt.IV.fromBase64(keys.last); + final encrypted = await encode(key: key, iv: iv, data: data); + final f = File(path); + f.writeAsStringSync(encrypted); +} + +Future read({required String path, required String password}) async { + final file = File(path); + + if (!file.existsSync()) { + file.createSync(); + } + + final encrypted = file.readAsStringSync(); + + return decode(password: password, data: encrypted); +} diff --git a/cw_tron/lib/pending_tron_transaction.dart b/cw_tron/lib/pending_tron_transaction.dart new file mode 100644 index 000000000..b6d064b31 --- /dev/null +++ b/cw_tron/lib/pending_tron_transaction.dart @@ -0,0 +1,33 @@ + + +import 'package:cw_core/pending_transaction.dart'; +import 'package:web3dart/crypto.dart'; + +class PendingTronTransaction with PendingTransaction { + final Function sendTransaction; + final List signedTransaction; + final String fee; + final String amount; + + PendingTronTransaction({ + required this.sendTransaction, + required this.signedTransaction, + required this.fee, + required this.amount, + }); + + @override + String get amountFormatted => amount; + + @override + Future commit() async => await sendTransaction(); + + @override + String get feeFormatted => fee; + + @override + String get hex => bytesToHex(signedTransaction); + + @override + String get id => ''; +} diff --git a/cw_tron/lib/tron_abi.dart b/cw_tron/lib/tron_abi.dart new file mode 100644 index 000000000..fdb998636 --- /dev/null +++ b/cw_tron/lib/tron_abi.dart @@ -0,0 +1,436 @@ +final trc20Abi = [ + {"inputs": [], "stateMutability": "nonpayable", "type": "constructor"}, + { + "anonymous": false, + "inputs": [ + {"indexed": true, "internalType": "address", "name": "owner", "type": "address"}, + {"indexed": true, "internalType": "address", "name": "spender", "type": "address"}, + {"indexed": false, "internalType": "uint256", "name": "value", "type": "uint256"} + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + {"indexed": false, "internalType": "uint256", "name": "total", "type": "uint256"}, + {"indexed": true, "internalType": "uint16", "name": "order_id", "type": "uint16"}, + {"indexed": true, "internalType": "address", "name": "buyer", "type": "address"}, + {"indexed": true, "internalType": "address", "name": "seller", "type": "address"}, + {"indexed": false, "internalType": "address", "name": "contract_address", "type": "address"} + ], + "name": "OrderPaid", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + {"indexed": true, "internalType": "address", "name": "previousOwner", "type": "address"}, + {"indexed": true, "internalType": "address", "name": "newOwner", "type": "address"} + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + {"indexed": false, "internalType": "address", "name": "token", "type": "address"}, + {"indexed": false, "internalType": "bool", "name": "active", "type": "bool"} + ], + "name": "TokenUpdate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + {"indexed": true, "internalType": "address", "name": "from", "type": "address"}, + {"indexed": true, "internalType": "address", "name": "to", "type": "address"}, + {"indexed": false, "internalType": "uint256", "name": "value", "type": "uint256"} + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + {"indexed": false, "internalType": "string", "name": "username", "type": "string"}, + {"indexed": true, "internalType": "address", "name": "seller", "type": "address"} + ], + "name": "UserRegistred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + {"indexed": true, "internalType": "uint16", "name": "order_id", "type": "uint16"}, + {"indexed": true, "internalType": "address", "name": "buyer", "type": "address"}, + {"indexed": false, "internalType": "address", "name": "seller", "type": "address"} + ], + "name": "WBuyer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + {"indexed": true, "internalType": "uint16", "name": "order_id", "type": "uint16"}, + {"indexed": true, "internalType": "address", "name": "seller", "type": "address"}, + {"indexed": false, "internalType": "address", "name": "buyer", "type": "address"} + ], + "name": "WSeller", + "type": "event" + }, + { + "inputs": [], + "name": "CONTRACTPERCENTAGE", + "outputs": [ + {"internalType": "uint8", "name": "", "type": "uint8"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint16", "name": "order_id", "type": "uint16"}, + {"internalType": "uint256", "name": "order_total", "type": "uint256"}, + {"internalType": "address", "name": "contractAddress", "type": "address"}, + {"internalType": "address", "name": "seller", "type": "address"} + ], + "name": "PayWithTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "TOKENINCREAMENT", + "outputs": [ + {"internalType": "uint16", "name": "", "type": "uint16"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "", "type": "address"} + ], + "name": "_signer", + "outputs": [ + {"internalType": "bool", "name": "", "type": "bool"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "", "type": "address"} + ], + "name": "_tokens", + "outputs": [ + {"internalType": "bool", "name": "active", "type": "bool"}, + {"internalType": "uint16", "name": "token", "type": "uint16"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "", "type": "address"} + ], + "name": "_users", + "outputs": [ + {"internalType": "bool", "name": "active", "type": "bool"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "owner", "type": "address"}, + {"internalType": "address", "name": "spender", "type": "address"} + ], + "name": "allowance", + "outputs": [ + {"internalType": "uint256", "name": "", "type": "uint256"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "spender", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "approve", + "outputs": [ + {"internalType": "bool", "name": "", "type": "bool"} + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "account", "type": "address"} + ], + "name": "balanceOf", + "outputs": [ + {"internalType": "uint256", "name": "", "type": "uint256"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "token", "type": "address"} + ], + "name": "balanceOfContract", + "outputs": [ + {"internalType": "uint256", "name": "", "type": "uint256"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "account", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "burnFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint256", "name": "value", "type": "uint256"}, + {"internalType": "address", "name": "_contractAddress", "type": "address"} + ], + "name": "contractWithdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + {"internalType": "uint8", "name": "", "type": "uint8"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "spender", "type": "address"}, + {"internalType": "uint256", "name": "subtractedValue", "type": "uint256"} + ], + "name": "decreaseAllowance", + "outputs": [ + {"internalType": "bool", "name": "", "type": "bool"} + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "spender", "type": "address"}, + {"internalType": "uint256", "name": "addedValue", "type": "uint256"} + ], + "name": "increaseAllowance", + "outputs": [ + {"internalType": "bool", "name": "", "type": "bool"} + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "to", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + {"internalType": "string", "name": "", "type": "string"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + {"internalType": "address", "name": "", "type": "address"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "token", "type": "address"}, + {"internalType": "uint256", "name": "value", "type": "uint256"} + ], + "name": "payToContract", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint16", "name": "order_id", "type": "uint16"}, + {"internalType": "address", "name": "seller", "type": "address"} + ], + "name": "payWithNativeToken", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "string", "name": "username", "type": "string"} + ], + "name": "regiserUser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint16", "name": "id", "type": "uint16"}, + {"internalType": "address", "name": "buyer", "type": "address"}, + {"internalType": "address", "name": "seller", "type": "address"} + ], + "name": "selectOrder", + "outputs": [ + {"internalType": "uint232", "name": "", "type": "uint232"}, + {"internalType": "uint16", "name": "", "type": "uint16"}, + {"internalType": "uint8", "name": "", "type": "uint8"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + {"internalType": "string", "name": "", "type": "string"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "signer", "type": "address"} + ], + "name": "toggleSigner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "tokenAddress", "type": "address"} + ], + "name": "toggleToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + {"internalType": "uint256", "name": "", "type": "uint256"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "to", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "transfer", + "outputs": [ + {"internalType": "bool", "name": "", "type": "bool"} + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "from", "type": "address"}, + {"internalType": "address", "name": "to", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "transferFrom", + "outputs": [ + {"internalType": "bool", "name": "", "type": "bool"} + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "newOwner", "type": "address"} + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint8", "name": "newPercentage", "type": "uint8"} + ], + "name": "updateContractPercentage", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address[]", "name": "buyer", "type": "address[]"}, + {"internalType": "bytes[]", "name": "signature", "type": "bytes[]"}, + {"internalType": "uint16[]", "name": "order_id", "type": "uint16[]"}, + {"internalType": "address", "name": "contractAddress", "type": "address"} + ], + "name": "widthrawForSellers", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "seller", "type": "address"}, + {"internalType": "bytes", "name": "signature", "type": "bytes"}, + {"internalType": "uint16", "name": "order_id", "type": "uint16"}, + {"internalType": "address", "name": "contractAddress", "type": "address"} + ], + "name": "widthrowForBuyers", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +]; diff --git a/cw_tron/lib/tron_balance.dart b/cw_tron/lib/tron_balance.dart new file mode 100644 index 000000000..5b2ba3fa7 --- /dev/null +++ b/cw_tron/lib/tron_balance.dart @@ -0,0 +1,34 @@ +import 'dart:convert'; + +import 'package:cw_core/balance.dart'; +import 'package:on_chain/on_chain.dart'; + +class TronBalance extends Balance { + TronBalance(this.balance) : super(balance.toInt(), balance.toInt()); + + final BigInt balance; + + @override + String get formattedAdditionalBalance => TronHelper.fromSun(balance); + + @override + String get formattedAvailableBalance => TronHelper.fromSun(balance); + + String toJSON() => json.encode({ + 'balance': balance.toString(), + }); + + static TronBalance? fromJSON(String? jsonSource) { + if (jsonSource == null) { + return null; + } + + final decoded = json.decode(jsonSource) as Map; + + try { + return TronBalance(BigInt.parse(decoded['balance'])); + } catch (e) { + return TronBalance(BigInt.zero); + } + } +} diff --git a/cw_tron/lib/tron_client.dart b/cw_tron/lib/tron_client.dart new file mode 100644 index 000000000..8eca02af6 --- /dev/null +++ b/cw_tron/lib/tron_client.dart @@ -0,0 +1,563 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; + +import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_tron/pending_tron_transaction.dart'; +import 'package:cw_tron/tron_abi.dart'; +import 'package:cw_tron/tron_balance.dart'; +import 'package:cw_tron/tron_http_provider.dart'; +import 'package:cw_tron/tron_token.dart'; +import 'package:cw_tron/tron_transaction_model.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:http/http.dart'; +import '.secrets.g.dart' as secrets; +import 'package:on_chain/on_chain.dart'; + +class TronClient { + final httpClient = Client(); + TronProvider? _provider; + // This is an internal tracker, so we don't have to "refetch". + int _nativeTxEstimatedFee = 0; + + int get chainId => 1000; + + Future> fetchTransactions(String address, + {String? contractAddress}) async { + try { + final response = await httpClient.get( + Uri.https( + "api.trongrid.io", + "/v1/accounts/$address/transactions", + { + "only_confirmed": "true", + "limit": "200", + }, + ), + headers: { + 'Content-Type': 'application/json', + 'TRON-PRO-API-KEY': secrets.tronGridApiKey, + }, + ); + final jsonResponse = json.decode(response.body) as Map; + + if (response.statusCode >= 200 && + response.statusCode < 300 && + jsonResponse['status'] != false) { + return (jsonResponse['data'] as List).map((e) { + return TronTransactionModel.fromJson(e as Map); + }).toList(); + } + + return []; + } catch (e, s) { + log('Error getting tx: ${e.toString()}\n ${s.toString()}'); + return []; + } + } + + Future> fetchTrc20ExcludedTransactions(String address) async { + try { + final response = await httpClient.get( + Uri.https( + "api.trongrid.io", + "/v1/accounts/$address/transactions/trc20", + { + "only_confirmed": "true", + "limit": "200", + }, + ), + headers: { + 'Content-Type': 'application/json', + 'TRON-PRO-API-KEY': secrets.tronGridApiKey, + }, + ); + final jsonResponse = json.decode(response.body) as Map; + + if (response.statusCode >= 200 && + response.statusCode < 300 && + jsonResponse['status'] != false) { + return (jsonResponse['data'] as List).map((e) { + return TronTRC20TransactionModel.fromJson(e as Map); + }).toList(); + } + + return []; + } catch (e, s) { + log('Error getting trc20 tx: ${e.toString()}\n ${s.toString()}'); + return []; + } + } + + bool connect(Node node) { + try { + final formattedUrl = '${node.isSSL ? 'https' : 'http'}://${node.uriRaw}'; + _provider = TronProvider(TronHTTPProvider(url: formattedUrl)); + + return true; + } catch (e) { + return false; + } + } + + Future getBalance(TronAddress address) async { + try { + final accountDetails = await _provider!.request(TronRequestGetAccount(address: address)); + + return accountDetails?.balance ?? BigInt.zero; + } catch (_) { + return BigInt.zero; + } + } + + Future getFeeLimit( + TransactionRaw rawTransaction, + TronAddress address, + TronAddress receiverAddress, { + int energyUsed = 0, + bool isEstimatedFeeFlow = false, + }) async { + try { + // Get the tron chain parameters. + final chainParams = await _provider!.request(TronRequestGetChainParameters()); + + final bandWidthInSun = chainParams.getTransactionFee!; + log('BandWidth In Sun: $bandWidthInSun'); + + final energyInSun = chainParams.getEnergyFee!; + log('Energy In Sun: $energyInSun'); + + final fakeTransaction = Transaction( + rawData: rawTransaction, + signature: [Uint8List(65)], + ); + + // Calculate the total size of the fake transaction, considering the required network overhead. + final transactionSize = fakeTransaction.length + 64; + + // Assign the calculated size to the variable representing the required bandwidth. + int neededBandWidth = transactionSize; + log('Initial Needed Bandwidth: $neededBandWidth'); + + int neededEnergy = energyUsed; + log('Initial Needed Energy: $neededEnergy'); + + // Fetch account resources to assess the available bandwidth and energy + final accountResource = + await _provider!.request(TronRequestGetAccountResource(address: address)); + + neededEnergy -= accountResource.howManyEnergy.toInt(); + log('Account resource energy: ${accountResource.howManyEnergy.toInt()}'); + log('Needed Energy after deducting from account resource energy: $neededEnergy'); + + // Deduct the bandwidth from the account's available bandwidth. + final BigInt accountBandWidth = accountResource.howManyBandwIth; + log('Account resource bandwidth: ${accountResource.howManyBandwIth.toInt()}'); + + if (accountBandWidth >= BigInt.from(neededBandWidth) && !isEstimatedFeeFlow) { + log('Account has more bandwidth than required'); + neededBandWidth = 0; + } + + if (neededEnergy < 0) { + neededEnergy = 0; + } + + final energyBurn = neededEnergy * energyInSun.toInt(); + log('Energy Burn: $energyBurn'); + + final bandWidthBurn = neededBandWidth * bandWidthInSun; + log('Bandwidth Burn: $bandWidthBurn'); + + int totalBurn = energyBurn + bandWidthBurn; + log('Total Burn: $totalBurn'); + + /// If there is a note (memo), calculate the memo fee. + if (rawTransaction.data != null) { + totalBurn += chainParams.getMemoFee!; + } + + log('Final total burn: $totalBurn'); + + return totalBurn; + } catch (_) { + return 0; + } + } + + Future getEstimatedFee(TronAddress ownerAddress) async { + const constantAmount = '1000'; + // Fetch the latest Tron block + final block = await _provider!.request(TronRequestGetNowBlock()); + + // Create the transfer contract + final contract = TransferContract( + amount: TronHelper.toSun(constantAmount), + ownerAddress: ownerAddress, + toAddress: ownerAddress, + ); + + // Prepare the contract parameter for the transaction. + final parameter = Any(typeUrl: contract.typeURL, value: contract); + + // Create a TransactionContract object with the contract type and parameter. + final transactionContract = + TransactionContract(type: contract.contractType, parameter: parameter); + + // Set the transaction expiration time (maximum 24 hours) + final expireTime = DateTime.now().add(const Duration(minutes: 30)); + + // Create a raw transaction + TransactionRaw rawTransaction = TransactionRaw( + refBlockBytes: block.blockHeader.rawData.refBlockBytes, + refBlockHash: block.blockHeader.rawData.refBlockHash, + expiration: BigInt.from(expireTime.millisecondsSinceEpoch), + contract: [transactionContract], + timestamp: block.blockHeader.rawData.timestamp, + ); + + final estimatedFee = await getFeeLimit( + rawTransaction, + ownerAddress, + ownerAddress, + isEstimatedFeeFlow: true, + ); + + _nativeTxEstimatedFee = estimatedFee; + + return estimatedFee; + } + + Future getTRCEstimatedFee(TronAddress ownerAddress) async { + String contractAddress = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'; + String constantAmount = + '0'; // We're using 0 as the base amount here as we get an error when balance is zero i.e for new wallets. + final contract = ContractABI.fromJson(trc20Abi, isTron: true); + + final function = contract.functionFromName("transfer"); + + /// address /// amount + final transferparams = [ + ownerAddress, + TronHelper.toSun(constantAmount), + ]; + + final contractAddr = TronAddress(contractAddress); + + final request = await _provider!.request( + TronRequestTriggerConstantContract( + ownerAddress: ownerAddress, + contractAddress: contractAddr, + data: function.encodeHex(transferparams), + ), + ); + + if (!request.isSuccess) { + log("Tron TRC20 error: ${request.error} \n ${request.respose}"); + } + + final feeLimit = await getFeeLimit( + request.transactionRaw!, + ownerAddress, + ownerAddress, + energyUsed: request.energyUsed ?? 0, + isEstimatedFeeFlow: true, + ); + return feeLimit; + } + + Future signTransaction({ + required TronPrivateKey ownerPrivKey, + required String toAddress, + required String amount, + required CryptoCurrency currency, + required BigInt tronBalance, + required bool sendAll, + }) async { + // Get the owner tron address from the key + final ownerAddress = ownerPrivKey.publicKey().toAddress(); + + // Define the receiving Tron address for the transaction. + final receiverAddress = TronAddress(toAddress); + + bool isNativeTransaction = currency == CryptoCurrency.trx; + + String totalAmount; + TransactionRaw rawTransaction; + if (isNativeTransaction) { + if (sendAll) { + final accountResource = + await _provider!.request(TronRequestGetAccountResource(address: ownerAddress)); + + final availableBandWidth = accountResource.howManyBandwIth.toInt(); + + // 269 is the current middle ground for bandwidth per transaction + if (availableBandWidth >= 269) { + totalAmount = amount; + } else { + final amountInSun = TronHelper.toSun(amount).toInt(); + + // 5000 added here is a buffer since we're working with "estimated" value of the fee. + final result = amountInSun - (_nativeTxEstimatedFee + 5000); + + totalAmount = TronHelper.fromSun(BigInt.from(result)); + } + } else { + totalAmount = amount; + } + rawTransaction = await _signNativeTransaction( + ownerAddress, + receiverAddress, + totalAmount, + tronBalance, + sendAll, + ); + } else { + final tokenAddress = (currency as TronToken).contractAddress; + totalAmount = amount; + rawTransaction = await _signTrcTokenTransaction( + ownerAddress, + receiverAddress, + totalAmount, + tokenAddress, + tronBalance, + ); + } + + final signature = ownerPrivKey.sign(rawTransaction.toBuffer()); + + sendTx() async => await sendTransaction( + rawTransaction: rawTransaction, + signature: signature, + ); + + return PendingTronTransaction( + signedTransaction: signature, + amount: totalAmount, + fee: TronHelper.fromSun(rawTransaction.feeLimit ?? BigInt.zero), + sendTransaction: sendTx, + ); + } + + Future _signNativeTransaction( + TronAddress ownerAddress, + TronAddress receiverAddress, + String amount, + BigInt tronBalance, + bool sendAll, + ) async { + // This is introduce to server as a limit in cases where feeLimit is 0 + // The transaction signing will fail if the feeLimit is explicitly 0. + int defaultFeeLimit = 269000; + + final block = await _provider!.request(TronRequestGetNowBlock()); + // Create the transfer contract + final contract = TransferContract( + amount: TronHelper.toSun(amount), + ownerAddress: ownerAddress, + toAddress: receiverAddress, + ); + + // Prepare the contract parameter for the transaction. + final parameter = Any(typeUrl: contract.typeURL, value: contract); + + // Create a TransactionContract object with the contract type and parameter. + final transactionContract = + TransactionContract(type: contract.contractType, parameter: parameter); + + // Set the transaction expiration time (maximum 24 hours) + final expireTime = DateTime.now().add(const Duration(minutes: 30)); + + // Create a raw transaction + TransactionRaw rawTransaction = TransactionRaw( + refBlockBytes: block.blockHeader.rawData.refBlockBytes, + refBlockHash: block.blockHeader.rawData.refBlockHash, + expiration: BigInt.from(expireTime.millisecondsSinceEpoch), + contract: [transactionContract], + timestamp: block.blockHeader.rawData.timestamp, + ); + + final feeLimit = await getFeeLimit(rawTransaction, ownerAddress, receiverAddress); + final feeLimitToUse = feeLimit != 0 ? feeLimit : defaultFeeLimit; + final tronBalanceInt = tronBalance.toInt(); + + if (feeLimit > tronBalanceInt) { + final feeInTrx = TronHelper.fromSun(BigInt.parse(feeLimit.toString())); + throw Exception( + 'You don\'t have enough TRX to cover the transaction fee for this transaction. Please top up.\nTransaction fee: $feeInTrx TRX', + ); + } + + rawTransaction = rawTransaction.copyWith( + feeLimit: BigInt.from(feeLimitToUse), + ); + + return rawTransaction; + } + + Future _signTrcTokenTransaction( + TronAddress ownerAddress, + TronAddress receiverAddress, + String amount, + String contractAddress, + BigInt tronBalance, + ) async { + final contract = ContractABI.fromJson(trc20Abi, isTron: true); + + final function = contract.functionFromName("transfer"); + + /// address /// amount + final transferparams = [ + receiverAddress, + TronHelper.toSun(amount), + ]; + + final contractAddr = TronAddress(contractAddress); + + final request = await _provider!.request( + TronRequestTriggerConstantContract( + ownerAddress: ownerAddress, + contractAddress: contractAddr, + data: function.encodeHex(transferparams), + ), + ); + + if (!request.isSuccess) { + log("Tron TRC20 error: ${request.error} \n ${request.respose}"); + throw Exception( + 'An error occured while creating the transfer request. Please try again.', + ); + } + + final feeLimit = await getFeeLimit( + request.transactionRaw!, + ownerAddress, + receiverAddress, + energyUsed: request.energyUsed ?? 0, + ); + + final tronBalanceInt = tronBalance.toInt(); + + if (feeLimit > tronBalanceInt) { + final feeInTrx = TronHelper.fromSun(BigInt.parse(feeLimit.toString())); + throw Exception( + 'You don\'t have enough TRX to cover the transaction fee for this transaction. Please top up. Transaction fee: $feeInTrx TRX', + ); + } + + final rawTransaction = request.transactionRaw!.copyWith( + feeLimit: BigInt.from(feeLimit), + ); + + return rawTransaction; + } + + Future sendTransaction({ + required TransactionRaw rawTransaction, + required List signature, + }) async { + try { + final transaction = Transaction(rawData: rawTransaction, signature: [signature]); + + final raw = BytesUtils.toHexString(transaction.toBuffer()); + + final txBroadcastResult = await _provider!.request(TronRequestBroadcastHex(transaction: raw)); + + if (txBroadcastResult.isSuccess) { + return txBroadcastResult.txId!; + } else { + throw Exception(txBroadcastResult.error); + } + } catch (e) { + log('Send block Exception: ${e.toString()}'); + throw Exception(e); + } + } + + Future fetchTronTokenBalances(String userAddress, String contractAddress) async { + try { + final ownerAddress = TronAddress(userAddress); + + final tokenAddress = TronAddress(contractAddress); + + final contract = ContractABI.fromJson(trc20Abi, isTron: true); + + final function = contract.functionFromName("balanceOf"); + + final request = await _provider!.request( + TronRequestTriggerConstantContract.fromMethod( + ownerAddress: ownerAddress, + contractAddress: tokenAddress, + function: function, + params: [ownerAddress], + ), + ); + + final outputResult = request.outputResult?.first ?? BigInt.zero; + + return TronBalance(outputResult); + } catch (_) { + return TronBalance(BigInt.zero); + } + } + + Future getTronToken(String contractAddress, String userAddress) async { + try { + final tokenAddress = TronAddress(contractAddress); + + final ownerAddress = TronAddress(userAddress); + + final contract = ContractABI.fromJson(trc20Abi, isTron: true); + + final name = + (await getTokenDetail(contract, "name", ownerAddress, tokenAddress) as String?) ?? ''; + + final symbol = + (await getTokenDetail(contract, "symbol", ownerAddress, tokenAddress) as String?) ?? ''; + + final decimal = + (await getTokenDetail(contract, "decimals", ownerAddress, tokenAddress) as BigInt?) ?? + BigInt.zero; + + return TronToken( + name: name, + symbol: symbol, + contractAddress: contractAddress, + decimal: decimal.toInt(), + ); + } catch (e) { + return null; + } + } + + Future getTokenDetail( + ContractABI contract, + String functionName, + TronAddress ownerAddress, + TronAddress tokenAddress, + ) async { + final function = contract.functionFromName(functionName); + + try { + final request = await _provider!.request( + TronRequestTriggerConstantContract.fromMethod( + ownerAddress: ownerAddress, + contractAddress: tokenAddress, + function: function, + params: [], + ), + ); + + final outputResult = request.outputResult?.first; + + return outputResult; + } catch (_) { + log('Erorr fetching detail: ${_.toString()}'); + + return null; + } + } +} diff --git a/cw_tron/lib/tron_exception.dart b/cw_tron/lib/tron_exception.dart new file mode 100644 index 000000000..13b98c024 --- /dev/null +++ b/cw_tron/lib/tron_exception.dart @@ -0,0 +1,16 @@ +import 'package:cw_core/crypto_currency.dart'; + +class TronMnemonicIsIncorrectException implements Exception { + @override + String toString() => + 'Tron mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; +} +class TronTransactionCreationException implements Exception { + final String exceptionMessage; + + TronTransactionCreationException(CryptoCurrency currency) + : exceptionMessage = 'Wrong balance. Not enough ${currency.title} on your balance.'; + + @override + String toString() => exceptionMessage; +} \ No newline at end of file diff --git a/cw_tron/lib/tron_http_provider.dart b/cw_tron/lib/tron_http_provider.dart new file mode 100644 index 000000000..58d313378 --- /dev/null +++ b/cw_tron/lib/tron_http_provider.dart @@ -0,0 +1,41 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:on_chain/tron/tron.dart'; +import '.secrets.g.dart' as secrets; + +class TronHTTPProvider implements TronServiceProvider { + TronHTTPProvider( + {required this.url, + http.Client? client, + this.defaultRequestTimeout = const Duration(seconds: 30)}) + : client = client ?? http.Client(); + @override + final String url; + final http.Client client; + final Duration defaultRequestTimeout; + + @override + Future> get(TronRequestDetails params, [Duration? timeout]) async { + final response = await client.get(Uri.parse(params.url(url)), headers: { + 'Content-Type': 'application/json', + if (url.contains("trongrid")) 'TRON-PRO-API-KEY': secrets.tronGridApiKey, + }).timeout(timeout ?? defaultRequestTimeout); + final data = json.decode(response.body) as Map; + return data; + } + + @override + Future> post(TronRequestDetails params, [Duration? timeout]) async { + final response = await client + .post(Uri.parse(params.url(url)), + headers: { + 'Content-Type': 'application/json', + if (url.contains("trongrid")) 'TRON-PRO-API-KEY': secrets.tronGridApiKey, + }, + body: params.toRequestBody()) + .timeout(timeout ?? defaultRequestTimeout); + final data = json.decode(response.body) as Map; + return data; + } +} diff --git a/cw_tron/lib/tron_token.dart b/cw_tron/lib/tron_token.dart new file mode 100644 index 000000000..8c45ab486 --- /dev/null +++ b/cw_tron/lib/tron_token.dart @@ -0,0 +1,80 @@ +// ignore_for_file: annotate_overrides, overridden_fields + +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/hive_type_ids.dart'; +import 'package:hive/hive.dart'; + +part 'tron_token.g.dart'; + +@HiveType(typeId: TronToken.typeId) +class TronToken extends CryptoCurrency with HiveObjectMixin { + @HiveField(0) + final String name; + + @HiveField(1) + final String symbol; + + @HiveField(2) + final String contractAddress; + + @HiveField(3) + final int decimal; + + @HiveField(4, defaultValue: true) + bool _enabled; + + @HiveField(5) + final String? iconPath; + + @HiveField(6) + final String? tag; + + bool get enabled => _enabled; + + set enabled(bool value) => _enabled = value; + + TronToken({ + required this.name, + required this.symbol, + required this.contractAddress, + required this.decimal, + bool enabled = true, + this.iconPath, + this.tag = 'TRX', + }) : _enabled = enabled, + super( + name: symbol.toLowerCase(), + title: symbol.toUpperCase(), + fullName: name, + tag: tag, + iconPath: iconPath, + decimals: decimal); + + TronToken.copyWith(TronToken other, String? icon, String? tag) + : name = other.name, + symbol = other.symbol, + contractAddress = other.contractAddress, + decimal = other.decimal, + _enabled = other.enabled, + tag = tag ?? other.tag, + iconPath = icon ?? other.iconPath, + super( + name: other.name, + title: other.symbol.toUpperCase(), + fullName: other.name, + tag: tag ?? other.tag, + iconPath: icon ?? other.iconPath, + decimals: other.decimal, + ); + + static const typeId = TRON_TOKEN_TYPE_ID; + static const boxName = 'TronTokens'; + + @override + bool operator ==(other) => + (other is TronToken && other.contractAddress == contractAddress) || + (other is CryptoCurrency && other.title == title); + + @override + int get hashCode => contractAddress.hashCode; +} diff --git a/cw_tron/lib/tron_transaction_credentials.dart b/cw_tron/lib/tron_transaction_credentials.dart new file mode 100644 index 000000000..e68d5525b --- /dev/null +++ b/cw_tron/lib/tron_transaction_credentials.dart @@ -0,0 +1,12 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/output_info.dart'; + +class TronTransactionCredentials { + TronTransactionCredentials( + this.outputs, { + required this.currency, + }); + + final List outputs; + final CryptoCurrency currency; +} diff --git a/cw_tron/lib/tron_transaction_history.dart b/cw_tron/lib/tron_transaction_history.dart new file mode 100644 index 000000000..7d7274226 --- /dev/null +++ b/cw_tron/lib/tron_transaction_history.dart @@ -0,0 +1,80 @@ +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/file.dart'; +import 'package:cw_tron/tron_transaction_info.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cw_core/transaction_history.dart'; + +part 'tron_transaction_history.g.dart'; + +class TronTransactionHistory = TronTransactionHistoryBase with _$TronTransactionHistory; + +abstract class TronTransactionHistoryBase extends TransactionHistoryBase + with Store { + TronTransactionHistoryBase({required this.walletInfo, required String password}) + : _password = password { + transactions = ObservableMap(); + } + + String _password; + + final WalletInfo walletInfo; + + Future init() async => await _load(); + + @override + Future save() async { + String transactionsHistoryFileNameForWallet = 'tron_transactions.json'; + try { + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + String path = '$dirPath/$transactionsHistoryFileNameForWallet'; + final transactionMaps = transactions.map((key, value) => MapEntry(key, value.toJson())); + final data = json.encode({'transactions': transactionMaps}); + 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(TronTransactionInfo transaction) => transactions[transaction.id] = transaction; + + @override + void addMany(Map transactions) => + this.transactions.addAll(transactions); + + Future> _read() async { + String transactionsHistoryFileNameForWallet = 'tron_transactions.json'; + 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; + } + + Future _load() async { + try { + final content = await _read(); + final txs = content['transactions'] as Map? ?? {}; + + for (var entry in txs.entries) { + final val = entry.value; + + if (val is Map) { + final tx = TronTransactionInfo.fromJson(val); + _update(tx); + } + } + } catch (e) { + log(e.toString()); + } + } + + void _update(TronTransactionInfo transaction) => transactions[transaction.id] = transaction; +} diff --git a/cw_tron/lib/tron_transaction_info.dart b/cw_tron/lib/tron_transaction_info.dart new file mode 100644 index 000000000..28c704d20 --- /dev/null +++ b/cw_tron/lib/tron_transaction_info.dart @@ -0,0 +1,93 @@ +import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:on_chain/on_chain.dart' as onchain; +import 'package:on_chain/tron/tron.dart'; + +class TronTransactionInfo extends TransactionInfo { + TronTransactionInfo({ + required this.id, + required this.tronAmount, + required this.txFee, + required this.direction, + required this.blockTime, + required this.to, + required this.from, + required this.isPending, + this.tokenSymbol = 'TRX', + }) : amount = tronAmount.toInt(); + + final String id; + final String? to; + final String? from; + final int amount; + final BigInt tronAmount; + final String tokenSymbol; + final DateTime blockTime; + final bool isPending; + final int? txFee; + final TransactionDirection direction; + + factory TronTransactionInfo.fromJson(Map data) { + return TronTransactionInfo( + id: data['id'] as String, + tronAmount: BigInt.parse(data['tronAmount']), + txFee: data['txFee'], + direction: parseTransactionDirectionFromInt(data['direction'] as int), + blockTime: DateTime.fromMillisecondsSinceEpoch(data['blockTime'] as int), + tokenSymbol: data['tokenSymbol'] as String, + to: data['to'], + from: data['from'], + isPending: data['isPending'], + ); + } + + Map toJson() => { + 'id': id, + 'tronAmount': tronAmount.toString(), + 'txFee': txFee, + 'direction': direction.index, + 'blockTime': blockTime.millisecondsSinceEpoch, + 'tokenSymbol': tokenSymbol, + 'to': to, + 'from': from, + 'isPending': isPending, + }; + + @override + DateTime get date => blockTime; + + String? _fiatAmount; + + @override + String amountFormatted() { + String formattedAmount = _rawAmountAsString(tronAmount); + + return '$formattedAmount $tokenSymbol'; + } + + @override + String fiatAmount() => _fiatAmount ?? ''; + + @override + void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); + + @override + String feeFormatted() { + final formattedFee = onchain.TronHelper.fromSun(BigInt.from(txFee ?? 0)); + + return '$formattedFee TRX'; + } + + String _rawAmountAsString(BigInt amount) { + String formattedAmount = TronHelper.fromSun(amount); + + if (formattedAmount.length >= 8) { + formattedAmount = formattedAmount.substring(0, 8); + } + + return formattedAmount; + } + + String rawTronAmount() => _rawAmountAsString(tronAmount); +} diff --git a/cw_tron/lib/tron_transaction_model.dart b/cw_tron/lib/tron_transaction_model.dart new file mode 100644 index 000000000..1748adc53 --- /dev/null +++ b/cw_tron/lib/tron_transaction_model.dart @@ -0,0 +1,205 @@ +import 'package:blockchain_utils/hex/hex.dart'; +import 'package:on_chain/on_chain.dart'; + +class TronTRC20TransactionModel extends TronTransactionModel { + String? transactionId; + + String? tokenSymbol; + + int? timestamp; + + @override + String? from; + + @override + String? to; + + String? value; + + @override + String get hash => transactionId!; + + @override + DateTime get date => DateTime.fromMillisecondsSinceEpoch(timestamp ?? 0); + + @override + BigInt? get amount => BigInt.parse(value ?? '0'); + + @override + int? get fee => 0; + + TronTRC20TransactionModel({ + this.transactionId, + this.tokenSymbol, + this.timestamp, + this.from, + this.to, + this.value, + }); + + TronTRC20TransactionModel.fromJson(Map json) { + transactionId = json['transaction_id']; + tokenSymbol = json['token_info'] != null ? json['token_info']['symbol'] : null; + timestamp = json['block_timestamp']; + from = json['from']; + to = json['to']; + value = json['value']; + } +} + +class TronTransactionModel { + List? ret; + String? txID; + int? blockTimestamp; + List? contracts; + + /// Getters to extract out the needed/useful information directly from the model params + /// Without having to go through extra steps in the methods that use this model. + bool get isError { + if (ret?.first.contractRet == null) return true; + + return ret?.first.contractRet != "SUCCESS"; + } + + String get hash => txID!; + + DateTime get date => DateTime.fromMillisecondsSinceEpoch(blockTimestamp ?? 0); + + String? get from => contracts?.first.parameter?.value?.ownerAddress; + + String? get to => contracts?.first.parameter?.value?.receiverAddress; + + BigInt? get amount => contracts?.first.parameter?.value?.txAmount; + + int? get fee => ret?.first.fee; + + String? get contractAddress => contracts?.first.parameter?.value?.contractAddress; + + TronTransactionModel({ + this.ret, + this.txID, + this.blockTimestamp, + this.contracts, + }); + + TronTransactionModel.fromJson(Map json) { + if (json['ret'] != null) { + ret = []; + json['ret'].forEach((v) { + ret!.add(Ret.fromJson(v)); + }); + } + txID = json['txID']; + blockTimestamp = json['block_timestamp']; + contracts = json['raw_data'] != null + ? (json['raw_data']['contract'] as List) + .map((e) => Contract.fromJson(e as Map)) + .toList() + : null; + } +} + +class Ret { + String? contractRet; + int? fee; + + Ret({this.contractRet, this.fee}); + + Ret.fromJson(Map json) { + contractRet = json['contractRet']; + fee = json['fee']; + } +} + +class Contract { + Parameter? parameter; + String? type; + + Contract({this.parameter, this.type}); + + Contract.fromJson(Map json) { + parameter = json['parameter'] != null ? Parameter.fromJson(json['parameter']) : null; + type = json['type']; + } +} + +class Parameter { + Value? value; + String? typeUrl; + + Parameter({this.value, this.typeUrl}); + + Parameter.fromJson(Map json) { + value = json['value'] != null ? Value.fromJson(json['value']) : null; + typeUrl = json['type_url']; + } +} + +class Value { + String? data; + String? ownerAddress; + String? contractAddress; + int? amount; + String? toAddress; + String? assetName; + + //Getters to extract address for tron transactions + /// If the contract address is null, it returns the toAddress + /// If it's not null, it decodes the data field and gets the receiver address. + String? get receiverAddress { + if (contractAddress == null) return toAddress; + + if (data == null) return null; + + return _decodeAddressFromEncodedDataField(data!); + } + + //Getters to extract amount for tron transactions + /// If the contract address is null, it returns the amount + /// If it's not null, it decodes the data field and gets the tx amount. + BigInt? get txAmount { + if (contractAddress == null) return BigInt.from(amount ?? 0); + + if (data == null) return null; + + return _decodeAmountInvolvedFromEncodedDataField(data!); + } + + Value( + {this.data, + this.ownerAddress, + this.contractAddress, + this.amount, + this.toAddress, + this.assetName}); + + Value.fromJson(Map json) { + data = json['data']; + ownerAddress = json['owner_address']; + contractAddress = json['contract_address']; + amount = json['amount']; + toAddress = json['to_address']; + assetName = json['asset_name']; + } + + /// To get the address from the encoded data field + String _decodeAddressFromEncodedDataField(String output) { + // To get the receiver address from the encoded params + output = output.replaceFirst('0x', '').substring(8); + final abiCoder = ABICoder.fromType('address'); + final decoded = abiCoder.decode(AbiParameter.bytes, hex.decode(output)); + final tronAddress = TronAddress.fromEthAddress((decoded.result as ETHAddress).toBytes()); + + return tronAddress.toString(); + } + + /// To get the amount from the encoded data field + BigInt _decodeAmountInvolvedFromEncodedDataField(String output) { + output = output.replaceFirst('0x', '').substring(72); + final amountAbiCoder = ABICoder.fromType('uint256'); + final decodedA = amountAbiCoder.decode(AbiParameter.uint256, hex.decode(output)); + final amount = decodedA.result as BigInt; + + return amount; + } +} diff --git a/cw_tron/lib/tron_wallet.dart b/cw_tron/lib/tron_wallet.dart new file mode 100644 index 000000000..96f92e450 --- /dev/null +++ b/cw_tron/lib/tron_wallet.dart @@ -0,0 +1,566 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; + +import 'package:bip39/bip39.dart' as bip39; +import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_core/cake_hive.dart'; +import 'package:cw_core/crypto_currency.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_core/wallet_type.dart'; +import 'package:cw_tron/default_tron_tokens.dart'; +import 'package:cw_tron/file.dart'; +import 'package:cw_tron/tron_abi.dart'; +import 'package:cw_tron/tron_balance.dart'; +import 'package:cw_tron/tron_client.dart'; +import 'package:cw_tron/tron_exception.dart'; +import 'package:cw_tron/tron_token.dart'; +import 'package:cw_tron/tron_transaction_credentials.dart'; +import 'package:cw_tron/tron_transaction_history.dart'; +import 'package:cw_tron/tron_transaction_info.dart'; +import 'package:cw_tron/tron_wallet_addresses.dart'; +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; +import 'package:on_chain/on_chain.dart'; + +part 'tron_wallet.g.dart'; + +class TronWallet = TronWalletBase with _$TronWallet; + +abstract class TronWalletBase + extends WalletBase with Store { + TronWalletBase({ + required WalletInfo walletInfo, + String? mnemonic, + String? privateKey, + required String password, + TronBalance? initialBalance, + }) : syncStatus = const NotConnectedSyncStatus(), + _password = password, + _mnemonic = mnemonic, + _hexPrivateKey = privateKey, + _client = TronClient(), + walletAddresses = TronWalletAddresses(walletInfo), + balance = ObservableMap.of( + {CryptoCurrency.trx: initialBalance ?? TronBalance(BigInt.zero)}, + ), + super(walletInfo) { + this.walletInfo = walletInfo; + transactionHistory = TronTransactionHistory(walletInfo: walletInfo, password: password); + + if (!CakeHive.isAdapterRegistered(TronToken.typeId)) { + CakeHive.registerAdapter(TronTokenAdapter()); + } + } + + final String? _mnemonic; + final String? _hexPrivateKey; + final String _password; + + late final Box tronTokensBox; + + late final TronPrivateKey _tronPrivateKey; + + late final TronPublicKey _tronPublicKey; + + TronPublicKey get tronPublicKey => _tronPublicKey; + + TronPrivateKey get tronPrivateKey => _tronPrivateKey; + + late String _tronAddress; + + late final TronClient _client; + + Timer? _transactionsUpdateTimer; + + @override + WalletAddresses walletAddresses; + + @observable + String? nativeTxEstimatedFee; + + @observable + String? trc20EstimatedFee; + + @override + @observable + SyncStatus syncStatus; + + @override + @observable + late ObservableMap balance; + + Future init() async { + await initTronTokensBox(); + + await walletAddresses.init(); + await transactionHistory.init(); + _tronPrivateKey = await getPrivateKey( + mnemonic: _mnemonic, + privateKey: _hexPrivateKey, + password: _password, + ); + + _tronPublicKey = _tronPrivateKey.publicKey(); + + _tronAddress = _tronPublicKey.toAddress().toString(); + + walletAddresses.address = _tronAddress; + + await save(); + } + + static Future 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 = TronBalance.fromJSON(data['balance'] as String) ?? TronBalance(BigInt.zero); + + return TronWallet( + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + privateKey: privateKey, + initialBalance: balance, + ); + } + + void addInitialTokens() { + final initialTronTokens = DefaultTronTokens().initialTronTokens; + + for (var token in initialTronTokens) { + tronTokensBox.put(token.contractAddress, token); + } + } + + Future initTronTokensBox() async { + final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${TronToken.boxName}"; + + tronTokensBox = await CakeHive.openBox(boxName); + } + + String idFor(String name, WalletType type) => '${walletTypeToString(type).toLowerCase()}_$name'; + + Future getPrivateKey({ + String? mnemonic, + String? privateKey, + required String password, + }) async { + assert(mnemonic != null || privateKey != null); + + if (privateKey != null) { + return TronPrivateKey(privateKey); + } + + final seed = bip39.mnemonicToSeed(mnemonic!); + + // Derive a TRON private key from the seed + final bip44 = Bip44.fromSeed(seed, Bip44Coins.tron); + + final childKey = bip44.deriveDefaultPath; + + return TronPrivateKey.fromBytes(childKey.privateKey.raw); + } + + @override + int calculateEstimatedFee(TransactionPriority priority, int? amount) => 0; + + @override + Future changePassword(String password) { + throw UnimplementedError("changePassword"); + } + + @override + void close() { + _transactionsUpdateTimer?.cancel(); + } + + @action + @override + Future connectToNode({required Node node}) async { + try { + syncStatus = ConnectingSyncStatus(); + + final isConnected = _client.connect(node); + + if (!isConnected) { + throw Exception("${walletInfo.type.name.toUpperCase()} Node connection failed"); + } + + _getEstimatedFees(); + _setTransactionUpdateTimer(); + + syncStatus = ConnectedSyncStatus(); + } catch (e) { + syncStatus = FailedSyncStatus(); + } + } + + Future _getEstimatedFees() async { + final nativeFee = await _getNativeTxFee(); + nativeTxEstimatedFee = TronHelper.fromSun(BigInt.from(nativeFee)); + + final trc20Fee = await _getTrc20TxFee(); + trc20EstimatedFee = TronHelper.fromSun(BigInt.from(trc20Fee)); + + log('Native Estimated Fee: $nativeTxEstimatedFee'); + log('TRC20 Estimated Fee: $trc20EstimatedFee'); + } + + Future _getNativeTxFee() async { + try { + final fee = await _client.getEstimatedFee(_tronPublicKey.toAddress()); + return fee; + } catch (e) { + log(e.toString()); + return 0; + } + } + + Future _getTrc20TxFee() async { + try { + final trc20fee = await _client.getTRCEstimatedFee(_tronPublicKey.toAddress()); + return trc20fee; + } catch (e) { + log(e.toString()); + return 0; + } + } + + @action + @override + Future startSync() async { + try { + syncStatus = AttemptingSyncStatus(); + await _updateBalance(); + await fetchTransactions(); + fetchTrc20ExcludedTransactions(); + + syncStatus = SyncedSyncStatus(); + } catch (e) { + syncStatus = FailedSyncStatus(); + } + } + + @override + Future createTransaction(Object credentials) async { + final tronCredentials = credentials as TronTransactionCredentials; + + final outputs = tronCredentials.outputs; + + final hasMultiDestination = outputs.length > 1; + + final CryptoCurrency transactionCurrency = + balance.keys.firstWhere((element) => element.title == tronCredentials.currency.title); + + final walletBalanceForCurrency = balance[transactionCurrency]!.balance; + + BigInt totalAmount = BigInt.zero; + bool shouldSendAll = false; + if (hasMultiDestination) { + if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { + throw TronTransactionCreationException(transactionCurrency); + } + + final totalAmountFromCredentials = + outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); + + totalAmount = BigInt.from(totalAmountFromCredentials); + + if (walletBalanceForCurrency < totalAmount) { + throw TronTransactionCreationException(transactionCurrency); + } + } else { + final output = outputs.first; + + shouldSendAll = output.sendAll; + + if (shouldSendAll) { + totalAmount = walletBalanceForCurrency; + } else { + final totalOriginalAmount = double.parse(output.cryptoAmount ?? '0.0'); + totalAmount = TronHelper.toSun(totalOriginalAmount.toString()); + } + + if (walletBalanceForCurrency < totalAmount || totalAmount < BigInt.zero) { + throw TronTransactionCreationException(transactionCurrency); + } + } + + final tronBalance = balance[CryptoCurrency.trx]?.balance ?? BigInt.zero; + + final pendingTransaction = await _client.signTransaction( + ownerPrivKey: _tronPrivateKey, + toAddress: tronCredentials.outputs.first.isParsedAddress + ? tronCredentials.outputs.first.extractedAddress! + : tronCredentials.outputs.first.address, + amount: TronHelper.fromSun(totalAmount), + currency: transactionCurrency, + tronBalance: tronBalance, + sendAll: shouldSendAll, + ); + + return pendingTransaction; + } + + @override + Future> fetchTransactions() async { + final address = _tronAddress; + + final transactions = await _client.fetchTransactions(address); + + final Map result = {}; + + final contract = ContractABI.fromJson(trc20Abi, isTron: true); + + final ownerAddress = TronAddress(_tronAddress); + + for (var transactionModel in transactions) { + if (transactionModel.isError) { + continue; + } + + String? tokenSymbol; + if (transactionModel.contractAddress != null) { + final tokenAddress = TronAddress(transactionModel.contractAddress!); + + tokenSymbol = (await _client.getTokenDetail( + contract, + "symbol", + ownerAddress, + tokenAddress, + ) as String?) ?? + ''; + } + + result[transactionModel.hash] = TronTransactionInfo( + id: transactionModel.hash, + tronAmount: transactionModel.amount ?? BigInt.zero, + direction: TronAddress(transactionModel.from!, visible: false).toAddress() == address + ? TransactionDirection.outgoing + : TransactionDirection.incoming, + blockTime: transactionModel.date, + txFee: transactionModel.fee, + tokenSymbol: tokenSymbol ?? "TRX", + to: transactionModel.to, + from: transactionModel.from, + isPending: false, + ); + } + + transactionHistory.addMany(result); + + await transactionHistory.save(); + + return transactionHistory.transactions; + } + + Future fetchTrc20ExcludedTransactions() async { + final address = _tronAddress; + + final transactions = await _client.fetchTrc20ExcludedTransactions(address); + + final Map result = {}; + + for (var transactionModel in transactions) { + if (transactionHistory.transactions.containsKey(transactionModel.hash)) { + continue; + } + + result[transactionModel.hash] = TronTransactionInfo( + id: transactionModel.hash, + tronAmount: transactionModel.amount ?? BigInt.zero, + direction: transactionModel.from! == address + ? TransactionDirection.outgoing + : TransactionDirection.incoming, + blockTime: transactionModel.date, + txFee: transactionModel.fee, + tokenSymbol: transactionModel.tokenSymbol ?? "TRX", + to: transactionModel.to, + from: transactionModel.from, + isPending: false, + ); + } + + transactionHistory.addMany(result); + + await transactionHistory.save(); + } + + @override + Object get keys => throw UnimplementedError("keys"); + + @override + Future rescan({required int height}) { + throw UnimplementedError("rescan"); + } + + @override + Future 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 => _tronPrivateKey.toHex(); + + Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); + + String toJSON() => json.encode({ + 'mnemonic': _mnemonic, + 'private_key': privateKey, + 'balance': balance[currency]!.toJSON(), + }); + + Future _updateBalance() async { + balance[currency] = await _fetchTronBalance(); + + await _fetchTronTokenBalances(); + await save(); + } + + Future _fetchTronBalance() async { + final balance = await _client.getBalance(_tronPublicKey.toAddress()); + return TronBalance(balance); + } + + Future _fetchTronTokenBalances() async { + for (var token in tronTokensBox.values) { + try { + if (token.enabled) { + balance[token] = await _client.fetchTronTokenBalances( + _tronAddress, + token.contractAddress, + ); + } else { + balance.remove(token); + } + } catch (_) {} + } + } + + @override + Future? updateBalance() async => await _updateBalance(); + + List get tronTokenCurrencies => tronTokensBox.values.toList(); + + Future addTronToken(TronToken token) async { + String? iconPath; + try { + iconPath = CryptoCurrency.all + .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase()) + .iconPath; + } catch (_) {} + + final newToken = TronToken( + name: token.name, + symbol: token.symbol, + contractAddress: token.contractAddress, + decimal: token.decimal, + enabled: token.enabled, + tag: token.tag ?? "TRX", + iconPath: iconPath, + ); + + await tronTokensBox.put(newToken.contractAddress, newToken); + + if (newToken.enabled) { + balance[newToken] = await _client.fetchTronTokenBalances( + _tronAddress, + newToken.contractAddress, + ); + } else { + balance.remove(newToken); + } + } + + Future deleteTronToken(TronToken token) async { + await token.delete(); + + balance.remove(token); + await _removeTokenTransactionsInHistory(token); + _updateBalance(); + } + + Future _removeTokenTransactionsInHistory(TronToken token) async { + transactionHistory.transactions.removeWhere((key, value) => value.tokenSymbol == token.title); + await transactionHistory.save(); + } + + Future getTronToken(String contractAddress) async => + await _client.getTronToken(contractAddress, _tronAddress); + + @override + Future renameWalletFiles(String newWalletName) async { + String transactionHistoryFileNameForWallet = 'tron_transactions.json'; + + 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: 30), (_) async { + _updateBalance(); + await fetchTransactions(); + fetchTrc20ExcludedTransactions(); + }); + } + + @override + Future signMessage(String message, {String? address}) async => + _tronPrivateKey.signPersonalMessage(ascii.encode(message)); + + String getTronBase58AddressFromHex(String hexAddress) { + return TronAddress(hexAddress).toAddress(); + } + + void updateScanProviderUsageState(bool isEnabled) { + if (isEnabled) { + fetchTransactions(); + fetchTrc20ExcludedTransactions(); + _setTransactionUpdateTimer(); + } else { + _transactionsUpdateTimer?.cancel(); + } + } +} diff --git a/cw_tron/lib/tron_wallet_addresses.dart b/cw_tron/lib/tron_wallet_addresses.dart new file mode 100644 index 000000000..35939de26 --- /dev/null +++ b/cw_tron/lib/tron_wallet_addresses.dart @@ -0,0 +1,36 @@ +import 'dart:developer'; + +import 'package:cw_core/wallet_addresses.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:mobx/mobx.dart'; + +part 'tron_wallet_addresses.g.dart'; + +class TronWalletAddresses = TronWalletAddressesBase with _$TronWalletAddresses; + +abstract class TronWalletAddressesBase extends WalletAddresses with Store { + TronWalletAddressesBase(WalletInfo walletInfo) + : address = '', + super(walletInfo); + + @override + @observable + String address; + + @override + Future init() async { + address = walletInfo.address; + await updateAddressesInBox(); + } + + @override + Future updateAddressesInBox() async { + try { + addressesMap.clear(); + addressesMap[address] = ''; + await saveAddressesInBox(); + } catch (e) { + log(e.toString()); + } + } +} diff --git a/cw_tron/lib/tron_wallet_creation_credentials.dart b/cw_tron/lib/tron_wallet_creation_credentials.dart new file mode 100644 index 000000000..dc4f389aa --- /dev/null +++ b/cw_tron/lib/tron_wallet_creation_credentials.dart @@ -0,0 +1,29 @@ +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; + +class TronNewWalletCredentials extends WalletCredentials { + TronNewWalletCredentials({required String name, WalletInfo? walletInfo}) + : super(name: name, walletInfo: walletInfo); +} + +class TronRestoreWalletFromSeedCredentials extends WalletCredentials { + TronRestoreWalletFromSeedCredentials( + {required String name, + required String password, + required this.mnemonic, + WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String mnemonic; +} + +class TronRestoreWalletFromPrivateKey extends WalletCredentials { + TronRestoreWalletFromPrivateKey( + {required String name, + required String password, + required this.privateKey, + WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String privateKey; +} diff --git a/cw_tron/lib/tron_wallet_service.dart b/cw_tron/lib/tron_wallet_service.dart new file mode 100644 index 000000000..c8344d5f4 --- /dev/null +++ b/cw_tron/lib/tron_wallet_service.dart @@ -0,0 +1,160 @@ +import 'dart:io'; + +import 'package:bip39/bip39.dart' as bip39; +import 'package:cw_core/balance.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_core/transaction_info.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_tron/tron_client.dart'; +import 'package:cw_tron/tron_exception.dart'; +import 'package:cw_tron/tron_wallet.dart'; +import 'package:cw_tron/tron_wallet_creation_credentials.dart'; +import 'package:hive/hive.dart'; +import 'package:collection/collection.dart'; + +class TronWalletService extends WalletService< + TronNewWalletCredentials, + TronRestoreWalletFromSeedCredentials, + TronRestoreWalletFromPrivateKey, + TronNewWalletCredentials> { + TronWalletService(this.walletInfoSource, {required this.client}); + + late TronClient client; + + final Box walletInfoSource; + + @override + WalletType getType() => WalletType.tron; + + @override + Future create( + TronNewWalletCredentials credentials, { + bool? isTestnet, + }) async { + final strength = credentials.seedPhraseLength == 24 ? 256 : 128; + + final mnemonic = bip39.generateMnemonic(strength: strength); + + final wallet = TronWallet( + walletInfo: credentials.walletInfo!, + mnemonic: mnemonic, + password: credentials.password!, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + Future openWallet(String name, String password) async { + final walletInfo = + walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType())); + + try { + final wallet = await TronWalletBase.open( + name: name, + password: password, + walletInfo: walletInfo, + ); + + await wallet.init(); + await wallet.save(); + saveBackup(name); + return wallet; + } catch (_) { + await restoreWalletFilesFromBackup(name); + + final wallet = await TronWalletBase.open( + name: name, + password: password, + walletInfo: walletInfo, + ); + + await wallet.init(); + await wallet.save(); + return wallet; + } + } + + @override + Future restoreFromKeys( + TronRestoreWalletFromPrivateKey credentials, { + bool? isTestnet, + }) async { + final wallet = TronWallet( + password: credentials.password!, + privateKey: credentials.privateKey, + walletInfo: credentials.walletInfo!, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + Future restoreFromSeed( + TronRestoreWalletFromSeedCredentials credentials, { + bool? isTestnet, + }) async { + if (!bip39.validateMnemonic(credentials.mnemonic)) { + throw TronMnemonicIsIncorrectException(); + } + + final wallet = TronWallet( + password: credentials.password!, + mnemonic: credentials.mnemonic, + walletInfo: credentials.walletInfo!, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + Future rename(String currentName, String password, String newName) async { + final currentWalletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType())); + final currentWallet = await TronWalletBase.open( + password: password, name: currentName, walletInfo: currentWalletInfo); + + await currentWallet.renameWalletFiles(newName); + await saveBackup(newName); + + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); + } + + @override + Future isWalletExit(String name) async => + File(await pathForWallet(name: name, type: getType())).existsSync(); + + @override + Future 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); + } + + @override + Future, TransactionInfo>> restoreFromHardwareWallet(TronNewWalletCredentials credentials) { + // TODO: implement restoreFromHardwareWallet + throw UnimplementedError(); + } +} diff --git a/cw_tron/pubspec.yaml b/cw_tron/pubspec.yaml new file mode 100644 index 000000000..9d32c4290 --- /dev/null +++ b/cw_tron/pubspec.yaml @@ -0,0 +1,33 @@ +name: cw_tron +description: A new Flutter package project. +version: 0.0.1 +publish_to: none +homepage: https://cakewallet.com + +environment: + sdk: '>=3.0.6 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + cw_core: + path: ../cw_core + cw_evm: + path: ../cw_evm + on_chain: ^3.0.1 + blockchain_utils: ^2.1.1 + mobx: ^2.3.0+1 + bip39: ^1.0.6 + hive: ^2.2.3 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + build_runner: ^2.3.3 + mobx_codegen: ^2.1.1 + hive_generator: ^1.1.3 +flutter: + # assets: + # - images/a_dot_burr.jpeg diff --git a/cw_tron/test/cw_tron_test.dart b/cw_tron/test/cw_tron_test.dart new file mode 100644 index 000000000..55a2b04be --- /dev/null +++ b/cw_tron/test/cw_tron_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:cw_tron/cw_tron.dart'; + +void main() { + test('adds one to input values', () { + final calculator = Calculator(); + expect(calculator.addOne(2), 3); + expect(calculator.addOne(-7), -6); + expect(calculator.addOne(0), 1); + }); +} diff --git a/how_to_add_new_wallet_type.md b/how_to_add_new_wallet_type.md new file mode 100644 index 000000000..95b82d802 --- /dev/null +++ b/how_to_add_new_wallet_type.md @@ -0,0 +1,301 @@ +# Guide to adding a new wallet type in Cake Wallet + +## Wallet Integration + +**N:B** Throughout this guide, `walletx` refers to the specific wallet type you want to add. If you're adding `BNB` to CakeWallet, then `walletx` for you here is `bnb`. + +**Core Folder/Files Setup** +- Idenitify your core component/package (major project component), which would power the integration e.g web3dart, solana, onchain etc +- Add a new entry to `WalletType` class in `cw_core/wallet_type.dart`. +- Fill out the necessary information int he various functions in the files, concerning the wallet name, the native currency type, symbol etc. +- Go to `cw_core/lib/currency_for_wallet_type.dart`, in the `currencyForWalletType` function, add a case for `walletx`, returning the native cryptocurrency for `walletx`. +- If the cryptocurrency for walletx is not available among the default cryptocurrencies, add a new cryptocurrency entry in `cw_core/lib/cryptocurrency.dart`. +- Add the newly created cryptocurrency name to the list named `all` in this file. +- Create a package for the wallet specific integration, name it. `cw_walletx` +- Add the following initial common files and replicate to fit the wallet + - walletx_transaction_history.dart + - walletx_transaction_info.dart + - walletx_mnemonics_exception.dart + - walletx_tokens.dart + - walletx_wallet_service.dart: + - walletx_wallet.dart + - etc. + +- Add the code to run the code generation needed for the files in the `cw_walletx` package to the `model_generator.sh` script + + cd cw_walletx && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. + +- Add the relevant dev_dependencies for generating the files also + - build_runner + - mobx_codegen + - hive_generator + +**WalletX Proxy Setup** + +A `Proxy` class is used to communicate with the specific wallet package we have. Instead of directly making use of methods and parameters in `cw_walletx` within the `lib` directory, we use a proxy to access these data. All important functions, calls and interactions we want to make with our `cw_walletx` package would be defined and done through the proxy class. The class would define the import + +- Create a proxy folder titled `walletx` to handle the wallet operations. It would contain 2 files: `cw_walletx.dart` and `walletx.dart`. +- `cw_walletx.dart` file would hold an implementation class containing major operations to be done in the lib directory. It serves as the link between the cw_walletx package and the rest of the codebase(lib directory files and folders). +- `walletx.dart` would contain the abstract class highlighting the methods that would bring the functionalities and features in the `cw_walletx` package to the rest of the `lib` directory. +- Add `walletx.dart` to `.gitignore` as we won’t be pushing it: `lib/tron/tron.dart`. +- `walletx.dart` would always be generated based on the configure files we would be setting up in the next step. + +**Configuration Files Setup** +- Before we populate the field, head over to `tool/configure.dart` to setup the necessary configurations for the `walletx` proxy. +- Define the output path, it’ll follow the format `lib/walletx/walletx.dart`. +- Add the variable to check if `walletx` is to be activated +- Define the function that would generate the abstract class for the proxy.(We will flesh out this function in the next steps). +- Add the defined variable in step 2 to the `generatePubspec` and `generateWalletTypes`. +- Next, modify the following functions: + - generatePubspec function + 1. Add the parameters to the method params (i.e required bool hasWalletX) + 2. Define a variable to hold the entry for the pubspec.yaml file + + const cwWalletX = """ + cw_tron: + path: ./cw_walletx + """; + + 3. Add an if block that takes in the passed parameter and adds the defined variable(inn the previous step) to the list of outputs + + if (hasWalletX) { + output += '\n$cwWalletX’; + } + + - generateWalletTypes function + 1. Add the parameters to the method params (i.e required bool hasWalletX) + 2. Add an if block to add the wallet type to the list of outputs this function generates + + if (hasWalletX) { + outputContent += '\tWalletType.walletx,\n’; + } + +- Head over to `scripts/android/pubspec.sh` script, and modify the `CONFIG_ARGS` under `$CAKEWALLET`. Add `"—walletx”` to the end of the passed in params. +- Repeat this in `scripts/ios/app_config.sh` and `scripts/macos/app_config.sh` +- Open a terminal and cd into `scripts/android/`. Run the following commands to run setup configuration scripts(proxy class, add walletx to list of wallet types and add cw_walletx to pubspec). + + source ./app_env.sh cakewallet + + ./app_config.sh + + cd cw_walletx && flutter pub get && flutter packages pub run build_runner build + + flutter packages pub run build_runner build --delete-conflicting-outputs + +Moving forward, our interactions with the cw_walletx package would be through the proxy class and its methods. + +**Pre-Wallet Creation for WalletX** +- Go to `di.dart` and locate the block to `registerWalletService`. In this, add the case to handle creating the WalletXWalletService + + case WalletType.walletx: + return walletx!.createWalletXWalletService(_walletInfoSource); + +- Go to `lib/view_model/wallet_new_vm.dart`, in the getCredentials method, which gets the new wallet credentials for walletX add the case for the new wallet + + case WalletType.walletx: + return walletx!.createWalletXNewWalletCredentials(name: name); + +**Node Setup** +- Before we can be able to successfully create a new wallet of wallet type walletx we need to setup the node that the wallet would use: +- In the assets directory, create a new file and name it `walletx_node_list.yml`. This yml file would contain the details for nodes to be used for walletX. An example structure for each node entry + + uri: "api.nodeurl.io" + is_default: true + useSSL: true + +You can add as many node entries as desired. + +- Add the path to the yml file created to the `pubspec_base.yaml` file (`“assets/walletx_node_list.yml”`) +- Go to `lib/entities/node_list.dart`, add a function to load the node entries we made in `walletx_node_list.yml` for walletx. +- Name your function `loadDefaultWalletXNodes()`. The function would handle loading the yml file as a string and parsing it into a Node Object to be used within the app. Here’s a template for the function. + + Future> loadDefaultWalletXNodes() async { + final nodesRaw = await rootBundle.loadString('assets/tron_node_list.yml'); + final loadedNodes = loadYaml(nodesRaw) as YamlList; + final nodes = []; + for (final raw in loadedNodes) { + if (raw is Map) { + final node = Node.fromMap(Map.from(raw)); + node.type = WalletType.tron; + nodes.add(node); + } + } + return nodes; + } + +- Inside the `resetToDefault` function, call the function you created and add the result to the nodes result variable. +- Go to `lib/entities/default_settings_migration.dart` file, we’ll be adding the following to the file. +- At the top of the file, after the imports, define the default nodeUrl for wallet-name. +- Next, write a function to fetch the node for this default uri you added above. + + Node? getWalletXDefaultNode({required Box nodes}) { + return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == walletXDefaultNodeUri) ?? + nodes.values.firstWhereOrNull((node) => node.type == WalletType.walletx); + } + +- Next, write a function that will add the list of nodes we declared in the `walletx_node_list.yml` file to the Nodes Box, to be used in the app. Here’s the format for this function + + Future addWalletXNodeList({required Box nodes}) async { + final nodeList = await loadDefaultWalletXNodes(); + for (var node in nodeList) { + if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { + await nodes.add(node); + } + } + } + +- Next, we’ll write the function to change walletX current node to default. An handy function we would make use of later on. Add a new preference key in `lib/entities/preference_key.dart` with the format `PreferencesKey.currentWalletXNodeIdKey`, we’ll use it to identify the current node id. + + Future changeWalletXCurrentNodeToDefault( + {required SharedPreferences sharedPreferences, required Box nodes}) async { + final node = getWalletXDefaultNode(nodes: nodes); + final nodeId = node?.key as int? ?? 0; + await sharedPreferences.setInt(PreferencesKey.currentWalletXNodeIdKey, nodeId); + } + +- Next, in the `defaultSettingsMigration` function at the top of the file, add a new case to handle both `addWalletXNodeList` and `changeWalletXCurrentNodeToDefault` + + case “next-number-increment”: + await addWalletXNodeList(nodes: nodes); + await changeWalletXCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); + break; + +- Next, increase the `initialMigrationVersion` number in `main.dart` to be the new case entry number you entered in the step above for the `defaultSettingsMigration` function. +- Next, go to `lib/view_model/node_list/node_list_view_model.dart` +- In the `reset` function, add a case for walletX: + + case WalletType.tron: + node = getTronDefaultNode(nodes: _nodeSource)!; + break; + +- Lastly, go to `cw_core/lib/node.dart`, +- In the uri getter, add a case to handle the uri setup for walletX. If the node uses http, return `Uri.http`, if not, return `Uri.https` + + case WalletType.walletX: + return Uri.https(uriRaw, ‘’); + +- Also, in the `requestNode` method, add a case for `WalletType.walletx` +- Next is the modifications to `lib/store/settings_store.dart` file: +- In the `load` function, create a variable to fetch the currentWalletxNodeId using the `PreferencesKey.currentWalletXNodeIdKey` we created earlier. +- Create another variable `walletXNode` which gets the walletx node using the nodeId variable assigned in the step above. +- Add a check to see if walletXNode is not null, if it’s not null, assign the created tronNode variable to the nodeMap with a type of walletX + + final walletXNode = nodeSource.get(walletXNodeId); + final walletXNodeId = sharedPreferences.getInt(PreferencesKey.currentWalletXNodeIdKey); + if (walletXNode != null) { + nodes[WalletType.walletx] = walletXNode; + } + +- Repeat the steps above in the `reload` function +- Next, add a case for walletX in the `_saveCurrentNode` function. + +- Run the following commands after to generate modified files in cw_core and lib + + cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. + + flutter packages pub run build_runner build --delete-conflicting-outputs + +- Lastly, before we run the app to test what we’ve done so far, +- Go to `lib/src/dashboard/widgets/menu_widget.dart` and add an icon for walletX to be used within the app. +- Go to `lib/src/screens/wallet_list/wallet_list_page.dart` and add an icon for walletx, add a case for walletx also in the `imageFor` method. +- Do the same thing in `lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart` + +- One last thing before we can create a wallet for walletx, go to `lib/view_model/wallet_new_vm.dart` +- Modify the `seedPhraseWordsLength` getter by adding a case for `WalletType.walletx` + +Now you can run the codebase and successfully create a wallet for type walletX successfully. + +**Display Seeds/Keys** +- Next, we want to set up our wallet to display the seeds and/or keys in the security page of the app. +- Go to `lib/view_model/wallet_keys_view_model.dart` +- Modify the `populateItems` function by adding a case for `WalletType.walletx` in it. +- Now your seeds and/or keys should display when you go to Security and Backup -> Show seed/keys page within the app. + +**Restore Wallet** +- Go to `lib/core/seed_validator.dart` +- In the `getWordList` method, add a case to handle `WalletType.walletx` which would return the word list to be used to validate the passed in seeds. +- Next, go to `lib/restore_view_model.dart` +- Modify the `hasRestoreFromPrivateKey` to reflect if walletx supports restore from Key +- Add a switch case to handle the various restore modes that walletX supports +- Modify the `getCredential` method to handle the restore flows for `WalletType.walletx` +- Run the build_runner code generation command + +**Receive** +- Go to `lib/view_model/wallet_address_list/wallet_address_list_view_model.dart` +- Create an implementation of `PaymentUri` for type WalletX. +- In the uri getter, add a case for `WalletType.walletx` returning the implementation class for `PaymentUri` +- Modify the `addressList` getter to return the address/addresses for walletx + +**Balance Screen** +- Go to `lib/view_model/dashboard/balance_view_model.dart` +- Modify the function to adjust the way the balance is being display on the app: `isHomeScreenSettingsEnabled` +- Add a case to the `availableBalanceLabel` getter to modify the text being displayed (Available or confirmed) +- Same for `additionalBalanceLabel` +- Next, go to `lib/reactions/fiat_rate_update.dart` +- Modify the `startFiatRateUpdate` function and add a check for `WalletType.walletx` to return all the token currencies +- Next, go to `lib/reactions/on_current_wallet_change.dart` +- Modify the `startCurrentWalletChangeReaction` function and add a check for `WalletType.walletx` to return all the token currencies +- Lastly, go to `lib/view_model/dashboard/transaction_list_item.dart` +- In the `formattedFiatAmount` getter, add a case to handle the fiat amount conversion for `WalletType.walletx` + +**Send ViewModel** +- Go to `lib/view_model/send/send_view_model.dart` +- Modify the `_credentials` function to reflect `WalletType.walletx` +- Modify `hasMultipleTokens` to reflect wallets + +**Exchange** +- Go to lib/view_model/exchange/exchange_view_model.dart +- First, add a case for WalletType.walletx in the `initialPairBasedOnWallet` method. +- If WalletX supports tokens, go to `lib/view_model/exchange/exchange_trade_view_model.dart` +- Modify the `_checkIfCanSend` method by creating a `_isWalletXToken` that checks if the from currency is WalletX and if its tag is for walletx +- Add `_isWalletXToken` to the return logic for the method. + +**Secrets** +- Create a json file named `wallet-secrets-config.json` and put an empty curly bracket “{}” in it +- Add a new entry to `tool/utils/secret_key.dart` for walletx +- Modify the `tool/generate_secrets_config.dart` file for walletx, don’t forget to call `secrets.clear()` before adding a new set of generation logic +- Modify the `tool/import_secrets_config.dart` file for walletx +- In the `.gitignore` file, add `**/tool/.walletx-secrets-config.json` and `**/cw_walletx/lib/.secrets.g.dart` + +**HomeSettings: WalletX Tokens Display and Management** +- Go to `lib/view_model/dashboard/home_settings_view_model.dart` +- Modify the `_updateTokensList` method to add all walletx tokens if the wallet type is `WalletType.walletx`. +- Modify the `getTokenAddressBasedOnWallet` method to include a case to fetch the address for a WalletX token. +- Modify the `getToken` method to return a specific walletx token +- Modify the `addToken`, `deleteToken` and `changeTokenAvailability` methods to handle cases where the walletType is walletx + +**Buy and Sell WalletX** +- Go to `lib/entities/provider_types.dart` +- Add a case for `WalletType.walletx` in the `getAvailableBuyProviderTypes` method. Return a list of providers that support buying WalletX. +- Add a case for `WalletType.walletx` in the `getAvailableSellProviderTypes` method. Return a list of providers that support selling WalletX. + +**Restore QR setup** +- Go to `lib/view_model/restore/wallet_restore_from_qr_code.dart` +- Add the scheme for walletx in `_walletTypeMap` +- Also modify `_determineWalletRestoreMode` to include a case for walletx +- Go to `lib/view_model/restore/restore_from_qr_vm.dart` +- Modify `getCredentialsFromRestoredWallet` method +- Go to `lib/core/address_validator.dart` +- Modify the `getAddressFromStringPattern` method to add a case for `WalletType.walletx` +- and if it has tokens (ex. erc20, trc20, spl tokens) then add them to the switch case as well +- Add the scheme for walletx for both Android in `AndroidManifestBase.xml` and iOS in `InfoBase.plist` + +**Transaction History** +- Go to `lib/view_model/transaction_details_view_model.dart` +- Add a case for `WalletType.walletx` to add the items to be displayed on the detailed view +- Modify the `_explorerUrl` method to add the blockchain explorer link for WalletX in order to view the more info on a transaction +- Modify the `_explorerDescription` to display the name of the explorer + + + + +# Points to note when adding the new wallet type + +1. if it has tokens (ex. ERC20, SPL, etc...) make sure to add that to this function `_checkIfCanSend` in `exchange_trade_view_model.dart` +1. if it has tokens (ex. ERC20, SPL, etc...) make sure to add a check for the tags as well in the +2. Check On/Off ramp providers that support the new wallet currency and add them accordingly in `provider_types.dart` +3. Add support for wallet uri scheme to restore from QR for both Android in `AndroidManifestBase.xml` and iOS in `InfoBase.plist` +4. Make sure no imports are using the wallet internal package files directly, instead use the proxy layers that is created in the main lib `lib/cw_ethereum.dart` for example. (i.e try building Monero.com if you get compilation errors, then you probably missed something) + + +Copyright (C) 2018-2023 Cake Labs LLC diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index cb6be3098..cae654377 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 67c0c9ee8..c4ee98c37 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -109,6 +109,8 @@ PODS: - flutter_inappwebview_ios/Core (0.0.1): - Flutter - OrderedSet (~> 5.0) + - flutter_local_authentication (1.2.0): + - Flutter - flutter_mailer (0.0.1): - Flutter - flutter_secure_storage (6.0.0): @@ -118,8 +120,6 @@ PODS: - Toast - in_app_review (0.2.0): - Flutter - - local_auth_ios (0.0.1): - - Flutter - MTBBarcodeScanner (5.0.11) - OrderedSet (5.0.0) - package_info (0.0.1): @@ -131,7 +131,12 @@ PODS: - FlutterMacOS - permission_handler_apple (9.1.1): - Flutter + - Protobuf (3.25.3) - ReachabilitySwift (5.0.0) + - reactive_ble_mobile (0.0.1): + - Flutter + - Protobuf (~> 3.5) + - SwiftProtobuf (~> 1.0) - SDWebImage (5.18.11): - SDWebImage/Core (= 5.18.11) - SDWebImage/Core (5.18.11) @@ -170,15 +175,16 @@ DEPENDENCIES: - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) + - flutter_local_authentication (from `.symlinks/plugins/flutter_local_authentication/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`) - in_app_review (from `.symlinks/plugins/in_app_review/ios`) - - local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`) - package_info (from `.symlinks/plugins/package_info/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - reactive_ble_mobile (from `.symlinks/plugins/reactive_ble_mobile/ios`) - sensitive_clipboard (from `.symlinks/plugins/sensitive_clipboard/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) @@ -196,6 +202,7 @@ SPEC REPOS: - DKPhotoGallery - MTBBarcodeScanner - OrderedSet + - Protobuf - ReachabilitySwift - SDWebImage - SwiftProtobuf @@ -226,6 +233,8 @@ EXTERNAL SOURCES: :path: Flutter flutter_inappwebview_ios: :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" + flutter_local_authentication: + :path: ".symlinks/plugins/flutter_local_authentication/ios" flutter_mailer: :path: ".symlinks/plugins/flutter_mailer/ios" flutter_secure_storage: @@ -234,8 +243,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/fluttertoast/ios" in_app_review: :path: ".symlinks/plugins/in_app_review/ios" - local_auth_ios: - :path: ".symlinks/plugins/local_auth_ios/ios" package_info: :path: ".symlinks/plugins/package_info/ios" package_info_plus: @@ -244,6 +251,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" + reactive_ble_mobile: + :path: ".symlinks/plugins/reactive_ble_mobile/ios" sensitive_clipboard: :path: ".symlinks/plugins/sensitive_clipboard/ios" share_plus: @@ -273,30 +282,32 @@ SPEC CHECKSUMS: DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0 + flutter_local_authentication: 1172a4dd88f6306dadce067454e2c4caf07977bb flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83 flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be fluttertoast: 48c57db1b71b0ce9e6bba9f31c940ff4b001293c in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d - local_auth_ios: 1ba1475238daa33a6ffa2a29242558437be435ac MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 - path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 + Protobuf: 8e9074797a13c484a79959fdb819ef4ae6da7dbe ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 + reactive_ble_mobile: 9ce6723d37ccf701dbffd202d487f23f5de03b4c SDWebImage: a3ba0b8faac7228c3c8eadd1a55c9c9fe5e16457 sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986 share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 - shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1 SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f Toast: ec33c32b8688982cecc6348adeae667c1b9938da uni_links: d97da20c7701486ba192624d99bffaaffcfc298a UnstoppableDomainsResolution: c3c67f4d0a5e2437cb00d4bd50c2e00d6e743841 - url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812 + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 7a8b99b49..8ed46a028 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 9F46EE5E2BC11178009318F5 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 9F46EE5D2BC11178009318F5 /* PrivacyInfo.xcprivacy */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -40,6 +41,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9F46EE5D2BC11178009318F5 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; AD0937B0140D5A4C24E73BEA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -123,6 +125,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, + 9F46EE5D2BC11178009318F5 /* PrivacyInfo.xcprivacy */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, @@ -196,6 +199,7 @@ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 9F46EE5E2BC11178009318F5 /* PrivacyInfo.xcprivacy in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/Runner/InfoBase.plist b/ios/Runner/InfoBase.plist index a7f208870..83e60b542 100644 --- a/ios/Runner/InfoBase.plist +++ b/ios/Runner/InfoBase.plist @@ -140,6 +140,16 @@ nano-wallet + + CFBundleTypeRole + Viewer + CFBundleURLName + nano-gpt + CFBundleURLSchemes + + nano-gpt + + CFBundleTypeRole Editor @@ -190,6 +200,26 @@ solana-wallet + + CFBundleTypeRole + Viewer + CFBundleURLName + tron + CFBundleURLSchemes + + tron + + + + CFBundleTypeRole + Viewer + CFBundleURLName + tron-wallet + CFBundleURLSchemes + + tron-wallet + + CFBundleVersion $(CURRENT_PROJECT_VERSION) @@ -208,6 +238,10 @@ Enable Face ID for fast and secure access to wallets and private keys NSPhotoLibraryUsageDescription We need access to documents folder for getting access to open/save backup file + NSBluetoothPeripheralUsageDescription + We need access to Bluetooth in order to connect to your hardware wallet when needed + NSBluetoothAlwaysUsageDescription + We need access to Bluetooth in order to connect to your hardware wallet when needed UIBackgroundModes fetch diff --git a/ios/Runner/PrivacyInfo.xcprivacy b/ios/Runner/PrivacyInfo.xcprivacy new file mode 100644 index 000000000..51fa92043 --- /dev/null +++ b/ios/Runner/PrivacyInfo.xcprivacy @@ -0,0 +1,24 @@ + + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + + \ No newline at end of file diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 862ce9db5..56c9c7dff 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -1,13 +1,22 @@ part of 'bitcoin.dart'; class CWBitcoin extends Bitcoin { - @override - TransactionPriority getMediumTransactionPriority() => BitcoinTransactionPriority.medium; - - @override - WalletCredentials createBitcoinRestoreWalletFromSeedCredentials( - {required String name, required String mnemonic, required String password}) => - BitcoinRestoreWalletFromSeedCredentials(name: name, mnemonic: mnemonic, password: password); + WalletCredentials createBitcoinRestoreWalletFromSeedCredentials({ + required String name, + required String mnemonic, + required String password, + required DerivationType derivationType, + required String derivationPath, + String? passphrase, + }) => + BitcoinRestoreWalletFromSeedCredentials( + name: name, + mnemonic: mnemonic, + password: password, + derivationType: derivationType, + derivationPath: derivationPath, + passphrase: passphrase, + ); @override WalletCredentials createBitcoinRestoreWalletFromWIFCredentials( @@ -23,6 +32,17 @@ class CWBitcoin extends Bitcoin { {required String name, WalletInfo? walletInfo}) => BitcoinNewWalletCredentials(name: name, walletInfo: walletInfo); + @override + WalletCredentials createBitcoinHardwareWalletCredentials( + {required String name, + required HardwareAccountData accountData, + WalletInfo? walletInfo}) => + BitcoinRestoreWalletFromHardware( + name: name, hwAccountData: accountData, walletInfo: walletInfo); + + @override + TransactionPriority getMediumTransactionPriority() => BitcoinTransactionPriority.medium; + @override List getWordList() => wordlist; @@ -78,21 +98,20 @@ class CWBitcoin extends Bitcoin { final bitcoinFeeRate = priority == BitcoinTransactionPriority.custom && feeRate != null ? feeRate : null; return BitcoinTransactionCredentials( - outputs - .map((out) => OutputInfo( - fiatAmount: out.fiatAmount, - cryptoAmount: out.cryptoAmount, - address: out.address, - note: out.note, - sendAll: out.sendAll, - extractedAddress: out.extractedAddress, - isParsedAddress: out.isParsedAddress, - formattedCryptoAmount: out.formattedCryptoAmount, - memo: out.memo)) - .toList(), - priority: priority as BitcoinTransactionPriority, - feeRate: bitcoinFeeRate - ); + outputs + .map((out) => OutputInfo( + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmount, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + formattedCryptoAmount: out.formattedCryptoAmount, + memo: out.memo)) + .toList(), + priority: priority as BitcoinTransactionPriority, + feeRate: bitcoinFeeRate); } @override @@ -248,6 +267,134 @@ class CWBitcoin extends Bitcoin { } } + @override + Future> compareDerivationMethods( + {required String mnemonic, required Node node}) async { + if (await checkIfMnemonicIsElectrum2(mnemonic)) { + return [DerivationType.electrum]; + } + + return [DerivationType.bip39, DerivationType.electrum]; + } + + int _countOccurrences(String str, String charToCount) { + int count = 0; + for (int i = 0; i < str.length; i++) { + if (str[i] == charToCount) { + count++; + } + } + return count; + } + + @override + Future> getDerivationsFromMnemonic({ + required String mnemonic, + required Node node, + String? passphrase, + }) async { + List list = []; + + List types = await compareDerivationMethods(mnemonic: mnemonic, node: node); + if (types.length == 1 && types.first == DerivationType.electrum) { + return [ + DerivationInfo( + derivationType: DerivationType.electrum, + derivationPath: "m/0'", + description: "Electrum", + scriptType: "p2wpkh", + ) + ]; + } + + final electrumClient = ElectrumClient(); + await electrumClient.connectToUri(node.uri); + + late BasedUtxoNetwork network; + btc.NetworkType networkType; + switch (node.type) { + case WalletType.litecoin: + network = LitecoinNetwork.mainnet; + networkType = litecoinNetwork; + break; + case WalletType.bitcoin: + default: + network = BitcoinNetwork.mainnet; + networkType = btc.bitcoin; + break; + } + + for (DerivationType dType in electrum_derivations.keys) { + late Uint8List seedBytes; + if (dType == DerivationType.electrum) { + seedBytes = await mnemonicToSeedBytes(mnemonic); + } else if (dType == DerivationType.bip39) { + seedBytes = bip39.mnemonicToSeed(mnemonic, passphrase: passphrase ?? ''); + } + + for (DerivationInfo dInfo in electrum_derivations[dType]!) { + try { + DerivationInfo dInfoCopy = DerivationInfo( + derivationType: dInfo.derivationType, + derivationPath: dInfo.derivationPath, + description: dInfo.description, + scriptType: dInfo.scriptType, + ); + + String derivationPath = dInfoCopy.derivationPath!; + int derivationDepth = _countOccurrences(derivationPath, "/"); + + // the correct derivation depth is dependant on the derivation type: + // the derivation paths defined in electrum_derivations are at the ROOT level, i.e.: + // electrum's format doesn't specify subaddresses, just subaccounts: + + // for BIP44 + if (derivationDepth == 3) { + // we add "/0/0" so that we generate account 0, index 0 and correctly get balance + derivationPath += "/0/0"; + } + + // var hd = bip32.BIP32.fromSeed(seedBytes).derivePath(derivationPath); + final hd = btc.HDWallet.fromSeed( + seedBytes, + network: networkType, + ).derivePath(derivationPath); + + String? address; + switch (dInfoCopy.scriptType) { + case "p2wpkh": + address = generateP2WPKHAddress(hd: hd, network: network); + break; + case "p2pkh": + address = generateP2PKHAddress(hd: hd, network: network); + break; + case "p2wpkh-p2sh": + address = generateP2SHAddress(hd: hd, network: network); + break; + default: + continue; + } + + final sh = scriptHash(address, network: network); + final history = await electrumClient.getHistory(sh); + + final balance = await electrumClient.getBalance(sh); + dInfoCopy.balance = balance.entries.first.value.toString(); + dInfoCopy.address = address; + dInfoCopy.transactionsCount = history.length; + + list.add(dInfoCopy); + } catch (e) { + print(e); + } + } + } + + // sort the list such that derivations with the most transactions are first: + list.sort((a, b) => b.transactionsCount.compareTo(a.transactionsCount)); + return list; + } + @override bool hasTaprootInput(PendingTransaction pendingTransaction) { return (pendingTransaction as PendingBitcoinTransaction).hasTaprootInputs; @@ -282,13 +429,37 @@ class CWBitcoin extends Bitcoin { } @override - int getFeeAmountWithFeeRate(Object wallet, int feeRate, int inputsCount, int outputsCount, - {int? size}) { + int getEstimatedFeeWithFeeRate(Object wallet, int feeRate, int? amount, + {int? outputsCount, int? size}) { final bitcoinWallet = wallet as ElectrumWallet; - return bitcoinWallet.feeAmountWithFeeRate( + return bitcoinWallet.calculateEstimatedFeeWithFeeRate( feeRate, - inputsCount, - outputsCount, + amount, + outputsCount: outputsCount, + size: size, ); } + + @override + int getMaxCustomFeeRate(Object wallet) { + final bitcoinWallet = wallet as ElectrumWallet; + return (bitcoinWallet.feeRate(BitcoinTransactionPriority.fast) * 1.1).round(); + } + + @override + void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device) { + (wallet as BitcoinWallet).setLedger(ledger, device); + } + + @override + Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, + {int index = 0, int limit = 5}) async { + final hardwareWalletService = BitcoinHardwareWalletService(ledgerVM.ledger, ledgerVM.device); + try { + return hardwareWalletService.getAvailableAccounts(index: index, limit: limit); + } on LedgerException catch (err) { + print(err.message); + throw err; + } + } } diff --git a/lib/buy/buy_provider.dart b/lib/buy/buy_provider.dart index 4e4c113f4..1a37e09b3 100644 --- a/lib/buy/buy_provider.dart +++ b/lib/buy/buy_provider.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/buy/buy_amount.dart'; import 'package:cake_wallet/buy/order.dart'; +import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:flutter/material.dart'; @@ -7,10 +8,12 @@ abstract class BuyProvider { BuyProvider({ required this.wallet, required this.isTestEnvironment, + required this.ledgerVM, }); final WalletBase wallet; final bool isTestEnvironment; + final LedgerViewModel? ledgerVM; String get title; diff --git a/lib/buy/dfx/dfx_buy_provider.dart b/lib/buy/dfx/dfx_buy_provider.dart index 8e2d58d11..2a7e2ab13 100644 --- a/lib/buy/dfx/dfx_buy_provider.dart +++ b/lib/buy/dfx/dfx_buy_provider.dart @@ -3,9 +3,11 @@ import 'dart:convert'; import 'package:cake_wallet/buy/buy_provider.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; @@ -13,17 +15,16 @@ import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; class DFXBuyProvider extends BuyProvider { - DFXBuyProvider({required WalletBase wallet, bool isTestEnvironment = false}) - : super(wallet: wallet, isTestEnvironment: isTestEnvironment); + DFXBuyProvider({required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM}) + : super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: ledgerVM); static const _baseUrl = 'api.dfx.swiss'; - static const _authPath = '/v1/auth/signMessage'; - static const _signUpPath = '/v1/auth/signUp'; - static const _signInPath = '/v1/auth/signIn'; + // static const _signMessagePath = '/v1/auth/signMessage'; + static const _authPath = '/v1/auth'; static const walletName = 'CakeWallet'; @override - String get title => 'DFX Connect'; + String get title => 'DFX.swiss'; @override String get providerDescription => S.current.dfx_option_description; @@ -73,22 +74,26 @@ class DFXBuyProvider extends BuyProvider { String get walletAddress => wallet.walletAddresses.primaryAddress ?? wallet.walletAddresses.address; - Future getSignMessage() async { - final uri = Uri.https(_baseUrl, _authPath, {'address': walletAddress}); + Future getSignMessage() async => + "By_signing_this_message,_you_confirm_that_you_are_the_sole_owner_of_the_provided_Blockchain_address._Your_ID:_$walletAddress"; - var response = await http.get(uri, headers: {'accept': 'application/json'}); + // // Lets keep this just in case, but we can avoid this API Call + // Future getSignMessage() async { + // final uri = Uri.https(_baseUrl, _signMessagePath, {'address': walletAddress}); + // + // final response = await http.get(uri, headers: {'accept': 'application/json'}); + // + // if (response.statusCode == 200) { + // final responseBody = jsonDecode(response.body); + // return responseBody['message'] as String; + // } else { + // throw Exception( + // 'Failed to get sign message. Status: ${response.statusCode} ${response.body}'); + // } + // } - if (response.statusCode == 200) { - final responseBody = jsonDecode(response.body); - return responseBody['message'] as String; - } else { - throw Exception( - 'Failed to get sign message. Status: ${response.statusCode} ${response.body}'); - } - } - - Future signUp() async { - final signMessage = getSignature(await getSignMessage()); + Future auth() async { + final signMessage = await getSignature(await getSignMessage()); final requestBody = jsonEncode({ 'wallet': walletName, @@ -96,7 +101,7 @@ class DFXBuyProvider extends BuyProvider { 'signature': signMessage, }); - final uri = Uri.https(_baseUrl, _signUpPath); + final uri = Uri.https(_baseUrl, _authPath); var response = await http.post( uri, headers: {'Content-Type': 'application/json'}, @@ -115,34 +120,7 @@ class DFXBuyProvider extends BuyProvider { } } - Future signIn() async { - final signMessage = getSignature(await getSignMessage()); - - final requestBody = jsonEncode({ - 'address': walletAddress, - 'signature': signMessage, - }); - - final uri = Uri.https(_baseUrl, _signInPath); - var response = await http.post( - uri, - headers: {'Content-Type': 'application/json'}, - body: requestBody, - ); - - if (response.statusCode == 201) { - final responseBody = jsonDecode(response.body); - return responseBody['accessToken'] as String; - } else if (response.statusCode == 403) { - final responseBody = jsonDecode(response.body); - final message = responseBody['message'] ?? 'Service unavailable in your country'; - throw Exception(message); - } else { - throw Exception('Failed to sign in. Status: ${response.statusCode} ${response.body}'); - } - } - - String getSignature(String message) { + Future getSignature(String message) async { switch (wallet.type) { case WalletType.ethereum: case WalletType.polygon: @@ -159,22 +137,26 @@ class DFXBuyProvider extends BuyProvider { @override Future launchProvider(BuildContext context, bool? isBuyAction) async { + if (wallet.isHardwareWallet) { + if (!ledgerVM!.isConnected) { + await Navigator.of(context).pushNamed(Routes.connectDevices, + arguments: ConnectDevicePageParams( + walletType: wallet.walletInfo.type, + onConnectDevice: (BuildContext context, LedgerViewModel ledgerVM) { + ledgerVM.setLedger(wallet); + Navigator.of(context).pop(); + })); + } else { + ledgerVM!.setLedger(wallet); + } + } + try { final assetOut = this.assetOut; final blockchain = this.blockchain; final actionType = isBuyAction == true ? '/buy' : '/sell'; - String accessToken; - - try { - accessToken = await signUp(); - } on Exception catch (e) { - if (e.toString().contains('409')) { - accessToken = await signIn(); - } else { - rethrow; - } - } + final accessToken = await auth(); final uri = Uri.https('services.dfx.swiss', actionType, { 'session': accessToken, @@ -198,7 +180,7 @@ class DFXBuyProvider extends BuyProvider { context: context, builder: (BuildContext context) { return AlertWithOneAction( - alertTitle: "DFX Connect", + alertTitle: "DFX.swiss", alertContent: S.of(context).buy_provider_unavailable + ': $e', buttonText: S.of(context).ok, buttonAction: () => Navigator.of(context).pop()); diff --git a/lib/buy/moonpay/moonpay_provider.dart b/lib/buy/moonpay/moonpay_provider.dart index fea8fdabd..53f018d57 100644 --- a/lib/buy/moonpay/moonpay_provider.dart +++ b/lib/buy/moonpay/moonpay_provider.dart @@ -14,7 +14,6 @@ import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/utils/device_info.dart'; -import 'package:crypto/crypto.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; @@ -30,7 +29,7 @@ class MoonPayProvider extends BuyProvider { }) : baseSellUrl = isTestEnvironment ? _baseSellTestUrl : _baseSellProductUrl, baseBuyUrl = isTestEnvironment ? _baseBuyTestUrl : _baseBuyProductUrl, this._settingsStore = settingsStore, - super(wallet: wallet, isTestEnvironment: isTestEnvironment); + super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: null); final SettingsStore _settingsStore; diff --git a/lib/buy/onramper/onramper_buy_provider.dart b/lib/buy/onramper/onramper_buy_provider.dart index 3819f074d..1f1c86962 100644 --- a/lib/buy/onramper/onramper_buy_provider.dart +++ b/lib/buy/onramper/onramper_buy_provider.dart @@ -13,7 +13,7 @@ import 'package:url_launcher/url_launcher.dart'; class OnRamperBuyProvider extends BuyProvider { OnRamperBuyProvider(this._settingsStore, {required WalletBase wallet, bool isTestEnvironment = false}) - : super(wallet: wallet, isTestEnvironment: isTestEnvironment); + : super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: null); static const _baseUrl = 'buy.onramper.com'; diff --git a/lib/buy/robinhood/robinhood_buy_provider.dart b/lib/buy/robinhood/robinhood_buy_provider.dart index 7610e51f3..ab58754dd 100644 --- a/lib/buy/robinhood/robinhood_buy_provider.dart +++ b/lib/buy/robinhood/robinhood_buy_provider.dart @@ -3,8 +3,11 @@ import 'dart:convert'; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/buy/buy_provider.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; @@ -12,8 +15,8 @@ import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; class RobinhoodBuyProvider extends BuyProvider { - RobinhoodBuyProvider({required WalletBase wallet, bool isTestEnvironment = false}) - : super(wallet: wallet, isTestEnvironment: isTestEnvironment); + RobinhoodBuyProvider({required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM}) + : super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: ledgerVM); static const _baseUrl = 'applink.robinhood.com'; static const _cIdBaseUrl = 'exchange-helper.cakewallet.com'; @@ -34,7 +37,7 @@ class RobinhoodBuyProvider extends BuyProvider { String get _apiSecret => secrets.exchangeHelperApiKey; - String getSignature(String message) { + Future getSignature(String message) { switch (wallet.type) { case WalletType.ethereum: case WalletType.polygon: @@ -53,7 +56,7 @@ class RobinhoodBuyProvider extends BuyProvider { final valid_until = (DateTime.now().millisecondsSinceEpoch / 1000).round() + 10; final message = "$_apiSecret:${valid_until}"; - final signature = getSignature(message); + final signature = await getSignature(message); final uri = Uri.https(_cIdBaseUrl, "/api/robinhood"); @@ -84,6 +87,20 @@ class RobinhoodBuyProvider extends BuyProvider { } Future launchProvider(BuildContext context, bool? isBuyAction) async { + if (wallet.isHardwareWallet) { + if (!ledgerVM!.isConnected) { + await Navigator.of(context).pushNamed(Routes.connectDevices, + arguments: ConnectDevicePageParams( + walletType: wallet.walletInfo.type, + onConnectDevice: (BuildContext context, LedgerViewModel ledgerVM) { + ledgerVM.setLedger(wallet); + Navigator.of(context).pop(); + })); + } else { + ledgerVM!.setLedger(wallet); + } + } + try { final uri = await requestProviderUrl(); await launchUrl(uri, mode: LaunchMode.externalApplication); diff --git a/lib/buy/wyre/wyre_buy_provider.dart b/lib/buy/wyre/wyre_buy_provider.dart index 4dd091c33..e09186ad5 100644 --- a/lib/buy/wyre/wyre_buy_provider.dart +++ b/lib/buy/wyre/wyre_buy_provider.dart @@ -14,7 +14,7 @@ import 'package:cake_wallet/.secrets.g.dart' as secrets; class WyreBuyProvider extends BuyProvider { WyreBuyProvider({required WalletBase wallet, bool isTestEnvironment = false}) : baseApiUrl = isTestEnvironment ? _baseTestApiUrl : _baseProductApiUrl, - super(wallet: wallet, isTestEnvironment: isTestEnvironment); + super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: null); static const _baseTestApiUrl = 'https://api.testwyre.com'; static const _baseProductApiUrl = 'https://api.sendwyre.com'; diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 967cf9bf0..e507f5212 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -294,6 +294,8 @@ class AddressValidator extends TextValidator { '|([^0-9a-zA-Z]|^)q[0-9a-zA-Z]{42}([^0-9a-zA-Z]|\$)'; case CryptoCurrency.sol: return '([^0-9a-zA-Z]|^)[1-9A-HJ-NP-Za-km-z]{43,44}([^0-9a-zA-Z]|\$)'; + case CryptoCurrency.trx: + return '(T|t)[1-9A-HJ-NP-Za-km-z]{33}'; default: if (type.tag == CryptoCurrency.eth.title) { return '0x[0-9a-zA-Z]{42}'; @@ -304,6 +306,9 @@ class AddressValidator extends TextValidator { if (type.tag == CryptoCurrency.sol.title) { return '([^0-9a-zA-Z]|^)[1-9A-HJ-NP-Za-km-z]{43,44}([^0-9a-zA-Z]|\$)'; } + if (type.tag == CryptoCurrency.trx.title) { + return '(T|t)[1-9A-HJ-NP-Za-km-z]{33}'; + } return null; } diff --git a/lib/core/auth_service.dart b/lib/core/auth_service.dart index 48610784c..791701395 100644 --- a/lib/core/auth_service.dart +++ b/lib/core/auth_service.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:io'; import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/core/totp_request_details.dart'; @@ -7,7 +6,6 @@ import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:flutter/material.dart'; import 'package:mobx/mobx.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/secret_store_key.dart'; @@ -35,18 +33,13 @@ class AuthService with Store { Routes.restoreOptions, ]; - final FlutterSecureStorage secureStorage; + final SecureStorage secureStorage; final SharedPreferences sharedPreferences; final SettingsStore settingsStore; Future setPassword(String password) async { final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); final encodedPassword = encodedPinCode(pin: password); - // secure storage has a weird bug on macOS, where overwriting a key doesn't work, unless - // we delete what's there first: - if (Platform.isMacOS) { - await secureStorage.delete(key: key); - } await secureStorage.write(key: key, value: encodedPassword); } @@ -66,7 +59,7 @@ class AuthService with Store { Future authenticate(String pin) async { final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); - final encodedPin = await readSecureStorage(secureStorage, key); + final encodedPin = await secureStorage.read(key: key); final decodedPin = decodedPinCode(pin: encodedPin!); return decodedPin == pin; diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 2ec5f293d..d0d9a8a26 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -1,12 +1,12 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; +import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/themes/theme_list.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:path_provider/path_provider.dart'; import 'package:cryptography/cryptography.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -24,7 +24,7 @@ import 'package:cake_backup/backup.dart' as cake_backup; class BackupService { BackupService( - this._flutterSecureStorage, this._walletInfoSource, this._keyService, this._sharedPreferences) + this._secureStorage, this._walletInfoSource, this._keyService, this._sharedPreferences) : _cipher = Cryptography.instance.chacha20Poly1305Aead(), _correctWallets = []; @@ -34,7 +34,7 @@ class BackupService { static const _v2 = 2; final Cipher _cipher; - final FlutterSecureStorage _flutterSecureStorage; + final SecureStorage _secureStorage; final SharedPreferences _sharedPreferences; final Box _walletInfoSource; final KeyService _keyService; @@ -275,7 +275,7 @@ class BackupService { if (currentTransactionPriorityKeyLegacy != null) await _sharedPreferences.setInt( PreferencesKey.currentTransactionPriorityKeyLegacy, currentTransactionPriorityKeyLegacy); - + if (currentBitcoinElectrumSererId != null) await _sharedPreferences.setInt( PreferencesKey.currentBitcoinElectrumSererIdKey, currentBitcoinElectrumSererId); @@ -373,16 +373,14 @@ class BackupService { final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword); final backupPassword = keychainJSON[backupPasswordKey] as String; - await _flutterSecureStorage.delete(key: backupPasswordKey); - await _flutterSecureStorage.write(key: backupPasswordKey, value: backupPassword); + await _secureStorage.write(key: backupPasswordKey, value: backupPassword); keychainWalletsInfo.forEach((dynamic rawInfo) async { final info = rawInfo as Map; await importWalletKeychainInfo(info); }); - await _flutterSecureStorage.delete(key: pinCodeKey); - await _flutterSecureStorage.write(key: pinCodeKey, value: encodedPinCode(pin: decodedPin)); + await _secureStorage.write(key: pinCodeKey, value: encodedPinCode(pin: decodedPin)); keychainDumpFile.deleteSync(); } @@ -401,16 +399,14 @@ class BackupService { final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword); final backupPassword = keychainJSON[backupPasswordKey] as String; - await _flutterSecureStorage.delete(key: backupPasswordKey); - await _flutterSecureStorage.write(key: backupPasswordKey, value: backupPassword); + await _secureStorage.write(key: backupPasswordKey, value: backupPassword); keychainWalletsInfo.forEach((dynamic rawInfo) async { final info = rawInfo as Map; await importWalletKeychainInfo(info); }); - await _flutterSecureStorage.delete(key: pinCodeKey); - await _flutterSecureStorage.write(key: pinCodeKey, value: encodedPinCode(pin: decodedPin)); + await _secureStorage.write(key: pinCodeKey, value: encodedPinCode(pin: decodedPin)); keychainDumpFile.deleteSync(); } @@ -430,7 +426,7 @@ class BackupService { Future _exportKeychainDumpV2(String password, {String keychainSalt = secrets.backupKeychainSalt}) async { final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); - final encodedPin = await _flutterSecureStorage.read(key: key); + final encodedPin = await _secureStorage.read(key: key); final decodedPin = decodedPinCode(pin: encodedPin!); final wallets = await Future.wait(_walletInfoSource.values.map((walletInfo) async { return { @@ -440,7 +436,7 @@ class BackupService { }; })); final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword); - final backupPassword = await _flutterSecureStorage.read(key: backupPasswordKey); + final backupPassword = await _secureStorage.read(key: backupPasswordKey); final data = utf8.encode( json.encode({'pin': decodedPin, 'wallets': wallets, backupPasswordKey: backupPassword})); final encrypted = await _encryptV2(Uint8List.fromList(data), '$keychainSalt$password'); diff --git a/lib/core/create_trade_result.dart b/lib/core/create_trade_result.dart new file mode 100644 index 000000000..0e873d51e --- /dev/null +++ b/lib/core/create_trade_result.dart @@ -0,0 +1,9 @@ +class CreateTradeResult { + bool result; + String? errorMessage; + + CreateTradeResult({ + required this.result, + this.errorMessage, + }); +} diff --git a/lib/core/key_service.dart b/lib/core/key_service.dart index f829c22b5..71fb5a4fc 100644 --- a/lib/core/key_service.dart +++ b/lib/core/key_service.dart @@ -1,32 +1,30 @@ import 'package:cake_wallet/core/secure_storage.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:cake_wallet/entities/secret_store_key.dart'; import 'package:cake_wallet/entities/encrypt.dart'; class KeyService { KeyService(this._secureStorage); - final FlutterSecureStorage _secureStorage; + final SecureStorage _secureStorage; Future getWalletPassword({required String walletName}) async { - final key = generateStoreKeyFor( - key: SecretStoreKey.moneroWalletPassword, walletName: walletName); - final encodedPassword = await readSecureStorage(_secureStorage, key); + final key = + generateStoreKeyFor(key: SecretStoreKey.moneroWalletPassword, walletName: walletName); + final encodedPassword = await _secureStorage.read(key: key); return decodeWalletPassword(password: encodedPassword!); } Future saveWalletPassword({required String walletName, required String password}) async { - final key = generateStoreKeyFor( - key: SecretStoreKey.moneroWalletPassword, walletName: walletName); + final key = + generateStoreKeyFor(key: SecretStoreKey.moneroWalletPassword, walletName: walletName); final encodedPassword = encodeWalletPassword(password: password); - await _secureStorage.delete(key: key); await _secureStorage.write(key: key, value: encodedPassword); } Future deleteWalletPassword({required String walletName}) async { - final key = generateStoreKeyFor( - key: SecretStoreKey.moneroWalletPassword, walletName: walletName); + final key = + generateStoreKeyFor(key: SecretStoreKey.moneroWalletPassword, walletName: walletName); await _secureStorage.delete(key: key); } diff --git a/lib/core/node_address_validator.dart b/lib/core/node_address_validator.dart index c1fe4ba91..0c8a0c37c 100644 --- a/lib/core/node_address_validator.dart +++ b/lib/core/node_address_validator.dart @@ -11,5 +11,9 @@ class NodeAddressValidator extends TextValidator { class NodePathValidator extends TextValidator { NodePathValidator() - : super(errorMessage: S.current.error_text_node_address, pattern: '^([/0-9a-zA-Z.\-]+)?\$'); + : super( + errorMessage: S.current.error_text_node_address, + pattern: '^([/0-9a-zA-Z.\-]+)?\$', + isAutovalidate: true, + ); } diff --git a/lib/core/secure_storage.dart b/lib/core/secure_storage.dart deleted file mode 100644 index 4d9334a10..000000000 --- a/lib/core/secure_storage.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'dart:async'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -// For now, we can create a utility function to handle this. -// -// However, we could look into abstracting the entire FlutterSecureStorage package -// so the app doesn't depend on the package directly but an absraction. -// It'll make these kind of modifications to read/write come from a single point. - -Future readSecureStorage(FlutterSecureStorage secureStorage, String key) async { - String? result; - const maxWait = Duration(seconds: 3); - const checkInterval = Duration(milliseconds: 200); - - DateTime start = DateTime.now(); - - while (result == null && DateTime.now().difference(start) < maxWait) { - result = await secureStorage.read(key: key); - - if (result != null) { - break; - } - - await Future.delayed(checkInterval); - } - - return result; -} diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index 6d04055ba..3e3445757 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/core/validator.dart'; import 'package:cake_wallet/entities/mnemonic_item.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; +import 'package:cake_wallet/tron/tron.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/nano/nano.dart'; @@ -40,6 +41,8 @@ class SeedValidator extends Validator { return polygon!.getPolygonWordList(language); case WalletType.solana: return solana!.getSolanaWordList(language); + case WalletType.tron: + return tron!.getTronWordList(language); default: return []; } diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index 31a893ad6..a55e9ee3f 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -1,8 +1,8 @@ +import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hive/hive.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cake_wallet/core/key_service.dart'; @@ -25,7 +25,7 @@ class WalletCreationService { } WalletType type; - final FlutterSecureStorage secureStorage; + final SecureStorage secureStorage; final SharedPreferences sharedPreferences; final SettingsStore settingsStore; final KeyService keyService; @@ -101,4 +101,19 @@ class WalletCreationService { return wallet; } + + Future restoreFromHardwareWallet(WalletCredentials credentials) async { + checkIfExists(credentials.name); + final password = generateWalletPassword(); + credentials.password = password; + await keyService.saveWalletPassword(password: password, walletName: credentials.name); + final wallet = await _service!.restoreFromHardwareWallet(credentials); + + if (wallet.type == WalletType.monero) { + await sharedPreferences.setBool( + PreferencesKey.moneroWalletUpdateV1Key(wallet.name), _isNewMoneroWalletPasswordUpdated); + } + + return wallet; + } } diff --git a/lib/core/wallet_loading_service.dart b/lib/core/wallet_loading_service.dart index 3323e7831..1f17a7a1c 100644 --- a/lib/core/wallet_loading_service.dart +++ b/lib/core/wallet_loading_service.dart @@ -1,27 +1,28 @@ import 'package:cake_wallet/core/generate_wallet_password.dart'; import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; +import 'package:cake_wallet/utils/exception_handler.dart'; +import 'package:cw_core/cake_hive.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:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; class WalletLoadingService { - WalletLoadingService( - this.sharedPreferences, this.keyService, this.walletServiceFactory); + WalletLoadingService(this.sharedPreferences, this.keyService, this.walletServiceFactory); final SharedPreferences sharedPreferences; final KeyService keyService; final WalletService Function(WalletType type) walletServiceFactory; - Future renameWallet( - WalletType type, String name, String newName) async { + Future renameWallet(WalletType type, String name, String newName) async { final walletService = walletServiceFactory.call(type); final password = await keyService.getWalletPassword(walletName: name); // Save the current wallet's password to the new wallet name's key - await keyService.saveWalletPassword( - walletName: newName, password: password); + await keyService.saveWalletPassword(walletName: newName, password: password); // Delete previous wallet name from keyService to keep only new wallet's name // otherwise keeps duplicate (old and new names) await keyService.deleteWalletPassword(walletName: name); @@ -38,15 +39,43 @@ class WalletLoadingService { } Future load(WalletType type, String name) async { - final walletService = walletServiceFactory.call(type); - final password = await keyService.getWalletPassword(walletName: name); - final wallet = await walletService.openWallet(name, password); + try { + final walletService = walletServiceFactory.call(type); + final password = await keyService.getWalletPassword(walletName: name); + final wallet = await walletService.openWallet(name, password); - if (type == WalletType.monero) { - await updateMoneroWalletPassword(wallet); + if (type == WalletType.monero) { + await updateMoneroWalletPassword(wallet); + } + + return wallet; + } catch (error, stack) { + ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stack)); + + // try opening another wallet that is not corrupted to give user access to the app + final walletInfoSource = await CakeHive.openBox(WalletInfo.boxName); + + for (var walletInfo in walletInfoSource.values) { + try { + final walletService = walletServiceFactory.call(walletInfo.type); + final password = await keyService.getWalletPassword(walletName: walletInfo.name); + final wallet = await walletService.openWallet(walletInfo.name, password); + + if (walletInfo.type == WalletType.monero) { + await updateMoneroWalletPassword(wallet); + } + + await sharedPreferences.setString(PreferencesKey.currentWalletName, wallet.name); + await sharedPreferences.setInt( + PreferencesKey.currentWalletType, serializeToInt(wallet.type)); + + return wallet; + } catch (_) {} + } + + // if all user's wallets are corrupted throw exception + throw error; } - - return wallet; } Future updateMoneroWalletPassword(WalletBase wallet) async { @@ -61,11 +90,9 @@ class WalletLoadingService { // Save new generated password with backup key for case where // wallet will change password, but it will fail to update in secure storage final bakWalletName = '#__${wallet.name}_bak__#'; - await keyService.saveWalletPassword( - walletName: bakWalletName, password: password); + await keyService.saveWalletPassword(walletName: bakWalletName, password: password); await wallet.changePassword(password); - await keyService.saveWalletPassword( - walletName: wallet.name, password: password); + await keyService.saveWalletPassword(walletName: wallet.name, password: password); isPasswordUpdated = true; await sharedPreferences.setBool(key, isPasswordUpdated); } diff --git a/lib/di.dart b/lib/di.dart index d78da638c..92bf25c9a 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1,236 +1,239 @@ +import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/anonpay/anonpay_api.dart'; import 'package:cake_wallet/anonpay/anonpay_info_base.dart'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; +import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart'; +import 'package:cake_wallet/anypay/anypay_api.dart'; +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; -import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; +import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/buy/payfura/payfura_buy_provider.dart'; +import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart'; +import 'package:cake_wallet/core/auth_service.dart'; +import 'package:cake_wallet/core/backup_service.dart'; +import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart'; import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart'; -import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart'; import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart'; +import 'package:cake_wallet/core/wallet_creation_service.dart'; +import 'package:cake_wallet/core/wallet_loading_service.dart'; import 'package:cake_wallet/core/yat_service.dart'; import 'package:cake_wallet/entities/background_tasks.dart'; +import 'package:cake_wallet/entities/biometric_auth.dart'; +import 'package:cake_wallet/entities/contact.dart'; +import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; +import 'package:cake_wallet/view_model/link_view_model.dart'; +import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart'; import 'package:cw_core/receive_page_option.dart'; +import 'package:cake_wallet/entities/qr_view_data.dart'; +import 'package:cake_wallet/entities/template.dart'; +import 'package:cake_wallet/entities/transaction_description.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; -import 'package:cake_wallet/nano/nano.dart'; +import 'package:cake_wallet/exchange/exchange_template.dart'; +import 'package:cake_wallet/exchange/trade.dart'; +import 'package:cake_wallet/haven/haven.dart'; +import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart'; import 'package:cake_wallet/ionia/ionia_anypay.dart'; +import 'package:cake_wallet/ionia/ionia_api.dart'; import 'package:cake_wallet/ionia/ionia_gift_card.dart'; +import 'package:cake_wallet/ionia/ionia_merchant.dart'; +import 'package:cake_wallet/ionia/ionia_service.dart'; import 'package:cake_wallet/ionia/ionia_tip.dart'; +import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/reactions/on_authentication_state_change.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dart'; +import 'package:cake_wallet/src/screens/auth/auth_page.dart'; +import 'package:cake_wallet/src/screens/backup/backup_page.dart'; +import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart'; import 'package:cake_wallet/src/screens/buy/buy_options_page.dart'; +import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart'; import 'package:cake_wallet/src/screens/buy/webview_page.dart'; +import 'package:cake_wallet/src/screens/contact/contact_list_page.dart'; +import 'package:cake_wallet/src/screens/contact/contact_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/dashboard_page.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_dashboard_page.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart'; import 'package:cake_wallet/src/screens/dashboard/edit_token_page.dart'; import 'package:cake_wallet/src/screens/dashboard/home_settings_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/address_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/balance_page.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart'; +import 'package:cake_wallet/src/screens/exchange/exchange_page.dart'; +import 'package:cake_wallet/src/screens/exchange/exchange_template_page.dart'; +import 'package:cake_wallet/src/screens/exchange_trade/exchange_confirm_page.dart'; +import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_page.dart'; +import 'package:cake_wallet/src/screens/faq/faq_page.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_cards_page.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_page.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_custom_redeem_page.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_custom_tip_page.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_gift_card_detail_page.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_more_options_page.dart'; +import 'package:cake_wallet/src/screens/ionia/cards/ionia_payment_status_page.dart'; +import 'package:cake_wallet/src/screens/ionia/ionia.dart'; +import 'package:cake_wallet/src/screens/monero_accounts/monero_account_edit_or_create_page.dart'; +import 'package:cake_wallet/src/screens/monero_accounts/monero_account_list_page.dart'; import 'package:cake_wallet/src/screens/nano/nano_change_rep_page.dart'; import 'package:cake_wallet/src/screens/nano_accounts/nano_account_edit_or_create_page.dart'; import 'package:cake_wallet/src/screens/nano_accounts/nano_account_list_page.dart'; +import 'package:cake_wallet/src/screens/new_wallet/new_wallet_type_page.dart'; +import 'package:cake_wallet/src/screens/nodes/node_create_or_edit_page.dart'; import 'package:cake_wallet/src/screens/nodes/pow_node_create_or_edit_page.dart'; +import 'package:cake_wallet/src/screens/order_details/order_details_page.dart'; +import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart'; +import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart'; +import 'package:cake_wallet/src/screens/receive/receive_page.dart'; +import 'package:cake_wallet/src/screens/rescan/rescan_page.dart'; +import 'package:cake_wallet/src/screens/restore/restore_from_backup_page.dart'; +import 'package:cake_wallet/src/screens/restore/restore_options_page.dart'; import 'package:cake_wallet/src/screens/restore/wallet_restore_choose_derivation.dart'; +import 'package:cake_wallet/src/screens/restore/wallet_restore_page.dart'; +import 'package:cake_wallet/src/screens/seed/pre_seed_page.dart'; +import 'package:cake_wallet/src/screens/seed/wallet_seed_page.dart'; +import 'package:cake_wallet/src/screens/send/send_page.dart'; +import 'package:cake_wallet/src/screens/send/send_template_page.dart'; +import 'package:cake_wallet/src/screens/settings/connection_sync_page.dart'; import 'package:cake_wallet/src/screens/settings/display_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/domain_lookups_page.dart'; import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart'; import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/privacy_page.dart'; import 'package:cake_wallet/src/screens/settings/security_backup_page.dart'; -import 'package:cake_wallet/src/screens/ionia/cards/ionia_custom_redeem_page.dart'; -import 'package:cake_wallet/src/screens/ionia/cards/ionia_gift_card_detail_page.dart'; -import 'package:cake_wallet/src/screens/ionia/cards/ionia_more_options_page.dart'; -import 'package:cake_wallet/src/screens/settings/connection_sync_page.dart'; -import 'package:cake_wallet/src/screens/settings/trocador_providers_page.dart'; import 'package:cake_wallet/src/screens/settings/tor_page.dart'; +import 'package:cake_wallet/src/screens/settings/trocador_providers_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/modify_2fa_page.dart'; -import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_info_page.dart'; -import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_qr_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa.dart'; import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_enter_code_page.dart'; +import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_info_page.dart'; +import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_qr_page.dart'; +import 'package:cake_wallet/src/screens/setup_pin_code/setup_pin_code.dart'; +import 'package:cake_wallet/src/screens/subaddress/address_edit_or_create_page.dart'; +import 'package:cake_wallet/src/screens/support/support_page.dart'; import 'package:cake_wallet/src/screens/support_chat/support_chat_page.dart'; import 'package:cake_wallet/src/screens/support_other_links/support_other_links_page.dart'; -import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart'; -import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart'; -import 'package:cake_wallet/themes/theme_list.dart'; -import 'package:cake_wallet/utils/device_info.dart'; -import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; -import 'package:cake_wallet/utils/payment_request.dart'; -import 'package:cake_wallet/utils/responsive_layout_util.dart'; -import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart'; -import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart'; -import 'package:cake_wallet/view_model/anonpay_details_view_model.dart'; -import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart'; -import 'package:cake_wallet/view_model/dashboard/market_place_view_model.dart'; -import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; -import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_buy_card_view_model.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_custom_tip_view_model.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_custom_redeem_view_model.dart'; -import 'package:cake_wallet/ionia/ionia_service.dart'; -import 'package:cake_wallet/ionia/ionia_api.dart'; -import 'package:cake_wallet/ionia/ionia_merchant.dart'; -import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/haven/haven.dart'; -import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_cards_page.dart'; -import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_page.dart'; -import 'package:cake_wallet/src/screens/ionia/cards/ionia_custom_tip_page.dart'; -import 'package:cake_wallet/src/screens/ionia/ionia.dart'; -import 'package:cake_wallet/src/screens/dashboard/pages/balance_page.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_account_view_model.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_purchase_merch_view_model.dart'; -import 'package:cake_wallet/view_model/nano_account_list/nano_account_edit_or_create_view_model.dart'; -import 'package:cake_wallet/view_model/nano_account_list/nano_account_list_view_model.dart'; -import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart'; -import 'package:cake_wallet/view_model/seed_type_view_model.dart'; -import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart'; -import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart'; -import 'package:cake_wallet/view_model/settings/display_settings_view_model.dart'; -import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart'; -import 'package:cake_wallet/view_model/settings/privacy_settings_view_model.dart'; -import 'package:cake_wallet/view_model/settings/security_settings_view_model.dart'; -import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart'; -import 'package:cake_wallet/view_model/settings/trocador_providers_view_model.dart'; -import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; -import 'package:cake_wallet/view_model/wallet_list/wallet_edit_view_model.dart'; -import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; -import 'package:cake_wallet/view_model/wallet_restore_choose_derivation_view_model.dart'; -import 'package:cw_core/erc20_token.dart'; -import 'package:cw_core/nano_account.dart'; -import 'package:cw_core/unspent_coins_info.dart'; -import 'package:cake_wallet/core/backup_service.dart'; -import 'package:cw_core/wallet_service.dart'; -import 'package:cake_wallet/entities/biometric_auth.dart'; -import 'package:cake_wallet/entities/contact_record.dart'; -import 'package:cake_wallet/buy/order.dart'; -import 'package:cake_wallet/entities/transaction_description.dart'; -import 'package:cw_core/transaction_info.dart'; -import 'package:cake_wallet/entities/contact.dart'; -import 'package:cw_core/node.dart'; -import 'package:cake_wallet/exchange/trade.dart'; -import 'package:cake_wallet/reactions/on_authentication_state_change.dart'; -import 'package:cake_wallet/src/screens/backup/backup_page.dart'; -import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart'; -import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart'; -import 'package:cake_wallet/src/screens/contact/contact_list_page.dart'; -import 'package:cake_wallet/src/screens/contact/contact_page.dart'; -import 'package:cake_wallet/src/screens/exchange_trade/exchange_confirm_page.dart'; -import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_page.dart'; -import 'package:cake_wallet/src/screens/faq/faq_page.dart'; -import 'package:cake_wallet/src/screens/new_wallet/new_wallet_type_page.dart'; -import 'package:cake_wallet/src/screens/nodes/node_create_or_edit_page.dart'; -import 'package:cake_wallet/src/screens/order_details/order_details_page.dart'; -import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; -import 'package:cake_wallet/src/screens/rescan/rescan_page.dart'; -import 'package:cake_wallet/src/screens/restore/restore_from_backup_page.dart'; -import 'package:cake_wallet/src/screens/restore/restore_options_page.dart'; -import 'package:cake_wallet/src/screens/restore/wallet_restore_page.dart'; -import 'package:cake_wallet/src/screens/seed/pre_seed_page.dart'; -import 'package:cake_wallet/src/screens/seed/wallet_seed_page.dart'; -import 'package:cake_wallet/src/screens/send/send_template_page.dart'; -import 'package:cake_wallet/src/screens/setup_pin_code/setup_pin_code.dart'; -import 'package:cake_wallet/src/screens/support/support_page.dart'; import 'package:cake_wallet/src/screens/trade_details/trade_details_page.dart'; import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart'; +import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart'; import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart'; -import 'package:cake_wallet/src/screens/exchange/exchange_page.dart'; -import 'package:cake_wallet/src/screens/exchange/exchange_template_page.dart'; +import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart'; +import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/store/authentication_store.dart'; +import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/dashboard/orders_store.dart'; +import 'package:cake_wallet/store/dashboard/trade_filter_store.dart'; +import 'package:cake_wallet/store/dashboard/trades_store.dart'; +import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart'; import 'package:cake_wallet/store/node_list_store.dart'; import 'package:cake_wallet/store/secret_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cake_wallet/core/auth_service.dart'; -import 'package:cake_wallet/core/key_service.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cake_wallet/src/screens/monero_accounts/monero_account_list_page.dart'; -import 'package:cake_wallet/src/screens/monero_accounts/monero_account_edit_or_create_page.dart'; -import 'package:cake_wallet/src/screens/auth/auth_page.dart'; -import 'package:cake_wallet/src/screens/dashboard/dashboard_page.dart'; -import 'package:cake_wallet/src/screens/receive/receive_page.dart'; -import 'package:cake_wallet/src/screens/send/send_page.dart'; -import 'package:cake_wallet/src/screens/subaddress/address_edit_or_create_page.dart'; -import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart'; +import 'package:cake_wallet/store/templates/exchange_template_store.dart'; +import 'package:cake_wallet/store/templates/send_template_store.dart'; import 'package:cake_wallet/store/wallet_list_store.dart'; import 'package:cake_wallet/store/yat/yat_store.dart'; +import 'package:cake_wallet/themes/theme_list.dart'; +import 'package:cake_wallet/utils/device_info.dart'; +import 'package:cake_wallet/utils/payment_request.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart'; +import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart'; +import 'package:cake_wallet/view_model/anonpay_details_view_model.dart'; +import 'package:cake_wallet/view_model/auth_view_model.dart'; import 'package:cake_wallet/view_model/backup_view_model.dart'; import 'package:cake_wallet/view_model/buy/buy_amount_view_model.dart'; import 'package:cake_wallet/view_model/buy/buy_view_model.dart'; import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/market_place_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart'; import 'package:cake_wallet/view_model/edit_backup_password_view_model.dart'; import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart'; +import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart'; +import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_account_view_model.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_buy_card_view_model.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_custom_redeem_view_model.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_custom_tip_view_model.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_gift_card_details_view_model.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_payment_status_view_model.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_purchase_merch_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; -import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart'; +import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart'; +import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart'; +import 'package:cake_wallet/view_model/nano_account_list/nano_account_edit_or_create_view_model.dart'; +import 'package:cake_wallet/view_model/nano_account_list/nano_account_list_view_model.dart'; import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; +import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart'; +import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart'; import 'package:cake_wallet/view_model/order_details_view_model.dart'; import 'package:cake_wallet/view_model/rescan_view_model.dart'; +import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart'; import 'package:cake_wallet/view_model/restore_from_backup_view_model.dart'; +import 'package:cake_wallet/view_model/seed_type_view_model.dart'; import 'package:cake_wallet/view_model/send/send_template_view_model.dart'; +import 'package:cake_wallet/view_model/send/send_view_model.dart'; +import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart'; +import 'package:cake_wallet/view_model/settings/display_settings_view_model.dart'; +import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart'; +import 'package:cake_wallet/view_model/settings/privacy_settings_view_model.dart'; +import 'package:cake_wallet/view_model/settings/security_settings_view_model.dart'; +import 'package:cake_wallet/view_model/settings/trocador_providers_view_model.dart'; import 'package:cake_wallet/view_model/setup_pin_code_view_model.dart'; import 'package:cake_wallet/view_model/support_view_model.dart'; -import 'package:cake_wallet/view_model/transaction_details_view_model.dart'; import 'package:cake_wallet/view_model/trade_details_view_model.dart'; +import 'package:cake_wallet/view_model/transaction_details_view_model.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_details_view_model.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart'; -import 'package:cake_wallet/view_model/auth_view_model.dart'; -import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; -import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; -import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart'; -import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart'; -import 'package:cake_wallet/view_model/send/send_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart'; import 'package:cake_wallet/view_model/wallet_keys_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_edit_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_new_vm.dart'; +import 'package:cake_wallet/view_model/wallet_restore_choose_derivation_view_model.dart'; import 'package:cake_wallet/view_model/wallet_restore_view_model.dart'; import 'package:cake_wallet/view_model/wallet_seed_view_model.dart'; -import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/nano_account.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:get_it/get_it.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:cake_wallet/core/wallet_creation_service.dart'; -import 'package:cake_wallet/store/app_store.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/view_model/wallet_new_vm.dart'; -import 'package:cake_wallet/store/authentication_store.dart'; -import 'package:cake_wallet/store/dashboard/trades_store.dart'; -import 'package:cake_wallet/store/dashboard/trade_filter_store.dart'; -import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart'; -import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; -import 'package:cake_wallet/store/templates/send_template_store.dart'; -import 'package:cake_wallet/store/templates/exchange_template_store.dart'; -import 'package:cake_wallet/entities/template.dart'; -import 'package:cake_wallet/exchange/exchange_template.dart'; -import 'package:cake_wallet/.secrets.g.dart' as secrets; -import 'package:cake_wallet/src/screens/dashboard/pages/address_page.dart'; -import 'package:cake_wallet/anypay/anypay_api.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_gift_card_details_view_model.dart'; -import 'package:cake_wallet/src/screens/ionia/cards/ionia_payment_status_page.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_payment_status_view_model.dart'; -import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart'; -import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart'; -import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart'; -import 'package:cake_wallet/core/wallet_loading_service.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cake_wallet/entities/qr_view_data.dart'; +import 'package:cake_wallet/core/secure_storage.dart'; import 'buy/dfx/dfx_buy_provider.dart'; import 'core/totp_request_details.dart'; @@ -263,7 +266,8 @@ Future setup({ required Box ordersSource, required Box unspentCoinsInfoSource, required Box anonpayInvoiceInfoSource, - required FlutterSecureStorage secureStorage, + required SecureStorage secureStorage, + required GlobalKey navigatorKey, }) async { _walletInfoSource = walletInfoSource; _nodeSource = nodeSource; @@ -279,7 +283,7 @@ Future setup({ if (!_isSetupFinished) { getIt.registerSingletonAsync(() => SharedPreferences.getInstance()); - getIt.registerSingleton(secureStorage); + getIt.registerSingleton(secureStorage); } if (!_isSetupFinished) { getIt.registerFactory(() => BackgroundTasks()); @@ -307,6 +311,7 @@ Future setup({ getIt.registerFactory>(() => _powNodeSource, instanceName: Node.boxName + "pow"); getIt.registerSingleton(AuthenticationStore()); + getIt.registerSingleton(LedgerViewModel()); getIt.registerSingleton(WalletListStore()); getIt.registerSingleton(NodeListStoreBase.instance); getIt.registerSingleton(settingsStore); @@ -326,22 +331,22 @@ Future setup({ getIt.registerSingleton( ExchangeTemplateStore(templateSource: _exchangeTemplates)); getIt.registerSingleton( - YatStore(appStore: getIt.get(), secureStorage: getIt.get()) + YatStore(appStore: getIt.get(), secureStorage: getIt.get()) ..init()); getIt.registerSingleton( AnonpayTransactionsStore(anonpayInvoiceInfoSource: _anonpayInvoiceInfoSource)); - final secretStore = await SecretStoreBase.load(getIt.get()); + final secretStore = await SecretStoreBase.load(getIt.get()); getIt.registerSingleton(secretStore); - getIt.registerFactory(() => KeyService(getIt.get())); + getIt.registerFactory(() => KeyService(getIt.get())); getIt.registerFactoryParam((type, _) => WalletCreationService( initialType: type, keyService: getIt.get(), - secureStorage: getIt.get(), + secureStorage: getIt.get(), sharedPreferences: getIt.get(), settingsStore: getIt.get(), walletInfoSource: _walletInfoSource)); @@ -366,6 +371,11 @@ Future setup({ getIt.get(param1: type), _walletInfoSource, type); }); + getIt.registerFactoryParam((type, _) => + WalletHardwareRestoreViewModel(getIt.get(), getIt.get(), + getIt.get(param1: type), _walletInfoSource, + type: type)); + getIt.registerFactory(() => WalletAddressListViewModel( appStore: getIt.get(), yatStore: getIt.get(), @@ -391,7 +401,7 @@ Future setup({ getIt.registerFactory( () => AuthService( - secureStorage: getIt.get(), + secureStorage: getIt.get(), sharedPreferences: getIt.get(), settingsStore: getIt.get(), ), @@ -419,68 +429,89 @@ Future setup({ ), ); - getIt.registerFactory(() { - return AuthPage(getIt.get(), + getIt.registerLazySingleton(() { + return LinkViewModel( + appStore: getIt.get(), + settingsStore: getIt.get(), + authenticationStore: getIt.get(), + navigatorKey: navigatorKey, + ); + }); + + getIt.registerFactory(instanceName: 'login', () { + return AuthPage(getIt.get(), closable: false, onAuthenticationFinished: (isAuthenticated, AuthPageState authPageState) { if (!isAuthenticated) { return; - } else { - final authStore = getIt.get(); - final appStore = getIt.get(); - final useTotp = appStore.settingsStore.useTOTP2FA; - final shouldUseTotp2FAToAccessWallets = - appStore.settingsStore.shouldRequireTOTP2FAForAccessingWallet; - if (useTotp && shouldUseTotp2FAToAccessWallets) { - authPageState.close( - route: Routes.totpAuthCodePage, - arguments: TotpAuthArgumentsModel( - isForSetup: false, - isClosable: false, - onTotpAuthenticationFinished: (bool isAuthenticatedSuccessfully, - TotpAuthCodePageState totpAuthPageState) async { - if (!isAuthenticatedSuccessfully) { - return; - } - if (appStore.wallet != null) { - authStore.allowed(); - return; - } - - totpAuthPageState.changeProcessText('Loading the wallet'); - - if (loginError != null) { - totpAuthPageState.changeProcessText('ERROR: ${loginError.toString()}'); - } - - ReactionDisposer? _reaction; - _reaction = reaction((_) => appStore.wallet, (Object? _) { - _reaction?.reaction.dispose(); - authStore.allowed(); - }); - }, - ), - ); - } else { - if (appStore.wallet != null) { - authStore.allowed(); - return; - } - - authPageState.changeProcessText('Loading the wallet'); - - if (loginError != null) { - authPageState.changeProcessText('ERROR: ${loginError.toString()}'); - } - - ReactionDisposer? _reaction; - _reaction = reaction((_) => appStore.wallet, (Object? _) { - _reaction?.reaction.dispose(); - authStore.allowed(); - }); - } } - }, closable: false); - }, instanceName: 'login'); + final authStore = getIt.get(); + final appStore = getIt.get(); + final useTotp = appStore.settingsStore.useTOTP2FA; + final shouldUseTotp2FAToAccessWallets = + appStore.settingsStore.shouldRequireTOTP2FAForAccessingWallet; + if (useTotp && shouldUseTotp2FAToAccessWallets) { + authPageState.close( + route: Routes.totpAuthCodePage, + arguments: TotpAuthArgumentsModel( + isForSetup: false, + isClosable: false, + onTotpAuthenticationFinished: + (bool isAuthenticatedSuccessfully, TotpAuthCodePageState totpAuthPageState) async { + if (!isAuthenticatedSuccessfully) { + return; + } + if (appStore.wallet != null) { + authStore.allowed(); + return; + } + + totpAuthPageState.changeProcessText('Loading the wallet'); + + if (loginError != null) { + totpAuthPageState.changeProcessText('ERROR: ${loginError.toString()}'); + } + + ReactionDisposer? _reaction; + _reaction = reaction((_) => appStore.wallet, (Object? _) { + _reaction?.reaction.dispose(); + authStore.allowed(); + }); + }, + ), + ); + } else { + // wallet is already loaded: + if (appStore.wallet != null) { + // goes to the dashboard: + authStore.allowed(); + // trigger any deep links: + final linkViewModel = getIt.get(); + if (linkViewModel.currentLink != null) { + linkViewModel.handleLink(); + } + return; + } + + // load the wallet: + + authPageState.changeProcessText('Loading the wallet'); + + if (loginError != null) { + authPageState.changeProcessText('ERROR: ${loginError.toString()}'); + } + + ReactionDisposer? _reaction; + _reaction = reaction((_) => appStore.wallet, (Object? _) { + _reaction?.reaction.dispose(); + authStore.allowed(); + final linkViewModel = getIt.get(); + if (linkViewModel.currentLink != null) { + linkViewModel.handleLink(); + } + }); + } + }); + }); getIt.registerSingleton(BottomSheetServiceImpl()); @@ -489,12 +520,8 @@ Future setup({ getIt.registerLazySingleton(() => KeyServiceImpl()); getIt.registerLazySingleton(() { - final Web3WalletService web3WalletService = Web3WalletService( - getIt.get(), - getIt.get(), - appStore, - getIt.get() - ); + final Web3WalletService web3WalletService = Web3WalletService(getIt.get(), + getIt.get(), appStore, getIt.get()); web3WalletService.create(); return web3WalletService; }); @@ -598,6 +625,7 @@ Future setup({ getIt.get(), getIt.get(), _transactionDescriptionBox, + getIt.get(), ), ); @@ -803,11 +831,11 @@ Future setup({ editingNode: editingNode, isSelected: isSelected)); - getIt.registerFactory( - () => RobinhoodBuyProvider(wallet: getIt.get().wallet!)); + getIt.registerFactory(() => RobinhoodBuyProvider( + wallet: getIt.get().wallet!, ledgerVM: getIt.get())); - getIt - .registerFactory(() => DFXBuyProvider(wallet: getIt.get().wallet!)); + getIt.registerFactory(() => DFXBuyProvider( + wallet: getIt.get().wallet!, ledgerVM: getIt.get())); getIt.registerFactory(() => MoonPayProvider( settingsStore: getIt.get().settingsStore, @@ -842,8 +870,10 @@ Future setup({ tradesStore: getIt.get(), sendViewModel: getIt.get())); - getIt.registerFactory( - () => ExchangePage(getIt.get(), getIt.get())); + getIt.registerFactoryParam( + (PaymentRequest? paymentRequest, __) { + return ExchangePage(getIt.get(), getIt.get(), paymentRequest); + }); getIt.registerFactory(() => ExchangeConfirmPage(tradesStore: getIt.get())); @@ -873,6 +903,8 @@ Future setup({ return polygon!.createPolygonWalletService(_walletInfoSource); case WalletType.solana: return solana!.createSolanaWalletService(_walletInfoSource); + case WalletType.tron: + return tron!.createTronWalletService(_walletInfoSource); default: throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService'); } @@ -925,8 +957,17 @@ Future setup({ transactionDetailsViewModel: getIt.get(param1: transactionInfo))); - getIt.registerFactoryParam( - (param1, isCreate) => NewWalletTypePage(onTypeSelected: param1, isCreate: isCreate ?? true)); + getIt.registerFactoryParam?>((param1, additionalParams) { + final isCreate = additionalParams?[0] ?? true; + final isHardwareWallet = additionalParams?[1] ?? false; + + return NewWalletTypePage( + onTypeSelected: param1, + isCreate: isCreate, + isHardwareWallet: isHardwareWallet, + ); + }); getIt.registerFactoryParam( (seedPhraseLength, _) => PreSeedPage(seedPhraseLength)); @@ -937,16 +978,16 @@ Future setup({ trades: _tradesSource, settingsStore: getIt.get())); - getIt.registerFactory(() => BackupService(getIt.get(), _walletInfoSource, + getIt.registerFactory(() => BackupService(getIt.get(), _walletInfoSource, getIt.get(), getIt.get())); getIt.registerFactory(() => BackupViewModel( - getIt.get(), getIt.get(), getIt.get())); + getIt.get(), getIt.get(), getIt.get())); getIt.registerFactory(() => BackupPage(getIt.get())); getIt.registerFactory(() => - EditBackupPasswordViewModel(getIt.get(), getIt.get())); + EditBackupPasswordViewModel(getIt.get(), getIt.get())); getIt.registerFactory(() => EditBackupPasswordPage(getIt.get())); @@ -995,7 +1036,7 @@ Future setup({ getIt.registerFactory(() => SupportPage(getIt.get())); getIt.registerFactory(() => SupportChatPage(getIt.get(), - secureStorage: getIt.get())); + secureStorage: getIt.get())); getIt.registerFactory(() => SupportOtherLinksPage(getIt.get())); @@ -1037,7 +1078,7 @@ Future setup({ getIt.registerFactory(() => AnyPayApi()); getIt.registerFactory( - () => IoniaService(getIt.get(), getIt.get())); + () => IoniaService(getIt.get(), getIt.get())); getIt.registerFactory(() => IoniaAnyPay( getIt.get(), getIt.get(), getIt.get().wallet!)); @@ -1141,9 +1182,9 @@ Future setup({ getIt.registerFactory(() => IoniaAccountCardsPage(getIt.get())); getIt.registerFactoryParam( - (TransactionInfo transactionInfo, _) => RBFDetailsPage( + (TransactionInfo transactionInfo, _) => RBFDetailsPage( transactionDetailsViewModel: - getIt.get(param1: transactionInfo))); + getIt.get(param1: transactionInfo))); getIt.registerFactory(() => AnonPayApi( useTorOnly: getIt.get().exchangeStatus == ExchangeApiMode.torOnly, diff --git a/lib/entities/biometric_auth.dart b/lib/entities/biometric_auth.dart index febbfa469..353cd0492 100644 --- a/lib/entities/biometric_auth.dart +++ b/lib/entities/biometric_auth.dart @@ -1,32 +1,29 @@ -import 'package:local_auth/local_auth.dart'; import 'package:flutter/services.dart'; -import 'package:cake_wallet/generated/i18n.dart'; +import 'package:flutter_local_authentication/flutter_local_authentication.dart'; class BiometricAuth { - final _localAuth = LocalAuthentication(); + final _flutterLocalAuthenticationPlugin = FlutterLocalAuthentication(); Future isAuthenticated() async { try { - return await _localAuth.authenticate( - localizedReason: S.current.biometric_auth_reason, - options: AuthenticationOptions( - biometricOnly: true, - useErrorDialogs: true, - stickyAuth: false)); - } on PlatformException catch (e) { + final authenticated = await _flutterLocalAuthenticationPlugin.authenticate(); + return authenticated; + } catch (e) { print(e); } - return false; } Future canCheckBiometrics() async { + bool canAuthenticate; try { - return await _localAuth.canCheckBiometrics; - } on PlatformException catch (e) { - print(e); + canAuthenticate = await _flutterLocalAuthenticationPlugin.canAuthenticate(); + await _flutterLocalAuthenticationPlugin.setTouchIDAuthenticationAllowableReuseDuration(0); + } catch (error) { + print("Exception checking support. $error"); + canAuthenticate = false; } - return false; + return canAuthenticate; } -} +} \ No newline at end of file diff --git a/lib/entities/calculate_fiat_amount.dart b/lib/entities/calculate_fiat_amount.dart index 689ada31b..b0b38eacb 100644 --- a/lib/entities/calculate_fiat_amount.dart +++ b/lib/entities/calculate_fiat_amount.dart @@ -3,6 +3,8 @@ String calculateFiatAmount({double? price, String? cryptoAmount}) { return '0.00'; } + cryptoAmount = cryptoAmount.replaceAll(',', '.'); + final _amount = double.parse(cryptoAmount); final _result = price * _amount; final result = _result < 0 ? _result * -1 : _result; diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index a0f570e95..e67bd2fc6 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -1,10 +1,10 @@ import 'dart:io' show Directory, File, Platform; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cake_wallet/entities/secret_store_key.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hive/hive.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -36,12 +36,13 @@ const cakeWalletBitcoinCashDefaultNodeUri = 'bitcoincash.stackwallet.com:50002'; const nanoDefaultNodeUri = 'rpc.nano.to'; const nanoDefaultPowNodeUri = 'rpc.nano.to'; const solanaDefaultNodeUri = 'rpc.ankr.com'; +const tronDefaultNodeUri = 'tron-rpc.publicnode.com:443'; const newCakeWalletBitcoinUri = 'btc-electrum.cakewallet.com:50002'; Future defaultSettingsMigration( {required int version, required SharedPreferences sharedPreferences, - required FlutterSecureStorage secureStorage, + required SecureStorage secureStorage, required Box nodes, required Box powNodes, required Box walletInfoSource, @@ -207,19 +208,22 @@ Future defaultSettingsMigration( case 28: await _updateMoneroPriority(sharedPreferences); break; - case 29: await changeDefaultBitcoinNode(nodes, sharedPreferences); break; - case 30: await disableServiceStatusFiatDisabled(sharedPreferences); break; - case 31: await updateNanoNodeList(nodes: nodes); break; - + case 32: + await updateBtcNanoWalletInfos(walletInfoSource); + break; + case 33: + await addTronNodeList(nodes: nodes); + await changeTronCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); + break; default: break; } @@ -474,9 +478,14 @@ Node? getSolanaDefaultNode({required Box nodes}) { nodes.values.firstWhereOrNull((node) => node.type == WalletType.solana); } +Node? getTronDefaultNode({required Box nodes}) { + return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == tronDefaultNodeUri) ?? + nodes.values.firstWhereOrNull((node) => node.type == WalletType.tron); +} + Future insecureStorageMigration({ required SharedPreferences sharedPreferences, - required FlutterSecureStorage secureStorage, + required SecureStorage secureStorage, }) async { bool? allowBiometricalAuthentication = sharedPreferences.getBool(SecureKey.allowBiometricalAuthenticationKey); @@ -550,7 +559,7 @@ Future insecureStorageMigration({ } } -Future rewriteSecureStoragePin({required FlutterSecureStorage secureStorage}) async { +Future rewriteSecureStoragePin({required SecureStorage secureStorage}) async { // the bug only affects ios/mac: if (!Platform.isIOS && !Platform.isMacOS) { return; @@ -576,8 +585,9 @@ Future rewriteSecureStoragePin({required FlutterSecureStorage secureStorag await secureStorage.write( key: keyForPinCode, value: encodedPin, - iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock), - mOptions: MacOsOptions(accessibility: KeychainAccessibility.first_unlock), + // TODO: find a way to add those with the generated secure storage + // iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock), + // mOptions: MacOsOptions(accessibility: KeychainAccessibility.first_unlock), ); } @@ -711,7 +721,7 @@ Future updateDisplayModes(SharedPreferences sharedPreferences) async { await sharedPreferences.setInt(PreferencesKey.currentBalanceDisplayModeKey, balanceDisplayMode); } -Future generateBackupPassword(FlutterSecureStorage secureStorage) async { +Future generateBackupPassword(SecureStorage secureStorage) async { final key = generateStoreKeyFor(key: SecretStoreKey.backupPassword); if ((await secureStorage.read(key: key))?.isNotEmpty ?? false) { @@ -756,6 +766,20 @@ Future changeDefaultMoneroNode( } } +Future updateBtcNanoWalletInfos(Box walletsInfoSource) async { + for (WalletInfo walletInfo in walletsInfoSource.values) { + if (walletInfo.type == WalletType.nano || walletInfo.type == WalletType.bitcoin) { + walletInfo.derivationInfo = DerivationInfo( + derivationPath: walletInfo.derivationPath, + derivationType: walletInfo.derivationType, + address: walletInfo.address, + transactionsCount: walletInfo.restoreHeight, + ); + await walletInfo.save(); + } + } +} + Future changeDefaultBitcoinNode( Box nodeSource, SharedPreferences sharedPreferences) async { const cakeWalletBitcoinNodeUriPattern = '.cakewallet.com'; @@ -791,6 +815,7 @@ Future checkCurrentNodes( final currentBitcoinCashNodeId = sharedPreferences.getInt(PreferencesKey.currentBitcoinCashNodeIdKey); final currentSolanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); + final currentTronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); final currentMoneroNode = nodeSource.values.firstWhereOrNull((node) => node.key == currentMoneroNodeId); final currentBitcoinElectrumServer = @@ -811,6 +836,8 @@ Future checkCurrentNodes( nodeSource.values.firstWhereOrNull((node) => node.key == currentBitcoinCashNodeId); final currentSolanaNodeServer = nodeSource.values.firstWhereOrNull((node) => node.key == currentSolanaNodeId); + final currentTronNodeServer = + nodeSource.values.firstWhereOrNull((node) => node.key == currentTronNodeId); if (currentMoneroNode == null) { final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero); await nodeSource.add(newCakeWalletNode); @@ -876,6 +903,12 @@ Future checkCurrentNodes( await nodeSource.add(node); await sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, node.key as int); } + + if (currentTronNodeServer == null) { + final node = Node(uri: tronDefaultNodeUri, type: WalletType.tron); + await nodeSource.add(node); + await sharedPreferences.setInt(PreferencesKey.currentTronNodeIdKey, node.key as int); + } } Future resetBitcoinElectrumServer( @@ -1004,3 +1037,20 @@ Future changeSolanaCurrentNodeToDefault( await sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, nodeId); } + +Future addTronNodeList({required Box nodes}) async { + final nodeList = await loadDefaultTronNodes(); + for (var node in nodeList) { + if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { + await nodes.add(node); + } + } +} + +Future changeTronCurrentNodeToDefault( + {required SharedPreferences sharedPreferences, required Box nodes}) async { + final node = getTronDefaultNode(nodes: nodes); + final nodeId = node?.key as int? ?? 0; + + await sharedPreferences.setInt(PreferencesKey.currentTronNodeIdKey, nodeId); +} diff --git a/lib/entities/fs_migration.dart b/lib/entities/fs_migration.dart index 4280949cd..14237f080 100644 --- a/lib/entities/fs_migration.dart +++ b/lib/entities/fs_migration.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'dart:convert'; +import 'package:cake_wallet/core/secure_storage.dart'; import 'package:collection/collection.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:hive/hive.dart'; import 'package:path_provider/path_provider.dart'; @@ -10,8 +10,7 @@ import 'package:cake_wallet/entities/contact.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/entities/encrypt.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; -import 'package:cake_wallet/entities/ios_legacy_helper.dart' - as ios_legacy_helper; +import 'package:cake_wallet/entities/ios_legacy_helper.dart' as ios_legacy_helper; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/secret_store_key.dart'; import 'package:cw_core/wallet_info.dart'; @@ -29,8 +28,8 @@ Future migrate_android_v1() async { await android_migrate_wallets(appDocDir: appDocDir); } -Future ios_migrate_v1(Box walletInfoSource, - Box tradeSource, Box contactSource) async { +Future ios_migrate_v1( + Box walletInfoSource, Box tradeSource, Box contactSource) async { final prefs = await SharedPreferences.getInstance(); if (prefs.getBool('ios_migration_v1_completed') ?? false) { @@ -66,10 +65,7 @@ Future ios_migrate_user_defaults() async { if (activeCurrency != null) { final convertedCurrency = convertFiatLegacy(activeCurrency); - if (convertedCurrency != null) { - await prefs.setString( - 'current_fiat_currency', convertedCurrency.serialize()); - } + await prefs.setString('current_fiat_currency', convertedCurrency.serialize()); } //translate fee priority @@ -80,24 +76,21 @@ Future ios_migrate_user_defaults() async { } //translate current balance mode - final currentBalanceMode = - await ios_legacy_helper.getInt('display_balance_mode'); + final currentBalanceMode = await ios_legacy_helper.getInt('display_balance_mode'); if (currentBalanceMode != null) { await prefs.setInt('current_balance_display_mode', currentBalanceMode); } //translate should save recipient address - final shouldSave = - await ios_legacy_helper.getBool('should_save_recipient_address'); - + final shouldSave = await ios_legacy_helper.getBool('should_save_recipient_address'); + if (shouldSave != null) { await prefs.setBool('save_recipient_address', shouldSave); } //translate biometric - final biometricOn = - await ios_legacy_helper.getBool('biometric_authentication_on'); - + final biometricOn = await ios_legacy_helper.getBool('biometric_authentication_on'); + if (biometricOn != null) { await prefs.setBool('allow_biometrical_authentication', biometricOn); } @@ -136,9 +129,8 @@ Future ios_migrate_pin() async { return; } - final flutterSecureStorage = FlutterSecureStorage(); - final pinPassword = await flutterSecureStorage.read( - key: 'pin_password', iOptions: IOSOptions()); + final flutterSecureStorage = secureStorageShared; + final pinPassword = await flutterSecureStorage.readNoIOptions(key: 'pin_password'); // No pin if (pinPassword == null) { await prefs.setBool('ios_migration_pin_completed', true); @@ -147,8 +139,8 @@ Future ios_migrate_pin() async { final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); final encodedPassword = encodedPinCode(pin: pinPassword); - await flutterSecureStorage.delete(key: key); await flutterSecureStorage.write(key: key, value: encodedPassword); + await prefs.setBool('ios_migration_pin_completed', true); } @@ -160,7 +152,7 @@ Future ios_migrate_wallet_passwords() async { } final appDocDir = await getApplicationDocumentsDirectory(); - final flutterSecureStorage = FlutterSecureStorage(); + final flutterSecureStorage = secureStorageShared; final keyService = KeyService(flutterSecureStorage); final walletsDir = Directory('${appDocDir.path}/wallets'); final moneroWalletsDir = Directory('${walletsDir.path}/monero'); @@ -175,10 +167,8 @@ Future ios_migrate_wallet_passwords() async { if (item is Directory) { final name = item.path.split('/').last; final oldKey = 'wallet_monero_' + name + '_password'; - final password = await flutterSecureStorage.read( - key: oldKey, iOptions: IOSOptions()); - await keyService.saveWalletPassword( - walletName: name, password: password!); + final password = await flutterSecureStorage.readNoIOptions(key: oldKey); + await keyService.saveWalletPassword(walletName: name, password: password!); } } catch (e) { print(e.toString()); @@ -310,18 +300,14 @@ Future ios_migrate_wallet_info(Box walletsInfoSource) async { return null; } - final config = json.decode(configFile.readAsStringSync()) - as Map; - final isRecovery = config['isRecovery'] as bool ?? false; + final config = json.decode(configFile.readAsStringSync()) as Map; + final isRecovery = config['isRecovery'] as bool? ?? false; final dateAsDouble = config['date'] as double; final timestamp = dateAsDouble.toInt() * 1000; final date = DateTime.fromMillisecondsSinceEpoch(timestamp); - final id = walletTypeToString(WalletType.monero).toLowerCase() + - '_' + - name; - final exist = walletsInfoSource.values - .firstWhereOrNull((el) => el.id == id) != null; - + final id = walletTypeToString(WalletType.monero).toLowerCase() + '_' + name; + final exist = walletsInfoSource.values.firstWhereOrNull((el) => el.id == id) != null; + if (exist) { return null; } @@ -372,12 +358,10 @@ Future ios_migrate_trades_list(Box tradeSource) async { } final content = file.readAsBytesSync(); - final flutterSecureStorage = FlutterSecureStorage(); - final masterPassword = await flutterSecureStorage.read( - key: 'master_password', iOptions: IOSOptions()); + final flutterSecureStorage = secureStorageShared; + final masterPassword = await flutterSecureStorage.readNoIOptions(key: 'master_password'); final key = masterPassword!.replaceAll('-', ''); - final decoded = - await ios_legacy_helper.decrypt(content, key: key, salt: secrets.salt); + final decoded = await ios_legacy_helper.decrypt(content, key: key, salt: secrets.salt); final decodedJson = json.decode(decoded) as List; final trades = decodedJson.map((dynamic el) { final elAsMap = el as Map; @@ -440,8 +424,7 @@ Future ios_migrate_address_book(Box contactSource) async { final address = _item["address"] as String; final name = _item["name"] as String; - return Contact( - address: address, name: name, type: CryptoCurrency.fromString(type)); + return Contact(address: address, name: name, type: CryptoCurrency.fromString(type)); }); await contactSource.addAll(contacts); diff --git a/lib/entities/get_encryption_key.dart b/lib/entities/get_encryption_key.dart index a32d4e311..618066cb8 100644 --- a/lib/entities/get_encryption_key.dart +++ b/lib/entities/get_encryption_key.dart @@ -1,8 +1,8 @@ -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cw_core/cake_hive.dart'; Future> getEncryptionKey( - {required String forKey, required FlutterSecureStorage secureStorage}) async { + {required String forKey, required SecureStorage secureStorage}) async { final stringifiedKey = await secureStorage.read(key: 'transactionDescriptionsBoxKey'); List key; @@ -10,7 +10,6 @@ Future> getEncryptionKey( key = CakeHive.generateSecureKey(); final keyStringified = key.join(','); String storageKey = 'transactionDescriptionsBoxKey'; - await secureStorage.delete(key: storageKey); await secureStorage.write(key: storageKey, value: keyStringified); } else { key = stringifiedKey.split(',').map((i) => int.parse(i)).toList(); diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index 3c82a3f6c..c1211d2fe 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -166,6 +166,23 @@ Future> loadDefaultSolanaNodes() async { return nodes; } +Future> loadDefaultTronNodes() async { + final nodesRaw = await rootBundle.loadString('assets/tron_node_list.yml'); + final loadedNodes = loadYaml(nodesRaw) as YamlList; + final nodes = []; + + for (final raw in loadedNodes) { + if (raw is Map) { + final node = Node.fromMap(Map.from(raw)); + + node.type = WalletType.tron; + nodes.add(node); + } + } + + return nodes; +} + Future resetToDefault(Box nodeSource) async { final moneroNodes = await loadDefaultNodes(); final bitcoinElectrumServerList = await loadBitcoinElectrumServerList(); @@ -176,6 +193,7 @@ Future resetToDefault(Box nodeSource) async { final nanoNodes = await loadDefaultNanoNodes(); final polygonNodes = await loadDefaultPolygonNodes(); final solanaNodes = await loadDefaultSolanaNodes(); + final tronNodes = await loadDefaultTronNodes(); final nodes = moneroNodes + bitcoinElectrumServerList + @@ -185,7 +203,7 @@ Future resetToDefault(Box nodeSource) async { bitcoinCashElectrumServerList + nanoNodes + polygonNodes + - solanaNodes; + solanaNodes + tronNodes; await nodeSource.clear(); await nodeSource.addAll(nodes); diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index bab0ef51d..409724c6e 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/entities/openalias_record.dart'; import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/entities/unstoppable_domain_address.dart'; import 'package:cake_wallet/entities/emoji_string_extension.dart'; +import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; import 'package:cake_wallet/mastodon/mastodon_api.dart'; import 'package:cake_wallet/nostr/nostr_api.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -51,10 +52,12 @@ class AddressResolver { } final match = RegExp(addressPattern).firstMatch(raw); - return match?.group(0)?.replaceAllMapped(RegExp('[^0-9a-zA-Z]|bitcoincash:|nano_'), + return match?.group(0)?.replaceAllMapped(RegExp('[^0-9a-zA-Z]|bitcoincash:|nano_|ban_'), (Match match) { String group = match.group(0)!; - if (group.startsWith('bitcoincash:') || group.startsWith('nano_')) { + if (group.startsWith('bitcoincash:') || + group.startsWith('nano_') || + group.startsWith('ban_')) { return group; } return ''; @@ -69,8 +72,8 @@ class AddressResolver { return emailRegex.hasMatch(address); } - // TODO: refactor this to take Crypto currency instead of ticker, or at least pass in the tag as well - Future resolve(BuildContext context, String text, String ticker) async { + Future resolve(BuildContext context, String text, CryptoCurrency currency) async { + final ticker = currency.title; try { if (text.startsWith('@') && !text.substring(1).contains('@')) { if (settingsStore.lookupsTwitter) { @@ -114,8 +117,7 @@ class AddressResolver { await MastodonAPI.lookupUserByUserName(userName: userName, apiHost: hostName); if (mastodonUser != null) { - String? addressFromBio = extractAddressByType( - raw: mastodonUser.note, type: CryptoCurrency.fromString(ticker)); + String? addressFromBio = extractAddressByType(raw: mastodonUser.note, type: currency); if (addressFromBio != null) { return ParsedAddress.fetchMastodonAddress( @@ -129,8 +131,8 @@ class AddressResolver { if (pinnedPosts.isNotEmpty) { final userPinnedPostsText = pinnedPosts.map((item) => item.content).join('\n'); - String? addressFromPinnedPost = extractAddressByType( - raw: userPinnedPostsText, type: CryptoCurrency.fromString(ticker)); + String? addressFromPinnedPost = + extractAddressByType(raw: userPinnedPostsText, type: currency); if (addressFromPinnedPost != null) { return ParsedAddress.fetchMastodonAddress( @@ -160,6 +162,16 @@ class AddressResolver { } } } + + final thorChainAddress = await ThorChainExchangeProvider.lookupAddressByName(text); + if (thorChainAddress != null) { + String? address = + thorChainAddress[ticker] ?? (ticker == 'RUNE' ? thorChainAddress['THOR'] : null); + if (address != null) { + return ParsedAddress.thorChainAddress(address: address, name: text); + } + } + final formattedName = OpenaliasRecord.formatDomainName(text); final domainParts = formattedName.split('.'); final name = domainParts.last; @@ -202,7 +214,7 @@ class AddressResolver { if (nostrUserData != null) { String? addressFromBio = extractAddressByType( - raw: nostrUserData.about, type: CryptoCurrency.fromString(ticker)); + raw: nostrUserData.about, type: currency); if (addressFromBio != null) { return ParsedAddress.nostrAddress( address: addressFromBio, diff --git a/lib/entities/parsed_address.dart b/lib/entities/parsed_address.dart index fc8ab2440..cfd69acbe 100644 --- a/lib/entities/parsed_address.dart +++ b/lib/entities/parsed_address.dart @@ -11,7 +11,8 @@ enum ParseFrom { ens, contact, mastodon, - nostr + nostr, + thorChain } class ParsedAddress { @@ -133,6 +134,14 @@ class ParsedAddress { ); } + factory ParsedAddress.thorChainAddress({required String address, required String name}) { + return ParsedAddress( + addresses: [address], + name: name, + parseFrom: ParseFrom.thorChain, + ); + } + final List addresses; final String name; final String description; diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index f512d6b72..79177178c 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -14,6 +14,7 @@ class PreferencesKey { static const currentFiatCurrencyKey = 'current_fiat_currency'; static const currentBitcoinCashNodeIdKey = 'current_node_id_bch'; static const currentSolanaNodeIdKey = 'current_node_id_sol'; + static const currentTronNodeIdKey = 'current_node_id_trx'; static const currentTransactionPriorityKeyLegacy = 'current_fee_priority'; static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; static const shouldSaveRecipientAddressKey = 'save_recipient_address'; @@ -55,13 +56,14 @@ class PreferencesKey { static const pinNativeTokenAtTop = 'pin_native_token_at_top'; static const useEtherscan = 'use_etherscan'; static const usePolygonScan = 'use_polygonscan'; + static const useTronGrid = 'use_trongrid'; static const defaultNanoRep = 'default_nano_representative'; static const defaultBananoRep = 'default_banano_representative'; static const lookupsTwitter = 'looks_up_twitter'; static const lookupsMastodon = 'looks_up_mastodon'; - static const lookupsYatService = 'looks_up_mastodon'; - static const lookupsUnstoppableDomains = 'looks_up_mastodon'; - static const lookupsOpenAlias = 'looks_up_mastodon'; + static const lookupsYatService = 'looks_up_yat'; + static const lookupsUnstoppableDomains = 'looks_up_unstoppable_domain'; + static const lookupsOpenAlias = 'looks_up_open_alias'; static const lookupsENS = 'looks_up_ens'; static String moneroWalletUpdateV1Key(String name) => diff --git a/lib/entities/priority_for_wallet_type.dart b/lib/entities/priority_for_wallet_type.dart index 5fc0b5566..0151c8115 100644 --- a/lib/entities/priority_for_wallet_type.dart +++ b/lib/entities/priority_for_wallet_type.dart @@ -23,10 +23,11 @@ List priorityForWalletType(WalletType type) { return bitcoinCash!.getTransactionPriorities(); case WalletType.polygon: return polygon!.getTransactionPriorities(); - // no such thing for nano/banano/solana: + // no such thing for nano/banano/solana/tron: case WalletType.nano: case WalletType.banano: case WalletType.solana: + case WalletType.tron: return []; default: return []; diff --git a/lib/entities/provider_types.dart b/lib/entities/provider_types.dart index 701781cc2..37a492987 100644 --- a/lib/entities/provider_types.dart +++ b/lib/entities/provider_types.dart @@ -22,7 +22,7 @@ extension ProviderTypeName on ProviderType { case ProviderType.robinhood: return 'Robinhood Connect'; case ProviderType.dfx: - return 'DFX Connect'; + return 'DFX.swiss'; case ProviderType.onramper: return 'Onramper'; case ProviderType.moonpay: @@ -69,6 +69,13 @@ class ProvidersHelper { return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood, ProviderType.moonpay]; case WalletType.solana: return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood]; + case WalletType.tron: + return [ + ProviderType.askEachTime, + ProviderType.onramper, + ProviderType.robinhood, + ProviderType.moonpay, + ]; case WalletType.none: case WalletType.haven: return []; @@ -96,6 +103,12 @@ class ProvidersHelper { ProviderType.robinhood, ProviderType.moonpay, ]; + case WalletType.tron: + return [ + ProviderType.askEachTime, + ProviderType.robinhood, + ProviderType.moonpay, + ]; case WalletType.monero: case WalletType.nano: case WalletType.banano: diff --git a/lib/entities/secret_store_key.dart b/lib/entities/secret_store_key.dart index 2ee490c74..df6347cca 100644 --- a/lib/entities/secret_store_key.dart +++ b/lib/entities/secret_store_key.dart @@ -1,4 +1,4 @@ -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:cake_wallet/core/secure_storage.dart'; import 'package:shared_preferences/shared_preferences.dart'; enum SecretStoreKey { moneroWalletPassword, pinCodePassword, backupPassword } @@ -66,7 +66,7 @@ class SecureKey { static const lastAuthTimeMilliseconds = 'last_auth_time_milliseconds'; static Future getInt({ - required FlutterSecureStorage secureStorage, + required SecureStorage secureStorage, required SharedPreferences sharedPreferences, required String key, }) async { @@ -76,7 +76,7 @@ class SecureKey { } static Future getBool({ - required FlutterSecureStorage secureStorage, + required SecureStorage secureStorage, required SharedPreferences sharedPreferences, required String key, }) async { @@ -91,7 +91,7 @@ class SecureKey { } static Future getString({ - required FlutterSecureStorage secureStorage, + required SecureStorage secureStorage, required SharedPreferences sharedPreferences, required String key, }) async { diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart index 13fe3aafd..e72108e79 100644 --- a/lib/ethereum/cw_ethereum.dart +++ b/lib/ethereum/cw_ethereum.dart @@ -30,14 +30,23 @@ class CWEthereum extends Ethereum { }) => EVMChainRestoreWalletFromPrivateKey(name: name, password: password, privateKey: privateKey); + @override + WalletCredentials createEthereumHardwareWalletCredentials({ + required String name, + required HardwareAccountData hwAccountData, + WalletInfo? walletInfo, + }) => + EVMChainRestoreWalletFromHardware( + name: name, hwAccountData: hwAccountData, walletInfo: walletInfo); + @override String getAddress(WalletBase wallet) => (wallet as EthereumWallet).walletAddresses.address; @override String getPrivateKey(WalletBase wallet) { final privateKeyHolder = (wallet as EthereumWallet).evmChainPrivateKey; - String stringKey = bytesToHex(privateKeyHolder.privateKey); - return stringKey; + if (privateKeyHolder is EthPrivateKey) return bytesToHex(privateKeyHolder.privateKey); + return ""; } @override @@ -142,8 +151,10 @@ class CWEthereum extends Ethereum { } wallet as EthereumWallet; - return wallet.erc20Currencies - .firstWhere((element) => transaction.tokenSymbol == element.symbol); + + return wallet.erc20Currencies.firstWhere( + (element) => transaction.tokenSymbol == element.symbol, + ); } @override @@ -157,4 +168,24 @@ class CWEthereum extends Ethereum { } String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress; + + @override + void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device) { + ((wallet as EVMChainWallet).evmChainPrivateKey as EvmLedgerCredentials).setLedger( + ledger, + device.connectionType == ConnectionType.usb ? device : null, + wallet.walletInfo.derivationInfo?.derivationPath); + } + + @override + Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, + {int index = 0, int limit = 5}) async { + final hardwareWalletService = EVMChainHardwareWalletService(ledgerVM.ledger, ledgerVM.device); + try { + return await hardwareWalletService.getAvailableAccounts(index: index, limit: limit); + } on LedgerException catch (err) { + print(err.message); + throw err; + } + } } diff --git a/lib/exchange/exchange_provider_description.dart b/lib/exchange/exchange_provider_description.dart index 4d9691035..c28de5b72 100644 --- a/lib/exchange/exchange_provider_description.dart +++ b/lib/exchange/exchange_provider_description.dart @@ -22,10 +22,11 @@ class ExchangeProviderDescription extends EnumerableItem with Serializable< ExchangeProviderDescription(title: 'Trocador', raw: 5, image: 'assets/images/trocador.png'); static const exolix = ExchangeProviderDescription(title: 'Exolix', raw: 6, image: 'assets/images/exolix.png'); - static const thorChain = - ExchangeProviderDescription(title: 'ThorChain' , raw: 8, image: 'assets/images/thorchain.png'); - static const all = ExchangeProviderDescription(title: 'All trades', raw: 7, image: ''); + static const thorChain = + ExchangeProviderDescription(title: 'ThorChain', raw: 8, image: 'assets/images/thorchain.png'); + static const quantex = + ExchangeProviderDescription(title: 'Quantex', raw: 9, image: 'assets/images/quantex.png'); static ExchangeProviderDescription deserialize({required int raw}) { switch (raw) { @@ -43,10 +44,12 @@ class ExchangeProviderDescription extends EnumerableItem with Serializable< return trocador; case 6: return exolix; - case 8: - return thorChain; case 7: return all; + case 8: + return thorChain; + case 9: + return quantex; default: throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize'); } diff --git a/lib/exchange/provider/quantex_exchange_provider.dart b/lib/exchange/provider/quantex_exchange_provider.dart new file mode 100644 index 000000000..9ab7fbb55 --- /dev/null +++ b/lib/exchange/provider/quantex_exchange_provider.dart @@ -0,0 +1,252 @@ +import 'dart:convert'; + +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/exchange/limits.dart'; +import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; +import 'package:cake_wallet/exchange/trade.dart'; +import 'package:cake_wallet/exchange/trade_not_created_exception.dart'; +import 'package:cake_wallet/exchange/trade_not_found_exception.dart'; +import 'package:cake_wallet/exchange/trade_request.dart'; +import 'package:cake_wallet/exchange/trade_state.dart'; +import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:http/http.dart'; + +class QuantexExchangeProvider extends ExchangeProvider { + QuantexExchangeProvider() : super(pairList: supportedPairs(_notSupported)); + + static final List _notSupported = [ + ...(CryptoCurrency.all + .where((element) => ![ + CryptoCurrency.btc, + CryptoCurrency.sol, + CryptoCurrency.eth, + CryptoCurrency.ltc, + CryptoCurrency.ada, + CryptoCurrency.bch, + CryptoCurrency.usdt, + CryptoCurrency.bnb, + CryptoCurrency.xmr, + ].contains(element)) + .toList()) + ]; + + static final markup = secrets.quantexExchangeMarkup; + + static const apiAuthority = 'api.myquantex.com'; + static const getRate = '/api/swap/get-rate'; + static const getCoins = '/api/swap/get-coins'; + static const createOrder = '/api/swap/create-order'; + + @override + String get title => 'Quantex'; + + @override + bool get isAvailable => true; + + @override + bool get isEnabled => true; + + @override + bool get supportsFixedRate => false; + + @override + ExchangeProviderDescription get description => ExchangeProviderDescription.quantex; + + @override + Future checkIsAvailable() async => true; + + @override + Future fetchLimits({ + required CryptoCurrency from, + required CryptoCurrency to, + required bool isFixedRateMode, + }) async { + try { + final uri = Uri.https(apiAuthority, getCoins); + final response = await get(uri); + + final responseJSON = json.decode(response.body) as Map; + + if (response.statusCode != 200) + throw Exception('Unexpected http status: ${response.statusCode}'); + + final coinsInfo = responseJSON['data'] as List; + + for (var coin in coinsInfo) { + if (coin['id'].toString().toUpperCase() == _normalizeCurrency(from)) { + return Limits( + min: double.parse(coin['min'].toString()), + max: double.parse(coin['max'].toString()), + ); + } + } + + // coin not found: + return Limits(min: 0, max: 0); + } catch (e) { + print(e.toString()); + return Limits(min: 0, max: 0); + } + } + + @override + Future fetchRate({ + required CryptoCurrency from, + required CryptoCurrency to, + required double amount, + required bool isFixedRateMode, + required bool isReceiveAmount, + }) async { + try { + if (amount == 0) return 0.0; + + final headers = {}; + final params = {}; + final body = { + 'coin_send': _normalizeCurrency(from), + 'coin_receive': _normalizeCurrency(to), + 'ref': 'cake', + }; + + final uri = Uri.https(apiAuthority, getRate, params); + final response = await post(uri, body: body, headers: headers); + final responseBody = json.decode(response.body) as Map; + + if (response.statusCode != 200) + throw Exception('Unexpected http status: ${response.statusCode}'); + + final data = responseBody['data'] as Map; + double rate = double.parse(data['price'].toString()); + return rate; + } catch (e) { + print("error fetching rate: ${e.toString()}"); + return 0.0; + } + } + + @override + Future createTrade({ + required TradeRequest request, + required bool isFixedRateMode, + required bool isSendAll, + }) async { + try { + final headers = {}; + final params = {}; + var body = { + 'coin_send': _normalizeCurrency(request.fromCurrency), + 'coin_receive': _normalizeCurrency(request.toCurrency), + 'amount_send': request.fromAmount, + 'recipient': request.toAddress, + 'ref': 'cake', + 'markup': markup, + }; + + String? fromNetwork = _networkFor(request.fromCurrency); + String? toNetwork = _networkFor(request.toCurrency); + if (fromNetwork != null) body['coin_send_network'] = fromNetwork; + if (toNetwork != null) body['coin_receive_network'] = toNetwork; + + final uri = Uri.https(apiAuthority, createOrder, params); + final response = await post(uri, body: body, headers: headers); + final responseBody = json.decode(response.body) as Map; + + if (response.statusCode == 400 || responseBody["success"] == false) { + final error = responseBody['errors'][0]['msg'] as String; + throw TradeNotCreatedException(description, description: error); + } + + if (response.statusCode != 200) + throw Exception('Unexpected http status: ${response.statusCode}'); + + final responseData = responseBody['data'] as Map; + + return Trade( + id: responseData["order_id"] as String, + inputAddress: responseData["server_address"] as String, + amount: request.fromAmount, + from: request.fromCurrency, + to: request.toCurrency, + provider: description, + createdAt: DateTime.now(), + state: TradeState.created, + payoutAddress: request.toAddress, + isSendAll: isSendAll, + ); + } catch (e) { + print("error creating trade: ${e.toString()}"); + throw TradeNotCreatedException(description, description: e.toString()); + } + } + + @override + Future findTradeById({required String id}) async { + try { + final headers = {}; + final params = {}; + var body = { + 'order_id': id, + }; + + final uri = Uri.https(apiAuthority, createOrder, params); + final response = await post(uri, body: body, headers: headers); + final responseBody = json.decode(response.body) as Map; + + if (response.statusCode == 400 || responseBody["success"] == false) { + final error = responseBody['errors'][0]['msg'] as String; + throw TradeNotCreatedException(description, description: error); + } + + if (response.statusCode != 200) + throw Exception('Unexpected http status: ${response.statusCode}'); + + final responseData = responseBody['data'] as Map; + final fromCurrency = responseData['coin_send'] as String; + final from = CryptoCurrency.fromString(fromCurrency); + final toCurrency = responseData['coin_receive'] as String; + final to = CryptoCurrency.fromString(toCurrency); + final inputAddress = responseData['server_address'] as String; + final status = responseData['status'] as String; + final state = TradeState.deserialize(raw: status); + final response_id = responseData['order_id'] as String; + final expectedSendAmount = responseData['amount_send'] as String; + + return Trade( + id: response_id, + from: from, + to: to, + provider: description, + inputAddress: inputAddress, + amount: expectedSendAmount, + state: state, + ); + } catch (e) { + print("error getting trade: ${e.toString()}"); + throw TradeNotFoundException( + id, + provider: description, + description: e.toString(), + ); + } + } + + String _normalizeCurrency(CryptoCurrency currency) { + switch (currency) { + default: + return currency.title.toUpperCase(); + } + } + + String? _networkFor(CryptoCurrency currency) { + switch (currency) { + case CryptoCurrency.usdt: + return "USDT_ERC20"; + case CryptoCurrency.bnb: + return "BNB_BSC"; + default: + return null; + } + } +} diff --git a/lib/exchange/provider/thorchain_exchange.provider.dart b/lib/exchange/provider/thorchain_exchange.provider.dart index 32dce7db8..22937e603 100644 --- a/lib/exchange/provider/thorchain_exchange.provider.dart +++ b/lib/exchange/provider/thorchain_exchange.provider.dart @@ -19,26 +19,28 @@ class ThorChainExchangeProvider extends ExchangeProvider { ...(CryptoCurrency.all .where((element) => ![ CryptoCurrency.btc, - CryptoCurrency.eth, + // CryptoCurrency.eth, CryptoCurrency.ltc, CryptoCurrency.bch, - CryptoCurrency.aave, - CryptoCurrency.dai, - CryptoCurrency.gusd, - CryptoCurrency.usdc, - CryptoCurrency.usdterc20, - CryptoCurrency.wbtc, + // CryptoCurrency.aave, + // CryptoCurrency.dai, + // CryptoCurrency.gusd, + // CryptoCurrency.usdc, + // CryptoCurrency.usdterc20, + // CryptoCurrency.wbtc, // TODO: temporarily commented until https://github.com/cake-tech/cake_wallet/pull/1436 is merged ].contains(element)) .toList()) ]; static final isRefundAddressSupported = [CryptoCurrency.eth]; - static const _baseURL = 'thornode.ninerealms.com'; + static const _baseNodeURL = 'thornode.ninerealms.com'; + static const _baseURL = 'midgard.ninerealms.com'; static const _quotePath = '/thorchain/quote/swap'; static const _txInfoPath = '/thorchain/tx/status/'; static const _affiliateName = 'cakewallet'; static const _affiliateBps = '175'; + static const _nameLookUpPath= 'v2/thorname/lookup/'; final Box tradesStore; @@ -154,7 +156,7 @@ class ThorChainExchangeProvider extends ExchangeProvider { Future findTradeById({required String id}) async { if (id.isEmpty) throw Exception('Trade id is empty'); final formattedId = id.startsWith('0x') ? id.substring(2) : id; - final uri = Uri.https(_baseURL, '$_txInfoPath$formattedId'); + final uri = Uri.https(_baseNodeURL, '$_txInfoPath$formattedId'); final response = await http.get(uri); if (response.statusCode == 404) { @@ -206,8 +208,35 @@ class ThorChainExchangeProvider extends ExchangeProvider { ); } + static Future?>? lookupAddressByName(String name) async { + final uri = Uri.https(_baseURL, '$_nameLookUpPath$name'); + final response = await http.get(uri); + + if (response.statusCode != 200) { + return null; + } + + final body = json.decode(response.body) as Map; + final entries = body['entries'] as List?; + + if (entries == null || entries.isEmpty) { + return null; + } + + Map chainToAddressMap = {}; + + for (final entry in entries) { + final chain = entry['chain'] as String; + final address = entry['address'] as String; + chainToAddressMap[chain] = address; + } + + return chainToAddressMap; + } + + Future> _getSwapQuote(Map params) async { - Uri uri = Uri.https(_baseURL, _quotePath, params); + Uri uri = Uri.https(_baseNodeURL, _quotePath, params); final response = await http.get(uri); diff --git a/lib/exchange/trade.dart b/lib/exchange/trade.dart index 6cc3fddbe..aeb544ece 100644 --- a/lib/exchange/trade.dart +++ b/lib/exchange/trade.dart @@ -5,9 +5,6 @@ import 'package:cw_core/format_amount.dart'; import 'package:cw_core/hive_type_ids.dart'; import 'package:hive/hive.dart'; -part 'trade.g.dart'; - -@HiveType(typeId: Trade.typeId) class Trade extends HiveObject { Trade({ required this.id, @@ -32,6 +29,7 @@ class Trade extends HiveObject { this.txId, this.isRefund, this.isSendAll, + this.router, }) { if (provider != null) providerRaw = provider.raw; @@ -121,21 +119,26 @@ class Trade extends HiveObject { @HiveField(21) bool? isSendAll; + @HiveField(22) + String? router; + static Trade fromMap(Map map) { return Trade( - id: map['id'] as String, - provider: ExchangeProviderDescription.deserialize(raw: map['provider'] as int), - from: CryptoCurrency.deserialize(raw: map['input'] as int), - to: CryptoCurrency.deserialize(raw: map['output'] as int), - createdAt: - map['date'] != null ? DateTime.fromMillisecondsSinceEpoch(map['date'] as int) : null, - amount: map['amount'] as String, - walletId: map['wallet_id'] as String, - fromWalletAddress: map['from_wallet_address'] as String?, - memo: map['memo'] as String?, - txId: map['tx_id'] as String?, - isRefund: map['isRefund'] as bool?, - isSendAll: map['isSendAll'] as bool?); + id: map['id'] as String, + provider: ExchangeProviderDescription.deserialize(raw: map['provider'] as int), + from: CryptoCurrency.deserialize(raw: map['input'] as int), + to: CryptoCurrency.deserialize(raw: map['output'] as int), + createdAt: + map['date'] != null ? DateTime.fromMillisecondsSinceEpoch(map['date'] as int) : null, + amount: map['amount'] as String, + walletId: map['wallet_id'] as String, + fromWalletAddress: map['from_wallet_address'] as String?, + memo: map['memo'] as String?, + txId: map['tx_id'] as String?, + isRefund: map['isRefund'] as bool?, + isSendAll: map['isSendAll'] as bool?, + router: map['router'] as String?, + ); } Map toMap() { @@ -152,8 +155,111 @@ class Trade extends HiveObject { 'tx_id': txId, 'isRefund': isRefund, 'isSendAll': isSendAll, + 'router': router, }; } String amountFormatted() => formatAmount(amount); } + +class TradeAdapter extends TypeAdapter { + @override + final int typeId = Trade.typeId; + + @override + Trade read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = {}; + for (int i = 0; i < numOfFields; i++) { + try { + fields[reader.readByte()] = reader.read(); + } catch (_) {} + } + + return Trade( + id: fields[0] == null ? '' : fields[0] as String, + amount: fields[7] == null ? '' : fields[7] as String, + createdAt: fields[5] as DateTime?, + expiredAt: fields[6] as DateTime?, + inputAddress: fields[8] as String?, + extraId: fields[9] as String?, + outputTransaction: fields[10] as String?, + refundAddress: fields[11] as String?, + walletId: fields[12] as String?, + payoutAddress: fields[13] as String?, + password: fields[14] as String?, + providerId: fields[15] as String?, + providerName: fields[16] as String?, + fromWalletAddress: fields[17] as String?, + memo: fields[18] as String?, + txId: fields[19] as String?, + isRefund: fields[20] as bool?, + isSendAll: fields[21] as bool?, + router: fields[22] as String?, + ) + ..providerRaw = fields[1] == null ? 0 : fields[1] as int + ..fromRaw = fields[2] == null ? 0 : fields[2] as int + ..toRaw = fields[3] == null ? 0 : fields[3] as int + ..stateRaw = fields[4] == null ? '' : fields[4] as String; + } + + @override + void write(BinaryWriter writer, Trade obj) { + writer + ..writeByte(23) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.providerRaw) + ..writeByte(2) + ..write(obj.fromRaw) + ..writeByte(3) + ..write(obj.toRaw) + ..writeByte(4) + ..write(obj.stateRaw) + ..writeByte(5) + ..write(obj.createdAt) + ..writeByte(6) + ..write(obj.expiredAt) + ..writeByte(7) + ..write(obj.amount) + ..writeByte(8) + ..write(obj.inputAddress) + ..writeByte(9) + ..write(obj.extraId) + ..writeByte(10) + ..write(obj.outputTransaction) + ..writeByte(11) + ..write(obj.refundAddress) + ..writeByte(12) + ..write(obj.walletId) + ..writeByte(13) + ..write(obj.payoutAddress) + ..writeByte(14) + ..write(obj.password) + ..writeByte(15) + ..write(obj.providerId) + ..writeByte(16) + ..write(obj.providerName) + ..writeByte(17) + ..write(obj.fromWalletAddress) + ..writeByte(18) + ..write(obj.memo) + ..writeByte(19) + ..write(obj.txId) + ..writeByte(20) + ..write(obj.isRefund) + ..writeByte(21) + ..write(obj.isSendAll) + ..writeByte(22) + ..write(obj.router); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is TradeAdapter && runtimeType == other.runtimeType && typeId == other.typeId; +} diff --git a/lib/exchange/trade_state.dart b/lib/exchange/trade_state.dart index 2c58a96f4..0a196835e 100644 --- a/lib/exchange/trade_state.dart +++ b/lib/exchange/trade_state.dart @@ -28,6 +28,7 @@ class TradeState extends EnumerableItem with Serializable { TradeState(raw: 'waitingAuthorization', title: 'Waiting authorization'); static const failed = TradeState(raw: 'failed', title: 'Failed'); static const completed = TradeState(raw: 'completed', title: 'Completed'); + static const expired = TradeState(raw: 'expired', title: 'Expired'); static const settling = TradeState(raw: 'settling', title: 'Settlement in progress'); static const settled = TradeState(raw: 'settled', title: 'Settlement completed'); static const wait = TradeState(raw: 'wait', title: 'Waiting'); @@ -39,7 +40,33 @@ class TradeState extends EnumerableItem with Serializable { static const exchanging = TradeState(raw: 'exchanging', title: 'Exchanging'); static const sending = TradeState(raw: 'sending', title: 'Sending'); static const success = TradeState(raw: 'success', title: 'Success'); + static TradeState deserialize({required String raw}) { + + switch (raw) { + case '1': + return unpaid; + case '2': + return paidUnconfirmed; + case '3': + return sending; + case '4': + return confirmed; + case '5': + case '6': + return exchanging; + case '7': + return sending; + case '8': + return complete; + case '9': + return expired; + case '10': + return underpaid; + case '11': + return failed; + } + switch (raw) { case 'NOT_FOUND': return notFound; diff --git a/lib/ionia/ionia_service.dart b/lib/ionia/ionia_service.dart index 0396ed7c1..821824a87 100644 --- a/lib/ionia/ionia_service.dart +++ b/lib/ionia/ionia_service.dart @@ -1,7 +1,7 @@ +import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/ionia/ionia_merchant.dart'; import 'package:cake_wallet/ionia/ionia_order.dart'; import 'package:cake_wallet/ionia/ionia_virtual_card.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/ionia/ionia_api.dart'; import 'package:cake_wallet/ionia/ionia_gift_card.dart'; @@ -16,7 +16,7 @@ class IoniaService { static String get clientId => secrets.ioniaClientId; - final FlutterSecureStorage secureStorage; + final SecureStorage secureStorage; final IoniaApi ioniaApi; // Create user diff --git a/lib/locales/hausa_intl.dart b/lib/locales/hausa_intl.dart index 749d39a4d..6cf757b60 100644 --- a/lib/locales/hausa_intl.dart +++ b/lib/locales/hausa_intl.dart @@ -751,6 +751,50 @@ class HaMaterialLocalizations extends GlobalMaterialLocalizations { @override String get scrimOnTapHintRaw => "Scrip on Tap"; + + @override + // TODO: implement collapsedHint + String get collapsedHint => "collapsedHint"; + + @override + // TODO: implement expandedHint + String get expandedHint => "expandedHint"; + + @override + // TODO: implement expansionTileCollapsedHint + String get expansionTileCollapsedHint => "expansionTileCollapsedHint"; + + @override + // TODO: implement expansionTileCollapsedTapHint + String get expansionTileCollapsedTapHint => "expansionTileCollapsedTapHint"; + + @override + // TODO: implement expansionTileExpandedHint + String get expansionTileExpandedHint => "expansionTileExpandedHint"; + + @override + // TODO: implement expansionTileExpandedTapHint + String get expansionTileExpandedTapHint => "expansionTileExpandedTapHint"; + + @override + // TODO: implement scanTextButtonLabel + String get scanTextButtonLabel => "scanTextButtonLabel"; + + @override + // TODO: implement lookUpButtonLabel + String get lookUpButtonLabel => "lookUpButtonLabel"; + + @override + // TODO: implement menuDismissLabel + String get menuDismissLabel => "menuDismissLabel"; + + @override + // TODO: implement searchWebButtonLabel + String get searchWebButtonLabel => "searchWebButtonLabel"; + + @override + // TODO: implement shareButtonLabel + String get shareButtonLabel => "shareButtonLabel"; } /// Cupertino Support @@ -955,4 +999,24 @@ class HaCupertinoLocalizations extends GlobalCupertinoLocalizations { @override String get noSpellCheckReplacementsLabel => ""; + + @override + // TODO: implement clearButtonLabel + String get clearButtonLabel => "clearButtonLabel"; + + @override + // TODO: implement lookUpButtonLabel + String get lookUpButtonLabel => "lookUpButtonLabel"; + + @override + // TODO: implement menuDismissLabel + String get menuDismissLabel => "menuDismissLabel"; + + @override + // TODO: implement searchWebButtonLabel + String get searchWebButtonLabel => "searchWebButtonLabel"; + + @override + // TODO: implement shareButtonLabel + String get shareButtonLabel => "shareButtonLabel"; } diff --git a/lib/locales/yoruba_intl.dart b/lib/locales/yoruba_intl.dart index 889c21cb7..3c720b80e 100644 --- a/lib/locales/yoruba_intl.dart +++ b/lib/locales/yoruba_intl.dart @@ -1,4 +1,3 @@ - import 'dart:async'; import 'package:flutter/cupertino.dart'; @@ -164,62 +163,62 @@ const yoDateSymbols = { 'ọjọ́ Àbámẹ́ta', ], 'STANDALONEWEEKDAYS': [ -'Ọjọ́ Ajé', -'Ọjọ́ Ìsẹ́gun', -'Ọjọ́ Ìsẹ́gun-Ẹtì', -'Ọjọ́ Ìsẹ́gun-Ọ̀rú', -'Ọjọ́ Àìkú', -'Ọjọ́ Jímọ̀', -'Ọjọ́ Àbámẹ́ta', -], -'SHORTWEEKDAYS': [ -'Ajé', -'Ìsẹ́gun', -'Ìsẹ́gun-Ẹtì', -'Ìsẹ́gun-Ọ̀rú', -'Àìkú', -'Jímọ̀', -'Àbámẹ́ta', -], -'STANDALONESHORTWEEKDAYS': [ -'Ajé', -'Ìsẹ́gun', -'Ìsẹ́gun-Ẹtì', -'Ìsẹ́gun-Ọ̀rú', -'Àìkú', -'Jímọ̀', -'Àbámẹ́ta', -], -'NARROWWEEKDAYS': [ -'A', -'A', -'Ì', -'A', -'À', -'J', -'À', -], -'STANDALONENARROWWEEKDAYS': [ -'A', -'A', -'Ì', -'A', -'À', -'J', -'À', -], -'SHORTQUARTERS': [ -'K1', -'K2', -'K3', -'K4', -], -'QUARTERS': [ -'1. kwata', -'2. kwata', -'3. kwata', -'4. kwata', -], + 'Ọjọ́ Ajé', + 'Ọjọ́ Ìsẹ́gun', + 'Ọjọ́ Ìsẹ́gun-Ẹtì', + 'Ọjọ́ Ìsẹ́gun-Ọ̀rú', + 'Ọjọ́ Àìkú', + 'Ọjọ́ Jímọ̀', + 'Ọjọ́ Àbámẹ́ta', + ], + 'SHORTWEEKDAYS': [ + 'Ajé', + 'Ìsẹ́gun', + 'Ìsẹ́gun-Ẹtì', + 'Ìsẹ́gun-Ọ̀rú', + 'Àìkú', + 'Jímọ̀', + 'Àbámẹ́ta', + ], + 'STANDALONESHORTWEEKDAYS': [ + 'Ajé', + 'Ìsẹ́gun', + 'Ìsẹ́gun-Ẹtì', + 'Ìsẹ́gun-Ọ̀rú', + 'Àìkú', + 'Jímọ̀', + 'Àbámẹ́ta', + ], + 'NARROWWEEKDAYS': [ + 'A', + 'A', + 'Ì', + 'A', + 'À', + 'J', + 'À', + ], + 'STANDALONENARROWWEEKDAYS': [ + 'A', + 'A', + 'Ì', + 'A', + 'À', + 'J', + 'À', + ], + 'SHORTQUARTERS': [ + 'K1', + 'K2', + 'K3', + 'K4', + ], + 'QUARTERS': [ + '1. kwata', + '2. kwata', + '3. kwata', + '4. kwata', + ], 'AMPMS': [ 'a.m.', 'p.m.', @@ -316,339 +315,339 @@ class YoMaterialLocalizations extends GlobalMaterialLocalizations { }); // #docregion Getters -@override -String get moreButtonTooltip => r'Kò sí ìròhùn tí ó múni'; + @override + String get moreButtonTooltip => r'Kò sí ìròhùn tí ó múni'; -@override -String get aboutListTileTitleRaw => r'Fun Àpótí àwọn $applicationname'; + @override + String get aboutListTileTitleRaw => r'Fun Àpótí àwọn $applicationname'; -@override -String get alertDialogLabel => r'Ìròhùn Àlàyé'; + @override + String get alertDialogLabel => r'Ìròhùn Àlàyé'; // #enddocregion Getters -@override -String get anteMeridiemAbbreviation => r'AM'; + @override + String get anteMeridiemAbbreviation => r'AM'; -@override -String get backButtonTooltip => r'Fíran'; + @override + String get backButtonTooltip => r'Fíran'; -@override -String get cancelButtonLabel => r'FAGILE'; + @override + String get cancelButtonLabel => r'FAGILE'; -@override -String get closeButtonLabel => r'KÚ'; + @override + String get closeButtonLabel => r'KÚ'; -@override -String get closeButtonTooltip => r'Kú'; + @override + String get closeButtonTooltip => r'Kú'; -@override -String get collapsedIconTapHint => r'Tá'; + @override + String get collapsedIconTapHint => r'Tá'; -@override -String get continueButtonLabel => r'TÓ WÁ'; + @override + String get continueButtonLabel => r'TÓ WÁ'; -@override -String get copyButtonLabel => r'DÚPLÍKÉTÍ'; + @override + String get copyButtonLabel => r'DÚPLÍKÉTÍ'; -@override -String get cutButtonLabel => r'TÒ'; + @override + String get cutButtonLabel => r'TÒ'; -@override -String get deleteButtonTooltip => r'Máa kú'; + @override + String get deleteButtonTooltip => r'Máa kú'; -@override -String get dialogLabel => r'Ìròhùn'; + @override + String get dialogLabel => r'Ìròhùn'; -@override -String get drawerLabel => r'Àgbèjọ àwọn àpọ̀tí'; + @override + String get drawerLabel => r'Àgbèjọ àwọn àpọ̀tí'; -@override -String get expandedIconTapHint => r'Tá'; + @override + String get expandedIconTapHint => r'Tá'; -@override -String get firstPageTooltip => r'Ojú ewe'; + @override + String get firstPageTooltip => r'Ojú ewe'; -@override -String get hideAccountsLabel => r'Fí èrò àpótí wáyé sílẹ̀'; + @override + String get hideAccountsLabel => r'Fí èrò àpótí wáyé sílẹ̀'; -@override -String get lastPageTooltip => r'Ojú ayé'; + @override + String get lastPageTooltip => r'Ojú ayé'; -@override -String get licensesPageTitle => r'Ìròhùn Ọdún'; + @override + String get licensesPageTitle => r'Ìròhùn Ọdún'; -@override -String get modalBarrierDismissLabel => r'Sọ'; + @override + String get modalBarrierDismissLabel => r'Sọ'; -@override -String get nextMonthTooltip => r'Oṣù kọja'; + @override + String get nextMonthTooltip => r'Oṣù kọja'; -@override -String get nextPageTooltip => r'Ojú ọjọ́ kẹta'; + @override + String get nextPageTooltip => r'Ojú ọjọ́ kẹta'; -@override -String get okButtonLabel => r'Ò daájú'; -@override + @override + String get okButtonLabel => r'Ò daájú'; + @override // A custom drawer tooltip message. -String get openAppDrawerTooltip => r'Aya ntọju Iwe Awọn Aka'; + String get openAppDrawerTooltip => r'Aya ntọju Iwe Awọn Aka'; // #docregion Raw -@override -String get pageRowsInfoTitleRaw => r'$firstRow–$lastRow lati $rowCount'; + @override + String get pageRowsInfoTitleRaw => r'$firstRow–$lastRow lati $rowCount'; -@override -String get pageRowsInfoTitleApproximateRaw => r'$firstRow–$lastRow lati kiakia $rowCount'; + @override + String get pageRowsInfoTitleApproximateRaw => r'$firstRow–$lastRow lati kiakia $rowCount'; // #enddocregion Raw -@override -String get pasteButtonLabel => r'TÌ'; - -@override -String get popupMenuLabel => r'Meniu Pop-up'; - -@override -String get menuBarMenuLabel => r'Meniu Akọkọ'; - -@override -String get postMeridiemAbbreviation => r'PM'; - -@override -String get previousMonthTooltip => r'Oṣu Kanakana'; - -@override -String get previousPageTooltip => r'Ojú ewé akọkọ kan'; - -@override -String get refreshIndicatorSemanticLabel => r'Gbiyanju'; - -@override -String? get remainingTextFieldCharacterCountFew => null; - -@override -String? get remainingTextFieldCharacterCountMany => null; - -@override -String get remainingTextFieldCharacterCountOne => r'1 àmì báálẹ̀'; - -@override -String get remainingTextFieldCharacterCountOther => r'$remainingCount àmì báálẹ̀'; - -@override -String? get remainingTextFieldCharacterCountTwo => null; - -@override -String get remainingTextFieldCharacterCountZero => r'Kò sí ìwọlé létà láti ń ṣe'; - -@override -String get reorderItemDown => r'Jù sí ilẹ'; - -@override -String get reorderItemLeft => r'Jù sí àrà'; - -@override -String get reorderItemRight => r'Jù sí òtútù'; - -@override -String get reorderItemToEnd => r'Jù sí ìbẹ̀jì'; - -@override -String get reorderItemToStart => r'Jù sí àkọ́kọ́'; - -@override -String get reorderItemUp => r'Jù sí ọ̀rùn'; - -@override -String get rowsPerPageTitle => r'Ìlò Fún àwọn Ìtọ́kasíwájú:'; - -@override -ScriptCategory get scriptCategory => ScriptCategory.englishLike; - -@override -String get searchFieldLabel => 'Ṣẹda'; - -@override -String get selectAllButtonLabel => 'FADỌHỌN DỌFÚN GBÁJÚMỌ̀'; - -@override -String? get selectedRowCountTitleFew => null; - -@override -String? get selectedRowCountTitleMany => null; - -@override -String get selectedRowCountTitleOne => '1 káyé'; - -@override -String get selectedRowCountTitleOther => r'$selectedRowCount káyé'; - -@override -String? get selectedRowCountTitleTwo => null; - -@override -String get selectedRowCountTitleZero => 'Kò sí káyé ti o wọlé'; - -@override -String get showAccountsLabel => 'Fi iyipada mu kọ'; - -@override -String get showMenuTooltip => 'Fi Meniu mu kọ'; - -@override -String get signedInLabel => 'Ọ̀nà'; - -@override -String get tabLabelRaw => r'Àwọn tabin $tabIndex lati $tabCount'; - @override -TimeOfDayFormat get timeOfDayFormatRaw => TimeOfDayFormat.h_colon_mm_space_a; - -@override -String get timePickerHourModeAnnouncement => 'Tuntun waqtu lọ'; - -@override -String get timePickerMinuteModeAnnouncement => 'Tuntun daɗi minti'; - -@override -String get viewLicensesButtonLabel => 'WO NIKI'; - -@override -List get narrowWeekdays => const ['L', 'L', 'A', 'O', 'Ọ', 'Ẹ', 'Ẹ']; - -@override -int get firstDayOfWeekIndex => 0; - -static const LocalizationsDelegate delegate = -_YoMaterialLocalizationsDelegate(); - -@override -String get calendarModeButtonLabel => 'Tọ́rọ̀ kálẹ̀ndà'; - -@override -String get dateHelpText => 'mm/dd/yyyy'; - -@override -String get dateInputLabel => 'Firanṣẹ̀ Ọjọ́'; - -@override -String get dateOutOfRangeLabel => 'Nínú iwọ̀ lọ́wọ́'; - -@override -String get datePickerHelpText => 'WÁSÍ'; - -@override -String get dateRangeEndDateSemanticLabelRaw => r'Ọjọ́ tuntun to ṣà'; - -@override -String get dateRangeEndLabel => 'Ọjọ́ tuntun to ṣà'; - -@override -String get dateRangePickerHelpText => 'WÁSÍ ÌGBÀ'; - -@override -String get dateRangeStartDateSemanticLabelRaw => 'Ọjọ́ tuntun ti dá'; - -@override -String get dateRangeStartLabel => 'Ọjọ́ tuntun ti dá'; - -@override -String get dateSeparator => '/'; - -@override -String get dialModeButtonLabel => 'Tọ́rọ̀ wakati'; - -@override -String get inputDateModeButtonLabel => 'Tọ́rọ̀ firanṣẹ̀ ọjọ́'; - -@override -String get inputTimeModeButtonLabel => 'Tọ́rọ̀ wakati bayi lọ́wọ́'; - -@override -String get invalidDateFormatLabel => 'Akọ́kọ́tọ́ tó jẹ́kúnrin'; - -@override -String get invalidDateRangeLabel => 'Àmì jẹ́ káàkiri lẹ́yìn ilé'; - -@override -String get invalidTimeLabel => 'Akọ́kọ́tọ́ àkójọ ìwádìí'; - -@override -String get licensesPackageDetailTextOther => r'$licenseCount àwọn níkí'; - -@override -String get saveButtonLabel => 'TÙN DÁRA'; - -@override -String get selectYearSemanticsLabel => 'Fọ́ọ̀ shẹ́kàrà'; - -@override -String get timePickerDialHelpText => 'WÁSÍ WÁKÀTÌ'; - -@override -String get timePickerHourLabel => 'Wákàtì àṣà'; - -@override -String get timePickerInputHelpText => 'Shìgárà wákàtì'; - -@override -String get timePickerMinuteLabel => 'Mìntì'; - -@override -String get unspecifiedDate => 'Ọjọ̀kúnrin'; - -@override -String get unspecifiedDateRange => 'Ọjọ̀kúnrin àdáyọ̀'; - -@override -String get keyboardKeyAlt => 'Alt'; - -@override -String get keyboardKeyAltGraph => 'AltGraph'; - -@override -String get keyboardKeyBackspace => 'Báckspàcè'; - -@override -String get keyboardKeyCapsLock => 'Caps Lock'; - -@override -String get keyboardKeyChannelDown => 'Báyàkàmmàlàsàké'; - -@override -String get keyboardKeyChannelUp => 'Yíkàmmàlàsàké'; - -@override -String get keyboardKeyControl => 'Kọ́ntírọ̀l'; - -@override -String get keyboardKeyDelete => 'Shápè'; - -@override -String get keyboardKeyEject => 'Èjẹ̀tì'; - -@override -String get keyboardKeyEnd => 'Tàbí'; - -@override -String get keyboardKeyEscape => 'Tòkè'; + String get pasteButtonLabel => r'TÌ'; @override -String get keyboardKeyFn => 'Fn'; + String get popupMenuLabel => r'Meniu Pop-up'; -@override -String get keyboardKeyHome => 'Ile'; + @override + String get menuBarMenuLabel => r'Meniu Akọkọ'; -@override -String get keyboardKeyInsert => 'Fi sori'; + @override + String get postMeridiemAbbreviation => r'PM'; -@override -String get keyboardKeyMeta => 'Meta'; + @override + String get previousMonthTooltip => r'Oṣu Kanakana'; -@override -String get keyboardKeyMetaMacOs => 'Amfani pẹlu Command'; + @override + String get previousPageTooltip => r'Ojú ewé akọkọ kan'; -@override -String get keyboardKeyMetaWindows => 'Windows'; + @override + String get refreshIndicatorSemanticLabel => r'Gbiyanju'; + + @override + String? get remainingTextFieldCharacterCountFew => null; + + @override + String? get remainingTextFieldCharacterCountMany => null; + + @override + String get remainingTextFieldCharacterCountOne => r'1 àmì báálẹ̀'; + + @override + String get remainingTextFieldCharacterCountOther => r'$remainingCount àmì báálẹ̀'; + + @override + String? get remainingTextFieldCharacterCountTwo => null; + + @override + String get remainingTextFieldCharacterCountZero => r'Kò sí ìwọlé létà láti ń ṣe'; + + @override + String get reorderItemDown => r'Jù sí ilẹ'; + + @override + String get reorderItemLeft => r'Jù sí àrà'; + + @override + String get reorderItemRight => r'Jù sí òtútù'; + + @override + String get reorderItemToEnd => r'Jù sí ìbẹ̀jì'; + + @override + String get reorderItemToStart => r'Jù sí àkọ́kọ́'; + + @override + String get reorderItemUp => r'Jù sí ọ̀rùn'; + + @override + String get rowsPerPageTitle => r'Ìlò Fún àwọn Ìtọ́kasíwájú:'; + + @override + ScriptCategory get scriptCategory => ScriptCategory.englishLike; + + @override + String get searchFieldLabel => 'Ṣẹda'; + + @override + String get selectAllButtonLabel => 'FADỌHỌN DỌFÚN GBÁJÚMỌ̀'; + + @override + String? get selectedRowCountTitleFew => null; + + @override + String? get selectedRowCountTitleMany => null; + + @override + String get selectedRowCountTitleOne => '1 káyé'; + + @override + String get selectedRowCountTitleOther => r'$selectedRowCount káyé'; + + @override + String? get selectedRowCountTitleTwo => null; + + @override + String get selectedRowCountTitleZero => 'Kò sí káyé ti o wọlé'; + + @override + String get showAccountsLabel => 'Fi iyipada mu kọ'; + + @override + String get showMenuTooltip => 'Fi Meniu mu kọ'; + + @override + String get signedInLabel => 'Ọ̀nà'; + + @override + String get tabLabelRaw => r'Àwọn tabin $tabIndex lati $tabCount'; + + @override + TimeOfDayFormat get timeOfDayFormatRaw => TimeOfDayFormat.h_colon_mm_space_a; + + @override + String get timePickerHourModeAnnouncement => 'Tuntun waqtu lọ'; + + @override + String get timePickerMinuteModeAnnouncement => 'Tuntun daɗi minti'; + + @override + String get viewLicensesButtonLabel => 'WO NIKI'; + + @override + List get narrowWeekdays => const ['L', 'L', 'A', 'O', 'Ọ', 'Ẹ', 'Ẹ']; + + @override + int get firstDayOfWeekIndex => 0; + + static const LocalizationsDelegate delegate = + _YoMaterialLocalizationsDelegate(); + + @override + String get calendarModeButtonLabel => 'Tọ́rọ̀ kálẹ̀ndà'; + + @override + String get dateHelpText => 'mm/dd/yyyy'; + + @override + String get dateInputLabel => 'Firanṣẹ̀ Ọjọ́'; + + @override + String get dateOutOfRangeLabel => 'Nínú iwọ̀ lọ́wọ́'; + + @override + String get datePickerHelpText => 'WÁSÍ'; + + @override + String get dateRangeEndDateSemanticLabelRaw => r'Ọjọ́ tuntun to ṣà'; + + @override + String get dateRangeEndLabel => 'Ọjọ́ tuntun to ṣà'; + + @override + String get dateRangePickerHelpText => 'WÁSÍ ÌGBÀ'; + + @override + String get dateRangeStartDateSemanticLabelRaw => 'Ọjọ́ tuntun ti dá'; + + @override + String get dateRangeStartLabel => 'Ọjọ́ tuntun ti dá'; + + @override + String get dateSeparator => '/'; + + @override + String get dialModeButtonLabel => 'Tọ́rọ̀ wakati'; + + @override + String get inputDateModeButtonLabel => 'Tọ́rọ̀ firanṣẹ̀ ọjọ́'; + + @override + String get inputTimeModeButtonLabel => 'Tọ́rọ̀ wakati bayi lọ́wọ́'; + + @override + String get invalidDateFormatLabel => 'Akọ́kọ́tọ́ tó jẹ́kúnrin'; + + @override + String get invalidDateRangeLabel => 'Àmì jẹ́ káàkiri lẹ́yìn ilé'; + + @override + String get invalidTimeLabel => 'Akọ́kọ́tọ́ àkójọ ìwádìí'; + + @override + String get licensesPackageDetailTextOther => r'$licenseCount àwọn níkí'; + + @override + String get saveButtonLabel => 'TÙN DÁRA'; + + @override + String get selectYearSemanticsLabel => 'Fọ́ọ̀ shẹ́kàrà'; + + @override + String get timePickerDialHelpText => 'WÁSÍ WÁKÀTÌ'; + + @override + String get timePickerHourLabel => 'Wákàtì àṣà'; + + @override + String get timePickerInputHelpText => 'Shìgárà wákàtì'; + + @override + String get timePickerMinuteLabel => 'Mìntì'; + + @override + String get unspecifiedDate => 'Ọjọ̀kúnrin'; + + @override + String get unspecifiedDateRange => 'Ọjọ̀kúnrin àdáyọ̀'; + + @override + String get keyboardKeyAlt => 'Alt'; + + @override + String get keyboardKeyAltGraph => 'AltGraph'; + + @override + String get keyboardKeyBackspace => 'Báckspàcè'; + + @override + String get keyboardKeyCapsLock => 'Caps Lock'; + + @override + String get keyboardKeyChannelDown => 'Báyàkàmmàlàsàké'; + + @override + String get keyboardKeyChannelUp => 'Yíkàmmàlàsàké'; + + @override + String get keyboardKeyControl => 'Kọ́ntírọ̀l'; + + @override + String get keyboardKeyDelete => 'Shápè'; + + @override + String get keyboardKeyEject => 'Èjẹ̀tì'; + + @override + String get keyboardKeyEnd => 'Tàbí'; + + @override + String get keyboardKeyEscape => 'Tòkè'; + + @override + String get keyboardKeyFn => 'Fn'; + + @override + String get keyboardKeyHome => 'Ile'; + + @override + String get keyboardKeyInsert => 'Fi sori'; + + @override + String get keyboardKeyMeta => 'Meta'; + + @override + String get keyboardKeyMetaMacOs => 'Amfani pẹlu Command'; + + @override + String get keyboardKeyMetaWindows => 'Windows'; @override String get keyboardKeyNumLock => 'Num Lock'; @@ -751,6 +750,50 @@ String get keyboardKeyMetaWindows => 'Windows'; @override String get scrimOnTapHintRaw => "Scrip on Tap"; + + @override + // TODO: implement collapsedHint + String get collapsedHint => "collapsedHint"; + + @override + // TODO: implement expandedHint + String get expandedHint => "expandedHint"; + + @override + // TODO: implement expansionTileCollapsedHint + String get expansionTileCollapsedHint => "expansionTileCollapsedHint"; + + @override + // TODO: implement expansionTileCollapsedTapHint + String get expansionTileCollapsedTapHint => "expansionTileCollapsedTapHint"; + + @override + // TODO: implement expansionTileExpandedHint + String get expansionTileExpandedHint => "expansionTileExpandedHint"; + + @override + // TODO: implement expansionTileExpandedTapHint + String get expansionTileExpandedTapHint => "expansionTileExpandedTapHint"; + + @override + // TODO: implement scanTextButtonLabel + String get scanTextButtonLabel => "scanTextButtonLabel"; + + @override + // TODO: implement lookUpButtonLabel + String get lookUpButtonLabel => "lookUpButtonLabel"; + + @override + // TODO: implement menuDismissLabel + String get menuDismissLabel => "menuDismissLabel"; + + @override + // TODO: implement searchWebButtonLabel + String get searchWebButtonLabel => "searchWebButtonLabel"; + + @override + // TODO: implement shareButtonLabel + String get shareButtonLabel => "shareButtonLabel"; } /// Cupertino Support @@ -821,138 +864,158 @@ class YoCupertinoLocalizations extends GlobalCupertinoLocalizations { required super.singleDigitSecondFormat, }); -@override -String get alertDialogLabel => 'Àdàkárò'; + @override + String get alertDialogLabel => 'Àdàkárò'; -@override -String get anteMeridiemAbbreviation => 'AM'; + @override + String get anteMeridiemAbbreviation => 'AM'; -@override -String get copyButtonLabel => 'Kòpy'; + @override + String get copyButtonLabel => 'Kòpy'; -@override -String get cutButtonLabel => 'Kọ́t'; + @override + String get cutButtonLabel => 'Kọ́t'; -@override -String get datePickerDateOrderString => 'mdy'; + @override + String get datePickerDateOrderString => 'mdy'; -@override -String get datePickerDateTimeOrderString => 'date_time_dayPeriod'; + @override + String get datePickerDateTimeOrderString => 'date_time_dayPeriod'; -@override -String? get datePickerHourSemanticsLabelFew => null; + @override + String? get datePickerHourSemanticsLabelFew => null; -@override -String? get datePickerHourSemanticsLabelMany => null; + @override + String? get datePickerHourSemanticsLabelMany => null; -@override -String? get datePickerHourSemanticsLabelOne => r"$hour o'clock"; + @override + String? get datePickerHourSemanticsLabelOne => r"$hour o'clock"; -@override -String get datePickerHourSemanticsLabelOther => r"$hour o'clock"; + @override + String get datePickerHourSemanticsLabelOther => r"$hour o'clock"; -@override -String? get datePickerHourSemanticsLabelTwo => null; + @override + String? get datePickerHourSemanticsLabelTwo => null; -@override -String? get datePickerHourSemanticsLabelZero => null; + @override + String? get datePickerHourSemanticsLabelZero => null; -@override -String? get datePickerMinuteSemanticsLabelFew => null; + @override + String? get datePickerMinuteSemanticsLabelFew => null; -@override -String? get datePickerMinuteSemanticsLabelMany => null; + @override + String? get datePickerMinuteSemanticsLabelMany => null; -@override -String? get datePickerMinuteSemanticsLabelOne => '1 wakati'; + @override + String? get datePickerMinuteSemanticsLabelOne => '1 wakati'; -@override -String get datePickerMinuteSemanticsLabelOther => r'$minute wakati'; + @override + String get datePickerMinuteSemanticsLabelOther => r'$minute wakati'; -@override -String? get datePickerMinuteSemanticsLabelTwo => null; + @override + String? get datePickerMinuteSemanticsLabelTwo => null; -@override -String? get datePickerMinuteSemanticsLabelZero => null; + @override + String? get datePickerMinuteSemanticsLabelZero => null; -@override -String get modalBarrierDismissLabel => 'Búta'; + @override + String get modalBarrierDismissLabel => 'Búta'; -@override -String get pasteButtonLabel => 'Tẹ́ẹ́'; + @override + String get pasteButtonLabel => 'Tẹ́ẹ́'; -@override -String get postMeridiemAbbreviation => 'PM'; + @override + String get postMeridiemAbbreviation => 'PM'; -@override -String get searchTextFieldPlaceholderLabel => 'Wúró àtúntà'; + @override + String get searchTextFieldPlaceholderLabel => 'Wúró àtúntà'; -@override -String get selectAllButtonLabel => 'Fírànsé gbógbo'; + @override + String get selectAllButtonLabel => 'Fírànsé gbógbo'; -@override -String get tabSemanticsLabelRaw => r'Tab $tabIndex nínú $tabCount'; + @override + String get tabSemanticsLabelRaw => r'Tab $tabIndex nínú $tabCount'; -@override -String? get timerPickerHourLabelFew => null; + @override + String? get timerPickerHourLabelFew => null; -@override -String? get timerPickerHourLabelMany => null; + @override + String? get timerPickerHourLabelMany => null; -@override -String? get timerPickerHourLabelOne => 'òǹdì'; + @override + String? get timerPickerHourLabelOne => 'òǹdì'; -@override -String get timerPickerHourLabelOther => 'òǹdì'; + @override + String get timerPickerHourLabelOther => 'òǹdì'; -@override -String? get timerPickerHourLabelTwo => null; + @override + String? get timerPickerHourLabelTwo => null; -@override -String? get timerPickerHourLabelZero => null; + @override + String? get timerPickerHourLabelZero => null; -@override -String? get timerPickerMinuteLabelFew => null; + @override + String? get timerPickerMinuteLabelFew => null; -@override -String? get timerPickerMinuteLabelMany => null; + @override + String? get timerPickerMinuteLabelMany => null; -@override -String? get timerPickerMinuteLabelOne => 'wakati.'; + @override + String? get timerPickerMinuteLabelOne => 'wakati.'; -@override -String get timerPickerMinuteLabelOther => 'wakati.'; + @override + String get timerPickerMinuteLabelOther => 'wakati.'; -@override -String? get timerPickerMinuteLabelTwo => null; + @override + String? get timerPickerMinuteLabelTwo => null; -@override -String? get timerPickerMinuteLabelZero => null; + @override + String? get timerPickerMinuteLabelZero => null; -@override -String? get timerPickerSecondLabelFew => null; + @override + String? get timerPickerSecondLabelFew => null; -@override -String? get timerPickerSecondLabelMany => null; + @override + String? get timerPickerSecondLabelMany => null; -@override -String? get timerPickerSecondLabelOne => 'dákìkà.'; + @override + String? get timerPickerSecondLabelOne => 'dákìkà.'; -@override -String get timerPickerSecondLabelOther => 'dákìkà.'; + @override + String get timerPickerSecondLabelOther => 'dákìkà.'; -@override -String? get timerPickerSecondLabelTwo => null; + @override + String? get timerPickerSecondLabelTwo => null; -@override -String? get timerPickerSecondLabelZero => null; + @override + String? get timerPickerSecondLabelZero => null; -@override -String get todayLabel => 'Oyọ'; + @override + String get todayLabel => 'Oyọ'; static const LocalizationsDelegate delegate = _YoCupertinoLocalizationsDelegate(); @override String get noSpellCheckReplacementsLabel => ""; + + @override + // TODO: implement clearButtonLabel + String get clearButtonLabel => "clearButtonLabel"; + + @override + // TODO: implement lookUpButtonLabel + String get lookUpButtonLabel => "lookUpButtonLabel"; + + @override + // TODO: implement menuDismissLabel + String get menuDismissLabel => "menuDismissLabel"; + + @override + // TODO: implement searchWebButtonLabel + String get searchWebButtonLabel => "searchWebButtonLabel"; + + @override + // TODO: implement shareButtonLabel + String get shareButtonLabel => "shareButtonLabel"; } diff --git a/lib/main.dart b/lib/main.dart index b80c9eb85..eeee4fbc3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,13 @@ import 'dart:async'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/core/auth_service.dart'; +import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/entities/language_service.dart'; import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/locales/locale.dart'; -import 'package:cake_wallet/store/yat/yat_store.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; +import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cw_core/address_info.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cw_core/hive_type_ids.dart'; @@ -17,7 +18,6 @@ import 'package:hive/hive.dart'; import 'package:cake_wallet/di.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/router.dart' as Router; @@ -37,16 +37,17 @@ import 'package:cake_wallet/entities/template.dart'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/exchange/exchange_template.dart'; import 'package:cake_wallet/src/screens/root/root.dart'; -import 'package:uni_links/uni_links.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cw_core/cake_hive.dart'; +import 'package:cw_core/window_size.dart'; final navigatorKey = GlobalKey(); final rootKey = GlobalKey(); -final RouteObserver routeObserver = RouteObserver(); +final RouteObserver> routeObserver = RouteObserver>(); Future main() async { + bool isAppRunning = false; await runZonedGuarded(() async { WidgetsFlutterBinding.ensureInitialized(); @@ -60,12 +61,43 @@ Future main() async { return true; }; + await setDefaultMinimumWindowSize(); + await CakeHive.close(); await initializeAppConfigs(); runApp(App()); + + isAppRunning = true; }, (error, stackTrace) async { + if (!isAppRunning) { + runApp( + MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold( + body: SingleChildScrollView( + child: Container( + margin: EdgeInsets.only(top: 50, left: 20, right: 20, bottom: 20), + child: Column( + children: [ + Text( + 'Error:\n${error.toString()}', + style: TextStyle(fontSize: 22), + ), + Text( + 'Stack trace:\n${stackTrace.toString()}', + style: TextStyle(fontSize: 16), + ), + ], + ), + ), + ), + ), + ), + ); + } + ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stackTrace)); }); } @@ -102,6 +134,14 @@ Future initializeAppConfigs() async { CakeHive.registerAdapter(DerivationTypeAdapter()); } + if (!CakeHive.isAdapterRegistered(DERIVATION_INFO_TYPE_ID)) { + CakeHive.registerAdapter(DerivationInfoAdapter()); + } + + if (!CakeHive.isAdapterRegistered(HARDWARE_WALLET_TYPE_TYPE_ID)) { + CakeHive.registerAdapter(HardwareWalletTypeAdapter()); + } + if (!CakeHive.isAdapterRegistered(WALLET_TYPE_TYPE_ID)) { CakeHive.registerAdapter(WalletTypeAdapter()); } @@ -126,9 +166,8 @@ Future initializeAppConfigs() async { CakeHive.registerAdapter(AnonpayInvoiceInfoAdapter()); } - final secureStorage = FlutterSecureStorage( - iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock), - ); + final secureStorage = secureStorageShared; + final transactionDescriptionsBoxKey = await getEncryptionKey(secureStorage: secureStorage, forKey: TransactionDescription.boxKey); final tradesBoxKey = await getEncryptionKey(secureStorage: secureStorage, forKey: Trade.boxKey); @@ -163,7 +202,7 @@ Future initializeAppConfigs() async { transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, - initialMigrationVersion: 31, + initialMigrationVersion: 33, ); } @@ -179,7 +218,7 @@ Future initialSetup( required Box