diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index dc231df42..46924cb35 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -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,7 @@ 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 tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart - name: Rename app run: | diff --git a/.gitignore b/.gitignore index 6f2d0a182..f1e5b6da3 100644 --- a/.gitignore +++ b/.gitignore @@ -94,9 +94,11 @@ android/app/key.jks **/tool/.evm-secrets-config.json **/tool/.ethereum-secrets-config.json **/tool/.solana-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 +134,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 diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index eea9b5521..23207d629 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_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 33401d666..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; @@ -20,11 +25,12 @@ 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, @@ -33,24 +39,26 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Map? initialChangeAddressIndex, String? passphrase, }) : super( - mnemonic: mnemonic, - passphrase: passphrase, - 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"; + // 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, @@ -58,7 +66,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, mainHd: hd, - sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath(sideDerivationPath), + sideHd: accountHD.derive(1), network: networkParam ?? network, ); autorun((_) { @@ -129,23 +137,26 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { walletInfo.derivationInfo!.derivationPath = snp.derivationPath ?? "m/0'/0"; walletInfo.derivationInfo!.derivationType = snp.derivationType ?? DerivationType.electrum; - late Uint8List seedBytes; + Uint8List? seedBytes = 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; + 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, @@ -159,4 +170,49 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { 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 981c7a466..915d7cc10 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart +++ b/cw_bitcoin/lib/bitcoin_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'; @@ -36,9 +37,22 @@ class BitcoinRestoreWalletFromSeedCredentials extends WalletCredentials { } 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; } + +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 e0548771b..cf99324da 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -14,8 +14,11 @@ 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; @@ -99,9 +102,28 @@ 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 diff --git a/cw_bitcoin/lib/electrum_derivations.dart b/cw_bitcoin/lib/electrum_derivations.dart index 631805c54..19d444a41 100644 --- a/cw_bitcoin/lib/electrum_derivations.dart +++ b/cw_bitcoin/lib/electrum_derivations.dart @@ -4,7 +4,7 @@ Map> electrum_derivations = { DerivationType.electrum: [ DerivationInfo( derivationType: DerivationType.electrum, - derivationPath: "m/0'/0", + derivationPath: "m/0'", description: "Electrum", scriptType: "p2wpkh", ), diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 8342e4816..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,17 +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(walletInfo.derivationInfo?.derivationPath ?? "m/0'/0"), + : accountHD = + getAccountHDWallet(currency, networkType, seedBytes, xpub, walletInfo.derivationInfo), syncStatus = NotConnectedSyncStatus(), _password = password, _feeRates = [], @@ -80,20 +79,44 @@ 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 @@ -123,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; @@ -203,7 +226,9 @@ abstract class ElectrumWalletBase int credentialsAmount = 0, }) async { final utxos = []; - List privateKeys = []; + final privateKeys = []; + final publicKeys = {}; + int allInputsAmount = 0; bool spendsUnconfirmedTX = false; @@ -217,12 +242,22 @@ abstract class ElectrumWalletBase 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( @@ -233,7 +268,7 @@ abstract class ElectrumWalletBase scriptType: _getScriptType(address), ), ownerDetails: UtxoAddressDetails( - publicKey: privkey.getPublic().toHex(), + publicKey: pubKeyHex, address: address, ), ), @@ -294,6 +329,7 @@ abstract class ElectrumWalletBase return EstimatedTxResult( utxos: utxos, privateKeys: privateKeys, + publicKeys: publicKeys, fee: fee, amount: amount, isSendAll: true, @@ -312,7 +348,9 @@ abstract class ElectrumWalletBase bool? useUnconfirmed, }) async { final utxos = []; - List privateKeys = []; + final privateKeys = []; + final publicKeys = {}; + int allInputsAmount = 0; bool spendsUnconfirmedTX = false; @@ -332,12 +370,23 @@ abstract class ElectrumWalletBase 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( @@ -348,7 +397,7 @@ abstract class ElectrumWalletBase scriptType: _getScriptType(address), ), ownerDetails: UtxoAddressDetails( - publicKey: privkey.getPublic().toHex(), + publicKey: pubKeyHex, address: address, ), ), @@ -490,6 +539,7 @@ abstract class ElectrumWalletBase return EstimatedTxResult( utxos: utxos, privateKeys: privateKeys, + publicKeys: publicKeys, fee: fee, amount: amount, hasChange: true, @@ -557,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( @@ -618,8 +697,22 @@ 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, @@ -1263,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; @@ -1286,6 +1379,9 @@ abstract class ElectrumWalletBase return BitcoinNetwork.mainnet; } + + static String _hardenedDerivationPath(String derivationPath) => + derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1); } class EstimateTxParams { @@ -1307,6 +1403,7 @@ class EstimatedTxResult { EstimatedTxResult({ required this.utxos, required this.privateKeys, + required this.publicKeys, required this.fee, required this.amount, required this.hasChange, @@ -1317,6 +1414,7 @@ class EstimatedTxResult { final List utxos; final List privateKeys; + final Map publicKeys; // PubKey to derivationPath final int fee; final int amount; final bool hasChange; @@ -1325,6 +1423,13 @@ class EstimatedTxResult { final bool spendsUnconfirmedTX; } +class PublicKeyWithDerivationPath { + const PublicKeyWithDerivationPath(this.publicKey, this.derivationPath); + + final String derivationPath; + final String publicKey; +} + BitcoinBaseAddress addressTypeFromStr(String address, BasedUtxoNetwork network) { if (network is BitcoinCashNetwork) { if (!address.startsWith("bitcoincash:") && diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 218792e3c..340b17cfb 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -13,6 +13,7 @@ class ElectrumWalletSnapshot { required this.type, required this.password, required this.mnemonic, + required this.xpub, required this.addresses, required this.balance, required this.regularAddressIndex, @@ -28,7 +29,8 @@ class ElectrumWalletSnapshot { final WalletType type; final String? addressPageType; - String mnemonic; + String? mnemonic; + String? xpub; List addresses; ElectrumBalance balance; Map regularAddressIndex; @@ -43,7 +45,8 @@ 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() @@ -79,6 +82,7 @@ class ElectrumWalletSnapshot { password: password, passphrase: passphrase, mnemonic: mnemonic, + xpub: xpub, addresses: addresses, balance: balance, regularAddressIndex: regularAddressIndexByType, diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 4d166e47b..2ffb99405 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -50,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((_) { @@ -106,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 9143556ab..bb51a4eaa 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -16,7 +16,7 @@ import 'package:bip39/bip39.dart' as bip39; class LitecoinWalletService extends WalletService< BitcoinNewWalletCredentials, BitcoinRestoreWalletFromSeedCredentials, - BitcoinRestoreWalletFromWIFCredentials> { + BitcoinRestoreWalletFromWIFCredentials,BitcoinNewWalletCredentials> { LitecoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); final Box walletInfoSource; @@ -94,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 => 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/pubspec.lock b/cw_bitcoin/pubspec.lock index 86d58b9b1..a398a11e0 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -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: @@ -313,6 +321,14 @@ packages: url: "https://pub.dev" source: hosted 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 @@ -326,6 +342,14 @@ packages: url: "https://pub.dev" source: hosted 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,31 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.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: @@ -582,6 +631,14 @@ 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: @@ -606,6 +663,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: @@ -788,5 +861,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.0.0 <4.0.0" + dart: ">=3.0.6 <4.0.0" flutter: ">=3.10.0" diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 632a3140a..9adf77652 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -35,6 +35,10 @@ 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: 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_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index f1c1cd8ae..a35418eee 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 = 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/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 e0896bab1..4da616a79 100644 --- a/cw_core/lib/hive_type_ids.dart +++ b/cw_core/lib/hive_type_ids.dart @@ -16,3 +16,5 @@ const POW_NODE_TYPE_ID = 14; const DERIVATION_TYPE_TYPE_ID = 15; 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 9d0806851..1195b6819 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -94,6 +94,7 @@ class Node extends HiveObject with Keyable { case WalletType.ethereum: case WalletType.polygon: case WalletType.solana: + 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/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 9b28680f9..30ae2546c 100644 --- a/cw_core/lib/wallet_credentials.dart +++ b/cw_core/lib/wallet_credentials.dart @@ -9,6 +9,7 @@ abstract class WalletCredentials { this.password, this.passphrase, this.derivationInfo, + this.hardwareWalletType, }) { if (this.walletInfo != null && derivationInfo != null) { this.walletInfo!.derivationInfo = derivationInfo; @@ -22,4 +23,5 @@ abstract class WalletCredentials { 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 4892f6d1d..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'; @@ -20,6 +21,12 @@ enum DerivationType { electrum, } +@HiveType(typeId: HARDWARE_WALLET_TYPE_TYPE_ID) +enum HardwareWalletType { + @HiveField(0) + ledger, +} + @HiveType(typeId: DerivationInfo.typeId) class DerivationInfo extends HiveObject { DerivationInfo({ @@ -40,7 +47,7 @@ class DerivationInfo extends HiveObject { @HiveField(1, defaultValue: '') String balance; - @HiveField(2) + @HiveField(2, defaultValue: 0) int transactionsCount; @HiveField(3) @@ -71,8 +78,9 @@ class WalletInfo extends HiveObject { this.yatEid, this.yatLastUsedAddressRaw, this.showIntroCakePayCard, - this.derivationInfo) - : _yatLastUsedAddressController = StreamController.broadcast(); + this.derivationInfo, + this.hardwareWalletType, + ): _yatLastUsedAddressController = StreamController.broadcast(); factory WalletInfo.external({ required String id, @@ -88,6 +96,7 @@ class WalletInfo extends HiveObject { String yatEid = '', String yatLastUsedAddressRaw = '', DerivationInfo? derivationInfo, + HardwareWalletType? hardwareWalletType, }) { return WalletInfo( id, @@ -103,6 +112,7 @@ class WalletInfo extends HiveObject { yatLastUsedAddressRaw, showIntroCakePayCard, derivationInfo, + hardwareWalletType, ); } @@ -154,9 +164,11 @@ class WalletInfo extends HiveObject { @HiveField(15) List? usedAddresses; + @deprecated @HiveField(16) DerivationType? derivationType; // no longer used + @deprecated @HiveField(17) String? derivationPath; // no longer used @@ -169,6 +181,9 @@ class WalletInfo extends HiveObject { @HiveField(20) DerivationInfo? derivationInfo; + @HiveField(21) + HardwareWalletType? hardwareWalletType; + String get yatLastUsedAddress => yatLastUsedAddressRaw ?? ''; set yatLastUsedAddress(String address) { @@ -185,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_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..5f78fba3d 100644 --- a/cw_ethereum/pubspec.yaml +++ b/cw_ethereum/pubspec.yaml @@ -19,6 +19,12 @@ dependencies: path: ../cw_evm hive: ^2.2.3 +dependency_overrides: + web3dart: + git: + url: https://github.com/cake-tech/web3dart.git + ref: cake + dev_dependencies: flutter_test: sdk: flutter 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 8f0df3926..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 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 558013252..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); @@ -535,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..fb0384064 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,6 +22,20 @@ 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 dev_dependencies: flutter_test: 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_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_nano/lib/nano_wallet_service.dart b/cw_nano/lib/nano_wallet_service.dart index b1497a625..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; @@ -109,6 +109,11 @@ 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(' ')) { 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..dbef40b46 100644 --- a/cw_polygon/pubspec.yaml +++ b/cw_polygon/pubspec.yaml @@ -23,6 +23,11 @@ dependencies: bip39: ^1.0.6 collection: ^1.17.1 +dependency_overrides: + web3dart: + git: + url: https://github.com/cake-tech/web3dart.git + ref: cake dev_dependencies: flutter_test: diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index f3eef465c..6692b65a6 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -145,12 +145,17 @@ abstract class SolanaWalletBase Future getWalletPair({String? mnemonic, String? privateKey}) async { assert(mnemonic != null || privateKey != null); - if (privateKey != null) { - final privateKeyBytes = base58decode(privateKey); - return await Wallet.fromPrivateKeyBytes(privateKey: privateKeyBytes.take(32).toList()); + 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 @@ -360,7 +365,7 @@ abstract class SolanaWalletBase String toJSON() => json.encode({ 'mnemonic': _mnemonic, - 'private_key': privateKey, + 'private_key': _hexPrivateKey, 'balance': balance[currency]!.toJSON(), }); 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_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..ad70f28cd --- /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: true, + ), + TronToken( + name: "Ethereum", + symbol: "ETH", + contractAddress: "TRFe3hT5oYhjSZ6f3ji5FJ7YCfrkWnHRvh", + decimal: 18, + enabled: true, + ), + 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..f03a8abce --- /dev/null +++ b/cw_tron/lib/tron_client.dart @@ -0,0 +1,574 @@ +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'); + + log( + 'Create Account Fee In System Contract for Chain: ${chainParams.getCreateNewAccountFeeInSystemContract!}', + ); + log('Create Account Fee for Chain: ${chainParams.getCreateAccountFee}'); + + 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!; + } + + // Check if receiver's account is active + final receiverAccountInfo = + await _provider!.request(TronRequestGetAccount(address: receiverAddress)); + + /// Calculate the resources required to create a new account. + if (receiverAccountInfo == null) { + totalBurn += chainParams.getCreateNewAccountFeeInSystemContract!; + + totalBurn += (chainParams.getCreateAccountFee! * bandWidthInSun); + } + + 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().toUtc().add(const Duration(hours: 24)); + + // 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 = 100000; + + 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().toUtc().add(const Duration(hours: 24)); + + // 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) { + throw Exception( + 'You don\'t have enough TRX to cover the transaction fee for this transaction. Kindly top up.', + ); + } + + 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}"); + } + + final feeLimit = await getFeeLimit( + request.transactionRaw!, + ownerAddress, + receiverAddress, + energyUsed: request.energyUsed ?? 0, + ); + + final tronBalanceInt = tronBalance.toInt(); + + if (feeLimit > tronBalanceInt) { + throw Exception( + 'You don\'t have enough TRX to cover the transaction fee for this transaction. Kindly top up.', + ); + } + + 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..193a3dbdd --- /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', + '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', + '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..6cef05348 --- /dev/null +++ b/cw_tron/lib/tron_wallet.dart @@ -0,0 +1,560 @@ +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'; +import 'package:shared_preferences/shared_preferences.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()); + } + + sharedPrefs.complete(SharedPreferences.getInstance()); + } + + 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 TronClient _client; + + Timer? _transactionsUpdateTimer; + + @override + WalletAddresses walletAddresses; + + @observable + String? nativeTxEstimatedFee; + + @observable + String? trc20EstimatedFee; + + @override + @observable + SyncStatus syncStatus; + + @override + @observable + late ObservableMap balance; + + Completer sharedPrefs = Completer(); + + 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 (_) {} + } + } + + 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: 20), (_) 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(); + } +} 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/Podfile.lock b/ios/Podfile.lock index 67c0c9ee8..cd03e10a9 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -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) @@ -179,6 +184,7 @@ DEPENDENCIES: - 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 @@ -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: @@ -286,7 +295,9 @@ SPEC CHECKSUMS: package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 + Protobuf: 8e9074797a13c484a79959fdb819ef4ae6da7dbe ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 + reactive_ble_mobile: 9ce6723d37ccf701dbffd202d487f23f5de03b4c SDWebImage: a3ba0b8faac7228c3c8eadd1a55c9c9fe5e16457 sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986 share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 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..02365bda7 100644 --- a/ios/Runner/InfoBase.plist +++ b/ios/Runner/InfoBase.plist @@ -190,6 +190,26 @@ solana-wallet + + CFBundleTypeRole + Viewer + CFBundleURLName + tron + CFBundleURLSchemes + + tron + + + + CFBundleTypeRole + Viewer + CFBundleURLName + tron-wallet + CFBundleURLSchemes + + tron-wallet + + CFBundleVersion $(CURRENT_PROJECT_VERSION) @@ -208,6 +228,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 7ae01df1c..56c9c7dff 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -32,6 +32,14 @@ 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; @@ -292,7 +300,7 @@ class CWBitcoin extends Bitcoin { return [ DerivationInfo( derivationType: DerivationType.electrum, - derivationPath: "m/0'/0", + derivationPath: "m/0'", description: "Electrum", scriptType: "p2wpkh", ) @@ -344,9 +352,6 @@ class CWBitcoin extends Bitcoin { if (derivationDepth == 3) { // we add "/0/0" so that we generate account 0, index 0 and correctly get balance derivationPath += "/0/0"; - // we don't support sub-ACCOUNTS in bitcoin like we do monero, and so the path dInfoCopy - // expects should be ACCOUNT 0, index unspecified: - dInfoCopy.derivationPath = dInfoCopy.derivationPath! + "/0"; } // var hd = bip32.BIP32.fromSeed(seedBytes).derivePath(derivationPath); @@ -440,4 +445,21 @@ class CWBitcoin extends Bitcoin { 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 bf67edd23..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,8 +15,8 @@ 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 _signMessagePath = '/v1/auth/signMessage'; @@ -91,7 +93,7 @@ class DFXBuyProvider extends BuyProvider { // } Future auth() async { - final signMessage = getSignature(await getSignMessage()); + final signMessage = await getSignature(await getSignMessage()); final requestBody = jsonEncode({ 'wallet': walletName, @@ -118,7 +120,7 @@ class DFXBuyProvider extends BuyProvider { } } - String getSignature(String message) { + Future getSignature(String message) async { switch (wallet.type) { case WalletType.ethereum: case WalletType.polygon: @@ -135,6 +137,20 @@ 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; 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/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..646e47537 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -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/di.dart b/lib/di.dart index a188475dd..c69157dc4 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1,236 +1,240 @@ +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/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/rbf_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/receive_page_option.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:flutter_secure_storage/flutter_secure_storage.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 'buy/dfx/dfx_buy_provider.dart'; import 'core/totp_request_details.dart'; @@ -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); @@ -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(), @@ -491,12 +501,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; }); @@ -600,6 +606,7 @@ Future setup({ getIt.get(), getIt.get(), _transactionDescriptionBox, + getIt.get(), ), ); @@ -805,11 +812,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, @@ -875,6 +882,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'); } @@ -928,8 +937,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)); @@ -1144,9 +1162,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/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 99178c815..94b23d3c9 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -36,6 +36,7 @@ const cakeWalletBitcoinCashDefaultNodeUri = 'bitcoincash.stackwallet.com:50002'; const nanoDefaultNodeUri = 'rpc.nano.to'; const nanoDefaultPowNodeUri = 'rpc.nano.to'; const solanaDefaultNodeUri = 'rpc.ankr.com'; +const tronDefaultNodeUri = 'api.trongrid.io'; const newCakeWalletBitcoinUri = 'btc-electrum.cakewallet.com:50002'; Future defaultSettingsMigration( @@ -207,23 +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; } @@ -478,6 +478,11 @@ 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, @@ -809,6 +814,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 = @@ -829,6 +835,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); @@ -894,6 +902,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( @@ -1022,3 +1036,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/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/preferences_key.dart b/lib/entities/preferences_key.dart index be4f354e0..e49c4105c 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'; 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 ca168a299..37a492987 100644 --- a/lib/entities/provider_types.dart +++ b/lib/entities/provider_types.dart @@ -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/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart index 61d5b6ae3..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 @@ -159,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/main.dart b/lib/main.dart index ff5b0e5c0..b274c7a84 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -105,7 +105,11 @@ Future initializeAppConfigs() async { 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()); } @@ -167,7 +171,7 @@ Future initializeAppConfigs() async { transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, - initialMigrationVersion: 32, + initialMigrationVersion: 33, ); } diff --git a/lib/nano/cw_nano.dart b/lib/nano/cw_nano.dart index 13800cff6..ad02d2ccb 100644 --- a/lib/nano/cw_nano.dart +++ b/lib/nano/cw_nano.dart @@ -228,35 +228,47 @@ class CWNanoUtil extends NanoUtil { }) async { NanoClient nanoClient = NanoClient(); nanoClient.connect(node); - late String publicAddress; + String? publicAddress; - if (seedKey != null) { - if (seedKey.length == 64) { - try { - mnemonic = NanoDerivations.standardSeedToMnemonic(seedKey); - } catch (e) { - print("not a valid 'nano' seed key"); + if (seedKey == null && mnemonic == null) { + throw Exception("One of seed key OR mnemonic must be provided!"); + } + + try { + if (seedKey != null) { + if (seedKey.length == 64) { + try { + mnemonic = NanoDerivations.standardSeedToMnemonic(seedKey); + } catch (e) { + print("not a valid 'nano' seed key"); + } + } + if (derivationType == DerivationType.bip39) { + publicAddress = await NanoDerivations.hdSeedToAddress(seedKey, index: 0); + } else if (derivationType == DerivationType.nano) { + publicAddress = await NanoDerivations.standardSeedToAddress(seedKey, index: 0); } } + if (derivationType == DerivationType.bip39) { - publicAddress = await NanoDerivations.hdSeedToAddress(seedKey, index: 0); - } else if (derivationType == DerivationType.nano) { - publicAddress = await NanoDerivations.standardSeedToAddress(seedKey, index: 0); + if (mnemonic != null) { + seedKey = await NanoDerivations.hdMnemonicListToSeed(mnemonic.split(' ')); + publicAddress = await NanoDerivations.hdSeedToAddress(seedKey, index: 0); + } } - } - if (derivationType == DerivationType.bip39) { - if (mnemonic != null) { - seedKey = await NanoDerivations.hdMnemonicListToSeed(mnemonic.split(' ')); - publicAddress = await NanoDerivations.hdSeedToAddress(seedKey, index: 0); + if (derivationType == DerivationType.nano) { + if (mnemonic != null) { + seedKey = await NanoDerivations.standardMnemonicToSeed(mnemonic); + publicAddress = await NanoDerivations.standardSeedToAddress(seedKey, index: 0); + } } - } + } catch (_) {} - if (derivationType == DerivationType.nano) { - if (mnemonic != null) { - seedKey = await NanoDerivations.standardMnemonicToSeed(mnemonic); - publicAddress = await NanoDerivations.standardSeedToAddress(seedKey, index: 0); - } + if (publicAddress == null) { + // we couldn't derive a public address for the derivation type provided + // i.e. a bip39 seed was provided and we were instructed to derive a "nano" type address + return null; } AccountInfoResponse? accountInfo = await nanoClient.getAccountInfo(publicAddress); diff --git a/lib/polygon/cw_polygon.dart b/lib/polygon/cw_polygon.dart index 5baf4fbbc..16aba284d 100644 --- a/lib/polygon/cw_polygon.dart +++ b/lib/polygon/cw_polygon.dart @@ -30,14 +30,23 @@ class CWPolygon extends Polygon { }) => EVMChainRestoreWalletFromPrivateKey(name: name, password: password, privateKey: privateKey); + @override + WalletCredentials createPolygonHardwareWalletCredentials({ + required String name, + required HardwareAccountData hwAccountData, + WalletInfo? walletInfo, + }) => + EVMChainRestoreWalletFromHardware( + name: name, hwAccountData: hwAccountData, walletInfo: walletInfo); + @override String getAddress(WalletBase wallet) => (wallet as PolygonWallet).walletAddresses.address; @override String getPrivateKey(WalletBase wallet) { final privateKeyHolder = (wallet as PolygonWallet).evmChainPrivateKey; - String stringKey = bytesToHex(privateKeyHolder.privateKey); - return stringKey; + if (privateKeyHolder is EthPrivateKey) return bytesToHex(privateKeyHolder.privateKey); + return ""; } @override @@ -67,21 +76,21 @@ class CWPolygon extends Polygon { int? feeRate, }) => EVMChainTransactionCredentials( - outputs - .map((out) => OutputInfo( - fiatAmount: out.fiatAmount, - cryptoAmount: out.cryptoAmount, - address: out.address, - note: out.note, - sendAll: out.sendAll, - extractedAddress: out.extractedAddress, - isParsedAddress: out.isParsedAddress, - formattedCryptoAmount: out.formattedCryptoAmount)) - .toList(), - priority: priority as EVMChainTransactionPriority, - currency: currency, - feeRate: feeRate, - ); + outputs + .map((out) => OutputInfo( + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmount, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + formattedCryptoAmount: out.formattedCryptoAmount)) + .toList(), + priority: priority as EVMChainTransactionPriority, + currency: currency, + feeRate: feeRate, + ); Object createPolygonTransactionCredentialsRaw( List outputs, { @@ -157,4 +166,23 @@ class CWPolygon extends Polygon { } 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) { + throw err; + } + } } diff --git a/lib/reactions/fiat_rate_update.dart b/lib/reactions/fiat_rate_update.dart index fb1d4cd1a..e46ef4b64 100644 --- a/lib/reactions/fiat_rate_update.dart +++ b/lib/reactions/fiat_rate_update.dart @@ -8,6 +8,7 @@ import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/tron/tron.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/wallet_type.dart'; @@ -53,6 +54,11 @@ Future startFiatRateUpdate( solana!.getSPLTokenCurrencies(appStore.wallet!).where((element) => element.enabled); } + if (appStore.wallet!.type == WalletType.tron) { + currencies = + tron!.getTronTokenCurrencies(appStore.wallet!).where((element) => element.enabled); + } + if (currencies != null) { for (final currency in currencies) { diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index 6558b916a..8825eb759 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -5,8 +5,8 @@ import 'package:cake_wallet/entities/update_haven_rate.dart'; import 'package:cake_wallet/ethereum/ethereum.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/crypto_currency.dart'; -import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/transaction_info.dart'; @@ -75,8 +75,10 @@ void startCurrentWalletChangeReaction( .get() .setInt(PreferencesKey.currentWalletType, serializeToInt(wallet.type)); - if (wallet.type == WalletType.monero || wallet.type == WalletType.bitcoin || - wallet.type == WalletType.litecoin || wallet.type == WalletType.bitcoinCash ) { + if (wallet.type == WalletType.monero || + wallet.type == WalletType.bitcoin || + wallet.type == WalletType.litecoin || + wallet.type == WalletType.bitcoinCash) { _setAutoGenerateSubaddressStatus(wallet, settingsStore); } @@ -134,7 +136,11 @@ void startCurrentWalletChangeReaction( currencies = solana!.getSPLTokenCurrencies(appStore.wallet!).where((element) => element.enabled); } - + if (wallet.type == WalletType.tron) { + currencies = + tron!.getTronTokenCurrencies(appStore.wallet!).where((element) => element.enabled); + } + if (currencies != null) { for (final currency in currencies) { () async { diff --git a/lib/router.dart b/lib/router.dart index 9f5dfb838..3032ceb6a 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -1,29 +1,74 @@ 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/buy/order.dart'; import 'package:cake_wallet/core/totp_request_details.dart'; import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart'; +import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/contact_record.dart'; -import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/entities/wallet_nft_response.dart'; +import 'package:cake_wallet/exchange/trade.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart'; +import 'package:cake_wallet/routes.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/connect_device/connect_device_page.dart'; +import 'package:cake_wallet/src/screens/connect_device/select_hardware_wallet_account_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_widgets/desktop_dashboard_actions.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/nft_details_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart'; +import 'package:cake_wallet/src/screens/disclaimer/disclaimer_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/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/new_wallet/advanced_privacy_settings_page.dart'; +import 'package:cake_wallet/src/screens/new_wallet/new_wallet_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/restore/sweeping_wallet_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/dashboard/desktop_widgets/desktop_dashboard_actions.dart'; -import 'package:cake_wallet/src/screens/dashboard/pages/transactions_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/sweeping_wallet_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/desktop_settings/desktop_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/display_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/domain_lookups_page.dart'; @@ -31,90 +76,49 @@ 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_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/new_wallet/advanced_privacy_settings_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/restore/restore_from_backup_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/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/trade_details/trade_details_page.dart'; +import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart'; import 'package:cake_wallet/src/screens/transaction_details/rbf_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/wallet_list/wallet_list_page.dart'; +import 'package:cake_wallet/src/screens/welcome/create_welcome_page.dart'; import 'package:cake_wallet/utils/payment_request.dart'; +import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; +import 'package:cake_wallet/view_model/hardware_wallet/ledger_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_create_or_edit_view_model.dart'; -import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart'; import 'package:cake_wallet/view_model/seed_type_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_new_vm.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; +import 'package:cake_wallet/wallet_types.g.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/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/di.dart'; -import 'package:cake_wallet/view_model/wallet_new_vm.dart'; -import 'package:cake_wallet/exchange/trade.dart'; -import 'package:cw_core/transaction_info.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/src/screens/dashboard/dashboard_page.dart'; -import 'package:cake_wallet/src/screens/seed/wallet_seed_page.dart'; -import 'package:cake_wallet/src/screens/auth/auth_page.dart'; -import 'package:cake_wallet/src/screens/nodes/node_create_or_edit_page.dart'; -import 'package:cake_wallet/src/screens/receive/receive_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/src/screens/wallet/wallet_edit_page.dart'; -import 'package:cake_wallet/src/screens/new_wallet/new_wallet_page.dart'; -import 'package:cake_wallet/src/screens/setup_pin_code/setup_pin_code.dart'; -import 'package:cake_wallet/src/screens/restore/restore_options_page.dart'; -import 'package:cake_wallet/src/screens/send/send_page.dart'; -import 'package:cake_wallet/src/screens/disclaimer/disclaimer_page.dart'; -import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart'; -import 'package:cake_wallet/src/screens/monero_accounts/monero_account_edit_or_create_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/wallet_keys/wallet_keys_page.dart'; -import 'package:cake_wallet/src/screens/exchange/exchange_page.dart'; -import 'package:cake_wallet/src/screens/rescan/rescan_page.dart'; -import 'package:cake_wallet/src/screens/faq/faq_page.dart'; -import 'package:cake_wallet/src/screens/trade_details/trade_details_page.dart'; -import 'package:cake_wallet/src/screens/welcome/create_welcome_page.dart'; -import 'package:cake_wallet/src/screens/new_wallet/new_wallet_type_page.dart'; -import 'package:cake_wallet/src/screens/send/send_template_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:flutter/services.dart'; -import 'package:cake_wallet/wallet_types.g.dart'; -import 'package:cake_wallet/src/screens/dashboard/pages/address_page.dart'; -import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart'; -import 'package:cake_wallet/src/screens/ionia/ionia.dart'; -import 'package:cake_wallet/src/screens/ionia/cards/ionia_payment_status_page.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:cw_core/crypto_currency.dart'; -import 'package:cw_core/node.dart'; import 'src/screens/dashboard/pages/nft_import_page.dart'; @@ -151,7 +155,15 @@ Route createRoute(RouteSettings settings) { final walletNewVM = getIt.get(param1: type); final seedTypeViewModel = getIt.get(); - return CupertinoPageRoute(builder: (_) => NewWalletPage(walletNewVM, seedTypeViewModel)); + return CupertinoPageRoute( + builder: (_) => NewWalletPage(walletNewVM, seedTypeViewModel)); + + case Routes.chooseHardwareWalletAccount: + final arguments = settings.arguments as List; + final type = arguments[0] as WalletType; + final walletVM = getIt.get(param1: type); + + return CupertinoPageRoute(builder: (_) => SelectHardwareWalletAccountPage(walletVM)); case Routes.setupPin: Function(PinCodeState, String)? callback; @@ -168,7 +180,7 @@ Route createRoute(RouteSettings settings) { builder: (_) => getIt.get( param1: (BuildContext context, WalletType type) => Navigator.of(context).pushNamed(Routes.restoreWallet, arguments: type), - param2: false)); + param2: [false, false])); case Routes.restoreOptions: final isNewInstall = settings.arguments as bool; @@ -199,7 +211,46 @@ Route createRoute(RouteSettings settings) { builder: (_) => getIt.get( param1: (BuildContext context, WalletType type) => Navigator.of(context).pushNamed(Routes.restoreWallet, arguments: type), - param2: false)); + param2: [false, false])); + } + + case Routes.restoreWalletFromHardwareWallet: + final isNewInstall = settings.arguments as bool; + + if (isNewInstall) { + return CupertinoPageRoute( + builder: (_) => getIt.get( + param1: (PinCodeState context, dynamic _) => + Navigator.of(context.context).pushNamed(Routes.restoreWalletFromHardwareWallet, arguments: false), + ), + fullscreenDialog: true, + ); + } + if (isSingleCoin) { + return MaterialPageRoute( + builder: (_) => ConnectDevicePage( + ConnectDevicePageParams( + walletType: availableWalletTypes.first, + onConnectDevice: (BuildContext context, _) => + Navigator.of(context).pushNamed(Routes.chooseHardwareWalletAccount, + arguments: [availableWalletTypes.first]), + ), + getIt.get(), + )); + } else { + return CupertinoPageRoute( + builder: (_) => getIt.get( + param1: (BuildContext context, WalletType type) { + final arguments = ConnectDevicePageParams( + walletType: type, + onConnectDevice: (BuildContext context, _) => + Navigator.of(context).pushNamed(Routes.chooseHardwareWalletAccount, + arguments: [type]), + ); + + Navigator.of(context).pushNamed(Routes.connectDevices, arguments: arguments); + }, + param2: [false, true])); } case Routes.restoreWalletTypeFromQR: @@ -400,8 +451,7 @@ Route createRoute(RouteSettings settings) { case Routes.buySellPage: final args = settings.arguments as bool; - return MaterialPageRoute( - builder: (_) => getIt.get(param1: args)); + return MaterialPageRoute(builder: (_) => getIt.get(param1: args)); case Routes.buyWebView: final args = settings.arguments as List; @@ -424,8 +474,7 @@ Route createRoute(RouteSettings settings) { case Routes.preSeedPage: return MaterialPageRoute( - builder: (_) => getIt.get( - param1: settings.arguments as int)); + builder: (_) => getIt.get(param1: settings.arguments as int)); case Routes.backup: return CupertinoPageRoute( @@ -647,6 +696,11 @@ Route createRoute(RouteSettings settings) { case Routes.torPage: return MaterialPageRoute(builder: (_) => getIt.get()); + case Routes.connectDevices: + final params = settings.arguments as ConnectDevicePageParams; + return MaterialPageRoute( + builder: (_) => ConnectDevicePage(params, getIt.get())); + default: return MaterialPageRoute( builder: (_) => Scaffold( diff --git a/lib/routes.dart b/lib/routes.dart index 9c4e21651..1b518d328 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -6,9 +6,11 @@ class Routes { static const seed = '/seed'; static const restoreOptions = '/restore_options'; static const restoreWalletFromSeedKeys = '/restore_wallet_from_seeds_keys'; + static const restoreWalletFromHardwareWallet = '/restore/hardware_wallet'; static const restoreWalletTypeFromQR = '/restore_wallet_from_qr_code'; static const restoreWalletChooseDerivation = '/restore_wallet_choose_derivation'; + static const chooseHardwareWalletAccount = '/restore/hardware_wallet/accounts'; static const dashboard = '/dashboard'; static const send = '/send'; static const transactionDetails = '/transaction_info'; @@ -107,4 +109,5 @@ class Routes { static const nftDetailsPage = '/nft_details_page'; static const importNFTPage = '/import_nft_page'; static const torPage = '/tor_page'; + static const connectDevices = '/device/connect'; } diff --git a/lib/src/screens/connect_device/connect_device_page.dart b/lib/src/screens/connect_device/connect_device_page.dart new file mode 100644 index 000000000..dfb32beba --- /dev/null +++ b/lib/src/screens/connect_device/connect_device_page.dart @@ -0,0 +1,221 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/connect_device/widgets/device_tile.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:permission_handler/permission_handler.dart'; + +typedef OnConnectDevice = void Function(BuildContext, LedgerViewModel); + +class ConnectDevicePageParams { + final WalletType walletType; + final OnConnectDevice onConnectDevice; + + ConnectDevicePageParams({required this.walletType, required this.onConnectDevice}); +} + +class ConnectDevicePage extends BasePage { + final WalletType walletType; + final OnConnectDevice onConnectDevice; + final LedgerViewModel ledgerVM; + + ConnectDevicePage(ConnectDevicePageParams params, this.ledgerVM) + : walletType = params.walletType, + onConnectDevice = params.onConnectDevice; + + @override + String get title => S.current.restore_title_from_hardware_wallet; + + @override + Widget body(BuildContext context) => ConnectDevicePageBody(walletType, onConnectDevice, ledgerVM); +} + +class ConnectDevicePageBody extends StatefulWidget { + final WalletType walletType; + final OnConnectDevice onConnectDevice; + final LedgerViewModel ledgerVM; + + const ConnectDevicePageBody(this.walletType, this.onConnectDevice, this.ledgerVM); + + @override + ConnectDevicePageBodyState createState() => ConnectDevicePageBodyState(); +} + +class ConnectDevicePageBodyState extends State { + final imageLedger = 'assets/images/ledger_nano.png'; + + final ledger = Ledger( + options: LedgerOptions( + scanMode: ScanMode.balanced, + maxScanDuration: const Duration(minutes: 5), + ), + onPermissionRequest: (_) async { + Map statuses = await [ + Permission.bluetoothScan, + Permission.bluetoothConnect, + Permission.bluetoothAdvertise, + ].request(); + + return statuses.values.where((status) => status.isDenied).isEmpty; + }, + ); + + var bleIsEnabled = true; + var bleDevices = []; + var usbDevices = []; + + late Timer? _usbRefreshTimer = null; + late Timer? _bleRefreshTimer = null; + late StreamSubscription? _bleRefresh = null; + + @override + void initState() { + super.initState(); + Future.delayed( + Duration(seconds: 1), + () => _bleRefresh = ledger.scan().listen((device) => setState(() => bleDevices.add(device))), + ); + // _bleRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshBleDevices()); + + if (Platform.isAndroid) { + _usbRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshUsbDevices()); + } + } + + @override + void dispose() { + _bleRefreshTimer?.cancel(); + _usbRefreshTimer?.cancel(); + _bleRefresh?.cancel(); + super.dispose(); + } + + Future _refreshUsbDevices() async { + final dev = await ledger.listUsbDevices(); + if (usbDevices.length != dev.length) setState(() => usbDevices = dev); + } + + Future _refreshBleDevices() async { + final isBleEnabled = await Permission.bluetooth.serviceStatus.isEnabled; + + setState(() => bleIsEnabled = isBleEnabled); + + if (isBleEnabled) { + _bleRefresh = ledger.scan().listen((device) => setState(() => bleDevices.add(device))); + _bleRefreshTimer?.cancel(); + _bleRefreshTimer = null; + } + } + + Future _connectToDevice(LedgerDevice device) async { + await widget.ledgerVM.connectLedger(device); + widget.onConnectDevice(context, widget.ledgerVM); + } + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + width: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint, + height: double.infinity, + padding: EdgeInsets.symmetric(vertical: 24, horizontal: 24), + child: SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: EdgeInsets.only(left: 20, right: 20, bottom: 20), + child: Text( + Platform.isIOS + ? S.of(context).connect_your_hardware_wallet_ios + : S.of(context).connect_your_hardware_wallet, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.titleColor), + textAlign: TextAlign.center, + ), + ), + if (!bleIsEnabled) + Padding( + padding: EdgeInsets.only(left: 20, right: 20, bottom: 20), + child: Text( + S.of(context).ledger_please_enable_bluetooth, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.titleColor), + textAlign: TextAlign.center, + ), + ), + if (bleDevices.length > 0) ...[ + Padding( + padding: EdgeInsets.only(left: 20, right: 20, bottom: 20), + child: Container( + width: double.infinity, + child: Text( + S.of(context).bluetooth, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.titleColor, + ), + ), + ), + ), + ...bleDevices + .map( + (device) => Padding( + padding: EdgeInsets.only(bottom: 20), + child: DeviceTile( + onPressed: () => _connectToDevice(device), + title: device.name, + leading: imageLedger, + connectionType: device.connectionType, + ), + ), + ) + .toList() + ], + if (usbDevices.length > 0) ...[ + Padding( + padding: EdgeInsets.only(left: 20, right: 20, bottom: 20), + child: Container( + width: double.infinity, + child: Text( + S.of(context).usb, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.titleColor, + ), + ), + ), + ), + ...usbDevices + .map( + (device) => Padding( + padding: EdgeInsets.only(bottom: 20), + child: DeviceTile( + onPressed: () => _connectToDevice(device), + title: device.name, + leading: imageLedger, + connectionType: device.connectionType, + ), + ), + ) + .toList(), + ] + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/screens/connect_device/debug_device_page.dart b/lib/src/screens/connect_device/debug_device_page.dart new file mode 100644 index 000000000..f5a9ef2a4 --- /dev/null +++ b/lib/src/screens/connect_device/debug_device_page.dart @@ -0,0 +1,212 @@ +// import 'dart:convert'; +// +// import 'package:cake_wallet/src/screens/base_page.dart'; +// import 'package:cake_wallet/src/screens/connect_device/widgets/device_tile.dart'; +// import 'package:cake_wallet/src/widgets/primary_button.dart'; +// import 'package:cake_wallet/utils/responsive_layout_util.dart'; +// import 'package:convert/convert.dart'; +// import 'package:flutter/material.dart'; +// import 'package:ledger_bitcoin/ledger_bitcoin.dart'; +// import 'package:ledger_flutter/ledger_flutter.dart'; +// import 'package:permission_handler/permission_handler.dart'; +// import 'package:polyseed/polyseed.dart'; +// +// class DebugDevicePage extends BasePage { +// @override +// String get title => "Connect Ledger"; +// +// @override +// Widget body(BuildContext context) => DebugDevicePageBody(); +// } +// +// class DebugDevicePageBody extends StatefulWidget { +// @override +// DebugDevicePageBodyState createState() => DebugDevicePageBodyState(); +// } +// +// class DebugDevicePageBodyState extends State { +// final imageLedger = Image.asset( +// 'assets/images/ledger_icon_black.png', +// width: 40, +// ); +// final ledger = Ledger( +// options: LedgerOptions( +// scanMode: ScanMode.balanced, +// maxScanDuration: const Duration(milliseconds: 5000), +// ), +// onPermissionRequest: (status) async { +// Map statuses = await [ +// // Permission.location, +// Permission.bluetoothScan, +// Permission.bluetoothConnect, +// Permission.bluetoothAdvertise, +// ].request(); +// +// if (status != BleStatus.ready) { +// return false; +// } +// +// return statuses.values.where((status) => status.isDenied).isEmpty; +// }, +// ); +// +// late BitcoinLedgerApp btc; +// var devices = []; +// var status = ""; +// var counter = 0; +// LedgerDevice? selectedDevice = null; +// +// @override +// void initState() { +// super.initState(); +// btc = BitcoinLedgerApp(ledger); +// } +// +// @override +// void dispose() { +// super.dispose(); +// ledger.close(ConnectionType.ble); +// ledger.close(ConnectionType.usb); +// } +// +// Future reconnectCurrentDevice() async { +// // await ledger.disconnect(selectedDevice!); +// // await ledger.connect(selectedDevice!); +// } +// +// Future disconnectCurrentDevice() async { +// await ledger.disconnect(selectedDevice!); +// setState(() => selectedDevice = null); +// } +// +// @override +// Widget build(BuildContext context) { +// final imageLedger = 'assets/images/ledger_nano.png'; +// +// return Center( +// child: Container( +// width: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint, +// height: double.infinity, +// padding: EdgeInsets.symmetric(vertical: 24, horizontal: 24), +// child: SingleChildScrollView( +// child: Column( +// children: [ +// Padding( +// padding: EdgeInsets.only(top: 20), +// child: Text(status), +// ), +// if (selectedDevice != null) ...[ +// DebugButton( +// title: "Get Version", +// method: "Version", +// func: () async => await btc.getVersion(selectedDevice!), +// ), +// DebugButton( +// title: "Get Master Fingerprint", +// method: "Master Fingerprint", +// func: () async => hex.encode(await btc.getMasterFingerprint(selectedDevice!)), +// ), +// DebugButton( +// title: "Get XPub", +// method: "XPub", +// func: () async => await btc.getXPubKey(selectedDevice!, derivationPath: "m/84'/0'/$counter'"), +// ), +// DebugButton( +// title: "Get Wallet Address", +// method: "Wallet Address", +// func: () async { +// setState(() => counter++); +// final derivationPath = "m/84'/0'/$counter'/0/0"; +// return await btc.getAccounts(selectedDevice!, accountsDerivationPath: derivationPath); +// // return await ethereum!.getHardwareWalletAccounts(selectedDevice!); +// }, +// ), +// DebugButton( +// title: "Send Money", +// method: "Sig", +// func: () async { +// final psbt = PsbtV2(); +// final psbtBuf = base64.decode( +// "cHNidP8BAgQCAAAAAQQBAQEFAQIAAQ4gTW6k/cwKKu1u7m9oKr5ob7VcAC0IPkfaDitRi/FkD7sBDwQAAAAAARAE/////wEA/ekBAQAAAAABA9AYVQLI722H0osKMa/4dvMucrnKV1Myxtlp0l0BoOBDAQAAAAD/////ku6r2ABaHt9N26f/P4eMljX8t1f4lBcFfEwuNm/uXYoBAAAAAP////+YeAl8arEGKOcyrWJAYwSboyCstkhHN8zn7/vy7pkYTAEAAAAA/////wHlHgAAAAAAABYAFKdq0umSucBGVkl2MpT6Hgo/0a/xAkcwRAIgMkiJmNFbEi2I3CQYOwyV/JepCnFQRvj4xghkySpFcJMCIGAypkkWltfj+ucvqUIu27tusDAIAAB+rBhX/GV7hPlEASEDyLmWyTLjLfC9kn8pnW42jW5N6EJo5fObjWWEyfLDu9UCSDBFAiEAg9crVtwBPF+sWk+Th6pLwzDjJGItwsUCvoBPtmMTEb4CIDGuM7WOguV0TP21oidF3bSUZlEAjUHWfWzxLKw+3LofASEDfN16xKb70UZSeQyX5Tlh8iRq7np5Nlz9GYdcSU50sKwCSDBFAiEAvotOblaEiBptRWkvb6bj2MGyRjTphKLBLiHYmrRMTCgCIEKJH+z65uPSSz1NIb0d/u3bU9l0xcWk0idEsXjB+BIiASEDrAEiEtrSNKxbh6F/KPaCTafF2LVjCzb75WB+x4xSuoQAAAAAAQEf5R4AAAAAAAAWABSnatLpkrnARlZJdjKU+h4KP9Gv8SIGA3xMuxmPsBAm9aMEUBs3N46DB+Kdts3bZR/Wxt+uM0H4GKtN6bpUAACAAAAAgAAAAIAAAAAAAAAAAAABBBTk7bEOxYcdXDi1eeWraYDufm6eJgEDCOgDAAAAAAAAAAEEFDX3g/pnDXIfsRw8shK42NZn+SdpAQMIiBMAAAAAAAAiAgN8TLsZj7AQJvWjBFAbNzeOgwfinbbN22Uf1sbfrjNB+BirTem6VAAAgAAAAIAAAACAAAAAAAAAAAAA" +// ); +// psbt.deserialize(psbtBuf); +// final result = await btc.signPsbt(selectedDevice!, psbt: psbt); +// return result.toHexString(); +// }, +// ), +// Padding( +// padding: EdgeInsets.only(top: 20), +// child: PrimaryButton( +// text: "Disconnect", +// onPressed: () => disconnectCurrentDevice(), +// color: Theme.of(context).primaryColor, +// textColor: Colors.white), +// ), +// ], +// if (selectedDevice == null) ...[ +// ...devices +// .map( +// (device) => Padding( +// padding: EdgeInsets.only(bottom: 20), +// child: DeviceTile( +// onPressed: () { +// setState(() => selectedDevice = device); +// ledger.connect(device); +// }, +// title: device.name, +// leading: imageLedger, +// connectionType: device.connectionType, +// ), +// ), +// ) +// .toList(), +// PrimaryButton( +// text: "Refresh BLE", +// onPressed: () async { +// setState(() => devices = []); +// ledger.scan().listen((device) => setState(() { +// devices.add(device); +// })); +// }, +// color: Theme.of(context).primaryColor, +// textColor: Colors.white), +// Padding( +// padding: EdgeInsets.only(top: 20), +// child: PrimaryButton( +// text: "Use USB", +// onPressed: () async { +// final dev = await ledger.listUsbDevices(); +// setState(() => devices = dev); +// }, +// color: Theme.of(context).primaryColor, +// textColor: Colors.white), +// ), +// ], +// ], +// ), +// )), +// ); +// } +// +// Widget DebugButton( +// {required String title, required String method, required Future Function() func}) { +// return Padding( +// padding: EdgeInsets.only(top: 20), +// child: PrimaryButton( +// text: title, +// onPressed: () async { +// try { +// setState(() => status = "Sending..."); +// final acc = await func(); +// setState(() => status = "$method: $acc"); +// print("$method: $acc"); +// } on LedgerException catch (ex) { +// setState(() => status = "${ex.errorCode.toRadixString(16)} ${ex.message}"); +// print("${ex.errorCode.toRadixString(16)} ${ex.message}"); +// } +// }, +// color: Theme.of(context).primaryColor, +// textColor: Colors.white), +// ); +// } +// } diff --git a/lib/src/screens/connect_device/select_hardware_wallet_account_page.dart b/lib/src/screens/connect_device/select_hardware_wallet_account_page.dart new file mode 100644 index 000000000..31542ab5f --- /dev/null +++ b/lib/src/screens/connect_device/select_hardware_wallet_account_page.dart @@ -0,0 +1,256 @@ +import 'package:cake_wallet/core/wallet_name_validator.dart'; +import 'package:cake_wallet/entities/generate_name.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/themes/extensions/new_wallet_theme.dart'; +import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; + +class SelectHardwareWalletAccountPage extends BasePage { + SelectHardwareWalletAccountPage(this._walletHardwareRestoreVM); + + final WalletHardwareRestoreViewModel _walletHardwareRestoreVM; + + @override + String get title => S.current.restore_title_from_hardware_wallet; + + @override + Widget body(BuildContext context) => SelectHardwareWalletAccountForm(_walletHardwareRestoreVM); +} + +class SelectHardwareWalletAccountForm extends StatefulWidget { + SelectHardwareWalletAccountForm(this._walletHardwareRestoreVM); + + final WalletHardwareRestoreViewModel _walletHardwareRestoreVM; + + @override + _SelectHardwareWalletAccountFormState createState() => + _SelectHardwareWalletAccountFormState(_walletHardwareRestoreVM); +} + +class _SelectHardwareWalletAccountFormState extends State { + _SelectHardwareWalletAccountFormState(this._walletHardwareRestoreVM) + : _formKey = GlobalKey(), + _controller = TextEditingController(); + + final GlobalKey _formKey; + final WalletHardwareRestoreViewModel _walletHardwareRestoreVM; + final TextEditingController _controller; + + @override + void initState() { + super.initState(); + _setEffects(context); + if (_walletHardwareRestoreVM.availableAccounts.length == 0) _loadMoreAccounts(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(top: 24), + child: ScrollableWithBottomSection( + contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), + content: Center( + child: ConstrainedBox( + constraints: + BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.only(top: 0), + child: Form( + key: _formKey, + child: Stack( + alignment: Alignment.centerRight, + children: [ + TextFormField( + onChanged: (value) => _walletHardwareRestoreVM.name = value, + controller: _controller, + style: TextStyle( + fontSize: 20.0, + fontWeight: FontWeight.w600, + color: Theme.of(context).extension()!.titleColor, + ), + decoration: InputDecoration( + hintStyle: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.hintTextColor, + ), + hintText: S.of(context).wallet_name, + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: + Theme.of(context).extension()!.underlineColor, + width: 1.0, + ), + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: + Theme.of(context).extension()!.underlineColor, + width: 1.0, + ), + ), + suffixIcon: Semantics( + label: S.of(context).generate_name, + child: IconButton( + onPressed: () async { + final rName = await generateName(); + FocusManager.instance.primaryFocus?.unfocus(); + + setState(() { + _controller.text = rName; + _walletHardwareRestoreVM.name = rName; + _controller.selection = TextSelection.fromPosition( + TextPosition(offset: _controller.text.length)); + }); + }, + icon: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6.0), + color: Theme.of(context).hintColor, + ), + width: 34, + height: 34, + child: Image.asset( + 'assets/images/refresh_icon.png', + color: Theme.of(context) + .extension()! + .textFieldButtonIconColor, + ), + ), + ), + ), + ), + validator: WalletNameValidator(), + ), + ], + ), + ), + ), + Padding( + padding: EdgeInsets.only(top: 20), + child: Container( + width: double.infinity, + child: Text( + "Available accounts", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.titleColor, + ), + ), + ), + ), + Observer( + builder: (context) => Column( + children: _walletHardwareRestoreVM.availableAccounts.map((acc) { + final address = acc.address; + return Padding( + padding: EdgeInsets.only(top: 10), + child: SelectButton( + image: Image.asset( + walletTypeToCryptoCurrency(_walletHardwareRestoreVM.type).iconPath ?? + '', + height: 24, + width: 24, + ), + text: + "${address.substring(0, 6)}...${address.substring(address.length - 6)}", + showTrailingIcon: false, + height: 54, + isSelected: _walletHardwareRestoreVM.selectedAccount == acc, + onTap: () => + setState(() => _walletHardwareRestoreVM.selectedAccount = acc), + ), + ); + }).toList(), + ), + ), + Padding( + padding: EdgeInsets.only(top: 10), + child: Observer(builder: (context) { + return LoadingPrimaryButton( + onPressed: _loadMoreAccounts, + text: S.of(context).load_more, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + isLoading: _walletHardwareRestoreVM.isLoadingMoreAccounts, + ); + }), + ), + ], + ), + ), + ), + bottomSectionPadding: EdgeInsets.all(24), + bottomSection: Observer( + builder: (context) { + return LoadingPrimaryButton( + onPressed: _confirmForm, + text: S.of(context).seed_language_next, + color: Colors.green, + textColor: Colors.white, + isDisabled: _walletHardwareRestoreVM.name.isEmpty, + ); + }, + ), + ), + ); + } + + Future _loadMoreAccounts() async { + _walletHardwareRestoreVM.isLoadingMoreAccounts = true; + _walletHardwareRestoreVM.getNextAvailableAccounts(5); + } + + Future _confirmForm() async { + await _walletHardwareRestoreVM.create(); + } + + bool _effectsInstalled = false; + + void _setEffects(BuildContext context) { + if (_effectsInstalled) return; + + reaction((_) => _walletHardwareRestoreVM.error, (String? error) { + + if (error != null) { + + if (error == S.current.ledger_connection_error) + Navigator.of(context).pop(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: error, + buttonText: S.of(context).ok, + buttonAction: () { + _walletHardwareRestoreVM.error = null; + Navigator.of(context).pop(); + }); + }); + }); + } + }); + + _effectsInstalled = true; + } +} diff --git a/lib/src/screens/connect_device/widgets/device_tile.dart b/lib/src/screens/connect_device/widgets/device_tile.dart new file mode 100644 index 000000000..8367d1606 --- /dev/null +++ b/lib/src/screens/connect_device/widgets/device_tile.dart @@ -0,0 +1,78 @@ +import 'package:cake_wallet/themes/extensions/option_tile_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:ledger_flutter/ledger_flutter.dart'; + +class DeviceTile extends StatelessWidget { + const DeviceTile({ + required this.onPressed, + required this.title, + this.leading, + this.connectionType, + }); + + final VoidCallback onPressed; + final String title; + final String? leading; + final ConnectionType? connectionType; + + String? get connectionTypeIcon { + switch (connectionType) { + case ConnectionType.ble: + return 'assets/images/bluetooth.png'; + case ConnectionType.usb: + return 'assets/images/usb.png'; + case null: + return null; + } + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onPressed, + child: Container( + width: double.infinity, + padding: EdgeInsets.all(24), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(12)), + color: Theme.of(context).cardColor, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (leading != null) + Image.asset( + leading!, + height: 30, + color: Theme.of(context).extension()!.titleColor, + ), + Expanded( + child: Padding( + padding: EdgeInsets.only(left: 16), + child: Text( + title, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.titleColor, + ), + ), + ), + ), + if (connectionTypeIcon != null) + Center( + child: Image.asset( + connectionTypeIcon!, + height: 25, + color: Theme.of(context).extension()!.titleColor, + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu_item.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu_item.dart index 940ab54ca..f839f9d27 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu_item.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu_item.dart @@ -45,6 +45,7 @@ class SideMenuItem extends StatelessWidget { ? Icon( icon, color: _setColor(context), + size: 30, ) : Image.asset( imagePath ?? '', diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart index 7ba169154..d9b509136 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart @@ -82,7 +82,8 @@ class DesktopSidebarWrapper extends BasePage { width: sideMenuWidth, topItems: [ SideMenuItem( - imagePath: 'assets/images/wallet_outline.png', + // imagePath: 'assets/images/wallet_outline.png', + icon: Icons.home, isSelected: desktopSidebarViewModel.currentPage == SidebarItem.dashboard, onTap: () { desktopSidebarViewModel.onPageChange(SidebarItem.dashboard); diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart index 17a22a88f..18ab9d030 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart @@ -38,6 +38,7 @@ class _DesktopWalletSelectionDropDownState extends State Image.asset( @@ -156,6 +157,8 @@ class _DesktopWalletSelectionDropDownState extends State()! + .pageTitleTextColor, + ), + ), if (dashboardViewModel .balanceViewModel.isHomeScreenSettingsEnabled) InkWell( diff --git a/lib/src/screens/dashboard/pages/transactions_page.dart b/lib/src/screens/dashboard/pages/transactions_page.dart index 691ae7f10..a4082a88b 100644 --- a/lib/src/screens/dashboard/pages/transactions_page.dart +++ b/lib/src/screens/dashboard/pages/transactions_page.dart @@ -83,6 +83,10 @@ class TransactionsPage extends StatelessWidget { } if (item is TransactionListItem) { + if (item.hasTokens && item.assetOfTransaction == null) { + return Container(); + } + final transaction = item.transaction; final historicalFiatValue = dashboardViewModel.getFormattedFiatAmount(transaction); diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index acd666025..d9e03dbf9 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -34,7 +34,8 @@ class MenuWidgetState extends State { this.bananoIcon = Image.asset('assets/images/nano_icon.png'), this.bitcoinCashIcon = Image.asset('assets/images/bch_icon.png'), this.polygonIcon = Image.asset('assets/images/matic_icon.png'), - this.solanaIcon = Image.asset('assets/images/sol_icon.png'); + this.solanaIcon = Image.asset('assets/images/sol_icon.png'), + this.tronIcon = Image.asset('assets/images/trx_icon.png'); final largeScreen = 731; @@ -57,6 +58,7 @@ class MenuWidgetState extends State { Image bananoIcon; Image polygonIcon; Image solanaIcon; + Image tronIcon; @override void initState() { @@ -226,6 +228,8 @@ class MenuWidgetState extends State { return polygonIcon; case WalletType.solana: return solanaIcon; + case WalletType.tron: + return tronIcon; default: throw Exception('No icon for ${type.toString()}'); } diff --git a/lib/src/screens/new_wallet/new_wallet_type_page.dart b/lib/src/screens/new_wallet/new_wallet_type_page.dart index 8c8a94a7e..dc22a60db 100644 --- a/lib/src/screens/new_wallet/new_wallet_type_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_type_page.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart'; @@ -10,14 +12,20 @@ import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/wallet_types.g.dart'; +import 'package:cw_core/hardware/device_connection_type.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; class NewWalletTypePage extends BasePage { - NewWalletTypePage({required this.onTypeSelected, required this.isCreate}); + NewWalletTypePage({ + required this.onTypeSelected, + required this.isCreate, + required this.isHardwareWallet, + }); final void Function(BuildContext, WalletType) onTypeSelected; final bool isCreate; + final bool isHardwareWallet; final walletTypeImage = Image.asset('assets/images/wallet_type.png'); final walletTypeLightImage = Image.asset('assets/images/wallet_type_light.png'); @@ -31,15 +39,22 @@ class NewWalletTypePage extends BasePage { onTypeSelected: onTypeSelected, walletImage: currentTheme.type == ThemeType.dark ? walletTypeImage : walletTypeLightImage, isCreate: isCreate, + isHardwareWallet: isHardwareWallet, ); } class WalletTypeForm extends StatefulWidget { - WalletTypeForm({required this.onTypeSelected, required this.walletImage, required this.isCreate}); + WalletTypeForm({ + required this.onTypeSelected, + required this.walletImage, + required this.isCreate, + required this.isHardwareWallet, + }); final void Function(BuildContext, WalletType) onTypeSelected; final Image walletImage; final bool isCreate; + final bool isHardwareWallet; @override WalletTypeFormState createState() => WalletTypeFormState(); @@ -58,7 +73,11 @@ class WalletTypeFormState extends State { @override void initState() { - types = filteredTypes = availableWalletTypes; + types = filteredTypes = availableWalletTypes + .where((element) => + !widget.isHardwareWallet || + DeviceConnectionType.supportedConnectionTypes(element, Platform.isIOS).isNotEmpty) + .toList(); super.initState(); searchController.addListener(() { @@ -74,76 +93,81 @@ class WalletTypeFormState extends State { @override Widget build(BuildContext context) { return Center( - child: ConstrainedBox( - constraints: - BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), - child: Column( - children: [ - Padding( - padding: EdgeInsets.only(top: 48), - child: Text( - S.of(context).choose_wallet_currency, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.titleColor), - ), + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), + child: Column( + children: [ + Padding( + padding: EdgeInsets.only(top: 48), + child: Text( + S.of(context).choose_wallet_currency, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.titleColor, ), - Padding( - padding: const EdgeInsets.fromLTRB(24, 24, 24, 12), - child: SearchBarWidget(searchController: searchController, borderRadius: 24), - ), - Expanded( - child: ScrollableWithBottomSection( - contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), - content: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ...filteredTypes.map((type) => Padding( - padding: EdgeInsets.only(top: 12), - child: SelectButton( - image: Image.asset( - walletTypeToCryptoCurrency(type).iconPath ?? '', - height: 24, - width: 24), - text: walletTypeToDisplayName(type), - showTrailingIcon: false, - height: 54, - isSelected: selected == type, - onTap: () => setState(() => selected = type)), - )) - ], + ), + ), + Padding( + padding: EdgeInsets.fromLTRB(24, 24, 24, 12), + child: SearchBarWidget(searchController: searchController, borderRadius: 24), + ), + Expanded( + child: ScrollableWithBottomSection( + contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), + content: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ...filteredTypes.map( + (type) => Padding( + padding: EdgeInsets.only(top: 12), + child: SelectButton( + image: Image.asset( + walletTypeToCryptoCurrency(type).iconPath ?? '', + height: 24, + width: 24, + ), + text: walletTypeToDisplayName(type), + showTrailingIcon: false, + height: 54, + isSelected: selected == type, + onTap: () => setState(() => selected = type), + deviceConnectionTypes: widget.isHardwareWallet + ? DeviceConnectionType.supportedConnectionTypes(type, Platform.isIOS) + : [], + ), + ), ), - bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), - bottomSection: PrimaryButton( - onPressed: () => onTypeSelected(), - text: S.of(context).seed_language_next, - color: Theme.of(context).primaryColor, - textColor: Colors.white, - isDisabled: selected == null, - ), - ), + ], ), - ], - ))); + bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), + bottomSection: PrimaryButton( + onPressed: () => onTypeSelected(), + text: S.of(context).seed_language_next, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + isDisabled: selected == null, + ), + ), + ), + ], + ), + ), + ); } Future onTypeSelected() async { - if (selected == null) { - throw Exception('Wallet Type is not selected yet.'); - } + if (selected == null) throw Exception('Wallet Type is not selected yet.'); if (selected == WalletType.haven && widget.isCreate) { return await showPopUp( context: context, - builder: (BuildContext context) { - return PopUpCancellableAlertDialog( - contentText: S.of(context).pause_wallet_creation, - actionButtonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop(), - ); - }, + builder: (BuildContext context) => PopUpCancellableAlertDialog( + contentText: S.of(context).pause_wallet_creation, + actionButtonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(), + ), ); } diff --git a/lib/src/screens/new_wallet/widgets/select_button.dart b/lib/src/screens/new_wallet/widgets/select_button.dart index d94c9767d..2eda77d01 100644 --- a/lib/src/screens/new_wallet/widgets/select_button.dart +++ b/lib/src/screens/new_wallet/widgets/select_button.dart @@ -1,7 +1,8 @@ -import 'package:flutter/material.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/filter_theme.dart'; import 'package:cake_wallet/themes/extensions/wallet_list_theme.dart'; +import 'package:cw_core/hardware/device_connection_type.dart'; +import 'package:flutter/material.dart'; class SelectButton extends StatelessWidget { SelectButton({ @@ -16,6 +17,7 @@ class SelectButton extends StatelessWidget { this.textColor, this.arrowColor, this.borderColor, + this.deviceConnectionTypes, }); final Image? image; @@ -24,6 +26,7 @@ class SelectButton extends StatelessWidget { final bool isSelected; final VoidCallback onTap; final bool showTrailingIcon; + final List? deviceConnectionTypes; final double height; final Color? color; final Color? textColor; @@ -33,15 +36,26 @@ class SelectButton extends StatelessWidget { @override Widget build(BuildContext context) { final backgroundColor = color ?? (isSelected ? Colors.green : Theme.of(context).cardColor); - final effectiveTextColor = textColor ?? (isSelected - ? Theme.of(context).extension()!.restoreWalletButtonTextColor - : Theme.of(context).extension()!.buttonTextColor); - final effectiveArrowColor = arrowColor ?? (isSelected - ? Theme.of(context).extension()!.restoreWalletButtonTextColor - : Theme.of(context).extension()!.titlesColor); + final effectiveTextColor = textColor ?? + (isSelected + ? Theme.of(context).extension()!.restoreWalletButtonTextColor + : Theme.of(context).extension()!.buttonTextColor); + final effectiveArrowColor = arrowColor ?? + (isSelected + ? Theme.of(context).extension()!.restoreWalletButtonTextColor + : Theme.of(context).extension()!.titlesColor); - final selectArrowImage = Image.asset('assets/images/select_arrow.png', - color: effectiveArrowColor); + final trailingIcons = []; + final selectArrowImage = + Image.asset('assets/images/select_arrow.png', color: effectiveArrowColor); + + deviceConnectionTypes?.forEach((element) => trailingIcons.add(Image.asset( + element.iconString, + color: effectiveArrowColor, + height: 24, + ))); + + if (showTrailingIcon) trailingIcons.add(selectArrowImage); return GestureDetector( onTap: onTap, @@ -54,34 +68,33 @@ class SelectButton extends StatelessWidget { borderRadius: BorderRadius.all(Radius.circular(30)), color: backgroundColor, border: borderColor != null ? Border.all(color: borderColor!) : null, - ), child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - image ?? Offstage(), - Padding( - padding: image != null - ? EdgeInsets.only(left: 15) - : EdgeInsets.only(left: 0), - child: Text( - text, - style: TextStyle( - fontSize: textSize, - fontWeight: FontWeight.w500, - color: effectiveTextColor, + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + image ?? Offstage(), + Padding( + padding: image != null ? EdgeInsets.only(left: 15) : EdgeInsets.only(left: 0), + child: Text( + text, + style: TextStyle( + fontSize: textSize, + fontWeight: FontWeight.w500, + color: effectiveTextColor, + ), ), - ), - ) - ], + ) + ], + ), ), - if (showTrailingIcon) selectArrowImage + ...trailingIcons ], ), ), diff --git a/lib/src/screens/restore/restore_options_page.dart b/lib/src/screens/restore/restore_options_page.dart index 91ee9bd0b..a0f3a597e 100644 --- a/lib/src/screens/restore/restore_options_page.dart +++ b/lib/src/screens/restore/restore_options_page.dart @@ -1,19 +1,19 @@ import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/option_tile.dart'; +import 'package:cake_wallet/themes/extensions/option_tile_theme.dart'; +import 'package:cake_wallet/utils/permission_handler.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart'; import 'package:cake_wallet/view_model/restore/wallet_restore_from_qr_code.dart'; -import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:flutter/material.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/generated/i18n.dart'; import 'package:permission_handler/permission_handler.dart'; -import 'package:cake_wallet/utils/permission_handler.dart'; class RestoreOptionsPage extends BasePage { RestoreOptionsPage({required this.isNewInstall}); @@ -22,12 +22,15 @@ class RestoreOptionsPage extends BasePage { String get title => S.current.restore_restore_wallet; final bool isNewInstall; - final imageSeedKeys = Image.asset('assets/images/restore_wallet_image.png'); - final imageBackup = Image.asset('assets/images/backup.png'); - final qrCode = Image.asset('assets/images/restore_qr.png'); @override Widget body(BuildContext context) { + final imageColor = Theme.of(context).extension()!.titleColor; + final imageLedger = Image.asset('assets/images/ledger_nano.png', width: 40, color: imageColor); + final imageSeedKeys = Image.asset('assets/images/restore_wallet_image.png', color: imageColor); + final imageBackup = Image.asset('assets/images/backup.png', color: imageColor); + final qrCode = Image.asset('assets/images/restore_qr.png', color: imageColor); + return Center( child: Container( width: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint, @@ -37,53 +40,37 @@ class RestoreOptionsPage extends BasePage { child: Column( children: [ OptionTile( - onPressed: () => Navigator.pushNamed(context, Routes.restoreWalletFromSeedKeys, - arguments: isNewInstall), - image: imageSeedKeys, - title: S.of(context).restore_title_from_seed_keys, - description: S.of(context).restore_description_from_seed_keys), + onPressed: () => Navigator.pushNamed(context, Routes.restoreWalletFromSeedKeys, + arguments: isNewInstall), + image: imageSeedKeys, + title: S.of(context).restore_title_from_seed_keys, + description: S.of(context).restore_description_from_seed_keys, + ), if (isNewInstall) Padding( padding: EdgeInsets.only(top: 24), child: OptionTile( - onPressed: () => Navigator.pushNamed(context, Routes.restoreFromBackup), - image: imageBackup, - title: S.of(context).restore_title_from_backup, - description: S.of(context).restore_description_from_backup), + onPressed: () => Navigator.pushNamed(context, Routes.restoreFromBackup), + image: imageBackup, + title: S.of(context).restore_title_from_backup, + description: S.of(context).restore_description_from_backup, + ), ), Padding( padding: EdgeInsets.only(top: 24), child: OptionTile( - onPressed: () async { - bool isCameraPermissionGranted = - await PermissionHandler.checkPermission(Permission.camera, context); - if (!isCameraPermissionGranted) return; - bool isPinSet = false; - if (isNewInstall) { - await Navigator.pushNamed(context, Routes.setupPin, - arguments: (PinCodeState setupPinContext, String _) { - setupPinContext.close(); - isPinSet = true; - }); - } - if (!isNewInstall || isPinSet) { - try { - final restoreWallet = - await WalletRestoreFromQRCode.scanQRCodeForRestoring(context); - - final restoreFromQRViewModel = - getIt.get(param1: restoreWallet.type); - - await restoreFromQRViewModel.create(restoreWallet: restoreWallet); - if (restoreFromQRViewModel.state is FailureState) { - _onWalletCreateFailure(context, - 'Create wallet state: ${(restoreFromQRViewModel.state as FailureState).error}'); - } - } catch (e) { - _onWalletCreateFailure(context, e.toString()); - } - } - }, + onPressed: () => Navigator.pushNamed( + context, Routes.restoreWalletFromHardwareWallet, + arguments: isNewInstall), + image: imageLedger, + title: S.of(context).restore_title_from_hardware_wallet, + description: S.of(context).restore_description_from_hardware_wallet, + ), + ), + Padding( + padding: EdgeInsets.only(top: 24), + child: OptionTile( + onPressed: () => _onScanQRCode(context), image: qrCode, title: S.of(context).scan_qr_code, description: S.of(context).cold_or_recover_wallet), @@ -105,4 +92,35 @@ class RestoreOptionsPage extends BasePage { buttonAction: () => Navigator.of(context).pop()); }); } + + Future _onScanQRCode(BuildContext context) async { + final isCameraPermissionGranted = + await PermissionHandler.checkPermission(Permission.camera, context); + + if (!isCameraPermissionGranted) return; + bool isPinSet = false; + if (isNewInstall) { + await Navigator.pushNamed(context, Routes.setupPin, + arguments: (PinCodeState setupPinContext, String _) { + setupPinContext.close(); + isPinSet = true; + }); + } + if (!isNewInstall || isPinSet) { + try { + final restoreWallet = await WalletRestoreFromQRCode.scanQRCodeForRestoring(context); + + final restoreFromQRViewModel = + getIt.get(param1: restoreWallet.type); + + await restoreFromQRViewModel.create(restoreWallet: restoreWallet); + if (restoreFromQRViewModel.state is FailureState) { + _onWalletCreateFailure(context, + 'Create wallet state: ${(restoreFromQRViewModel.state as FailureState).error}'); + } + } catch (e) { + _onWalletCreateFailure(context, e.toString()); + } + } + } } diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart index 8a75b78bb..e3472f510 100644 --- a/lib/src/screens/root/root.dart +++ b/lib/src/screens/root/root.dart @@ -52,6 +52,7 @@ class RootState extends State with WidgetsBindingObserver { StreamSubscription? stream; ReactionDisposer? _walletReactionDisposer; + ReactionDisposer? _deepLinksReactionDisposer; Uri? launchUri; @override @@ -76,6 +77,7 @@ class RootState extends State with WidgetsBindingObserver { void dispose() { stream?.cancel(); _walletReactionDisposer?.call(); + _deepLinksReactionDisposer?.call(); super.dispose(); } @@ -93,10 +95,32 @@ class RootState extends State with WidgetsBindingObserver { } } - void handleDeepLinking(Uri? uri) { + void handleDeepLinking(Uri? uri) async { if (uri == null || !mounted) return; launchUri = uri; + + bool requireAuth = await widget.authService.requireAuth(); + + if (!requireAuth && widget.authenticationStore.state == AuthenticationState.allowed) { + _navigateToDeepLinkScreen(); + return; + } + + _deepLinksReactionDisposer = reaction( + (_) => widget.authenticationStore.state, + (AuthenticationState state) { + if (state == AuthenticationState.allowed) { + if (widget.appStore.wallet == null) { + waitForWalletInstance(context, launchUri!); + } else { + _navigateToDeepLinkScreen(); + } + _deepLinksReactionDisposer?.call(); + _deepLinksReactionDisposer = null; + } + }, + ); } @override @@ -172,35 +196,8 @@ class RootState extends State with WidgetsBindingObserver { }, ); }); - } else if (_isValidPaymentUri()) { - if (widget.authenticationStore.state == AuthenticationState.uninitialized) { - launchUri = null; - } else { - if (widget.appStore.wallet == null) { - waitForWalletInstance(context, launchUri!); - launchUri = null; - } else { - widget.navigatorKey.currentState?.pushNamed( - Routes.send, - arguments: PaymentRequest.fromUri(launchUri), - ); - launchUri = null; - } - } - launchUri = null; - } else if (isWalletConnectLink) { - if (isEVMCompatibleChain(widget.appStore.wallet!.type)) { - widget.navigatorKey.currentState?.pushNamed( - Routes.walletConnectConnectionsListing, - arguments: launchUri, - ); - launchUri = null; - } else { - _nonETHWalletErrorToast(S.current.switchToEVMCompatibleWallet); - } } - launchUri = null; return WillPopScope( onWillPop: () async => false, child: widget.child, @@ -252,13 +249,10 @@ class RootState extends State with WidgetsBindingObserver { WidgetsBinding.instance.addPostFrameCallback((_) { if (context.mounted) { _walletReactionDisposer = reaction( - (_) => widget.appStore.wallet, - (WalletBase? wallet) { + (_) => widget.appStore.wallet, + (WalletBase? wallet) { if (wallet != null) { - widget.navigatorKey.currentState?.pushNamed( - Routes.send, - arguments: PaymentRequest.fromUri(tempLaunchUri), - ); + _navigateToDeepLinkScreen(); _walletReactionDisposer?.call(); _walletReactionDisposer = null; } @@ -267,4 +261,16 @@ class RootState extends State with WidgetsBindingObserver { } }); } + + void _navigateToDeepLinkScreen() { + if (_getRouteToGo() != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.navigatorKey.currentState?.pushNamed( + _getRouteToGo()!, + arguments: isWalletConnectLink ? launchUri : PaymentRequest.fromUri(launchUri), + ); + launchUri = null; + }); + } + } } diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 9067a2951..648133391 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -1,39 +1,40 @@ import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/entities/contact_record.dart'; +import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/entities/template.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart'; +import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart'; import 'package:cake_wallet/src/screens/send/widgets/send_card.dart'; import 'package:cake_wallet/src/widgets/add_template_button.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/picker.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/template_tile.dart'; +import 'package:cake_wallet/src/widgets/trail_button.dart'; import 'package:cake_wallet/themes/extensions/seed_widget_theme.dart'; import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/utils/request_review_handler.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:cake_wallet/view_model/send/send_view_model.dart'; +import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/view_model/send/send_view_model.dart'; -import 'package:cake_wallet/core/execution_state.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; -import 'package:cake_wallet/src/widgets/trail_button.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart'; -import 'package:cw_core/crypto_currency.dart'; class SendPage extends BasePage { SendPage({ @@ -369,6 +370,21 @@ class SendPage extends BasePage { return; } + if (sendViewModel.wallet.isHardwareWallet) { + if (!sendViewModel.ledgerViewModel.isConnected) { + await Navigator.of(context).pushNamed(Routes.connectDevices, + arguments: ConnectDevicePageParams( + walletType: sendViewModel.walletType, + onConnectDevice: (BuildContext context, _) { + sendViewModel.ledgerViewModel.setLedger(sendViewModel.wallet); + Navigator.of(context).pop(); + }, + )); + } else { + sendViewModel.ledgerViewModel.setLedger(sendViewModel.wallet); + } + } + final check = sendViewModel.shouldDisplayTotp(); authService.authenticateAction( context, @@ -384,7 +400,8 @@ class SendPage extends BasePage { color: Theme.of(context).primaryColor, textColor: Colors.white, isLoading: sendViewModel.state is IsExecutingState || - sendViewModel.state is TransactionCommitting, + sendViewModel.state is TransactionCommitting || + sendViewModel.state is IsAwaitingDeviceResponseState, isDisabled: !sendViewModel.isReadyForSend, ); }, @@ -395,12 +412,20 @@ class SendPage extends BasePage { ); } + BuildContext? dialogContext; + void _setEffects(BuildContext context) { if (_effectsInstalled) { return; } reaction((_) => sendViewModel.state, (ExecutionState state) { + + if (dialogContext != null && dialogContext?.mounted == true) { + Navigator.of(dialogContext!).pop(); + } + + if (state is FailureState) { WidgetsBinding.instance.addPostFrameCallback((_) { showPopUp( @@ -510,6 +535,21 @@ class SendPage extends BasePage { sendViewModel.clearOutputs(); }); } + + if (state is IsAwaitingDeviceResponseState) { + WidgetsBinding.instance.addPostFrameCallback((_) { + showPopUp( + context: context, + builder: (BuildContext context) { + dialogContext = context; + return AlertWithOneAction( + alertTitle: S.of(context).proceed_on_device, + alertContent: S.of(context).proceed_on_device_description, + buttonText: S.of(context).cancel, + buttonAction: () => Navigator.of(context).pop()); + }); + }); + } }); _effectsInstalled = true; diff --git a/lib/src/screens/settings/connection_sync_page.dart b/lib/src/screens/settings/connection_sync_page.dart index 8c4da4cc5..7b4fb3b1c 100644 --- a/lib/src/screens/settings/connection_sync_page.dart +++ b/lib/src/screens/settings/connection_sync_page.dart @@ -1,10 +1,14 @@ import 'dart:io'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; import 'package:cake_wallet/src/screens/settings/widgets/wallet_connect_button.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; @@ -12,10 +16,6 @@ import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/settings/sync_mode.dart'; import 'package:cw_core/battery_optimization_native.dart'; import 'package:flutter/material.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; class ConnectionSyncPage extends BasePage { @@ -106,7 +106,8 @@ class ConnectionSyncPage extends BasePage { ); }, ), - if (isWalletConnectCompatibleChain(dashboardViewModel.wallet.type)) ...[ + if (isWalletConnectCompatibleChain(dashboardViewModel.wallet.type) && + !dashboardViewModel.wallet.isHardwareWallet) ...[ // ToDo: Remove this line once WalletConnect is implemented WalletConnectTile( onTap: () => Navigator.of(context).pushNamed(Routes.walletConnectConnectionsListing), ), diff --git a/lib/src/screens/settings/domain_lookups_page.dart b/lib/src/screens/settings/domain_lookups_page.dart index 80849b8ea..aa7e68cd0 100644 --- a/lib/src/screens/settings/domain_lookups_page.dart +++ b/lib/src/screens/settings/domain_lookups_page.dart @@ -38,7 +38,7 @@ class DomainLookupsPage extends BasePage { value: _privacySettingsViewModel.looksUpUnstoppableDomains, onValueChange: (_, bool value) => _privacySettingsViewModel.setLookupsUnstoppableDomains(value)), SettingsSwitcherCell( - title: 'OpenAlias,', + title: 'OpenAlias', value: _privacySettingsViewModel.looksUpOpenAlias, onValueChange: (_, bool value) => _privacySettingsViewModel.setLookupsOpenAlias(value)), SettingsSwitcherCell( diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index 81c78b1ab..601f5d878 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -104,6 +104,7 @@ class WalletListBodyState extends State { final nanoIcon = Image.asset('assets/images/nano_icon.png', height: 24, width: 24); final polygonIcon = Image.asset('assets/images/matic_icon.png', height: 24, width: 24); final solanaIcon = Image.asset('assets/images/sol_icon.png', height: 24, width: 24); + final tronIcon = Image.asset('assets/images/trx_icon.png', height: 24, width: 24); final scrollController = ScrollController(); final double tileHeight = 60; Flushbar? _progressBar; @@ -316,6 +317,8 @@ class WalletListBodyState extends State { return polygonIcon; case WalletType.solana: return solanaIcon; + case WalletType.tron: + return tronIcon; default: return nonWalletTypeIcon; } diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 85e14d3e6..e94c46553 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -886,6 +886,7 @@ abstract class SettingsStoreBase with Store { final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final nanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey); final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); + final tronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); @@ -896,6 +897,7 @@ abstract class SettingsStoreBase with Store { final nanoNode = nodeSource.get(nanoNodeId); final nanoPowNode = powNodeSource.get(nanoPowNodeId); final solanaNode = nodeSource.get(solanaNodeId); + final tronNode = nodeSource.get(tronNodeId); final packageInfo = await PackageInfo.fromPlatform(); final deviceName = await _getDeviceName() ?? ''; final shouldShowYatPopup = sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? true; @@ -958,6 +960,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.solana] = solanaNode; } + if (tronNode != null) { + nodes[WalletType.tron] = tronNode; + } + final savedSyncMode = SyncMode.all.firstWhere((element) { return element.type.index == (sharedPreferences.getInt(PreferencesKey.syncModeKey) ?? 0); }); @@ -1256,6 +1262,7 @@ abstract class SettingsStoreBase with Store { final polygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey); final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); + final tronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); @@ -1265,6 +1272,7 @@ abstract class SettingsStoreBase with Store { final bitcoinCashNode = nodeSource.get(bitcoinCashElectrumServerId); final nanoNode = nodeSource.get(nanoNodeId); final solanaNode = nodeSource.get(solanaNodeId); + final tronNode = nodeSource.get(tronNodeId); if (moneroNode != null) { nodes[WalletType.monero] = moneroNode; } @@ -1301,6 +1309,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.solana] = solanaNode; } + if (tronNode != null) { + nodes[WalletType.tron] = tronNode; + } + // MIGRATED: useTOTP2FA = await SecureKey.getBool( @@ -1431,6 +1443,9 @@ abstract class SettingsStoreBase with Store { case WalletType.solana: await _sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, node.key as int); break; + case WalletType.tron: + await _sharedPreferences.setInt(PreferencesKey.currentTronNodeIdKey, node.key as int); + break; default: break; } diff --git a/lib/tron/cw_tron.dart b/lib/tron/cw_tron.dart new file mode 100644 index 000000000..6e4b0a7b0 --- /dev/null +++ b/lib/tron/cw_tron.dart @@ -0,0 +1,114 @@ +part of 'tron.dart'; + +class CWTron extends Tron { + @override + List getTronWordList(String language) => EVMChainMnemonics.englishWordlist; + + WalletService createTronWalletService(Box walletInfoSource) => + TronWalletService(walletInfoSource, client: TronClient()); + + @override + WalletCredentials createTronNewWalletCredentials({ + required String name, + WalletInfo? walletInfo, + }) => + TronNewWalletCredentials(name: name, walletInfo: walletInfo); + + @override + WalletCredentials createTronRestoreWalletFromSeedCredentials({ + required String name, + required String mnemonic, + required String password, + }) => + TronRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic); + + @override + WalletCredentials createTronRestoreWalletFromPrivateKey({ + required String name, + required String privateKey, + required String password, + }) => + TronRestoreWalletFromPrivateKey(name: name, password: password, privateKey: privateKey); + + @override + String getAddress(WalletBase wallet) => (wallet as TronWallet).walletAddresses.address; + + Object createTronTransactionCredentials( + List outputs, { + required CryptoCurrency currency, + }) => + TronTransactionCredentials( + outputs + .map( + (out) => OutputInfo( + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmount, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + formattedCryptoAmount: out.formattedCryptoAmount, + ), + ) + .toList(), + currency: currency, + ); + + @override + List getTronTokenCurrencies(WalletBase wallet) => + (wallet as TronWallet).tronTokenCurrencies; + + @override + Future addTronToken(WalletBase wallet, CryptoCurrency token, String contractAddress) async { + final tronToken = TronToken( + name: token.name, + symbol: token.title, + contractAddress: contractAddress, + decimal: token.decimals, + enabled: token.enabled, + iconPath: token.iconPath, + ); + await (wallet as TronWallet).addTronToken(tronToken); + } + + @override + Future deleteTronToken(WalletBase wallet, CryptoCurrency token) async => + await (wallet as TronWallet).deleteTronToken(token as TronToken); + + @override + Future getTronToken(WalletBase wallet, String contractAddress) async => + (wallet as TronWallet).getTronToken(contractAddress); + + @override + double getTransactionAmountRaw(TransactionInfo transactionInfo) { + final amount = (transactionInfo as TronTransactionInfo).rawTronAmount(); + return double.parse(amount); + } + + @override + CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) { + transaction as TronTransactionInfo; + if (transaction.tokenSymbol == CryptoCurrency.trx.title) { + return CryptoCurrency.trx; + } + + wallet as TronWallet; + return wallet.tronTokenCurrencies.firstWhere( + (element) => transaction.tokenSymbol.toLowerCase() == element.symbol.toLowerCase()); + } + + @override + String getTokenAddress(CryptoCurrency asset) => (asset as TronToken).contractAddress; + + @override + String getTronBase58Address(String hexAddress, WalletBase wallet) => + (wallet as TronWallet).getTronBase58AddressFromHex(hexAddress); + + @override + String? getTronNativeEstimatedFee(WalletBase wallet) => + (wallet as TronWallet).nativeTxEstimatedFee; + + @override + String? getTronTRC20EstimatedFee(WalletBase wallet) => (wallet as TronWallet).trc20EstimatedFee; +} diff --git a/lib/view_model/advanced_privacy_settings_view_model.dart b/lib/view_model/advanced_privacy_settings_view_model.dart index a17ddff36..c87e097c3 100644 --- a/lib/view_model/advanced_privacy_settings_view_model.dart +++ b/lib/view_model/advanced_privacy_settings_view_model.dart @@ -38,6 +38,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { case WalletType.bitcoinCash: case WalletType.polygon: case WalletType.solana: + case WalletType.tron: return true; case WalletType.monero: case WalletType.none: diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index eee53516e..6f4db52a6 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -80,7 +80,9 @@ abstract class BalanceViewModelBase with Store { @computed bool get isHomeScreenSettingsEnabled => - isEVMCompatibleChain(wallet.type) || wallet.type == WalletType.solana; + isEVMCompatibleChain(wallet.type) || + wallet.type == WalletType.solana || + wallet.type == WalletType.tron; @computed bool get hasAccounts => wallet.type == WalletType.monero; @@ -126,6 +128,7 @@ abstract class BalanceViewModelBase with Store { case WalletType.nano: case WalletType.banano: case WalletType.solana: + case WalletType.tron: return S.current.xmr_available_balance; default: return S.current.confirmed; @@ -140,6 +143,7 @@ abstract class BalanceViewModelBase with Store { case WalletType.ethereum: case WalletType.polygon: case WalletType.solana: + case WalletType.tron: return S.current.xmr_full_balance; case WalletType.nano: case WalletType.banano: @@ -287,6 +291,7 @@ abstract class BalanceViewModelBase with Store { case WalletType.ethereum: case WalletType.polygon: case WalletType.solana: + case WalletType.tron: return false; default: return true; diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 87f0e93f3..3d52133cb 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -536,7 +536,9 @@ abstract class DashboardViewModelBase with Store { final path = await pathForWallet(name: walletInfo.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 mnemonic = data['mnemonic'] as String?; + + if (mnemonic == null) continue; final hash = await Cryptography.instance.sha256().hash(utf8.encode(mnemonic)); final seedSha = bytesToHex(hash.bytes); diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart index e60a37ccf..9e3be746e 100644 --- a/lib/view_model/dashboard/home_settings_view_model.dart +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; @@ -79,6 +80,10 @@ abstract class HomeSettingsViewModelBase with Store { ); } + if (_balanceViewModel.wallet.type == WalletType.tron) { + await tron!.addTronToken(_balanceViewModel.wallet, token, contractAddress); + } + _updateTokensList(); _updateFiatPrices(token); } @@ -96,6 +101,9 @@ abstract class HomeSettingsViewModelBase with Store { await solana!.deleteSPLToken(_balanceViewModel.wallet, token); } + if (_balanceViewModel.wallet.type == WalletType.tron) { + await tron!.deleteTronToken(_balanceViewModel.wallet, token); + } _updateTokensList(); } @@ -112,6 +120,10 @@ abstract class HomeSettingsViewModelBase with Store { return await solana!.getSPLToken(_balanceViewModel.wallet, contractAddress); } + if (_balanceViewModel.wallet.type == WalletType.tron) { + return await tron!.getTronToken(_balanceViewModel.wallet, contractAddress); + } + return null; } @@ -143,6 +155,11 @@ abstract class HomeSettingsViewModelBase with Store { solana!.addSPLToken(_balanceViewModel.wallet, token, address); } + if (_balanceViewModel.wallet.type == WalletType.tron) { + final address = tron!.getTokenAddress(token); + tron!.addTronToken(_balanceViewModel.wallet, token, address); + } + _refreshTokensList(); } @@ -189,6 +206,14 @@ abstract class HomeSettingsViewModelBase with Store { .toList() ..sort(_sortFunc)); } + + if (_balanceViewModel.wallet.type == WalletType.tron) { + tokens.addAll(tron! + .getTronTokenCurrencies(_balanceViewModel.wallet) + .where((element) => _matchesSearchText(element)) + .toList() + ..sort(_sortFunc)); + } } @action @@ -207,7 +232,7 @@ abstract class HomeSettingsViewModelBase with Store { bool _matchesSearchText(CryptoCurrency asset) { final address = getTokenAddressBasedOnWallet(asset); - // The homes settings would only be displayed for either of Ethereum, Polygon or Solana Wallets. + // The homes settings would only be displayed for either of Tron, Ethereum, Polygon or Solana Wallets. if (address == null) return false; return searchText.isEmpty || @@ -217,6 +242,10 @@ abstract class HomeSettingsViewModelBase with Store { } String? getTokenAddressBasedOnWallet(CryptoCurrency asset) { + if (_balanceViewModel.wallet.type == WalletType.tron) { + return tron!.getTokenAddress(asset); + } + if (_balanceViewModel.wallet.type == WalletType.solana) { return solana!.getTokenAddress(asset); } @@ -229,7 +258,7 @@ abstract class HomeSettingsViewModelBase with Store { return polygon!.getTokenAddress(asset); } - // We return null if it's neither Polygin, Ethereum or Solana wallet (which is actually impossible because we only display home settings for either of these three wallets). + // We return null if it's neither Tron, Polygon, Ethereum or Solana wallet (which is actually impossible because we only display home settings for either of these three wallets). return null; } } diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 99de14a18..fb5348a29 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -4,7 +4,10 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/solana/solana.dart'; +import 'package:cake_wallet/tron/tron.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -34,6 +37,11 @@ class TransactionListItem extends ActionListItem with Keyable { @override dynamic get keyIndex => transaction.id; + bool get hasTokens => + isEVMCompatibleChain(balanceViewModel.wallet.type) || + balanceViewModel.wallet.type == WalletType.solana || + balanceViewModel.wallet.type == WalletType.tron; + String get formattedCryptoAmount { return displayMode == BalanceDisplayMode.hiddenBalance ? '---' : transaction.amountFormatted(); } @@ -63,6 +71,34 @@ class TransactionListItem extends ActionListItem with Keyable { return transaction.isPending ? S.current.pending : ''; } + CryptoCurrency? get assetOfTransaction { + try { + if (balanceViewModel.wallet.type == WalletType.ethereum) { + final asset = ethereum!.assetOfTransaction(balanceViewModel.wallet, transaction); + return asset; + } + + if (balanceViewModel.wallet.type == WalletType.polygon) { + final asset = polygon!.assetOfTransaction(balanceViewModel.wallet, transaction); + return asset; + } + + if (balanceViewModel.wallet.type == WalletType.solana) { + final asset = solana!.assetOfTransaction(balanceViewModel.wallet, transaction); + return asset; + } + + if (balanceViewModel.wallet.type == WalletType.tron) { + final asset = tron!.assetOfTransaction(balanceViewModel.wallet, transaction); + return asset; + } + } catch (e) { + return null; + } + + return null; + } + String get formattedFiatAmount { var amount = ''; @@ -114,6 +150,16 @@ class TransactionListItem extends ActionListItem with Keyable { price: price, ); break; + + case WalletType.tron: + final asset = tron!.assetOfTransaction(balanceViewModel.wallet, transaction); + final price = balanceViewModel.fiatConvertationStore.prices[asset]; + final cryptoAmount = tron!.getTransactionAmountRaw(transaction); + amount = calculateFiatAmountRaw( + cryptoAmount: cryptoAmount, + price: price, + ); + break; default: break; } diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 9bd9ef913..94fec2fa2 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -178,6 +178,10 @@ abstract class ExchangeTradeViewModelBase with Store { wallet.currency == CryptoCurrency.maticpoly && tradesStore.trade!.from.tag == CryptoCurrency.maticpoly.tag; + bool _isTronToken() => + wallet.currency == CryptoCurrency.trx && + tradesStore.trade!.from.tag == CryptoCurrency.trx.title; + bool _isSplToken() => wallet.currency == CryptoCurrency.sol && tradesStore.trade!.from.tag == CryptoCurrency.sol.title; @@ -186,6 +190,7 @@ abstract class ExchangeTradeViewModelBase with Store { tradesStore.trade!.provider == ExchangeProviderDescription.xmrto || _isEthToken() || _isPolygonToken() || - _isSplToken(); + _isSplToken() || + _isTronToken(); } } diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 4e5902faa..e5533f48a 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -676,6 +676,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with depositCurrency = CryptoCurrency.sol; receiveCurrency = CryptoCurrency.xmr; break; + case WalletType.tron: + depositCurrency = CryptoCurrency.trx; + receiveCurrency = CryptoCurrency.xmr; + break; default: break; } diff --git a/lib/view_model/hardware_wallet/ledger_view_model.dart b/lib/view_model/hardware_wallet/ledger_view_model.dart new file mode 100644 index 000000000..453df44db --- /dev/null +++ b/lib/view_model/hardware_wallet/ledger_view_model.dart @@ -0,0 +1,66 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class LedgerViewModel { + final Ledger ledger = Ledger( + options: LedgerOptions( + scanMode: ScanMode.balanced, + maxScanDuration: const Duration(minutes: 5), + ), + onPermissionRequest: (_) async { + Map statuses = await [ + Permission.bluetoothScan, + Permission.bluetoothConnect, + Permission.bluetoothAdvertise, + ].request(); + + return statuses.values.where((status) => status.isDenied).isEmpty; + }, + ); + + Future connectLedger(LedgerDevice device) async { + await ledger.connect(device); + + if (device.connectionType == ConnectionType.usb) _device = device; + } + + LedgerDevice? _device; + + bool get isConnected => ledger.devices.isNotEmpty || _device != null; + + LedgerDevice get device => _device ?? ledger.devices.first; + + void setLedger(WalletBase wallet) { + switch (wallet.type) { + case WalletType.bitcoin: + return bitcoin!.setLedger(wallet, ledger, device); + case WalletType.ethereum: + return ethereum!.setLedger(wallet, ledger, device); + case WalletType.polygon: + return polygon!.setLedger(wallet, ledger, device); + default: + throw Exception('Unexpected wallet type: ${wallet.type}'); + } + } + + String? interpretErrorCode(String errorCode) { + switch (errorCode) { + case "6985": + return S.current.ledger_error_tx_rejected_by_user; + case "5515": + return S.current.ledger_error_device_locked; + case "6d02": // UNKNOWN_APDU + case "6511": + case "6e00": + return S.current.ledger_error_wrong_app; + default: + return null; + } + } +} diff --git a/lib/view_model/node_list/node_create_or_edit_view_model.dart b/lib/view_model/node_list/node_create_or_edit_view_model.dart index 7fe3d1c98..000c9bdea 100644 --- a/lib/view_model/node_list/node_create_or_edit_view_model.dart +++ b/lib/view_model/node_list/node_create_or_edit_view_model.dart @@ -76,6 +76,7 @@ abstract class NodeCreateOrEditViewModelBase with Store { case WalletType.solana: case WalletType.banano: case WalletType.nano: + case WalletType.tron: return true; case WalletType.none: case WalletType.monero: diff --git a/lib/view_model/node_list/node_list_view_model.dart b/lib/view_model/node_list/node_list_view_model.dart index 9c2d2611e..a7fe9c6ca 100644 --- a/lib/view_model/node_list/node_list_view_model.dart +++ b/lib/view_model/node_list/node_list_view_model.dart @@ -82,6 +82,9 @@ abstract class NodeListViewModelBase with Store { case WalletType.solana: node = getSolanaDefaultNode(nodes: _nodeSource)!; break; + case WalletType.tron: + node = getTronDefaultNode(nodes: _nodeSource)!; + break; default: throw Exception('Unexpected wallet type: ${_appStore.wallet!.type}'); } diff --git a/lib/view_model/restore/restore_from_qr_vm.dart b/lib/view_model/restore/restore_from_qr_vm.dart index b9b493f04..1e9aea2c2 100644 --- a/lib/view_model/restore/restore_from_qr_vm.dart +++ b/lib/view_model/restore/restore_from_qr_vm.dart @@ -4,13 +4,14 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; +import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/view_model/restore/restore_mode.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/store/app_store.dart'; -import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_base.dart'; import 'package:cake_wallet/core/generate_wallet_password.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:cw_core/wallet_credentials.dart'; @@ -86,6 +87,9 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store case WalletType.solana: return solana!.createSolanaRestoreWalletFromPrivateKey( name: name, password: password, privateKey: restoreWallet.privateKey!); + case WalletType.tron: + return tron!.createTronRestoreWalletFromPrivateKey( + name: name, password: password, privateKey: restoreWallet.privateKey!); default: throw Exception('Unexpected type: ${restoreWallet.type.toString()}'); } @@ -130,6 +134,9 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store case WalletType.solana: return solana!.createSolanaRestoreWalletFromSeedCredentials( name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); + case WalletType.tron: + return tron!.createTronRestoreWalletFromSeedCredentials( + name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); default: throw Exception('Unexpected type: ${type.toString()}'); } diff --git a/lib/view_model/restore/wallet_restore_from_qr_code.dart b/lib/view_model/restore/wallet_restore_from_qr_code.dart index 925c08cca..09b5c9d96 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -33,6 +33,9 @@ class WalletRestoreFromQRCode { 'bitcoincash-wallet': WalletType.bitcoinCash, 'bitcoincash_wallet': WalletType.bitcoinCash, 'solana-wallet': WalletType.solana, + 'tron': WalletType.tron, + 'tron-wallet': WalletType.tron, + 'tron_wallet': WalletType.tron, }; static bool _containsAssetSpecifier(String code) => _extractWalletType(code) != null; @@ -184,6 +187,14 @@ class WalletRestoreFromQRCode { return WalletRestoreMode.keys; } + if (type == WalletType.tron && credentials.containsKey('private_key')) { + final privateKey = credentials['private_key'] as String; + if (privateKey.isEmpty) { + throw Exception('Unexpected restore mode: private_key'); + } + return WalletRestoreMode.keys; + } + throw Exception('Unexpected restore mode: restore params are invalid'); } } diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index 07d98ff32..a79baea48 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -8,6 +8,7 @@ import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart'; +import 'package:cake_wallet/tron/tron.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -117,6 +118,17 @@ abstract class OutputBase with Store { @computed double get estimatedFee { try { + if (_wallet.type == WalletType.tron) { + if (cryptoCurrencyHandler() == CryptoCurrency.trx) { + final nativeEstimatedFee = tron!.getTronNativeEstimatedFee(_wallet) ?? 0; + return double.parse(nativeEstimatedFee.toString()); + } else { + final trc20EstimatedFee = tron!.getTronTRC20EstimatedFee(_wallet) ?? 0; + return double.parse(trc20EstimatedFee.toString()); + } + + } + if (_wallet.type == WalletType.solana) { return solana!.getEstimateFees(_wallet) ?? 0.0; } @@ -163,8 +175,11 @@ abstract class OutputBase with Store { @computed String get estimatedFeeFiatAmount { try { - final currency = - isEVMCompatibleChain(_wallet.type) ? _wallet.currency : cryptoCurrencyHandler(); + final currency = (isEVMCompatibleChain(_wallet.type) || + _wallet.type == WalletType.solana || + _wallet.type == WalletType.tron) + ? _wallet.currency + : cryptoCurrencyHandler(); final fiat = calculateFiatAmountRaw( price: _fiatConversationStore.prices[currency]!, cryptoAmount: estimatedFee); return fiat; @@ -269,6 +284,9 @@ abstract class OutputBase with Store { case WalletType.solana: maximumFractionDigits = 12; break; + case WalletType.tron: + maximumFractionDigits = 12; + break; default: break; } diff --git a/lib/view_model/send/send_template_view_model.dart b/lib/view_model/send/send_template_view_model.dart index f79fbddc7..66a3c37c8 100644 --- a/lib/view_model/send/send_template_view_model.dart +++ b/lib/view_model/send/send_template_view_model.dart @@ -53,7 +53,8 @@ abstract class SendTemplateViewModelBase with Store { _wallet.type != WalletType.haven && _wallet.type != WalletType.ethereum && _wallet.type != WalletType.polygon && - _wallet.type != WalletType.solana; + _wallet.type != WalletType.solana && + _wallet.type != WalletType.tron; @computed CryptoCurrency get cryptoCurrency => _wallet.currency; diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 6c0c3870b..2a6bf553b 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -12,13 +12,16 @@ import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; +import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cw_core/exceptions.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cake_wallet/view_model/send/send_template_view_model.dart'; import 'package:hive/hive.dart'; +import 'package:ledger_flutter/ledger_flutter.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/entities/template.dart'; import 'package:cake_wallet/core/address_validator.dart'; @@ -50,7 +53,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor void onWalletChange(wallet) { currencies = wallet.balance.keys.toList(); selectedCryptoCurrency = wallet.currency; - hasMultipleTokens = isEVMCompatibleChain(wallet.type) || wallet.type == WalletType.solana; + hasMultipleTokens = isEVMCompatibleChain(wallet.type) || + wallet.type == WalletType.solana || + wallet.type == WalletType.tron; } SendViewModelBase( @@ -60,11 +65,13 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor this.balanceViewModel, this.contactListViewModel, this.transactionDescriptionBox, + this.ledgerViewModel, ) : state = InitialExecutionState(), currencies = appStore.wallet!.balance.keys.toList(), selectedCryptoCurrency = appStore.wallet!.currency, hasMultipleTokens = isEVMCompatibleChain(appStore.wallet!.type) || - appStore.wallet!.type == WalletType.solana, + appStore.wallet!.type == WalletType.solana || + appStore.wallet!.type == WalletType.tron, outputs = ObservableList(), _settingsStore = appStore.settingsStore, fiatFromSettings = appStore.settingsStore.fiatCurrency, @@ -110,6 +117,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor @computed bool get isBatchSending => outputs.length > 1; + bool get shouldDisplaySendALL => walletType != WalletType.solana || walletType != WalletType.tron; + @computed String get pendingTransactionFiatAmount { if (pendingTransaction == null) { @@ -236,7 +245,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor bool get hasFeesPriority => wallet.type != WalletType.nano && wallet.type != WalletType.banano && - wallet.type != WalletType.solana; + wallet.type != WalletType.solana && + wallet.type != WalletType.tron; + @observable CryptoCurrency selectedCryptoCurrency; @@ -258,6 +269,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor final SendTemplateViewModel sendTemplateViewModel; final BalanceViewModel balanceViewModel; final ContactListViewModel contactListViewModel; + final LedgerViewModel ledgerViewModel; final FiatConversionStore _fiatConversationStore; final Box transactionDescriptionBox; @@ -332,7 +344,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor try { state = IsExecutingState(); + if (wallet.isHardwareWallet) state = IsAwaitingDeviceResponseState(); + pendingTransaction = await wallet.createTransaction(_credentials()); + if (provider is ThorChainExchangeProvider) { final outputCount = pendingTransaction?.outputCount ?? 0; if (outputCount > 10) { @@ -346,7 +361,15 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor state = ExecutedSuccessfullyState(); return pendingTransaction; } catch (e) { - state = FailureState(translateErrorMessage(e, wallet.type, wallet.currency)); + if (e is LedgerException) { + final errorCode = e.errorCode.toRadixString(16); + final fallbackMsg = e.message.isNotEmpty ? e.message : "Unexpected Ledger Error Code: $errorCode"; + final errorMsg = ledgerViewModel.interpretErrorCode(errorCode) ?? fallbackMsg; + + state = FailureState(errorMsg); + } else { + state = FailureState(translateErrorMessage(e, wallet.type, wallet.currency)); + } } return null; } @@ -382,15 +405,13 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor String address = outputs.fold('', (acc, value) { return value.isParsedAddress - ? acc + value.address + '\n' + value.extractedAddress + '\n\n' - : acc + value.address + '\n\n'; + ? '$acc${value.address}\n${value.extractedAddress}\n\n' + : '$acc${value.address}\n\n'; }); address = address.trim(); - String note = outputs.fold('', (acc, value) { - return acc + value.note + '\n'; - }); + String note = outputs.fold('', (acc, value) => '$acc${value.note}\n'); note = note.trim(); @@ -423,7 +444,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor Object _credentials() { final priority = _settingsStore.priority[wallet.type]; - if (priority == null && wallet.type != WalletType.nano && wallet.type != WalletType.banano && wallet.type != WalletType.solana) { + if (priority == null && wallet.type != WalletType.nano && wallet.type != WalletType.banano && wallet.type != WalletType.solana && + wallet.type != WalletType.tron) { throw Exception('Priority is null for wallet type: ${wallet.type}'); } @@ -453,6 +475,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor case WalletType.solana: return solana! .createSolanaTransactionCredentials(outputs, currency: selectedCryptoCurrency); + case WalletType.tron: + return tron!.createTronTransactionCredentials(outputs, currency: selectedCryptoCurrency); default: throw Exception('Unexpected wallet type: ${wallet.type}'); } diff --git a/lib/view_model/send/send_view_model_state.dart b/lib/view_model/send/send_view_model_state.dart index 6d63eb9c2..df31041bc 100644 --- a/lib/view_model/send/send_view_model_state.dart +++ b/lib/view_model/send/send_view_model_state.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/core/execution_state.dart'; +class IsAwaitingDeviceResponseState extends IsExecutingState {} class TransactionCommitting extends ExecutionState {} class TransactionCommitted extends ExecutionState {} diff --git a/lib/view_model/settings/other_settings_view_model.dart b/lib/view_model/settings/other_settings_view_model.dart index 0493acf81..7e751a920 100644 --- a/lib/view_model/settings/other_settings_view_model.dart +++ b/lib/view_model/settings/other_settings_view_model.dart @@ -56,8 +56,9 @@ abstract class OtherSettingsViewModelBase with Store { _wallet.type == WalletType.nano || _wallet.type == WalletType.banano; @computed - bool get displayTransactionPriority => - !(changeRepresentativeEnabled || _wallet.type == WalletType.solana); + bool get displayTransactionPriority => !(changeRepresentativeEnabled || + _wallet.type == WalletType.solana || + _wallet.type == WalletType.tron); @computed bool get isEnabledBuyAction => !_settingsStore.disableBuy && _wallet.type != WalletType.haven; diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index cd8eb39de..69be6255c 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -1,7 +1,4 @@ - -import 'package:cake_wallet/entities/fiat_api_mode.dart'; -import 'package:cw_bitcoin/bitcoin_amount_format.dart'; -import 'package:cw_core/monero_amount_format.dart'; +import 'package:cake_wallet/tron/tron.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -21,10 +18,7 @@ import 'package:cake_wallet/utils/date_formatter.dart'; import 'package:cake_wallet/view_model/send/send_view_model.dart'; import 'package:collection/collection.dart'; import 'package:cw_core/transaction_direction.dart'; -import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_priority.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:hive/hive.dart'; import 'package:intl/src/intl/date_format.dart'; import 'package:mobx/mobx.dart'; @@ -83,6 +77,9 @@ abstract class TransactionDetailsViewModelBase with Store { case WalletType.solana: _addSolanaListItems(tx, dateFormat); break; + case WalletType.tron: + _addTronListItems(tx, dateFormat); + break; default: break; } @@ -180,6 +177,8 @@ abstract class TransactionDetailsViewModelBase with Store { return 'https://polygonscan.com/tx/${txId}'; case WalletType.solana: return 'https://solscan.io/tx/${txId}'; + case WalletType.tron: + return 'https://tronscan.org/#/transaction/${txId}'; default: return ''; } @@ -206,6 +205,8 @@ abstract class TransactionDetailsViewModelBase with Store { return S.current.view_transaction_on + 'polygonscan.com'; case WalletType.solana: return S.current.view_transaction_on + 'solscan.io'; + case WalletType.tron: + return S.current.view_transaction_on + 'tronscan.org'; default: return ''; } @@ -359,20 +360,19 @@ abstract class TransactionDetailsViewModelBase with Store { transactionInfo.inputAddresses?.length ?? 1, transactionInfo.outputAddresses?.length ?? 1); - RBFListItems.add(StandartListItem( - title: S.current.old_fee, - value: tx.feeFormatted() ?? '0.0')); + RBFListItems.add(StandartListItem(title: S.current.old_fee, value: tx.feeFormatted() ?? '0.0')); final priorities = priorityForWalletType(wallet.type); final selectedItem = priorities.indexOf(sendViewModel.transactionPriority); - final customItem = priorities.firstWhereOrNull( - (element) => element == sendViewModel.bitcoinTransactionPriorityCustom); + final customItem = priorities + .firstWhereOrNull((element) => element == sendViewModel.bitcoinTransactionPriorityCustom); final customItemIndex = customItem != null ? priorities.indexOf(customItem) : null; final maxCustomFeeRate = sendViewModel.maxCustomFeeRate?.toDouble(); RBFListItems.add(StandardPickerListItem( title: S.current.estimated_new_fee, - value: bitcoin!.formatterBitcoinAmountToString(amount: newFee) + ' ${walletTypeToCryptoCurrency(wallet.type)}', + value: bitcoin!.formatterBitcoinAmountToString(amount: newFee) + + ' ${walletTypeToCryptoCurrency(wallet.type)}', items: priorityForWalletType(wallet.type), customValue: settingsStore.customBitcoinFeeRate.toDouble(), maxValue: maxCustomFeeRate, @@ -398,6 +398,27 @@ abstract class TransactionDetailsViewModelBase with Store { } } + void _addTronListItems(TransactionInfo tx, DateFormat dateFormat) { + final _items = [ + StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.id), + StandartListItem( + title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), + StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), + if (tx.feeFormatted()?.isNotEmpty ?? false) + StandartListItem(title: S.current.transaction_details_fee, value: tx.feeFormatted()!), + if (showRecipientAddress && tx.to != null) + StandartListItem( + title: S.current.transaction_details_recipient_address, + value: tron!.getTronBase58Address(tx.to!, wallet)), + if (tx.from != null) + StandartListItem( + title: S.current.transaction_details_source_address, + value: tron!.getTronBase58Address(tx.from!, wallet)), + ]; + + items.addAll(_items); + } + @action Future _checkForRBF() async { if (wallet.type == WalletType.bitcoin && @@ -412,10 +433,10 @@ abstract class TransactionDetailsViewModelBase with Store { newFee = priority == bitcoin!.getBitcoinTransactionPriorityCustom() && value != null ? bitcoin!.getEstimatedFeeWithFeeRate(wallet, value.round(), transactionInfo.amount) : bitcoin!.getFeeAmountForPriority( - wallet, - priority, - transactionInfo.inputAddresses?.length ?? 1, - transactionInfo.outputAddresses?.length ?? 1); + wallet, + priority, + transactionInfo.inputAddresses?.length ?? 1, + transactionInfo.outputAddresses?.length ?? 1); return bitcoin!.formatterBitcoinAmountToString(amount: newFee); } diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 20980f5f0..f6e1359e1 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -12,6 +12,7 @@ import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/yat/yat_store.dart'; +import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/utils/list_item.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart'; @@ -175,6 +176,21 @@ class SolanaURI extends PaymentURI { } } +class TronURI extends PaymentURI { + TronURI({required String amount, required String address}) + : super(amount: amount, address: address); + + @override + String toString() { + var base = 'tron:' + address; + if (amount.isNotEmpty) { + base += '?amount=${amount.replaceAll(',', '.')}'; + } + + return base; + } +} + abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewModel with Store { WalletAddressListViewModelBase({ required AppStore appStore, @@ -273,6 +289,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo return SolanaURI(amount: amount, address: address.address); } + if (wallet.type == WalletType.tron) { + return TronURI(amount: amount, address: address.address); + } + throw Exception('Unexpected type: ${type.toString()}'); } @@ -348,6 +368,12 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); } + if (wallet.type == WalletType.tron) { + final primaryAddress = tron!.getAddress(wallet); + + addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); + } + if (searchText.isNotEmpty) { return ObservableList.of(addressList.where((item) { if (item is WalletAddressListItem) { diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 5c9c29a16..f825f0c47 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -72,6 +72,7 @@ abstract class WalletCreationVMBase with Store { address: '', showIntroCakePayCard: (!walletCreationService.typeExists(type)) && type != WalletType.haven, derivationInfo: credentials.derivationInfo ?? getDefaultDerivation(), + hardwareWalletType: credentials.hardwareWalletType, ); credentials.walletInfo = walletInfo; @@ -99,7 +100,7 @@ abstract class WalletCreationVMBase with Store { case WalletType.litecoin: return DerivationInfo( derivationType: DerivationType.electrum, - derivationPath: "m/0'/0", + derivationPath: "m/0'", ); default: return null; diff --git a/lib/view_model/wallet_hardware_restore_view_model.dart b/lib/view_model/wallet_hardware_restore_view_model.dart new file mode 100644 index 000000000..804ef7e3c --- /dev/null +++ b/lib/view_model/wallet_hardware_restore_view_model.dart @@ -0,0 +1,110 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/core/wallet_creation_service.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; +import 'package:cw_core/hardware/hardware_account_data.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:hive/hive.dart'; +import 'package:ledger_flutter/ledger_flutter.dart'; +import 'package:mobx/mobx.dart'; + +part 'wallet_hardware_restore_view_model.g.dart'; + +class WalletHardwareRestoreViewModel = WalletHardwareRestoreViewModelBase + with _$WalletHardwareRestoreViewModel; + +abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with Store { + final LedgerViewModel ledgerViewModel; + + int _nextIndex = 0; + + WalletHardwareRestoreViewModelBase(this.ledgerViewModel, AppStore appStore, + WalletCreationService walletCreationService, Box walletInfoSource, + {required WalletType type}) + : super(appStore, walletInfoSource, walletCreationService, type: type, isRecovery: true); + + @observable + String name = ""; + + @observable + HardwareAccountData? selectedAccount = null; + + @observable + bool isLoadingMoreAccounts = false; + + @observable + String? error = null; + + // @observable + ObservableList availableAccounts = ObservableList(); + + @action + Future getNextAvailableAccounts(int limit) async { + try { + List accounts; + switch (type) { + case WalletType.bitcoin: + accounts = await bitcoin! + .getHardwareWalletAccounts(ledgerViewModel, index: _nextIndex, limit: limit); + break; + case WalletType.ethereum: + accounts = await ethereum! + .getHardwareWalletAccounts(ledgerViewModel, index: _nextIndex, limit: limit); + break; + case WalletType.polygon: + accounts = await polygon! + .getHardwareWalletAccounts(ledgerViewModel, index: _nextIndex, limit: limit); + break; + default: + return; + } + + availableAccounts.addAll(accounts); + _nextIndex += limit; + } on LedgerException catch (e) { + error = ledgerViewModel.interpretErrorCode(e.errorCode.toRadixString(16)); + } catch (e) { + error = S.current.ledger_connection_error; + } + + isLoadingMoreAccounts = false; + _nextIndex += limit; + } + + @override + WalletCredentials getCredentials(dynamic _options) { + WalletCredentials credentials; + switch (type) { + case WalletType.bitcoin: + credentials = + bitcoin!.createBitcoinHardwareWalletCredentials(name: name, accountData: selectedAccount!); + break; + case WalletType.ethereum: + credentials = + ethereum!.createEthereumHardwareWalletCredentials(name: name, hwAccountData: selectedAccount!); + break; + case WalletType.polygon: + credentials = polygon!.createPolygonHardwareWalletCredentials(name: name, hwAccountData: selectedAccount!); + break; + default: + throw Exception('Unexpected type: ${type.toString()}'); + } + + credentials.hardwareWalletType = HardwareWalletType.ledger; + + return credentials; + } + + @override + Future process(WalletCredentials credentials) async { + walletCreationService.changeWalletType(type: type); + return walletCreationService.restoreFromHardwareWallet(credentials); + } +} diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index c33c85504..6b5ae5559 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -118,7 +118,8 @@ abstract class WalletKeysViewModelBase with Store { } if (isEVMCompatibleChain(_appStore.wallet!.type) || - _appStore.wallet!.type == WalletType.solana) { + _appStore.wallet!.type == WalletType.solana || + _appStore.wallet!.type == WalletType.tron) { items.addAll([ if (_appStore.wallet!.privateKey != null) StandartListItem(title: S.current.private_key, value: _appStore.wallet!.privateKey!), @@ -175,6 +176,8 @@ abstract class WalletKeysViewModelBase with Store { return 'polygon-wallet'; case WalletType.solana: return 'solana-wallet'; + case WalletType.tron: + return 'tron-wallet'; default: throw Exception('Unexpected wallet type: ${_appStore.wallet!.toString()}'); } diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index 8b19108ec..e19efabc5 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/solana/solana.dart'; +import 'package:cake_wallet/tron/tron.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/monero/monero.dart'; @@ -43,6 +44,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { return 16; } return 25; + case WalletType.tron: case WalletType.solana: case WalletType.polygon: case WalletType.ethereum: @@ -79,6 +81,8 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { return polygon!.createPolygonNewWalletCredentials(name: name); case WalletType.solana: return solana!.createSolanaNewWalletCredentials(name: name); + case WalletType.tron: + return tron!.createTronNewWalletCredentials(name: name); default: throw Exception('Unexpected type: ${type.toString()}'); } diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index 21339f1ae..e19a83bc3 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -6,6 +6,7 @@ import 'package:cw_core/nano_account_info_response.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; +import 'package:cake_wallet/tron/tron.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/store/app_store.dart'; @@ -34,7 +35,8 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { type == WalletType.polygon || type == WalletType.nano || type == WalletType.banano || - type == WalletType.solana, + type == WalletType.solana || + type == WalletType.tron, isButtonEnabled = false, mode = WalletRestoreMode.seed, super(appStore, walletInfoSource, walletCreationService, type: type, isRecovery: true) { @@ -48,6 +50,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { case WalletType.nano: case WalletType.banano: case WalletType.solana: + case WalletType.tron: availableModes = [WalletRestoreMode.seed, WalletRestoreMode.keys]; break; default: @@ -127,6 +130,12 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { mnemonic: seed, password: password, ); + case WalletType.tron: + return tron!.createTronRestoreWalletFromSeedCredentials( + name: name, + mnemonic: seed, + password: password, + ); default: break; } @@ -185,6 +194,12 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { password: password, privateKey: options['private_key'] as String, ); + case WalletType.tron: + return tron!.createTronRestoreWalletFromPrivateKey( + name: name, + password: password, + privateKey: options['private_key'] as String, + ); default: break; } diff --git a/macos/Runner/InfoBase.plist b/macos/Runner/InfoBase.plist index c849a42c7..dcd0de5a6 100644 --- a/macos/Runner/InfoBase.plist +++ b/macos/Runner/InfoBase.plist @@ -32,5 +32,22 @@ NSApplication LSApplicationCategoryType public.app-category.finance + NSCameraUsageDescription + Used for scanning QR code and can be used to capture images for identification purposes by third-party providers. + NSDocumentsFolderUsageDescription + We need access to documents folder for getting access to open/save backup file + NSFaceIDUsageDescription + 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 + processing + diff --git a/model_generator.sh b/model_generator.sh index 8a6098621..ee88644b6 100755 --- a/model_generator.sh +++ b/model_generator.sh @@ -6,6 +6,7 @@ cd cw_haven && flutter pub get && flutter packages pub run build_runner build -- cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_solana && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +cd cw_tron && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_ethereum && flutter pub get && cd .. cd cw_polygon && flutter pub get && cd .. flutter packages pub run build_runner build --delete-conflicting-outputs diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 3ec3e7978..cf04509ac 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -111,6 +111,7 @@ dependencies: git: url: https://github.com/cake-tech/bitcoin_base.git ref: cake-update-v2 + ledger_flutter: ^1.0.1 dev_dependencies: flutter_test: @@ -133,6 +134,14 @@ dependency_overrides: bech32: git: url: https://github.com/cake-tech/bech32.git + ledger_flutter: + git: + url: https://github.com/cake-tech/ledger-flutter.git + ref: cake + web3dart: + git: + url: https://github.com/cake-tech/web3dart.git + ref: cake flutter_icons: image_path: "assets/images/app_logo.png" @@ -158,6 +167,7 @@ flutter: - assets/nano_pow_node_list.yml - assets/polygon_node_list.yml - assets/solana_node_list.yml + - assets/tron_node_list.yml - assets/text/ - assets/faq/ - assets/animation/ diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index e1a093d52..81e2da431 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -79,6 +79,7 @@ "bitcoin_light_theme": "موضوع البيتكوين الخفيفة", "bitcoin_payments_require_1_confirmation": "تتطلب مدفوعات Bitcoin تأكيدًا واحدًا ، والذي قد يستغرق 20 دقيقة أو أكثر. شكرا لصبرك! سيتم إرسال بريد إلكتروني إليك عند تأكيد الدفع.", "Blocks_remaining": "بلوك متبقي ${status}", + "bluetooth": "بلوتوث", "bright_theme": "مشرق", "bump_fee": "رسوم عثرة", "buy": "اشتري", @@ -144,6 +145,8 @@ "congratulations": "تهانينا!", "connect_an_existing_yat": "توصيل Yat الحالي", "connect_yats": "توصيل Yats", + "connect_your_hardware_wallet": "قم بتوصيل محفظة الأجهزة الخاصة بك باستخدام Bluetooth أو USB", + "connect_your_hardware_wallet_ios": "قم بتوصيل محفظة الأجهزة الخاصة بك باستخدام Bluetooth", "connection_sync": "الاتصال والمزامنة", "connectWalletPrompt": "ﺕﻼﻣﺎﻌﻤﻟﺍ ءﺍﺮﺟﻹ WalletConnect ﻊﻣ ﻚﺘﻈﻔﺤﻣ ﻞﻴﺻﻮﺘﺑ ﻢﻗ", "contact": "تواصل", @@ -330,7 +333,13 @@ "is_percentage": "يكون", "last_30_days": "آخر 30 يومًا", "learn_more": "اعرف المزيد", + "ledger_connection_error": "فشل في الاتصال بك دفتر الأستاذ. حاول مرة اخرى.", + "ledger_error_device_locked": "تم قفل دفتر الأستاذ", + "ledger_error_tx_rejected_by_user": "تم رفض المعاملة على الجهاز", + "ledger_error_wrong_app": "يرجى التأكد", + "ledger_please_enable_bluetooth": "يرجى تمكين البلوتوث للكشف عن دفتر الأستاذ الخاص بك", "light_theme": "فاتح", + "load_more": "تحميل المزيد", "loading_your_wallet": "يتم تحميل محفظتك", "login": "تسجيل الدخول", "logout": "تسجيل خروج", @@ -444,6 +453,8 @@ "privacy_settings": "إعدادات الخصوصية", "private_key": "مفتاح خاص", "proceed_after_one_minute": "إذا لم تستمر الشاشة بعد دقيقة واحدة ، فتحقق من بريدك الإلكتروني.", + "proceed_on_device": "تابع جهازك", + "proceed_on_device_description": "يرجى اتباع الإرشادات المطلوبة على محفظة الأجهزة الخاصة بك", "profile": "حساب تعريفي", "provider_error": "خطأ ${provider}", "public_key": "مفتاح عمومي", @@ -492,6 +503,7 @@ "restore_bitcoin_description_from_seed": "قم باستعادة محفظتك من كود مكون من 24 كلمة", "restore_bitcoin_title_from_keys": "استعادة من WIF", "restore_description_from_backup": "يمكنك استعادة تطبيق Cake Wallet بالكامل من ملف النسخ الاحتياطي", + "restore_description_from_hardware_wallet": "استعادة من محفظة أجهزة دفتر الأستاذ", "restore_description_from_keys": "قم باستعادة محفظتك من ضغطات المفاتيح المولدة المحفوظة من مفاتيحك الخاصة", "restore_description_from_seed": "قم باستعادة محفظتك من الرمز المكون من 25 كلمة أو 13 كلمة", "restore_description_from_seed_keys": "استرجع محفظتك من السييد / المفاتيح التي قمت بحفظها في مكان آمن", @@ -504,6 +516,7 @@ "restore_seed_keys_restore": "استعادة السييد / المفاتيح", "restore_spend_key_private": "مفتاح الإنفاق (خاص)", "restore_title_from_backup": "استعادة من النسخة الاحتياطية", + "restore_title_from_hardware_wallet": "استعادة من محفظة الأجهزة", "restore_title_from_keys": "استعادة من المفاتيح", "restore_title_from_seed": "استعادة من السييد", "restore_title_from_seed_keys": "استعادة من السييد / المفاتيح", @@ -754,6 +767,7 @@ "unsupported_asset": ".ﻡﻮﻋﺪﻣ ﻞﺻﺃ ﻉﻮﻧ ﻦﻣ ﺔﻈﻔﺤﻣ ﻰﻟﺇ ﻞﻳﺪﺒﺘﻟﺍ ﻭﺃ ءﺎﺸﻧﺇ ﻰﺟﺮﻳ .ﻞﺻﻷﺍ ﺍﺬﻬﻟ ءﺍﺮﺟﻹﺍ ﺍﺬﻫ ﻢﻋﺪﻧ ﻻ ﻦﺤﻧ", "uptime": "مدة التشغيل", "upto": "حتى ${value}", + "usb": "USB", "use": "التبديل إلى", "use_card_info_three": "استخدم البطاقة الرقمية عبر الإنترنت أو مع طرق الدفع غير التلامسية.", "use_card_info_two": "يتم تحويل الأموال إلى الدولار الأمريكي عند الاحتفاظ بها في الحساب المدفوع مسبقًا ، وليس بالعملات الرقمية.", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 323114d45..0896d5c66 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -79,6 +79,7 @@ "bitcoin_light_theme": "Лека биткойн тема", "bitcoin_payments_require_1_confirmation": "Плащанията с Bitcoin изискват потвърждение, което може да отнеме 20 минути или повече. Благодарим за търпението! Ще получите имейл, когато плащането е потвърдено.", "Blocks_remaining": "${status} оставащи блока", + "bluetooth": "Bluetooth", "bright_theme": "Ярко", "bump_fee": "Такса за бум", "buy": "Купуване", @@ -144,6 +145,8 @@ "congratulations": "Поздравления!", "connect_an_existing_yat": "Добавете съществуващ Yat", "connect_yats": "Добавете Yats", + "connect_your_hardware_wallet": "Свържете хардуерния си портфейл с помощта на Bluetooth или USB", + "connect_your_hardware_wallet_ios": "Свържете хардуерния си портфейл с помощта на Bluetooth", "connection_sync": "Свързване и синхронизиране", "connectWalletPrompt": "Свържете портфейла си с WalletConnect, за да извършвате транзакции", "contact": "Контакт", @@ -330,7 +333,13 @@ "is_percentage": "е", "last_30_days": "Последните 30 дни", "learn_more": "Научете още", + "ledger_connection_error": "Не успя да се свърже с вашата книга. Моля, опитайте отново.", + "ledger_error_device_locked": "Главната книга е заключена", + "ledger_error_tx_rejected_by_user": "Транзакция, отхвърлена на устройство", + "ledger_error_wrong_app": "Моля, уверете се, че сте отворили правилното приложение на вашата книга", + "ledger_please_enable_bluetooth": "Моля, активирайте Bluetooth да открие вашата книга", "light_theme": "Светло", + "load_more": "Зареди още", "loading_your_wallet": "Зареждане на портфейл", "login": "Влизане", "logout": "Logout", @@ -444,6 +453,8 @@ "privacy_settings": "Настройки за поверителност", "private_key": "Таен ключ", "proceed_after_one_minute": "Ако процесът продължи повече от 1 минута, проверете своя имейл.", + "proceed_on_device": "Продължете на вашето устройство", + "proceed_on_device_description": "Моля, следвайте инструкциите, подканени на вашия хардуер", "profile": "Профил", "provider_error": "Грешка на ${provider} ", "public_key": "Публичен ключ", @@ -492,6 +503,7 @@ "restore_bitcoin_description_from_seed": "Възстановяване на портфейл чрез код от 24 думи", "restore_bitcoin_title_from_keys": "Възстановяване от WIF", "restore_description_from_backup": "Можете да възстановите цялото приложение Cake Wallet от своя резервен файл", + "restore_description_from_hardware_wallet": "Възстановяване от хардуерния портфейл на главната книга", "restore_description_from_keys": "Възстановяване на портфейл от генерираните от Вашите тайни ключове клавиши", "restore_description_from_seed": "Възстановяване на портфейл от кода от 13 или 25 думи", "restore_description_from_seed_keys": "Възстановете своя портфейл от seed/keys, които сте съхранили на сигурно място", @@ -504,6 +516,7 @@ "restore_seed_keys_restore": "Възстановяне от Seed/Keys", "restore_spend_key_private": "Spend key (публичен)", "restore_title_from_backup": "Възстановяване от резервно копие", + "restore_title_from_hardware_wallet": "Възстановяване от хардуерния портфейл", "restore_title_from_keys": "Възстановяване от keys", "restore_title_from_seed": "Възстановяване от seed", "restore_title_from_seed_keys": "Възстановяване от seed/keys", @@ -754,6 +767,7 @@ "unsupported_asset": "Не поддържаме това действие за този актив. Моля, създайте или преминете към портфейл от поддържан тип актив.", "uptime": "Време за работа", "upto": "до ${value}", + "usb": "USB", "use": "Смяна на ", "use_card_info_three": "Използвайте дигиталната карта онлайн или чрез безконтактен метод на плащане.", "use_card_info_two": "Средствата се обръщат в USD, когато биват запазени в предплатената карта, а не в дигитална валута.", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index ff074b6d6..4379b5691 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -79,6 +79,7 @@ "bitcoin_light_theme": "Světlé téma bitcoinů", "bitcoin_payments_require_1_confirmation": "U plateb Bitcoinem je vyžadováno alespoň 1 potvrzení, což může trvat 20 minut i déle. Děkujeme za vaši trpělivost! Až bude platba potvrzena, budete informováni e-mailem.", "Blocks_remaining": "Zbývá ${status} bloků", + "bluetooth": "Bluetooth", "bright_theme": "Jasný", "bump_fee": "Bump Fee", "buy": "Koupit", @@ -144,6 +145,8 @@ "congratulations": "Gratulujeme!", "connect_an_existing_yat": "Připojit existující Yat", "connect_yats": "Připojit Yaty", + "connect_your_hardware_wallet": "Připojte hardwarovou peněženku pomocí Bluetooth nebo USB", + "connect_your_hardware_wallet_ios": "Připojte hardwarovou peněženku pomocí Bluetooth", "connection_sync": "Připojení a synch.", "connectWalletPrompt": "Propojte svou peněženku s WalletConnect a provádějte transakce", "contact": "Kontakt", @@ -330,7 +333,13 @@ "is_percentage": "je", "last_30_days": "Posledních 30 dnů", "learn_more": "Zjistit více", + "ledger_connection_error": "Nepodařilo se připojit k vaší knize. Prosím zkuste to znovu.", + "ledger_error_device_locked": "Kniha je uzamčena", + "ledger_error_tx_rejected_by_user": "Transakce zamítnuta na zařízení", + "ledger_error_wrong_app": "Ujistěte se, že se na své knize otevřete správnou aplikaci", + "ledger_please_enable_bluetooth": "Umožněte prosím Bluetooth detekovat vaši knihu", "light_theme": "Světlý", + "load_more": "Načíst další", "loading_your_wallet": "Načítám peněženku", "login": "Login", "logout": "Odhlásit", @@ -444,6 +453,8 @@ "privacy_settings": "Nastavení soukromí", "private_key": "Soukromý klíč", "proceed_after_one_minute": "Pokud proces nepokročí během 1 minuty, zkontrolujte svůj e-mail.", + "proceed_on_device": "Pokračujte ve svém zařízení", + "proceed_on_device_description": "Postupujte podle pokynů na výzvu na vaší hardwarové peněžence", "profile": "Profil", "provider_error": "${provider} chyba", "public_key": "Veřejný klíč", @@ -492,6 +503,7 @@ "restore_bitcoin_description_from_seed": "Obnovte svou peněženku pomocí kombinace 24 slov", "restore_bitcoin_title_from_keys": "Obnovit z WIF", "restore_description_from_backup": "Můžete obnovit celou Cake Wallet aplikaci ze souboru se zálohou", + "restore_description_from_hardware_wallet": "Obnovit z peněženky hardwaru knihy", "restore_description_from_keys": "Obnovte svou peněženku pomocí generovaných stisků kláves uložených z vašich soukromých klíčů", "restore_description_from_seed": "Obnovte svou peněženku pomocí kombinace 25, nebo 13 slov", "restore_description_from_seed_keys": "Obnovte svou peněženku ze seedu/klíčů, které jste si uložili na bezpečném místě", @@ -504,6 +516,7 @@ "restore_seed_keys_restore": "Obnovit ze seedu/klíčů", "restore_spend_key_private": "Klíč pro platby (soukromý)", "restore_title_from_backup": "Obnovit ze zálohy", + "restore_title_from_hardware_wallet": "Obnovit z hardwarové peněženky", "restore_title_from_keys": "Obnovit z klíčů", "restore_title_from_seed": "Obnovit ze seedu", "restore_title_from_seed_keys": "Obnovit ze seedu/klíčů", @@ -754,6 +767,7 @@ "unsupported_asset": "Tuto akci u tohoto díla nepodporujeme. Vytvořte nebo přepněte na peněženku podporovaného typu aktiv.", "uptime": "Uptime", "upto": "až ${value}", + "usb": "USB", "use": "Přepnout na ", "use_card_info_three": "Použijte tuto digitální kartu online nebo bezkontaktními platebními metodami.", "use_card_info_two": "Prostředky jsou převedeny na USD, když jsou drženy na předplaceném účtu, nikoliv na digitální měnu.", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index aa6b15268..420efd379 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -79,6 +79,7 @@ "bitcoin_light_theme": "Bitcoin Light-Thema", "bitcoin_payments_require_1_confirmation": "Bitcoin-Zahlungen erfordern 1 Bestätigung, was 20 Minuten oder länger dauern kann. Danke für Ihre Geduld! Sie erhalten eine E-Mail, wenn die Zahlung bestätigt ist.", "Blocks_remaining": "${status} verbleibende Blöcke", + "bluetooth": "Bluetooth", "bright_theme": "Strahlend hell", "bump_fee": "Beulengebühr", "buy": "Kaufen", @@ -144,6 +145,8 @@ "congratulations": "Glückwunsch!", "connect_an_existing_yat": "Verbinden Sie ein vorhandenes Yat", "connect_yats": "Yats verbinden", + "connect_your_hardware_wallet": "Verbinden Sie Ihre Hardware-Wallet über Bluetooth oder USB", + "connect_your_hardware_wallet_ios": "Verbinden Sie Ihre Hardware-Wallet über Bluetooth", "connection_sync": "Verbindung und Synchronisierung", "connectWalletPrompt": "Verbinden Sie Ihr Wallet mit WalletConnect, um Transaktionen durchzuführen", "contact": "Kontakt", @@ -330,7 +333,13 @@ "is_percentage": "ist", "last_30_days": "Letzte 30 Tage", "learn_more": "Erfahren Sie mehr", + "ledger_connection_error": "Verbindung zum Ledger gescheitert. Bitte versuche es erneut.", + "ledger_error_device_locked": "Der Ledger ist gesperrt", + "ledger_error_tx_rejected_by_user": "Transaktion auf dem Gerät abgelehnt", + "ledger_error_wrong_app": "Bitte stellen Sie sicher, dass Sie die richtige App auf Ihrem Ledger geöffnet haben", + "ledger_please_enable_bluetooth": "Bitte aktivieren Sie Bluetooth um sich mit Ihren Ledger zu verbinden.", "light_theme": "Hell", + "load_more": "Mehr laden", "loading_your_wallet": "Wallet wird geladen", "login": "Einloggen", "logout": "Abmelden", @@ -445,6 +454,8 @@ "privacy_settings": "Datenschutzeinstellungen", "private_key": "Privater Schlüssel", "proceed_after_one_minute": "Wenn der Bildschirm nach 1 Minute nicht weitergeht, überprüfen Sie bitte Ihre E-Mail.", + "proceed_on_device": "Fahren Sie auf Ihrem Gerät fort", + "proceed_on_device_description": "Bitte befolgen Sie die Anweisungen, die auf Ihrer Hardware-Wallet angezeigt werden", "profile": "Profil", "provider_error": "${provider}-Fehler", "public_key": "Öffentlicher Schlüssel", @@ -493,6 +504,7 @@ "restore_bitcoin_description_from_seed": "Stellen Sie Ihre Wallet aus dem 24-Wort-Kombinationscode wieder her", "restore_bitcoin_title_from_keys": "Aus WIF wiederherstellen", "restore_description_from_backup": "Sie können die gesamte Cake Wallet-App aus Ihrer Sicherungsdatei wiederherstellen", + "restore_description_from_hardware_wallet": "Stellen Sie eine Wallet von Ledger wieder her", "restore_description_from_keys": "Stellen Sie Ihr Wallet aus generierten Tastenanschlägen her, die von Ihren privaten Schlüsseln gespeichert wurden", "restore_description_from_seed": "Stellen Sie Ihre Wallet aus den 25 Wörtern oder dem 13-Wort-Kombinationscode wieder her", "restore_description_from_seed_keys": "Stellen Sie Ihr Wallet aus Seed/Schlüsseln wieder her, die Sie sicher aufbewahrt haben", @@ -505,6 +517,7 @@ "restore_seed_keys_restore": "Seed/Schlüssel wiederherstellen", "restore_spend_key_private": "Spend Key (geheim)", "restore_title_from_backup": "Aus einer Sicherungsdatei wiederherstellen", + "restore_title_from_hardware_wallet": "Von Hardware-Wallet wiederherstellen", "restore_title_from_keys": "Aus Schlüsseln wiederherstellen", "restore_title_from_seed": "Aus Seed wiederherstellen", "restore_title_from_seed_keys": "Aus Seed/Schlüssel wiederherstellen", @@ -756,6 +769,7 @@ "unsupported_asset": "Wir unterstützen diese Aktion für dieses Asset nicht. Bitte erstellen Sie eine Wallet eines unterstützten Asset-Typs oder wechseln Sie zu einer Wallet.", "uptime": "Betriebszeit", "upto": "bis zu ${value}", + "usb": "USB", "use": "Wechsel zu ", "use_card_info_three": "Verwenden Sie die digitale Karte online oder mit kontaktlosen Zahlungsmethoden.", "use_card_info_two": "Guthaben werden auf dem Prepaid-Konto in USD umgerechnet, nicht in digitale Währung.", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 339d8b441..de65d839b 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -79,6 +79,7 @@ "bitcoin_light_theme": "Bitcoin Light Theme", "bitcoin_payments_require_1_confirmation": "Bitcoin payments require 1 confirmation, which can take 20 minutes or longer. Thanks for your patience! You will be emailed when the payment is confirmed.", "Blocks_remaining": "${status} Blocks Remaining", + "bluetooth": "Bluetooth", "bright_theme": "Bright", "bump_fee": "Bump fee", "buy": "Buy", @@ -144,6 +145,8 @@ "congratulations": "Congratulations!", "connect_an_existing_yat": "Connect an existing Yat", "connect_yats": "Connect Yats", + "connect_your_hardware_wallet": "Connect your hardware wallet using Bluetooth or USB", + "connect_your_hardware_wallet_ios": "Connect your hardware wallet using Bluetooth", "connection_sync": "Connection and sync", "connectWalletPrompt": "Connect your wallet with WalletConnect to make transactions", "contact": "Contact", @@ -330,7 +333,13 @@ "is_percentage": "is", "last_30_days": "Last 30 days", "learn_more": "Learn More", + "ledger_connection_error": "Failed to connect to you Ledger. Please try again.", + "ledger_error_device_locked": "The Ledger is locked", + "ledger_error_tx_rejected_by_user": "Transaction rejected on device", + "ledger_error_wrong_app": "Please make sure you opend the right app on your ledger", + "ledger_please_enable_bluetooth": "Please enable Bluetooth to detect your Ledger", "light_theme": "Light", + "load_more": "Load more", "loading_your_wallet": "Loading your wallet", "login": "Login", "logout": "Logout", @@ -444,6 +453,8 @@ "privacy_settings": "Privacy settings", "private_key": "Private key", "proceed_after_one_minute": "If the screen doesn’t proceed after 1 minute, check your email.", + "proceed_on_device": "Proceed on your device", + "proceed_on_device_description": "Please follow the instructions prompted on your hardware wallet", "profile": "Profile", "provider_error": "${provider} error", "public_key": "Public key", @@ -492,6 +503,7 @@ "restore_bitcoin_description_from_seed": "Restore your wallet from 24 word combination code", "restore_bitcoin_title_from_keys": "Restore from WIF", "restore_description_from_backup": "You can restore the whole Cake Wallet app from your back-up file", + "restore_description_from_hardware_wallet": "Restore from a Ledger hardware wallet", "restore_description_from_keys": "Restore your wallet from generated keystrokes saved from your private keys", "restore_description_from_seed": "Restore your wallet from either the 25 word or 13 word combination code", "restore_description_from_seed_keys": "Get back your wallet from seed/keys that you've saved to secure place", @@ -504,6 +516,7 @@ "restore_seed_keys_restore": "Seed/Keys Restore", "restore_spend_key_private": "Spend key (private)", "restore_title_from_backup": "Restore from backup", + "restore_title_from_hardware_wallet": "Restore from hardware wallet", "restore_title_from_keys": "Restore from keys", "restore_title_from_seed": "Restore from seed", "restore_title_from_seed_keys": "Restore from seed/keys", @@ -754,6 +767,7 @@ "unsupported_asset": "We don't support this action for this asset. Please create or switch to a wallet of a supported asset type.", "uptime": "Uptime", "upto": "up to ${value}", + "usb": "USB", "use": "Switch to ", "use_card_info_three": "Use the digital card online or with contactless payment methods.", "use_card_info_two": "Funds are converted to USD when they're held in the prepaid account, not in digital currencies.", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 76c49468a..29722b83c 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -79,6 +79,7 @@ "bitcoin_light_theme": "Tema de la luz de Bitcoin", "bitcoin_payments_require_1_confirmation": "Los pagos de Bitcoin requieren 1 confirmación, que puede demorar 20 minutos o más. ¡Gracias por su paciencia! Se le enviará un correo electrónico cuando se confirme el pago.", "Blocks_remaining": "${status} Bloques restantes", + "bluetooth": "Bluetooth", "bright_theme": "Brillante", "bump_fee": "Tarifa", "buy": "Comprar", @@ -144,6 +145,8 @@ "congratulations": "Felicidades!", "connect_an_existing_yat": "Conectar un Yat existente", "connect_yats": "Conectar Yats", + "connect_your_hardware_wallet": "Conecte su billetera de hardware con Bluetooth o USB", + "connect_your_hardware_wallet_ios": "Conecte su billetera de hardware con Bluetooth", "connection_sync": "Conexión y sincronización", "connectWalletPrompt": "Conecte su billetera con WalletConnect para realizar transacciones", "contact": "Contacto", @@ -330,7 +333,13 @@ "is_percentage": "es", "last_30_days": "Últimos 30 días", "learn_more": "Aprende más", + "ledger_connection_error": "No se pudo conectar con su libro mayor. Inténtalo de nuevo.", + "ledger_error_device_locked": "El libro mayor está bloqueado", + "ledger_error_tx_rejected_by_user": "Transacción rechazada en el dispositivo", + "ledger_error_wrong_app": "Por favor, asegúrese de abrir la aplicación correcta en su libro mayor.", + "ledger_please_enable_bluetooth": "Habilite Bluetooth para detectar su libro mayor", "light_theme": "Ligera", + "load_more": "Carga más", "loading_your_wallet": "Cargando tu billetera", "login": "Iniciar sesión", "logout": "Cerrar sesión", @@ -445,6 +454,8 @@ "privacy_settings": "Configuración de privacidad", "private_key": "Clave privada", "proceed_after_one_minute": "Si la pantalla no continúa después de 1 minuto, revisa tu correo electrónico.", + "proceed_on_device": "Continúe con su dispositivo", + "proceed_on_device_description": "Siga las instrucciones solicitadas en su billetera de hardware", "profile": "Perfil", "provider_error": "${provider} error", "public_key": "Clave pública", @@ -493,6 +504,7 @@ "restore_bitcoin_description_from_seed": "Restaure su billetera a partir del código de combinación de 24 palabras", "restore_bitcoin_title_from_keys": "Restaurar desde WIF", "restore_description_from_backup": "Puede restaurar toda la aplicación Cake Wallet desde ysu archivo de respaldo", + "restore_description_from_hardware_wallet": "Restaurar desde una billetera de hardware Ledger", "restore_description_from_keys": "Restaure su billetera de las pulsaciones de teclas generadas guardadas de sus claves privadas", "restore_description_from_seed": "Restaure su billetera desde el código de combinación de 25 palabras i de 13 palabras", "restore_description_from_seed_keys": "Recupere su billetera de las semillas/claves que ha guardado en un lugar seguro", @@ -505,6 +517,7 @@ "restore_seed_keys_restore": "Restauración de semillas / llaves", "restore_spend_key_private": "Spend clave (privado)", "restore_title_from_backup": "Restaurar desde un archivo de respaldo", + "restore_title_from_hardware_wallet": "Restaurar desde la billetera de hardware", "restore_title_from_keys": "De las claves", "restore_title_from_seed": "De la semilla", "restore_title_from_seed_keys": "Restaurar desde semilla/claves", @@ -755,6 +768,7 @@ "unsupported_asset": "No admitimos esta acción para este activo. Cree o cambie a una billetera de un tipo de activo admitido.", "uptime": "Tiempo de actividad", "upto": "hasta ${value}", + "usb": "USB", "use": "Utilizar a ", "use_card_info_three": "Utilice la tarjeta digital en línea o con métodos de pago sin contacto.", "use_card_info_two": "Los fondos se convierten a USD cuando se mantienen en la cuenta prepaga, no en monedas digitales.", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index f1566dac0..72593539b 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -79,6 +79,7 @@ "bitcoin_light_theme": "Thème léger Bitcoin", "bitcoin_payments_require_1_confirmation": "Les paiements Bitcoin nécessitent 1 confirmation, ce qui peut prendre 20 minutes ou plus. Merci pour votre patience ! Vous serez averti par e-mail lorsque le paiement sera confirmé.", "Blocks_remaining": "Blocs Restants : ${status}", + "bluetooth": "Bluetooth", "bright_theme": "Vif", "bump_fee": "Frais de bosse", "buy": "Acheter", @@ -144,6 +145,8 @@ "congratulations": "Félicitations !", "connect_an_existing_yat": "Connecter un Yat existant", "connect_yats": "Connecter Yats", + "connect_your_hardware_wallet": "Connectez votre portefeuille matériel à l'aide de Bluetooth ou USB", + "connect_your_hardware_wallet_ios": "Connectez votre portefeuille matériel à l'aide de Bluetooth", "connection_sync": "Connexion et synchronisation", "connectWalletPrompt": "Connectez votre portefeuille (wallet) avec WalletConnect pour effectuer des transactions", "contact": "Contact", @@ -330,7 +333,13 @@ "is_percentage": "est", "last_30_days": "30 derniers jours", "learn_more": "En savoir plus", + "ledger_connection_error": "Impossible de se connecter à votre grand livre. Veuillez réessayer.", + "ledger_error_device_locked": "Le grand livre est verrouillé", + "ledger_error_tx_rejected_by_user": "Transaction rejetée sur l'appareil", + "ledger_error_wrong_app": "Veuillez vous assurer d'ouvrir la bonne application sur votre grand livre", + "ledger_please_enable_bluetooth": "Veuillez activer Bluetooth pour détecter votre grand livre", "light_theme": "Clair", + "load_more": "Charger plus", "loading_your_wallet": "Chargement de votre portefeuille (wallet)", "login": "Utilisateur", "logout": "Déconnexion", @@ -444,6 +453,8 @@ "privacy_settings": "Paramètres de confidentialité", "private_key": "Clef privée", "proceed_after_one_minute": "Si l'écran ne s'affiche pas après 1 minute, vérifiez vos e-mails.", + "proceed_on_device": "Continuez sur votre appareil", + "proceed_on_device_description": "Veuillez suivre les instructions invitées sur votre portefeuille matériel", "profile": "Profil", "provider_error": "Erreur de ${provider}", "public_key": "Clef publique", @@ -492,6 +503,7 @@ "restore_bitcoin_description_from_seed": "Restaurer votre portefeuille (wallet) à l'aide d'une phrase secrète (seed) de 24 mots", "restore_bitcoin_title_from_keys": "Restaurer depuis la chaîne WIF", "restore_description_from_backup": "Vous pouvez restaurer l'intégralité de l'application Cake Wallet depuis un fichier de sauvegarde", + "restore_description_from_hardware_wallet": "Restaurer à partir d'un portefeuille matériel de grand livre", "restore_description_from_keys": "Restaurer votre portefeuille (wallet) d'après les séquences de touches générées d'après vos clefs privées", "restore_description_from_seed": "Restaurer votre portefeuille (wallet) depuis une phrase secrète (seed) de 25 ou 13 mots", "restore_description_from_seed_keys": "Restaurez votre portefeuille (wallet) depuis une phrase secrète (seed) ou des clefs que vous avez stockées en lieu sûr", @@ -504,6 +516,7 @@ "restore_seed_keys_restore": "Restaurer depuis Phrase secrète (seed)/Clefs", "restore_spend_key_private": "Clef de dépense (spend key) (privée)", "restore_title_from_backup": "Restaurer depuis une sauvegarde", + "restore_title_from_hardware_wallet": "Restaurer à partir du portefeuille matériel", "restore_title_from_keys": "Restaurer depuis des clefs", "restore_title_from_seed": "Restaurer depuis une phrase secrète (seed)", "restore_title_from_seed_keys": "Restaurer depuis une phrase secrète (seed) ou des clefs", @@ -754,6 +767,7 @@ "unsupported_asset": "Nous ne prenons pas en charge cette action pour cet élément. Veuillez créer ou passer à un portefeuille d'un type d'actif pris en charge.", "uptime": "Durée de la baisse", "upto": "jusqu'à ${value}", + "usb": "USB", "use": "Changer vers code PIN à ", "use_card_info_three": "Utilisez la carte numérique en ligne ou avec des méthodes de paiement sans contact.", "use_card_info_two": "Les fonds sont convertis en USD lorsqu'ils sont détenus sur le compte prépayé, et non en devises numériques.", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index dd1930dac..dceecd550 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -79,6 +79,7 @@ "bitcoin_light_theme": "Jigon Hasken Bitcoin", "bitcoin_payments_require_1_confirmation": "Akwatin Bitcoin na buɗe 1 sambumbu, da yake za ta samu mintuna 20 ko yawa. Ina kira ga sabuwar lafiya! Zaka sanarwa ta email lokacin da aka samu akwatin samun lambar waya.", "Blocks_remaining": "${status} Katanga ya rage", + "bluetooth": "Bluetooth", "bright_theme": "Mai haske", "bump_fee": "Buin", "buy": "Sayi", @@ -144,6 +145,8 @@ "congratulations": "Taya murna!", "connect_an_existing_yat": "Haɗa Yat da ke akwai", "connect_yats": "Haɗa Yats", + "connect_your_hardware_wallet": "Haɗa Wallake Wallware ɗinku ta Bluetooth ko USB", + "connect_your_hardware_wallet_ios": "Haɗa kayan aikinku ta Bluetooth", "connection_sync": "Haɗi da daidaitawa", "connectWalletPrompt": "Haɗa walat ɗin ku tare da WalletConnect don yin ma'amala", "contact": "Tuntuɓar", @@ -330,7 +333,13 @@ "is_percentage": "shine", "last_30_days": "Kwanaki 30 na ƙarshe", "learn_more": "Ƙara Koyi", + "ledger_connection_error": "Ba a yi nasarar haɗawa da ku ba. Da fatan za a sake gwadawa.", + "ledger_error_device_locked": "An kulle Ledger", + "ledger_error_tx_rejected_by_user": "Ma'amala da aka ƙi akan na'urar", + "ledger_error_wrong_app": "Da fatan za a tabbata kun yi amfani da app ɗin dama akan dillalarku", + "ledger_please_enable_bluetooth": "Da fatan za a kunna Bluetooth don gano Ledger ɗinku", "light_theme": "Haske", + "load_more": "Like more", "loading_your_wallet": "Ana loda walat ɗin ku", "login": "Shiga", "logout": "Fita", @@ -446,6 +455,8 @@ "privacy_settings": "Saitunan sirri", "private_key": "Keɓaɓɓen maɓalli", "proceed_after_one_minute": "Idan allon bai ci gaba ba bayan minti 1, duba imel ɗin ku.", + "proceed_on_device": "Ci gaba akan na'urarka", + "proceed_on_device_description": "Da fatan za a bi umarnin akan walatware", "profile": "Rabin fuska", "provider_error": "${provider} kuskure", "public_key": "Maɓallin jama'a", @@ -494,6 +505,7 @@ "restore_bitcoin_description_from_seed": "Dawo da kwalinku daga 24 lambar haɗin kalma", "restore_bitcoin_title_from_keys": "Dawo daga WIF", "restore_description_from_backup": "Kuna iya dawo da duk aikace-aikacen Wallet ɗin Cake daga fayil ɗin ajiyar ku", + "restore_description_from_hardware_wallet": "Maidowa da walatware mai wanki", "restore_description_from_keys": "Maido da walat ɗin ku daga maɓallan maɓalli da aka ƙera da aka ajiye daga maɓallan ku na sirri", "restore_description_from_seed": "Dawo da kwalinku daga 25 ko 13 lambar haɗin kalma", "restore_description_from_seed_keys": "Maido da walat ɗin ku daga iri/maɓallan da kuka adana don amintaccen wuri", @@ -506,6 +518,7 @@ "restore_seed_keys_restore": "Mayar da iri/Maɓallai", "restore_spend_key_private": "Maɓallin kashewa (key kalmar sirri)", "restore_title_from_backup": "Dawo daga madadin", + "restore_title_from_hardware_wallet": "Dawowa daga walatware mai wuya", "restore_title_from_keys": "Dawo daga maɓallai", "restore_title_from_seed": "Maidowa daga iri", "restore_title_from_seed_keys": "Dawo da iri/maɓallai", @@ -756,6 +769,7 @@ "unsupported_asset": "Ba mu goyi bayan wannan aikin don wannan kadara. Da fatan za a ƙirƙira ko canza zuwa walat na nau'in kadara mai tallafi.", "uptime": "Sama", "upto": "har zuwa ${value}", + "usb": "Alib", "use": "Canja zuwa", "use_card_info_three": "Yi amfani da katin dijital akan layi ko tare da hanyoyin biyan kuɗi mara lamba.", "use_card_info_two": "Ana canza kuɗi zuwa dalar Amurka lokacin da ake riƙe su a cikin asusun da aka riga aka biya, ba cikin agogon dijital ba.", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 2509baa26..fb393b0eb 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -79,6 +79,7 @@ "bitcoin_light_theme": "बिटकॉइन लाइट थीम", "bitcoin_payments_require_1_confirmation": "बिटकॉइन भुगतान के लिए 1 पुष्टिकरण की आवश्यकता होती है, जिसमें 20 मिनट या अधिक समय लग सकता है। आपके धैर्य के लिए धन्यवाद! भुगतान की पुष्टि होने पर आपको ईमेल किया जाएगा।", "Blocks_remaining": "${status} शेष रहते हैं", + "bluetooth": "ब्लूटूथ", "bright_theme": "उज्ज्वल", "bump_fee": "बम्प फीस", "buy": "खरीदें", @@ -144,6 +145,8 @@ "congratulations": "बधाई!", "connect_an_existing_yat": "मौजूदा Yat कनेक्ट करें", "connect_yats": "कनेक्ट Yats", + "connect_your_hardware_wallet": "ब्लूटूथ या यूएसबी का उपयोग करके अपने हार्डवेयर वॉलेट को कनेक्ट करें", + "connect_your_hardware_wallet_ios": "ब्लूटूथ का उपयोग करके अपने हार्डवेयर वॉलेट को कनेक्ट करें", "connection_sync": "कनेक्शन और सिंक", "connectWalletPrompt": "लेन-देन करने के लिए अपने वॉलेट को वॉलेटकनेक्ट से कनेक्ट करें", "contact": "संपर्क करें", @@ -330,7 +333,13 @@ "is_percentage": "है", "last_30_days": "पिछले 30 दिन", "learn_more": "और अधिक जानें", + "ledger_connection_error": "आप लेजर से जुड़ने में विफल रहे। कृपया पुन: प्रयास करें।", + "ledger_error_device_locked": "खाता बंद है", + "ledger_error_tx_rejected_by_user": "डिवाइस पर लेनदेन खारिज कर दिया गया", + "ledger_error_wrong_app": "कृपया सुनिश्चित करें कि आप अपने लेजर पर सही ऐप को खोलते हैं", + "ledger_please_enable_bluetooth": "कृपया अपने बहीखाने का पता लगाने के लिए ब्लूटूथ को सक्षम करें", "light_theme": "रोशनी", + "load_more": "और लोड करें", "loading_your_wallet": "अपना बटुआ लोड कर रहा है", "login": "लॉग इन करें", "logout": "लॉगआउट", @@ -445,6 +454,8 @@ "privacy_settings": "गोपनीयता सेटिंग्स", "private_key": "निजी चाबी", "proceed_after_one_minute": "यदि 1 मिनट के बाद भी स्क्रीन आगे नहीं बढ़ती है, तो अपना ईमेल देखें।", + "proceed_on_device": "अपने डिवाइस पर आगे बढ़ें", + "proceed_on_device_description": "कृपया अपने हार्डवेयर वॉलेट पर दिए गए निर्देशों का पालन करें", "profile": "प्रोफ़ाइल", "provider_error": "${provider} त्रुटि", "public_key": "सार्वजनिक कुंजी", @@ -494,6 +505,7 @@ "restore_bitcoin_description_from_seed": "24 शब्द संयोजन कोड से अपने वॉलेट को पुनर्स्थापित करें", "restore_bitcoin_title_from_keys": "WIF से पुनर्स्थापित करें", "restore_description_from_backup": "आप से पूरे केक वॉलेट एप्लिकेशन को पुनर्स्थापित कर सकते हैं आपकी बैक-अप फ़ाइल", + "restore_description_from_hardware_wallet": "एक लेजर हार्डवेयर वॉलेट से पुनर्स्थापित करें", "restore_description_from_keys": "अपने वॉलेट को जेनरेट से पुनर्स्थापित करें आपकी निजी कुंजी से कीस्ट्रोक्स सहेजे गए", "restore_description_from_seed": "या तो 25 शब्द से अपने वॉलेट को पुनर्स्थापित करें या 13 शब्द संयोजन कोड", "restore_description_from_seed_keys": "अपने बटुए को बीज से वापस लें/वे कुंजियाँ जिन्हें आपने सुरक्षित स्थान पर सहेजा है", @@ -506,6 +518,7 @@ "restore_seed_keys_restore": "बीज / कुंजी पुनर्स्थापित करें", "restore_spend_key_private": "कुंजी खर्च करें (निजीe)", "restore_title_from_backup": "बैक-अप फ़ाइल से पुनर्स्थापित करें", + "restore_title_from_hardware_wallet": "हार्डवेयर वॉलेट से पुनर्स्थापित करें", "restore_title_from_keys": "कुंजी से पुनर्स्थापित करें", "restore_title_from_seed": "बीज से पुनर्स्थापित करें", "restore_title_from_seed_keys": "बीज / कुंजियों से पुनर्स्थापित करें", @@ -756,6 +769,7 @@ "unsupported_asset": "हम इस संपत्ति के लिए इस कार्रवाई का समर्थन नहीं करते हैं. कृपया समर्थित परिसंपत्ति प्रकार का वॉलेट बनाएं या उस पर स्विच करें।", "uptime": "अपटाइम", "upto": "${value} तक", + "usb": "USB", "use": "उपयोग ", "use_card_info_three": "डिजिटल कार्ड का ऑनलाइन या संपर्क रहित भुगतान विधियों के साथ उपयोग करें।", "use_card_info_two": "डिजिटल मुद्राओं में नहीं, प्रीपेड खाते में रखे जाने पर निधियों को यूएसडी में बदल दिया जाता है।", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 8173202c1..fdadfe4dc 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -79,6 +79,7 @@ "bitcoin_light_theme": "Bitcoin Light Theme", "bitcoin_payments_require_1_confirmation": "Bitcoin plaćanja zahtijevaju 1 potvrdu, što može potrajati 20 minuta ili dulje. Hvala na Vašem strpljenju! Dobit ćete e-poruku kada plaćanje bude potvrđeno.", "Blocks_remaining": "${status} preostalih blokova", + "bluetooth": "Bluetooth", "bright_theme": "Jarka", "bump_fee": "Naplata", "buy": "Kupi", @@ -144,6 +145,8 @@ "congratulations": "Čestitamo!", "connect_an_existing_yat": "Povežite postojeći Yat", "connect_yats": "Povežite Yats", + "connect_your_hardware_wallet": "Spojite svoj hardverski novčanik pomoću Bluetooth -a ili USB -a", + "connect_your_hardware_wallet_ios": "Spojite svoj hardverski novčanik pomoću Bluetooth -a", "connection_sync": "Povezivanje i sinkronizacija", "connectWalletPrompt": "Povežite svoj novčanik s WalletConnectom za obavljanje transakcija", "contact": "Kontakt", @@ -330,7 +333,13 @@ "is_percentage": "je", "last_30_days": "Zadnjih 30 dana", "learn_more": "Saznajte više", + "ledger_connection_error": "Nije uspio povezati se s knjigom. Molim te pokušaj ponovno.", + "ledger_error_device_locked": "Knjiga je zaključana", + "ledger_error_tx_rejected_by_user": "Transakcija odbijena na uređaju", + "ledger_error_wrong_app": "Obavezno obavezno otvorite pravu aplikaciju na knjizi", + "ledger_please_enable_bluetooth": "Omogućite Bluetooth da otkrije svoju knjigu", "light_theme": "Svijetla", + "load_more": "Učitaj više", "loading_your_wallet": "Novčanik se učitava", "login": "Prijava", "logout": "Odjava", @@ -444,6 +453,8 @@ "privacy_settings": "Postavke privatnosti", "private_key": "Privatni ključ", "proceed_after_one_minute": "Ako se zaslon ne nastavi nakon 1 minute, provjerite svoju e-poštu.", + "proceed_on_device": "Nastavite na svom uređaju", + "proceed_on_device_description": "Slijedite upute zatražene na vašem hardverskom novčaniku", "profile": "Profil", "provider_error": "${provider} greška", "public_key": "Javni ključ", @@ -492,6 +503,7 @@ "restore_bitcoin_description_from_seed": "Oporavi novčanik pomoću koda od 12 riječi", "restore_bitcoin_title_from_keys": "Oporavi pomoću formata uvoza novčanika (WIF)", "restore_description_from_backup": "Možete oporaviti cijelu Cake Wallet aplikaciju pomoću vlastite datoteke sa sigurnosnom kopijom", + "restore_description_from_hardware_wallet": "Vratite se iz novčanice od knjige", "restore_description_from_keys": "Oporavi novčanik pomoću generiranih pritisaka na tipke spremljenih od vlastitih privatnih ključeva (keys)", "restore_description_from_seed": "Oporavi novčanik pomoću koda koji sadrži kombinaciju od 25 ili 13 riječi", "restore_description_from_seed_keys": "Oporavi novčanik pomoću pristupnog izraza/ključa spremljenog na sigurno mjesto", @@ -504,6 +516,7 @@ "restore_seed_keys_restore": "Oporavak pomoću pristupnog izraza/ključa", "restore_spend_key_private": "Spend key (privatni)", "restore_title_from_backup": "Oporavak pomoću sigurnosne kopije", + "restore_title_from_hardware_wallet": "Vrati iz hardverskog novčanika", "restore_title_from_keys": "Oporavi pomoću ključa", "restore_title_from_seed": "Oporavi pomoću pristupnog izraza", "restore_title_from_seed_keys": "Oporavi pomoću pristupnog izraza/ključa", @@ -754,6 +767,7 @@ "unsupported_asset": "Ne podržavamo ovu radnju za ovaj materijal. Izradite ili prijeđite na novčanik podržane vrste sredstava.", "uptime": "Radno vrijeme", "upto": "do ${value}", + "usb": "USB", "use": "Prebaci na", "use_card_info_three": "Koristite digitalnu karticu online ili s beskontaktnim metodama plaćanja.", "use_card_info_two": "Sredstva se pretvaraju u USD kada se drže na prepaid računu, a ne u digitalnim valutama.", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index bf773a272..8b7775ec9 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -79,6 +79,7 @@ "bitcoin_light_theme": "Tema Cahaya Bitcoin", "bitcoin_payments_require_1_confirmation": "Pembayaran Bitcoin memerlukan 1 konfirmasi, yang bisa memakan waktu 20 menit atau lebih. Terima kasih atas kesabaran Anda! Anda akan diemail saat pembayaran dikonfirmasi.", "Blocks_remaining": "${status} Blok Tersisa", + "bluetooth": "Bluetooth", "bright_theme": "Cerah", "bump_fee": "Biaya benjolan", "buy": "Beli", @@ -144,6 +145,8 @@ "congratulations": "Selamat!", "connect_an_existing_yat": "Hubungkan Yat yang ada", "connect_yats": "Hubungkan Yats", + "connect_your_hardware_wallet": "Hubungkan dompet perangkat keras Anda menggunakan Bluetooth atau USB", + "connect_your_hardware_wallet_ios": "Hubungkan dompet perangkat keras Anda menggunakan Bluetooth", "connection_sync": "Koneksi dan sinkronisasi", "connectWalletPrompt": "Hubungkan dompet Anda dengan WalletConnect untuk melakukan transaksi", "contact": "Kontak", @@ -330,7 +333,13 @@ "is_percentage": "adalah", "last_30_days": "30 hari terakhir", "learn_more": "Pelajari Lebih Lanjut", + "ledger_connection_error": "Gagal terhubung ke buku besar Anda. Tolong coba lagi.", + "ledger_error_device_locked": "Ledger terkunci", + "ledger_error_tx_rejected_by_user": "Transaksi ditolak pada perangkat", + "ledger_error_wrong_app": "Pastikan Anda membuka aplikasi yang tepat di buku besar Anda", + "ledger_please_enable_bluetooth": "Harap aktifkan Bluetooth untuk mendeteksi buku besar Anda", "light_theme": "Terang", + "load_more": "Muat lebih banyak", "loading_your_wallet": "Memuat dompet Anda", "login": "Masuk", "logout": "Keluar", @@ -446,6 +455,8 @@ "privacy_settings": "Pengaturan privasi", "private_key": "Kunci privat", "proceed_after_one_minute": "Jika layar tidak bergerak setelah 1 menit, periksa email Anda.", + "proceed_on_device": "Lanjutkan di perangkat Anda", + "proceed_on_device_description": "Harap ikuti instruksi yang diminta di dompet perangkat keras Anda", "profile": "Profil", "provider_error": "${provider} error", "public_key": "Kunci publik", @@ -494,6 +505,7 @@ "restore_bitcoin_description_from_seed": "Pulihkan dompet Anda dari kombinasi kode 24 kata", "restore_bitcoin_title_from_keys": "Pulihkan dari WIF", "restore_description_from_backup": "Anda dapat memulihkan seluruh aplikasi Cake Wallet dari file cadangan Anda", + "restore_description_from_hardware_wallet": "Kembalikan dari dompet perangkat keras Ledger", "restore_description_from_keys": "Pulihkan dompet Anda dari tombol yang dihasilkan yang disimpan dari kunci pribadi Anda", "restore_description_from_seed": "Pulihkan dompet Anda dari kombinasi kode 25 atau 13 kata", "restore_description_from_seed_keys": "Dapatkan kembali dompet Anda dari seed/kunci yang Anda simpan di tempat yang aman", @@ -507,6 +519,7 @@ "restore_seed_keys_restore": "Pulihkan Seed/Kunci", "restore_spend_key_private": "Habiskan kunci (pribadi)", "restore_title_from_backup": "Pulihkan dari cadangan", + "restore_title_from_hardware_wallet": "Kembalikan dari dompet perangkat keras", "restore_title_from_keys": "Pulihkan dari kunci", "restore_title_from_seed": "Pulihkan dari seed", "restore_title_from_seed_keys": "Pulihkan dari seed/kunci", @@ -757,6 +770,7 @@ "unsupported_asset": "Kami tidak mendukung tindakan ini untuk aset ini. Harap buat atau alihkan ke dompet dari jenis aset yang didukung.", "uptime": "Uptime", "upto": "hingga ${value}", + "usb": "USB", "use": "Beralih ke ", "use_card_info_three": "Gunakan kartu digital secara online atau dengan metode pembayaran tanpa kontak.", "use_card_info_two": "Dana dikonversi ke USD ketika disimpan dalam akun pra-bayar, bukan dalam mata uang digital.", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index a4661bd10..da222623d 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -79,6 +79,7 @@ "bitcoin_light_theme": "Tema luce Bitcoin", "bitcoin_payments_require_1_confirmation": "I pagamenti in bitcoin richiedono 1 conferma, che può richiedere 20 minuti o più. Grazie per la vostra pazienza! Riceverai un'e-mail quando il pagamento sarà confermato.", "Blocks_remaining": "${status} Blocchi Rimanenti", + "bluetooth": "Bluetooth", "bright_theme": "Colorato", "bump_fee": "Commissione per bump", "buy": "Comprare", @@ -145,6 +146,8 @@ "congratulations": "Congratulazioni!", "connect_an_existing_yat": "Collegare un Yat esistente", "connect_yats": "Connetti Yats", + "connect_your_hardware_wallet": "Collega il tuo portafoglio hardware con Bluetooth o USB", + "connect_your_hardware_wallet_ios": "Collega il tuo portafoglio hardware con Bluetooth", "connection_sync": "Connessione e sincronizzazione", "connectWalletPrompt": "Collega il tuo portafoglio con WalletConnect per effettuare transazioni", "contact": "Contatta", @@ -331,7 +334,13 @@ "is_percentage": "è", "last_30_days": "Ultimi 30 giorni", "learn_more": "Impara di più", + "ledger_connection_error": "Impossibile connettersi al libro mastro. Per favore riprova.", + "ledger_error_device_locked": "Il libro mastro è bloccato", + "ledger_error_tx_rejected_by_user": "Transazione rifiutata sul dispositivo", + "ledger_error_wrong_app": "Assicurati di aprire l'app giusta sul libro mastro", + "ledger_please_enable_bluetooth": "Si prega di consentire al Bluetooth di rilevare il libro mastro", "light_theme": "Bianco", + "load_more": "Carica di più", "loading_your_wallet": "Caricamento portafoglio", "login": "Accedi", "logout": "Logout", @@ -446,6 +455,8 @@ "privacy_settings": "Impostazioni privacy", "private_key": "Chiave privata", "proceed_after_one_minute": "Se lo schermo non procede dopo 1 minuto, controlla la tua email.", + "proceed_on_device": "Procedi sul tuo dispositivo", + "proceed_on_device_description": "Segui le istruzioni richieste sul portafoglio hardware", "profile": "Profilo", "provider_error": "${provider} errore", "public_key": "Chiave pubblica", @@ -494,6 +505,7 @@ "restore_bitcoin_description_from_seed": "Recupera il tuo portafoglio da una combinazione di 12 parole", "restore_bitcoin_title_from_keys": "Recupera da WIF", "restore_description_from_backup": "Puoi recuperare l'app Cake Wallet per intero dal tuo file di backup", + "restore_description_from_hardware_wallet": "Ripristina da un portafoglio hardware di libro mastro", "restore_description_from_keys": "Recupera il tuo portafoglio da una sequenza di caratteri generati dalle tue chiavi private", "restore_description_from_seed": "Recupera il tuo portafoglio da una combinazione di 25 o 13 parole", "restore_description_from_seed_keys": "Recupera il tuo portafoglio dal seme/chiavi che hai salvato in un posto sicuro", @@ -506,6 +518,7 @@ "restore_seed_keys_restore": "Recupera Seme/Chiavi", "restore_spend_key_private": "Chiave di Spesa (privata)", "restore_title_from_backup": "Recupera da backup", + "restore_title_from_hardware_wallet": "Ripristina dal portafoglio hardware", "restore_title_from_keys": "Recupera dalle chiavi", "restore_title_from_seed": "Recupera dal seme", "restore_title_from_seed_keys": "Recupera dal seme/chiavi", @@ -756,6 +769,7 @@ "unsupported_asset": "Non supportiamo questa azione per questa risorsa. Crea o passa a un portafoglio di un tipo di asset supportato.", "uptime": "Uptime", "upto": "fino a ${value}", + "usb": "USB", "use": "Passa a ", "use_card_info_three": "Utilizza la carta digitale online o con metodi di pagamento contactless.", "use_card_info_two": "I fondi vengono convertiti in USD quando sono detenuti nel conto prepagato, non in valute digitali.", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 28155346a..76ae9fd06 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -79,6 +79,7 @@ "bitcoin_light_theme": "ビットコインライトテーマ", "bitcoin_payments_require_1_confirmation": "ビットコインの支払いには 1 回の確認が必要で、これには 20 分以上かかる場合があります。お待ち頂きまして、ありがとうございます!支払いが確認されると、メールが送信されます。", "Blocks_remaining": "${status} 残りのブロック", + "bluetooth": "ブルートゥース", "bright_theme": "明るい", "bump_fee": "バンプ料金", "buy": "購入", @@ -144,6 +145,8 @@ "congratulations": "おめでとうございます!", "connect_an_existing_yat": "既存のYatを接続します", "connect_yats": "Yatsを接続します", + "connect_your_hardware_wallet": "BluetoothまたはUSBを使用して、ハードウェアウォレットを接続します", + "connect_your_hardware_wallet_ios": "Bluetoothを使用してハードウェアウォレットを接続します", "connection_sync": "接続と同期", "connectWalletPrompt": "ウォレットを WalletConnect に接続して取引を行う", "contact": "接触", @@ -331,7 +334,13 @@ "is_percentage": "is", "last_30_days": "過去30日", "learn_more": "もっと詳しく知る", + "ledger_connection_error": "元帳に接続できませんでした。もう一度やり直してください。", + "ledger_error_device_locked": "元帳がロックされています", + "ledger_error_tx_rejected_by_user": "トランザクションはデバイスで拒否されました", + "ledger_error_wrong_app": "元帳に適切なアプリを開始するようにしてください", + "ledger_please_enable_bluetooth": "Bluetoothが元帳を検出できるようにしてください", "light_theme": "光", + "load_more": "もっと読み込む", "loading_your_wallet": "ウォレットをロードしています", "login": "ログイン", "logout": "ログアウト", @@ -445,6 +454,8 @@ "privacy_settings": "プライバシー設定", "private_key": "秘密鍵", "proceed_after_one_minute": "1分経っても画面が進まない場合は、メールを確認してください。", + "proceed_on_device": "デバイスに進みます", + "proceed_on_device_description": "ハードウェアウォレットにプロンプ​​トされた指示に従ってください", "profile": "プロフィール", "provider_error": "${provider} エラー", "public_key": "公開鍵", @@ -493,6 +504,7 @@ "restore_bitcoin_description_from_seed": "24ワードの組み合わせコードからウォレットを復元する", "restore_bitcoin_title_from_keys": "WIFから復元", "restore_description_from_backup": "Cake Walletアプリ全体を復元できますバックアップファイル", + "restore_description_from_hardware_wallet": "元帳ハードウェアウォレットから復元します", "restore_description_from_keys": "生成されたウォレットを復元します秘密鍵から保存されたキーストローク", "restore_description_from_seed": "25ワードからウォレットを復元しますまたは13ワードの組み合わせコード", "restore_description_from_seed_keys": "安全な場所に保存したシード/キーから財布を取り戻す", @@ -505,6 +517,7 @@ "restore_seed_keys_restore": "シード/キーの復元", "restore_spend_key_private": "キーを使う (プライベート)", "restore_title_from_backup": "バックアップファイルから復元する", + "restore_title_from_hardware_wallet": "ハードウェアウォレットから復元します", "restore_title_from_keys": "キーから復元する", "restore_title_from_seed": "シードから復元", "restore_title_from_seed_keys": "シード/キーから復元", @@ -755,6 +768,7 @@ "unsupported_asset": "このアセットに対するこのアクションはサポートされていません。サポートされているアセットタイプのウォレットを作成するか、ウォレットに切り替えてください。", "uptime": "稼働時間", "upto": "up up ${value}", + "usb": "USB", "use": "使用する ", "use_card_info_three": "デジタルカードをオンラインまたは非接触型決済方法で使用してください。", "use_card_info_two": "デジタル通貨ではなく、プリペイドアカウントで保持されている場合、資金は米ドルに変換されます。", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 80416c790..f949b70ee 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -79,6 +79,7 @@ "bitcoin_light_theme": "비트코인 라이트 테마", "bitcoin_payments_require_1_confirmation": "비트코인 결제는 1번의 확인이 필요하며 20분 이상이 소요될 수 있습니다. 기다려 주셔서 감사합니다! 결제가 확인되면 이메일이 전송됩니다.", "Blocks_remaining": "${status} 남은 블록", + "bluetooth": "블루투스", "bright_theme": "선명한", "bump_fee": "범프 요금", "buy": "구입", @@ -144,6 +145,8 @@ "congratulations": "축하합니다!", "connect_an_existing_yat": "기존 Yat 연결", "connect_yats": "야츠 연결", + "connect_your_hardware_wallet": "Bluetooth 또는 USB를 사용하여 하드웨어 지갑을 연결하십시오", + "connect_your_hardware_wallet_ios": "Bluetooth를 사용하여 하드웨어 지갑을 연결하십시오", "connection_sync": "연결 및 동기화", "connectWalletPrompt": "거래를 하려면 WalletConnect에 지갑을 연결하세요.", "contact": "접촉", @@ -330,7 +333,13 @@ "is_percentage": "이다", "last_30_days": "지난 30일", "learn_more": "더 알아보기", + "ledger_connection_error": "원장에 연결하지 못했습니다. 다시 시도하십시오.", + "ledger_error_device_locked": "원장이 잠겨 있습니다", + "ledger_error_tx_rejected_by_user": "장치에서 거래가 거부되었습니다", + "ledger_error_wrong_app": "원장에서 올바른 앱을 반대하는지 확인하십시오.", + "ledger_please_enable_bluetooth": "Bluetooth가 원장을 감지 할 수 있도록하십시오", "light_theme": "빛", + "load_more": "더로드하십시오", "loading_your_wallet": "지갑 넣기", "login": "로그인", "logout": "로그아웃", @@ -445,6 +454,8 @@ "privacy_settings": "개인정보 설정", "private_key": "개인 키", "proceed_after_one_minute": "1분 후에도 화면이 진행되지 않으면 이메일을 확인하세요.", + "proceed_on_device": "장치를 진행하십시오", + "proceed_on_device_description": "하드웨어 지갑에 표시된 지침을 따르십시오", "profile": "프로필", "provider_error": "${provider} 오류", "public_key": "공개 키", @@ -493,6 +504,7 @@ "restore_bitcoin_description_from_seed": "24 단어 조합 코드에서 지갑 복원", "restore_bitcoin_title_from_keys": "WIF에서 복원", "restore_description_from_backup": "백업 파일에서 전체 Cake Wallet 앱을 복원 할 수 있습니다.", + "restore_description_from_hardware_wallet": "원장 하드웨어 지갑에서 복원하십시오", "restore_description_from_keys": "개인 키에서 저장된 생성 된 키 스트로크에서 월렛 복원", "restore_description_from_seed": "25 단어 또는 13 단어 조합 코드에서 지갑을 복원하십시오.", "restore_description_from_seed_keys": "안전한 장소에 저장 한 종자 / 키로 지갑을 되 찾으십시오.", @@ -505,6 +517,7 @@ "restore_seed_keys_restore": "종자 / 키 복원", "restore_spend_key_private": "지출 키 (은밀한)", "restore_title_from_backup": "백업 파일에서 복원", + "restore_title_from_hardware_wallet": "하드웨어 지갑에서 복원하십시오", "restore_title_from_keys": "키에서 복원", "restore_title_from_seed": "종자에서 복원", "restore_title_from_seed_keys": "시드 / 키에서 복원", @@ -755,6 +768,7 @@ "unsupported_asset": "이 저작물에 대해 이 작업을 지원하지 않습니다. 지원되는 자산 유형의 지갑을 생성하거나 전환하십시오.", "uptime": "가동 시간", "upto": "최대 ${value}", + "usb": "USB", "use": "사용하다 ", "use_card_info_three": "디지털 카드를 온라인 또는 비접촉식 결제 수단으로 사용하십시오.", "use_card_info_two": "디지털 화폐가 아닌 선불 계정에 보유하면 자금이 USD로 변환됩니다.", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 665a004e1..4506cfd1b 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -79,6 +79,7 @@ "bitcoin_light_theme": "Bitcoin Light အပြင်အဆင်", "bitcoin_payments_require_1_confirmation": "Bitcoin ငွေပေးချေမှုများသည် မိနစ် 20 သို့မဟုတ် ထို့ထက်ပိုကြာနိုင်သည် 1 အတည်ပြုချက် လိုအပ်သည်။ မင်းရဲ့စိတ်ရှည်မှုအတွက် ကျေးဇူးတင်ပါတယ်။ ငွေပေးချေမှုကို အတည်ပြုပြီးသောအခါ သင့်ထံ အီးမေးလ်ပို့ပါမည်။", "Blocks_remaining": "${status} ဘလောက်များ ကျန်နေပါသည်။", + "bluetooth": "ဘလူးတုသ်", "bright_theme": "တောက်ပ", "bump_fee": "ဝင်ငွေ", "buy": "ဝယ်ပါ။", @@ -144,6 +145,8 @@ "congratulations": "ဂုဏ်ယူပါသည်။", "connect_an_existing_yat": "ရှိပြီးသား Yat ကို ချိတ်ဆက်ပါ။", "connect_yats": "Yats ကိုချိတ်ဆက်ပါ။", + "connect_your_hardware_wallet": "သင်၏ hardware ပိုက်ဆံအိတ်ကို Bluetooth သို့မဟုတ် USB ကို သုံး. ချိတ်ဆက်ပါ", + "connect_your_hardware_wallet_ios": "သင်၏ hardware ပိုက်ဆံအိတ်ကို Bluetooth ကို အသုံးပြု. ချိတ်ဆက်ပါ", "connection_sync": "ချိတ်ဆက်မှုနှင့် ထပ်တူပြုခြင်း။", "connectWalletPrompt": "အရောင်းအဝယ်ပြုလုပ်ရန် သင့်ပိုက်ဆံအိတ်ကို WalletConnect နှင့် ချိတ်ဆက်ပါ။", "contact": "ဆက်သွယ်ရန်", @@ -330,7 +333,13 @@ "is_percentage": "သည်", "last_30_days": "လွန်ခဲ့သော ရက် 30", "learn_more": "ပိုမိုသိရှိရန်", + "ledger_connection_error": "သငျသညျ Ledger နှင့်ချိတ်ဆက်ရန်မအောင်မြင်ပါ။ ကျေးဇူးပြုပြီးထပ်ကြိုးစားပါ", + "ledger_error_device_locked": "အဆိုပါလမ်းပြသော့ခတ်သည်", + "ledger_error_tx_rejected_by_user": "ငွေပေးငွေယူ device ကိုအပေါ်ငြင်းပယ်ခဲ့သည်", + "ledger_error_wrong_app": "ကျေးဇူးပြု. သင့်လက်ျာအက်ပ်ကိုသင်၏ Ledger တွင်ဖွင့်ရန်သေချာစေပါ", + "ledger_please_enable_bluetooth": "သင်၏ Ledger ကိုရှာဖွေရန် Bluetooth ကိုဖွင့်ပါ", "light_theme": "အလင်း", + "load_more": "ပိုပြီး load", "loading_your_wallet": "သင့်ပိုက်ဆံအိတ်ကို ဖွင့်နေသည်။", "login": "လော့ဂ်အင်", "logout": "ထွက်လိုက်ပါ။", @@ -444,6 +453,8 @@ "privacy_settings": "Privacy settings တွေကို", "private_key": "သီးသန့်သော့", "proceed_after_one_minute": "မျက်နှာပြင်သည် ၁ မိနစ်အကြာတွင် ဆက်လက်မလုပ်ဆောင်ပါက သင့်အီးမေးလ်ကို စစ်ဆေးပါ။", + "proceed_on_device": "သင့်စက်ပေါ်တွင်ဆက်လက်ဆောင်ရွက်ပါ", + "proceed_on_device_description": "သင်၏ hardware ပိုက်ဆံအိတ်ပေါ်ရှိညွှန်ကြားချက်များကိုလိုက်နာပါ", "profile": "ကိုယ်ရေးအကျဉ်း", "provider_error": "${provider} အမှား", "public_key": "အများသူငှာသော့", @@ -492,6 +503,7 @@ "restore_bitcoin_description_from_seed": "24 စကားလုံးပေါင်းစပ်ကုဒ်မှ သင့်ပိုက်ဆံအိတ်ကို ပြန်ယူပါ။", "restore_bitcoin_title_from_keys": "WIF မှ ပြန်လည်ရယူပါ။", "restore_description_from_backup": "သင့်အရန်ဖိုင်မှ Cake Wallet အက်ပ်တစ်ခုလုံးကို သင်ပြန်လည်ရယူနိုင်သည်။", + "restore_description_from_hardware_wallet": "Ledger ဟာ့ဒ်ဝဲပိုက်ဆံအိတ်မှ Restore", "restore_description_from_keys": "သင့်ကိုယ်ပိုင်သော့များမှ သိမ်းဆည်းထားသော ထုတ်ပေးထားသော သော့ချက်များမှ သင့်ပိုက်ဆံအိတ်ကို ပြန်လည်ရယူပါ။", "restore_description_from_seed": "25 စကားလုံး သို့မဟုတ် 13 စကားလုံးပေါင်းစပ်ကုဒ်မှ သင့်ပိုက်ဆံအိတ်ကို ပြန်လည်ရယူပါ။", "restore_description_from_seed_keys": "သင့်ပိုက်ဆံအိတ်ကို လုံခြုံသောနေရာတွင် သိမ်းဆည်းထားသော မျိုးစေ့/သော့များမှ ပြန်လည်ရယူပါ။", @@ -504,6 +516,7 @@ "restore_seed_keys_restore": "မျိုးစေ့/သော့များ ပြန်လည်ရယူပါ။", "restore_spend_key_private": "သော့သုံးရန် (သီးသန့်)", "restore_title_from_backup": "အရန်သိမ်းခြင်းမှ ပြန်လည်ရယူပါ။", + "restore_title_from_hardware_wallet": "ဟာ့ဒ်ဝဲပိုက်ဆံအိတ်မှ restore", "restore_title_from_keys": "သော့များမှ ပြန်လည်ရယူပါ။", "restore_title_from_seed": "မျိုးစေ့မှပြန်လည်ရယူပါ။", "restore_title_from_seed_keys": "မျိုးစေ့/သော့များမှ ပြန်လည်ရယူပါ။", @@ -754,6 +767,7 @@ "unsupported_asset": "ဤပိုင်ဆိုင်မှုအတွက် ဤလုပ်ဆောင်ချက်ကို ကျွန်ုပ်တို့ မပံ့ပိုးပါ။ ကျေးဇူးပြု၍ ပံ့ပိုးပေးထားသော ပိုင်ဆိုင်မှုအမျိုးအစား၏ ပိုက်ဆံအိတ်ကို ဖန်တီးပါ သို့မဟုတ် ပြောင်းပါ။", "uptime": "အထက်က", "upto": "${value} အထိ", + "usb": "ယူအက်စ်ဘီ", "use": "သို့ပြောင်းပါ။", "use_card_info_three": "ဒစ်ဂျစ်တယ်ကတ်ကို အွန်လိုင်း သို့မဟုတ် ထိတွေ့မှုမဲ့ ငွေပေးချေမှုနည်းလမ်းများဖြင့် အသုံးပြုပါ။", "use_card_info_two": "ဒစ်ဂျစ်တယ်ငွေကြေးများဖြင့်မဟုတ်ဘဲ ကြိုတင်ငွေပေးချေသည့်အကောင့်တွင် သိမ်းထားသည့်အခါ ရန်ပုံငွေများကို USD သို့ ပြောင်းလဲပါသည်။", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index ed2460280..935b918ad 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -79,6 +79,7 @@ "bitcoin_light_theme": "Bitcoin Light-thema", "bitcoin_payments_require_1_confirmation": "Bitcoin-betalingen vereisen 1 bevestiging, wat 20 minuten of langer kan duren. Dank voor uw geduld! U ontvangt een e-mail wanneer de betaling is bevestigd.", "Blocks_remaining": "${status} Resterende blokken", + "bluetooth": "Bluetooth", "bright_theme": "Helder", "bump_fee": "Bult fee", "buy": "Kopen", @@ -144,6 +145,8 @@ "congratulations": "gefeliciteerd!", "connect_an_existing_yat": "Verbind een bestaande Yat", "connect_yats": "Verbind Yats", + "connect_your_hardware_wallet": "Sluit uw hardware -portemonnee aan met Bluetooth of USB", + "connect_your_hardware_wallet_ios": "Sluit uw hardware -portemonnee aan met Bluetooth", "connection_sync": "Verbinding en synchronisatie", "connectWalletPrompt": "Verbind uw portemonnee met WalletConnect om transacties uit te voeren", "contact": "Contact", @@ -330,7 +333,13 @@ "is_percentage": "is", "last_30_days": "Laatste 30 dagen", "learn_more": "Kom meer te weten", + "ledger_connection_error": "Kan geen verbinding maken met u grootboek. Probeer het opnieuw.", + "ledger_error_device_locked": "Het grootboek is vergrendeld", + "ledger_error_tx_rejected_by_user": "Transactie afgewezen op apparaat", + "ledger_error_wrong_app": "Zorg ervoor dat u de juiste app op uw grootboek opent", + "ledger_please_enable_bluetooth": "Schakel Bluetooth in staat om uw grootboek te detecteren", "light_theme": "Licht", + "load_more": "Meer laden", "loading_your_wallet": "Uw portemonnee laden", "login": "Log in", "logout": "Uitloggen", @@ -444,6 +453,8 @@ "privacy_settings": "Privacy-instellingen", "private_key": "Prive sleutel", "proceed_after_one_minute": "Als het scherm na 1 minuut niet verder gaat, controleer dan uw e-mail.", + "proceed_on_device": "Ga verder met uw apparaat", + "proceed_on_device_description": "Volg de instructies die zijn aangevraagd op uw hardware -portemonnee", "profile": "Profiel", "provider_error": "${provider} fout", "public_key": "Publieke sleutel", @@ -492,6 +503,7 @@ "restore_bitcoin_description_from_seed": "Herstel uw portemonnee met een combinatiecode van 24 woorden", "restore_bitcoin_title_from_keys": "Herstel van WIF", "restore_description_from_backup": "Je kunt de hele Cake Wallet-app herstellen van uw back-upbestand", + "restore_description_from_hardware_wallet": "Herstel van een grootboekhardware -portemonnee", "restore_description_from_keys": "Herstel uw portemonnee van gegenereerd toetsaanslagen opgeslagen van uw privésleutels", "restore_description_from_seed": "Herstel uw portemonnee van het 25 woord of 13 woord combinatiecode", "restore_description_from_seed_keys": "Ontvang uw portemonnee terug uit seed / keys die u hebt opgeslagen op een veilige plaats", @@ -504,6 +516,7 @@ "restore_seed_keys_restore": "Zaad / sleutels herstellen", "restore_spend_key_private": "Sleutel uitgeven (privaat)", "restore_title_from_backup": "Herstellen vanuit een back-upbestand", + "restore_title_from_hardware_wallet": "Herstel van de hardware -portemonnee", "restore_title_from_keys": "Herstel van sleutels", "restore_title_from_seed": "Herstel van zaad", "restore_title_from_seed_keys": "Herstel van zaad / sleutels", @@ -754,6 +767,7 @@ "unsupported_asset": "We ondersteunen deze actie niet voor dit item. Maak of schakel over naar een portemonnee van een ondersteund activatype.", "uptime": "Uptime", "upto": "tot ${value}", + "usb": "USB", "use": "Gebruik ", "use_card_info_three": "Gebruik de digitale kaart online of met contactloze betaalmethoden.", "use_card_info_two": "Tegoeden worden omgezet naar USD wanneer ze op de prepaid-rekening staan, niet in digitale valuta.", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 976fd5760..055707345 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -79,6 +79,7 @@ "bitcoin_light_theme": "Lekki motyw Bitcoin", "bitcoin_payments_require_1_confirmation": "Płatności Bitcoin wymagają 1 potwierdzenia, co może zająć 20 minut lub dłużej. Dziękuję za cierpliwość! Otrzymasz wiadomość e-mail, gdy płatność zostanie potwierdzona.", "Blocks_remaining": "Pozostało ${status} bloków", + "bluetooth": "Bluetooth", "bright_theme": "Biały", "bump_fee": "Opłata za nierówność", "buy": "Kup", @@ -144,6 +145,8 @@ "congratulations": "gratulacje!", "connect_an_existing_yat": "Podłącz istniejący Yat", "connect_yats": "Połącz Yats", + "connect_your_hardware_wallet": "Podłącz portfel sprzętowy za pomocą Bluetooth lub USB", + "connect_your_hardware_wallet_ios": "Podłącz portfel sprzętowy za pomocą Bluetooth", "connection_sync": "Połączenie i synchronizacja", "connectWalletPrompt": "Połącz swój portfel z WalletConnect, aby dokonywać transakcji", "contact": "Kontakt", @@ -330,7 +333,13 @@ "is_percentage": "jest", "last_30_days": "Ostatnie 30 dni", "learn_more": "Dowiedz się więcej", + "ledger_connection_error": "Nie udało się połączyć z twoją księgą. Proszę spróbuj ponownie.", + "ledger_error_device_locked": "Księga jest zamknięta", + "ledger_error_tx_rejected_by_user": "Transakcja odrzucona na urządzeniu", + "ledger_error_wrong_app": "Upewnij się, że opisz odpowiednią aplikację na swojej księdze", + "ledger_please_enable_bluetooth": "Włącz Bluetooth wykrywanie księgi", "light_theme": "Jasny", + "load_more": "Załaduj więcej", "loading_your_wallet": "Ładowanie portfela", "login": "Login", "logout": "Wyloguj", @@ -444,6 +453,8 @@ "privacy_settings": "Ustawienia prywatności", "private_key": "Klucz prywatny", "proceed_after_one_minute": "Jeśli ekran nie przejdzie dalej po 1 minucie, sprawdź pocztę.", + "proceed_on_device": "Kontynuuj swoje urządzenie", + "proceed_on_device_description": "Postępuj zgodnie z instrukcjami wyświetlonymi w portfelu sprzętowym", "profile": "Profil", "provider_error": "${provider} pomyłka", "public_key": "Klucz publiczny", @@ -492,6 +503,7 @@ "restore_bitcoin_description_from_seed": "Przywróć swój portfel z frazy seed złożonej z 24 słów", "restore_bitcoin_title_from_keys": "Przywróć z klucza prywatnego", "restore_description_from_backup": "Możesz przywrócić całą aplikację Cake Wallet z pliku kopii zapasowej", + "restore_description_from_hardware_wallet": "Przywróć z portfela sprzętowego księgi", "restore_description_from_keys": "Przywróć swój portfel z kluczy prywatnych", "restore_description_from_seed": "Przywróć swój portfel z 25 lub 13-słownej frazy seed", "restore_description_from_seed_keys": "Odzyskaj swój portfel z seedów / kluczy, które zapisałeś w bezpiecznym miejscu", @@ -504,6 +516,7 @@ "restore_seed_keys_restore": "Przywracanie seedów / kluczy", "restore_spend_key_private": "Podaj prywatny klucz wglądu (view key)", "restore_title_from_backup": "Przywróć z pliku kopii zapasowej", + "restore_title_from_hardware_wallet": "Przywróć z portfela sprzętowego", "restore_title_from_keys": "Przywróć z kluczy", "restore_title_from_seed": "Przywróć z seedów", "restore_title_from_seed_keys": "Przywróć z seedów / kluczy", @@ -754,6 +767,7 @@ "unsupported_asset": "Nie obsługujemy tego działania w przypadku tego zasobu. Utwórz lub przełącz się na portfel obsługiwanego typu aktywów.", "uptime": "Czas aktu", "upto": "do ${value}", + "usb": "USB", "use": "Użyj ", "use_card_info_three": "Użyj cyfrowej karty online lub za pomocą zbliżeniowych metod płatności.", "use_card_info_two": "Środki są przeliczane na USD, gdy są przechowywane na koncie przedpłaconym, a nie w walutach cyfrowych.", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index a4bd36de9..279365d42 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -79,6 +79,7 @@ "bitcoin_light_theme": "Tema claro de bitcoin", "bitcoin_payments_require_1_confirmation": "Os pagamentos em Bitcoin exigem 1 confirmação, o que pode levar 20 minutos ou mais. Obrigado pela sua paciência! Você receberá um e-mail quando o pagamento for confirmado.", "Blocks_remaining": "${status} blocos restantes", + "bluetooth": "Bluetooth", "bright_theme": "Brilhante", "bump_fee": "Taxa de aumento", "buy": "Comprar", @@ -144,6 +145,8 @@ "congratulations": "Parabéns!", "connect_an_existing_yat": "Conecte um Yat existente", "connect_yats": "Connect Yats", + "connect_your_hardware_wallet": "Conecte sua carteira de hardware usando Bluetooth ou USB", + "connect_your_hardware_wallet_ios": "Conecte sua carteira de hardware usando o Bluetooth", "connection_sync": "Conexão e sincronização", "connectWalletPrompt": "Conecte sua carteira ao WalletConnect para fazer transações", "contact": "Contato", @@ -330,7 +333,13 @@ "is_percentage": "é", "last_30_days": "Últimos 30 dias", "learn_more": "Saber mais", + "ledger_connection_error": "Falha ao se conectar ao seu livro. Por favor, tente novamente.", + "ledger_error_device_locked": "O livro está trancado", + "ledger_error_tx_rejected_by_user": "Transação rejeitada no dispositivo", + "ledger_error_wrong_app": "Por favor, certifique -se de optar pelo aplicativo certo no seu livro", + "ledger_please_enable_bluetooth": "Ative o Bluetooth para detectar seu livro", "light_theme": "Luz", + "load_more": "Carregue mais", "loading_your_wallet": "Abrindo sua carteira", "login": "Login", "logout": "Logout", @@ -446,6 +455,8 @@ "privacy_settings": "Configurações de privacidade", "private_key": "Chave privada", "proceed_after_one_minute": "Se a tela não prosseguir após 1 minuto, verifique seu e-mail.", + "proceed_on_device": "Prossiga no seu dispositivo", + "proceed_on_device_description": "Siga as instruções solicitadas em sua carteira de hardware", "profile": "Perfil", "provider_error": "${provider} erro", "public_key": "Chave pública", @@ -494,6 +505,7 @@ "restore_bitcoin_description_from_seed": "Restaure sua carteira a partir de um código de combinação de 24 palavras", "restore_bitcoin_title_from_keys": "Restaurar de WIF", "restore_description_from_backup": "Você pode restaurar todo o aplicativo Cake Wallet de seu arquivo de backup", + "restore_description_from_hardware_wallet": "Restaurar de uma carteira de hardware do Ledger", "restore_description_from_keys": "Restaure sua carteira a partir de suas chaves privadas", "restore_description_from_seed": "Restaure sua carteira a partir de semente com 25 palavras ou 13 palavras", "restore_description_from_seed_keys": "Restaure a sua carteira a partir de sementes/chaves que você salvou em um local seguro", @@ -506,6 +518,7 @@ "restore_seed_keys_restore": "Restauração com sementes/chaves", "restore_spend_key_private": "Chave de gastos (privada)", "restore_title_from_backup": "Restaurar a partir de um arquivo de backup", + "restore_title_from_hardware_wallet": "Restaurar da carteira de hardware", "restore_title_from_keys": "Restaurar a partir de chaves", "restore_title_from_seed": "Restaurar a partir de semente", "restore_title_from_seed_keys": "Restaurar a partir de sementes/chaves", @@ -756,6 +769,7 @@ "unsupported_asset": "Não oferecemos suporte a esta ação para este recurso. Crie ou mude para uma carteira de um tipo de ativo compatível.", "uptime": "Tempo de atividade", "upto": "até ${value}", + "usb": "USB", "use": "Use PIN de ", "use_card_info_three": "Use o cartão digital online ou com métodos de pagamento sem contato.", "use_card_info_two": "Os fundos são convertidos para USD quando mantidos na conta pré-paga, não em moedas digitais.", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 05e6f25ac..f7134e298 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -79,6 +79,7 @@ "bitcoin_light_theme": "Светлая биткойн-тема", "bitcoin_payments_require_1_confirmation": "Биткойн-платежи требуют 1 подтверждения, что может занять 20 минут или дольше. Спасибо тебе за твое терпение! Вы получите электронное письмо, когда платеж будет подтвержден.", "Blocks_remaining": "${status} Осталось блоков", + "bluetooth": "Bluetooth", "bright_theme": "Яркая", "bump_fee": "Повысить комиссию", "buy": "Купить", @@ -144,6 +145,8 @@ "congratulations": "Поздравляем!", "connect_an_existing_yat": "Подключить существующий Yat", "connect_yats": "Подключить Yats", + "connect_your_hardware_wallet": "Подключите свой аппаратный кошелек с помощью Bluetooth или USB", + "connect_your_hardware_wallet_ios": "Подключите свой аппаратный кошелек с помощью Bluetooth", "connection_sync": "Подключение и синхронизация", "connectWalletPrompt": "Подключите свой кошелек к WalletConnect для совершения транзакций.", "contact": "Контакт", @@ -330,7 +333,13 @@ "is_percentage": "есть", "last_30_days": "Последние 30 дней", "learn_more": "Узнать больше", + "ledger_connection_error": "Не удалось подключиться к вам, книги. Пожалуйста, попробуйте еще раз.", + "ledger_error_device_locked": "Ledger заблокирован", + "ledger_error_tx_rejected_by_user": "Транзакция отклоняется на устройстве", + "ledger_error_wrong_app": "Пожалуйста, убедитесь, что вы предлагаете правильное приложение в своей бухгалтерской книге", + "ledger_please_enable_bluetooth": "Пожалуйста, включите Bluetooth обнаружить вашу бухгалтерскую книгу", "light_theme": "Светлая", + "load_more": "Загрузи больше", "loading_your_wallet": "Загрузка кошелька", "login": "Логин", "logout": "Выйти", @@ -445,6 +454,8 @@ "privacy_settings": "Настройки конфиденциальности", "private_key": "Приватный ключ", "proceed_after_one_minute": "Если через 1 минуту экран не отображается, проверьте свою электронную почту.", + "proceed_on_device": "Пройдите на свое устройство", + "proceed_on_device_description": "Пожалуйста, следуйте инструкциям, представленным на вашем аппаратном кошельке", "profile": "Профиль", "provider_error": "${provider} ошибка", "public_key": "Публичный ключ", @@ -493,6 +504,7 @@ "restore_bitcoin_description_from_seed": "Вы можете восстановить кошелёк используя 24-ти значную мнемоническую фразу", "restore_bitcoin_title_from_keys": "Восстановить с помощью WIF", "restore_description_from_backup": "Вы можете восстановить Cake Wallet из вашего back-up файла", + "restore_description_from_hardware_wallet": "Восстановите из оборудования", "restore_description_from_keys": "Вы можете восстановить кошелёк с помощью приватных ключей", "restore_description_from_seed": "Вы можете восстановить кошелёк используя 25-ти значную мнемоническую фразу", "restore_description_from_seed_keys": "Вы можете восстановить кошелёк из мнемонической фразы/ключей, которые вы сохранили ранее", @@ -505,6 +517,7 @@ "restore_seed_keys_restore": "Восстановить из мнемонической фразы/ключей", "restore_spend_key_private": "Приватный ключ траты", "restore_title_from_backup": "Восстановить из back-up файла", + "restore_title_from_hardware_wallet": "Восстановить из аппаратного кошелька", "restore_title_from_keys": "Восстановить с помощью ключей", "restore_title_from_seed": "Восстановить из мнемонической фразы", "restore_title_from_seed_keys": "Восстановить из мнемонической фразы/ключей", @@ -755,6 +768,7 @@ "unsupported_asset": "Мы не поддерживаем это действие для этого объекта. Пожалуйста, создайте или переключитесь на кошелек поддерживаемого типа активов.", "uptime": "Время безотказной работы", "upto": "до ${value}", + "usb": "USB", "use": "Использовать ", "use_card_info_three": "Используйте цифровую карту онлайн или с помощью бесконтактных способов оплаты.", "use_card_info_two": "Средства конвертируются в доллары США, когда они хранятся на предоплаченном счете, а не в цифровых валютах.", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index cf8e5e207..ae647cde0 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -79,6 +79,7 @@ "bitcoin_light_theme": "ธีมแสง Bitcoin", "bitcoin_payments_require_1_confirmation": "การชำระเงินด้วย Bitcoin ต้องการการยืนยัน 1 ครั้ง ซึ่งอาจใช้เวลา 20 นาทีหรือนานกว่านั้น ขอบคุณสำหรับความอดทนของคุณ! คุณจะได้รับอีเมลเมื่อการชำระเงินได้รับการยืนยัน", "Blocks_remaining": "${status} บล็อกที่เหลืออยู่", + "bluetooth": "บลูทู ธ", "bright_theme": "สดใส", "bump_fee": "ค่าธรรมเนียมชน", "buy": "ซื้อ", @@ -144,6 +145,8 @@ "congratulations": "ขอแสดงความยินดี!", "connect_an_existing_yat": "เชื่อมต่อ Yat ที่มีอยู่", "connect_yats": "เชื่อมต่อ Yats", + "connect_your_hardware_wallet": "เชื่อมต่อกระเป๋าเงินฮาร์ดแวร์ของคุณโดยใช้บลูทู ธ หรือ USB", + "connect_your_hardware_wallet_ios": "เชื่อมต่อกระเป๋าเงินฮาร์ดแวร์ของคุณโดยใช้บลูทู ธ", "connection_sync": "การเชื่อมต่อและการซิงค์", "connectWalletPrompt": "เชื่อมต่อกระเป๋าเงินของคุณด้วย WalletConnect เพื่อทำธุรกรรม", "contact": "ผู้ติดต่อ", @@ -330,7 +333,13 @@ "is_percentage": "เป็น", "last_30_days": "30 วันล่าสุด", "learn_more": "ศึกษาเพิ่มเติม", + "ledger_connection_error": "ไม่สามารถเชื่อมต่อกับบัญชีแยกประเภทของคุณได้ กรุณาลองอีกครั้ง.", + "ledger_error_device_locked": "บัญชีแยกประเภทถูกล็อค", + "ledger_error_tx_rejected_by_user": "ธุรกรรมถูกปฏิเสธบนอุปกรณ์", + "ledger_error_wrong_app": "โปรดตรวจสอบให้แน่ใจว่าคุณเปิดแอพที่เหมาะสมในบัญชีแยกประเภทของคุณ", + "ledger_please_enable_bluetooth": "โปรดเปิดใช้งานบลูทู ธ ในการตรวจจับบัญชีแยกประเภทของคุณ", "light_theme": "สว่าง", + "load_more": "โหลดมากขึ้น", "loading_your_wallet": "กำลังโหลดกระเป๋าของคุณ", "login": "เข้าสู่ระบบ", "logout": "ออกจากระบบ", @@ -444,6 +453,8 @@ "privacy_settings": "การตั้งค่าความเป็นส่วนตัว", "private_key": "คีย์ส่วนตัว", "proceed_after_one_minute": "หากหน้าจอไม่ดำเนินการหลังจาก 1 นาทีโปรดตรวจสอบอีเมลของคุณ", + "proceed_on_device": "ดำเนินการบนอุปกรณ์ของคุณ", + "proceed_on_device_description": "โปรดทำตามคำแนะนำที่ได้รับแจ้งไว้ในกระเป๋าเงินฮาร์ดแวร์ของคุณ", "profile": "ประวัติโดยย่อ", "provider_error": "ข้อผิดพลาด ${provider}", "public_key": "คีย์สาธารณะ", @@ -492,6 +503,7 @@ "restore_bitcoin_description_from_seed": "กู้กระเป๋าของคุณจากรหัสผสมของ 24 คำ", "restore_bitcoin_title_from_keys": "กู้จาก WIF", "restore_description_from_backup": "คุณสามารถกู้แอพ Cake Wallet ทั้งหมดจากไฟล์สำรองข้อมูลของคุณ", + "restore_description_from_hardware_wallet": "กู้คืนจากกระเป๋าเงินฮาร์ดแวร์บัญชีแยกประเภท", "restore_description_from_keys": "กู้กระเป๋าของคุณจากการกดปุ่มที่สร้างขึ้นจาก private keys ของคุณที่บันทึกไว้", "restore_description_from_seed": "กู้กระเป๋าของคุณจากรหัสผสมของ 25 คำหรือ 13 คำ", "restore_description_from_seed_keys": "เรียกกระเป๋าของคุณกลับมาจาก seed/keys ที่คุณได้บันทึกไว้ในที่ปลอดภัย", @@ -504,6 +516,7 @@ "restore_seed_keys_restore": "กู้จาก Seed/Keys", "restore_spend_key_private": "คีย์สำหรับใช้ (ส่วนตัว)", "restore_title_from_backup": "กู้จากการสำรองข้อมูล", + "restore_title_from_hardware_wallet": "กู้คืนจากกระเป๋าเงินฮาร์ดแวร์", "restore_title_from_keys": "กู้จาก keys", "restore_title_from_seed": "กู้จาก seed", "restore_title_from_seed_keys": "กู้จาก seed/keys", @@ -754,6 +767,7 @@ "unsupported_asset": "เราไม่สนับสนุนการกระทำนี้สำหรับเนื้อหานี้ โปรดสร้างหรือเปลี่ยนเป็นกระเป๋าเงินประเภทสินทรัพย์ที่รองรับ", "uptime": "เวลาทำงาน", "upto": "สูงสุด ${value}", + "usb": "ยูเอสบี", "use": "สลับไปที่ ", "use_card_info_three": "ใช้บัตรดิจิตอลออนไลน์หรือผ่านวิธีการชำระเงินแบบไม่ต้องใช้บัตรกระดาษ", "use_card_info_two": "เงินจะถูกแปลงค่าเป็นดอลลาร์สหรัฐเมื่อถือไว้ในบัญชีสำรองเงิน ไม่ใช่สกุลเงินดิจิตอล", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index efbe33a6b..bf8453b08 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -79,6 +79,7 @@ "bitcoin_light_theme": "Tema ng ilaw ng bitcoin", "bitcoin_payments_require_1_confirmation": "Ang mga pagbabayad sa Bitcoin ay nangangailangan ng 1 kumpirmasyon, na maaaring tumagal ng 20 minuto o mas mahaba. Salamat sa iyong pasensya! Mag -email ka kapag nakumpirma ang pagbabayad.", "Blocks_remaining": "Ang natitirang ${status} ay natitira", + "bluetooth": "Bluetooth", "bright_theme": "Maliwanag", "bump_fee": "Bayad sa paga", "buy": "Bilhin", @@ -144,6 +145,8 @@ "congratulations": "Binabati kita!", "connect_an_existing_yat": "Ikonekta ang isang umiiral na yat", "connect_yats": "Ikonekta ang mga yats", + "connect_your_hardware_wallet": "Ikonekta ang iyong wallet ng hardware gamit ang Bluetooth o USB", + "connect_your_hardware_wallet_ios": "Ikonekta ang iyong wallet ng hardware gamit ang Bluetooth", "connection_sync": "Koneksyon at pag -sync", "connectWalletPrompt": "Ikonekta ang iyong wallet sa WalletConnect upang gumawa ng mga transaksyon", "contact": "Makipag -ugnay", @@ -330,7 +333,13 @@ "is_percentage": "ay", "last_30_days": "Huling 30 araw", "learn_more": "Matuto nang higit pa", + "ledger_connection_error": "Nabigong kumonekta sa iyo ledger. Pakisubukang muli.", + "ledger_error_device_locked": "Naka -lock ang ledger", + "ledger_error_tx_rejected_by_user": "Ang transaksyon ay tinanggihan sa aparato", + "ledger_error_wrong_app": "Mangyaring tiyaking pinipili mo ang tamang app sa iyong ledger", + "ledger_please_enable_bluetooth": "Mangyaring paganahin ang Bluetooth upang makita ang iyong ledger", "light_theme": "Ilaw", + "load_more": "Mag -load pa", "loading_your_wallet": "Naglo -load ng iyong pitaka", "login": "Mag log in", "logout": "Mag -logout", @@ -444,6 +453,8 @@ "privacy_settings": "Settings para sa pagsasa-pribado", "private_key": "Pribadong susi", "proceed_after_one_minute": "Kung ang screen ay hindi magpatuloy pagkatapos ng 1 minuto, suriin ang iyong email.", + "proceed_on_device": "Magpatuloy sa iyong aparato", + "proceed_on_device_description": "Mangyaring sundin ang mga tagubilin na sinenyasan sa iyong wallet ng hardware", "profile": "Profile", "provider_error": "${provider} error", "public_key": "Pampublikong susi", @@ -492,6 +503,7 @@ "restore_bitcoin_description_from_seed": "Ibalik ang iyong pitaka mula sa 24 na code ng kombinasyon ng salita", "restore_bitcoin_title_from_keys": "Ibalik mula sa WIF", "restore_description_from_backup": "Maaari mong ibalik ang buong cake wallet app mula sa iyong back-up file", + "restore_description_from_hardware_wallet": "Ibalik mula sa isang ledger hardware wallet", "restore_description_from_keys": "Ibalik ang iyong pitaka mula sa nabuong mga keystroke na na -save mula sa iyong mga pribadong susi", "restore_description_from_seed": "Ibalik ang iyong pitaka mula sa alinman sa 25 salita o 13 na code ng kombinasyon ng salita", "restore_description_from_seed_keys": "Ibalik ang iyong pitaka mula sa mga binhi/susi na na -save mo upang ma -secure ang lugar", @@ -504,6 +516,7 @@ "restore_seed_keys_restore": "Ibinalik ang mga binhi/susi", "restore_spend_key_private": "Gumastos ng susi (pribado)", "restore_title_from_backup": "Ibalik mula sa backup", + "restore_title_from_hardware_wallet": "Ibalik mula sa pitaka ng hardware", "restore_title_from_keys": "Ibalik mula sa mga susi", "restore_title_from_seed": "Ibalik mula sa binhi", "restore_title_from_seed_keys": "Ibalik mula sa mga binhi/susi", @@ -754,6 +767,7 @@ "unsupported_asset": "Hindi namin sinusuportahan ang pagkilos na ito para sa asset na ito. Mangyaring lumikha o lumipat sa isang pitaka ng isang suportadong uri ng pag -aari.", "uptime": "Uptime", "upto": "Hanggang sa ${value}", + "usb": "USB", "use": "Lumipat sa", "use_card_info_three": "Gamitin ang digital card online o sa mga pamamaraan ng pagbabayad na walang contact.", "use_card_info_two": "Ang mga pondo ay na -convert sa USD kapag gaganapin sila sa prepaid account, hindi sa mga digital na pera.", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 03c57a767..165d92b5a 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -79,6 +79,7 @@ "bitcoin_light_theme": "Bitcoin Hafif Tema", "bitcoin_payments_require_1_confirmation": "Bitcoin ödemeleri, 20 dakika veya daha uzun sürebilen 1 onay gerektirir. Sabrınız için teşekkürler! Ödeme onaylandığında e-posta ile bilgilendirileceksiniz.", "Blocks_remaining": "${status} Blok Kaldı", + "bluetooth": "Bluetooth", "bright_theme": "Parlak", "bump_fee": "Çarpma ücreti", "buy": "Alış", @@ -144,6 +145,8 @@ "congratulations": "Tebrikler!", "connect_an_existing_yat": "Mevcut bir Yat'ı bağla", "connect_yats": "Yat'lara bağlan", + "connect_your_hardware_wallet": "Bluetooth veya USB kullanarak donanım cüzdanınızı bağlayın", + "connect_your_hardware_wallet_ios": "Bluetooth kullanarak donanım cüzdanınızı bağlayın", "connection_sync": "Bağlantı ve senkronizasyon", "connectWalletPrompt": "İşlem yapmak için cüzdanınızı WalletConnect'e bağlayın", "contact": "Rehber", @@ -330,7 +333,13 @@ "is_percentage": "is", "last_30_days": "Son 30 gün", "learn_more": "Daha fazla öğren", + "ledger_connection_error": "Ledger'e bağlanamadı. Lütfen tekrar deneyin.", + "ledger_error_device_locked": "Defter kilitli", + "ledger_error_tx_rejected_by_user": "Cihazda reddedilen işlem", + "ledger_error_wrong_app": "Lütfen defterinizde doğru uygulamayı açtığınızdan emin olun", + "ledger_please_enable_bluetooth": "Defterinizi algılamak için lütfen Bluetooth'u etkinleştirin", "light_theme": "Aydınlık", + "load_more": "Daha fazla yükle", "loading_your_wallet": "Cüzdanın yükleniyor", "login": "Login", "logout": "Çıkış yap", @@ -444,6 +453,8 @@ "privacy_settings": "Gizlilik ayarları", "private_key": "Özel anahtar", "proceed_after_one_minute": "Ekran 1 dakika sonra ilerlemezse, e-postanızı kontrol edin.", + "proceed_on_device": "Cihazınıza devam edin", + "proceed_on_device_description": "Lütfen donanım cüzdanınızda istenen talimatları izleyin", "profile": "Profil", "provider_error": "${provider} hatası", "public_key": "Genel Anahtar", @@ -492,6 +503,7 @@ "restore_bitcoin_description_from_seed": "Cüzdanınızı 24 kelimelik kombinasyon kodundan geri yükle", "restore_bitcoin_title_from_keys": "WIF'den geri yükle", "restore_description_from_backup": "Yedek dosyandan tüm Cake Wallet uygulamasını geri döndürebilirsin", + "restore_description_from_hardware_wallet": "Bir defter donanım cüzdanından geri yükleyin", "restore_description_from_keys": "Cüzdanınızı özel anahtarlarınızdan kaydedilen oluşturulmuş tuş vuruşlarından geri yükleyin", "restore_description_from_seed": "Cüzdanınızı 25 veya 13 kelimelik kombinasyon kodundan geri döndürün", "restore_description_from_seed_keys": "Güvenli bir yere kaydettiğin tohumdan/anahtarlardan cüzdanını geri döndür", @@ -504,6 +516,7 @@ "restore_seed_keys_restore": "Tohumu/Anahtarları Geri Döndür", "restore_spend_key_private": "Harcama anahtarı (Özel)", "restore_title_from_backup": "Yedekten geri döndür", + "restore_title_from_hardware_wallet": "Donanım cüzdanından geri yükleme", "restore_title_from_keys": "Anahtarlardan geri döndür", "restore_title_from_seed": "Tohumdan geri döndür", "restore_title_from_seed_keys": "Tohumdan/anahtarlardan geri döndür", @@ -754,6 +767,7 @@ "unsupported_asset": "Bu öğe için bu eylemi desteklemiyoruz. Lütfen desteklenen bir varlık türünde bir cüzdan oluşturun veya cüzdana geçiş yapın.", "uptime": "Çalışma süresi", "upto": "Şu miktara kadar: ${value}", + "usb": "USB", "use": "Şuna geç: ", "use_card_info_three": "Dijital kartı çevrimiçi olarak veya temassız ödeme yöntemleriyle kullanın.", "use_card_info_two": "Paralar, dijital para birimlerinde değil, ön ödemeli hesapta tutulduğunda USD'ye dönüştürülür.", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index ad7e7a451..22af80721 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -79,6 +79,7 @@ "bitcoin_light_theme": "Світла тема Bitcoin", "bitcoin_payments_require_1_confirmation": "Платежі Bitcoin потребують 1 підтвердження, яке може зайняти 20 хвилин або більше. Дякую за Ваше терпіння! Ви отримаєте електронний лист, коли платіж буде підтверджено.", "Blocks_remaining": "${status} Залишилось блоків", + "bluetooth": "Блюдот", "bright_theme": "Яскрава", "bump_fee": "Підвищити комісію", "buy": "Купити", @@ -144,6 +145,8 @@ "congratulations": "Вітаємо!", "connect_an_existing_yat": "Підключити існуючий Yat", "connect_yats": "Підключіть Yats", + "connect_your_hardware_wallet": "Підключіть апаратний гаманець за допомогою Bluetooth або USB", + "connect_your_hardware_wallet_ios": "Підключіть апаратний гаманець за допомогою Bluetooth", "connection_sync": "Підключення та синхронізація", "connectWalletPrompt": "Підключіть свій гаманець до WalletConnect, щоб здійснювати транзакції", "contact": "Контакт", @@ -330,7 +333,13 @@ "is_percentage": "є", "last_30_days": "Останні 30 днів", "learn_more": "Дізнатися більше", + "ledger_connection_error": "Не вдалося підключитися до вас. Будь ласка спробуйте ще раз.", + "ledger_error_device_locked": "Книга заблокована", + "ledger_error_tx_rejected_by_user": "Транзакція відхилена на пристрої", + "ledger_error_wrong_app": "Будь ласка, переконайтеся, що ви відкриваєте потрібну програму на своїй книзі", + "ledger_please_enable_bluetooth": "Будь ласка, ввімкніть Bluetooth виявити свою книгу", "light_theme": "Світла", + "load_more": "Завантажити ще", "loading_your_wallet": "Завантаження гаманця", "login": "Логін", "logout": "Вийти", @@ -444,6 +453,8 @@ "privacy_settings": "Налаштування конфіденційності", "private_key": "Приватний ключ", "proceed_after_one_minute": "Якщо екран не продовжується через 1 хвилину, перевірте свою електронну пошту.", + "proceed_on_device": "Продовжуйте свій пристрій", + "proceed_on_device_description": "Будь ласка, дотримуйтесь інструкцій, підказаних на вашому апаратному гаманці", "profile": "Профіль", "provider_error": "${provider} помилка", "public_key": "Публічний ключ", @@ -493,6 +504,7 @@ "restore_bitcoin_description_from_seed": "Ви можете відновити гаманець використовуючи 24-ти слівну мнемонічну фразу", "restore_bitcoin_title_from_keys": "Відновити за допомогою WIF", "restore_description_from_backup": "Ви можете відновити Cake Wallet з вашого резервного файлу", + "restore_description_from_hardware_wallet": "Відновлення з апаратного гаманця", "restore_description_from_keys": "Ви можете відновити гаманець за допомогою приватних ключів", "restore_description_from_seed": "Ви можете відновити гаманець використовуючи 25-ти слівну мнемонічну фразу", "restore_description_from_seed_keys": "Ви можете відновити гаманець з мнемонічної фрази/ключів, які ви зберегли раніше", @@ -505,6 +517,7 @@ "restore_seed_keys_restore": "Відновити за допомогою мнемонічної фрази/ключів", "restore_spend_key_private": "Приватний ключ витрати", "restore_title_from_backup": "Відновити із резервного файлу", + "restore_title_from_hardware_wallet": "Відновити з апаратного гаманця", "restore_title_from_keys": "Відновити за допомогою ключів", "restore_title_from_seed": "Відновити з мнемонічної фрази", "restore_title_from_seed_keys": "Відновити з мнемонічної фрази/ключів", @@ -755,6 +768,7 @@ "unsupported_asset": "Ми не підтримуємо цю дію для цього ресурсу. Створіть або перейдіть на гаманець підтримуваного типу активів.", "uptime": "Час роботи", "upto": "до ${value}", + "usb": "USB", "use": "Використати ", "use_card_info_three": "Використовуйте цифрову картку онлайн або за допомогою безконтактних методів оплати.", "use_card_info_two": "Кошти конвертуються в долари США, якщо вони зберігаються на передплаченому рахунку, а не в цифрових валютах.", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index e7cb72d7e..a75df8252 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -79,6 +79,7 @@ "bitcoin_light_theme": "بٹ کوائن لائٹ تھیم", "bitcoin_payments_require_1_confirmation": "بٹ کوائن کی ادائیگی میں 1 تصدیق کی ضرورت ہوتی ہے ، جس میں 20 منٹ یا اس سے زیادہ وقت لگ سکتا ہے۔ آپ کے صبر کا شکریہ! ادائیگی کی تصدیق ہونے پر آپ کو ای میل کیا جائے گا۔", "Blocks_remaining": "${status} بلاکس باقی ہیں۔", + "bluetooth": "بلوٹوتھ", "bright_theme": "روشن", "bump_fee": "بمپ فیس", "buy": "خریدنے", @@ -144,6 +145,8 @@ "congratulations": "مبارک ہو!", "connect_an_existing_yat": "ایک موجودہ Yat کو جوڑیں۔", "connect_yats": "Yats کو جوڑیں۔", + "connect_your_hardware_wallet": "بلوٹوتھ یا USB کا استعمال کرتے ہوئے اپنے ہارڈ ویئر پرس کو مربوط کریں", + "connect_your_hardware_wallet_ios": "بلوٹوتھ کا استعمال کرتے ہوئے اپنے ہارڈ ویئر پرس کو جوڑیں", "connection_sync": "کنکشن اور مطابقت پذیری", "connectWalletPrompt": "۔ﮟﯾﮌﻮﺟ ﮫﺗﺎﺳ ﮯﮐ WalletConnect ﻮﮐ ﮮﻮﭩﺑ ﮯﻨﭘﺍ ﮯﯿﻟ ﮯﮐ ﮯﻧﺮﮐ ﻦﯾﺩ ﻦﯿﻟ", "contact": "رابطہ کریں۔", @@ -330,7 +333,13 @@ "is_percentage": "ہے", "last_30_days": "آخری 30 دن", "learn_more": "اورجانیے", + "ledger_connection_error": "آپ سے لیجر سے رابطہ قائم کرنے میں ناکام۔ دوبارہ کوشش کریں.", + "ledger_error_device_locked": "لیجر لاک ہے", + "ledger_error_tx_rejected_by_user": "آلہ پر لین دین کو مسترد کردیا گیا", + "ledger_error_wrong_app": "براہ کرم یقینی بنائیں کہ آپ اپنے لیجر پر صحیح ایپ کو کھولتے ہیں", + "ledger_please_enable_bluetooth": "براہ کرم بلوٹوتھ کو اپنے لیجر کا پتہ لگانے کے لئے اہل بنائیں", "light_theme": "روشنی", + "load_more": "مزید لوڈ کریں", "loading_your_wallet": "آپ کا بٹوہ لوڈ ہو رہا ہے۔", "login": "لاگ ان کریں", "logout": "لاگ آوٹ", @@ -446,6 +455,8 @@ "privacy_settings": "رازداری کی ترتیبات", "private_key": "نجی کلید", "proceed_after_one_minute": "اگر اسکرین 1 منٹ کے بعد آگے نہیں بڑھتی ہے تو اپنا ای میل چیک کریں۔", + "proceed_on_device": "اپنے آلے پر آگے بڑھیں", + "proceed_on_device_description": "براہ کرم اپنے ہارڈ ویئر پرس پر آنے والی ہدایات پر عمل کریں", "profile": "پروفائل", "provider_error": "${provider} خرابی۔", "public_key": "عوامی کلید", @@ -494,6 +505,7 @@ "restore_bitcoin_description_from_seed": "24 الفاظ کے مجموعہ کوڈ سے اپنے بٹوے کو بحال کریں۔", "restore_bitcoin_title_from_keys": "WIF سے بحال کریں۔", "restore_description_from_backup": "آپ اپنی بیک اپ فائل سے پوری کیک والیٹ ایپ کو بحال کر سکتے ہیں۔", + "restore_description_from_hardware_wallet": "لیجر ہارڈ ویئر پرس سے بحال کریں", "restore_description_from_keys": "اپنے بٹوے کو اپنی نجی کلیدوں سے محفوظ کردہ کی اسٹروکس سے بحال کریں۔", "restore_description_from_seed": "اپنے بٹوے کو 25 لفظ یا 13 الفاظ کے مجموعہ کوڈ سے بحال کریں۔", "restore_description_from_seed_keys": "اپنے بٹوے کو بیج / چابیاں سے واپس حاصل کریں جنہیں آپ نے محفوظ جگہ پر محفوظ کیا ہے۔", @@ -506,6 +518,7 @@ "restore_seed_keys_restore": "بیج/کیز کی بحالی", "restore_spend_key_private": "خرچ کی کلید (نجی)", "restore_title_from_backup": "بیک اپ سے بحال کریں۔", + "restore_title_from_hardware_wallet": "ہارڈ ویئر پرس سے بحال کریں", "restore_title_from_keys": "چابیاں سے بحال کریں۔", "restore_title_from_seed": "بیج سے بحال کریں۔", "restore_title_from_seed_keys": "بیج / چابیاں سے بحال کریں۔", @@ -756,6 +769,7 @@ "unsupported_asset": "۔ﮟﯾﺮﮐ ﭻﺋﻮﺳ ﺮﭘ ﺱﺍ ﺎﯾ ﮟﯿﺋﺎﻨﺑ ﺱﺮﭘ ﺎﮐ ﻢﺴﻗ ﯽﮐ ﮧﺛﺎﺛﺍ ﮧﺘﻓﺎﯾ ﻥﻭﺎﻌﺗ ﻡﺮﮐ ﮦﺍﺮﺑ ۔ﮟﯿﮨ ﮯﺗﺮﮐ ﮟﯿﮩﻧ ﺖﯾﺎﻤﺣ ﯽﮐ ﯽﺋﺍﻭﺭﺭﺎﮐ ﺱﺍ ﮯﯿﻟ ﮯﮐ ﮧﺛﺎﺛﺍ ﺱﺍ ﻢﮨ", "uptime": "اپ ٹائم", "upto": "${value} تک", + "usb": "یو ایس بی", "use": "تبدیل کرنا", "use_card_info_three": "ڈیجیٹل کارڈ آن لائن یا کنٹیکٹ لیس ادائیگی کے طریقوں کے ساتھ استعمال کریں۔", "use_card_info_two": "رقوم کو امریکی ڈالر میں تبدیل کیا جاتا ہے جب پری پیڈ اکاؤنٹ میں رکھا جاتا ہے، ڈیجیٹل کرنسیوں میں نہیں۔", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index bdbf5df6d..c4d83e94d 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -79,6 +79,7 @@ "bitcoin_light_theme": "Bitcoin Light Akori", "bitcoin_payments_require_1_confirmation": "Àwọn àránṣẹ́ Bitcoin nílò ìjẹ́rìísí kan. Ó lè lo ìṣéjú ogun tàbí ìṣéjú jù. A dúpẹ́ fún sùúrù yín! Ẹ máa gba ímeèlì t'ó bá jẹ́rìísí àránṣẹ́ náà.", "Blocks_remaining": "Àkójọpọ̀ ${status} kikù", + "bluetooth": "Bluetooth", "bright_theme": "Funfun", "bump_fee": "Ọya ija", "buy": "Rà", @@ -144,6 +145,8 @@ "congratulations": "Ẹ kúuṣẹ́ ooo!", "connect_an_existing_yat": "So Yat wíwà", "connect_yats": "So àwọn Yat", + "connect_your_hardware_wallet": "So apamọwọ irinṣẹ rẹ nipa lilo Bluetooth tabi USB", + "connect_your_hardware_wallet_ios": "So apamọwọ ẹrọ rẹ ni lilo Bluetooth", "connection_sync": "Ìkànpọ̀ àti ìbádọ́gba", "connectWalletPrompt": "So apamọwọ rẹ pọ pẹlu WalletConnect lati ṣe awọn iṣowo", "contact": "Olùbásọ̀rọ̀", @@ -331,7 +334,13 @@ "is_percentage": "jẹ́", "last_30_days": "Ọ̀jọ̀ mọ́gbọ̀n tó kọjà", "learn_more": "Túbọ̀ kọ́", + "ledger_connection_error": "O kuna lati sopọ mọ ọ. Jọwọ gbiyanju lẹẹkansi.", + "ledger_error_device_locked": "Ledger wa ni titiipa", + "ledger_error_tx_rejected_by_user": "Idunadura kọ lori ẹrọ", + "ledger_error_wrong_app": "Jọwọ rii daju pe iwọ yoo sọ app ti o tọ loju omi rẹ", + "ledger_please_enable_bluetooth": "Jọwọ jẹ ki Bluetooth lati rii iṣupọ rẹ", "light_theme": "Funfun bí eérú", + "load_more": "Ẹru diẹ sii", "loading_your_wallet": "A ń ṣí àpamọ́wọ́ yín", "login": "Orúkọ", "logout": "Jáde", @@ -445,6 +454,8 @@ "privacy_settings": "Ààtò àdáni", "private_key": "Kọ́kọ́rọ́ àdáni", "proceed_after_one_minute": "Tí aṣàfihàn kò bá tẹ̀síwájú l'áàárín ìṣẹ́jú kan, ẹ tọ́ ímeèlì yín wò.", + "proceed_on_device": "Tẹsiwaju lori ẹrọ rẹ", + "proceed_on_device_description": "Jọwọ tẹle awọn ilana ti a ṣe lori apamọwọ ohun elo rẹ", "profile": "profaili", "provider_error": "Àṣìṣe ${provider}", "public_key": "Kọ́kọ́rọ́ tó kò àdáni", @@ -493,6 +504,7 @@ "restore_bitcoin_description_from_seed": "Mú àpamọ́wọ́ yín padà láti àkànpọ̀ ọlọ́rọ̀ ẹ̀ẹ̀mẹrinlélógun", "restore_bitcoin_title_from_keys": "Mú padà láti WIF", "restore_description_from_backup": "Ẹ lè fi ẹ̀dà nípamọ́ yín mú odindi Cake Wallet áàpù padà.", + "restore_description_from_hardware_wallet": "Mu pada kuro ninu apamọwọ ohun elo ti o yanilenu", "restore_description_from_keys": "Mú àpamọ́wọ́ yín padà láti àwọn àtẹ̀ nípamọ́ láti àwọn kọ́kọ́rọ́ àdáni yín", "restore_description_from_seed": "Ẹ mú àpamọ́wọ́ yín padà láti àkànpọ̀ ọlọ́rọ̀ ẹ̀ẹ̀marùndínlọgbọ̀n tàbí ti mẹ́talá.", "restore_description_from_seed_keys": "Mú àpamọ́wọ́ yín padà láti hóró/kọ́kọ́rọ́ t'ẹ́ ti pamọ́ sí ibi láìléwu", @@ -505,6 +517,7 @@ "restore_seed_keys_restore": "Mú hóró/kọ́kọ́rọ́ padà", "restore_spend_key_private": "kọ́kọ́rọ́ àdáni fún níná", "restore_title_from_backup": "Fi ẹ̀dà nípamọ́ mú padà", + "restore_title_from_hardware_wallet": "Mu pada kuro ninu apamọwọ ohun elo", "restore_title_from_keys": "Fi kọ́kọ́rọ́ ṣẹ̀dá", "restore_title_from_seed": "Fi hóró mú padà", "restore_title_from_seed_keys": "Fi hóró/kọ́kọ́rọ́ mú padà", @@ -755,6 +768,7 @@ "unsupported_asset": "A ko ṣe atilẹyin iṣẹ yii fun dukia yii. Jọwọ ṣẹda tabi yipada si apamọwọ iru dukia atilẹyin.", "uptime": "Iduro", "upto": "kò tóbi ju ${value}", + "usb": "USB", "use": "Lo", "use_card_info_three": "Ẹ lo káàdí ayélujára lórí wẹ́ẹ̀bù tàbí ẹ lò ó lórí àwọn ẹ̀rọ̀ ìrajà tíwọn kò kò.", "use_card_info_two": "A pààrọ̀ owó sí owó Amẹ́ríkà tó bá wà nínú àkanti t'á ti fikún tẹ́lẹ̀tẹ́lẹ̀. A kò kó owó náà nínú owó ayélujára.", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index d0e607e63..9893afeed 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -79,6 +79,7 @@ "bitcoin_light_theme": "比特币浅色主题", "bitcoin_payments_require_1_confirmation": "比特币支付需要 1 次确认,这可能需要 20 分钟或更长时间。谢谢你的耐心!确认付款后,您将收到电子邮件。", "Blocks_remaining": "${status} 剩余的块", + "bluetooth": "蓝牙", "bright_theme": "明亮", "bump_fee": "撞费", "buy": "购买", @@ -144,6 +145,8 @@ "congratulations": "恭喜!", "connect_an_existing_yat": "連接現有 Yat", "connect_yats": "连接 Yats", + "connect_your_hardware_wallet": "使用蓝牙或USB连接硬件钱包", + "connect_your_hardware_wallet_ios": "使用蓝牙连接硬件钱包", "connection_sync": "连接和同步", "connectWalletPrompt": "将您的钱包与 WalletConnect 连接以进行交易", "contact": "联系", @@ -330,7 +333,13 @@ "is_percentage": "是", "last_30_days": "过去 30 天", "learn_more": "了解更多", + "ledger_connection_error": "无法连接到您的分类帐。请再试一次。", + "ledger_error_device_locked": "分类帐已锁定", + "ledger_error_tx_rejected_by_user": "交易在设备上拒绝", + "ledger_error_wrong_app": "请确保您在分类帐中操作正确的应用程序", + "ledger_please_enable_bluetooth": "请启用蓝牙来检测您的分类帐", "light_theme": "艳丽", + "load_more": "装载更多", "loading_your_wallet": "加载您的钱包", "login": "登录", "logout": "注销", @@ -444,6 +453,8 @@ "privacy_settings": "隐私设置", "private_key": "私钥", "proceed_after_one_minute": "如果屏幕在 1 分钟后没有继续,请检查您的电子邮件。", + "proceed_on_device": "在设备上继续", + "proceed_on_device_description": "请按照您的硬件钱包上提示的说明进行操作", "profile": "轮廓", "provider_error": "${provider} 错误", "public_key": "公钥", @@ -492,6 +503,7 @@ "restore_bitcoin_description_from_seed": "从24个文字的组成码恢复您的钱包", "restore_bitcoin_title_from_keys": "从WIF还原", "restore_description_from_backup": "您可以从还原整个Cake Wallet应用您的备份文件", + "restore_description_from_hardware_wallet": "从分类帐硬件钱包还原", "restore_description_from_keys": "使用私钥恢复钱包", "restore_description_from_seed": "从25个字中恢复您的钱包或13个字的组合码", "restore_description_from_seed_keys": "从保存到安全地方的种子/钥匙取回钱包", @@ -504,6 +516,7 @@ "restore_seed_keys_restore": "种子/密钥还原", "restore_spend_key_private": "Spend私钥", "restore_title_from_backup": "从备份文件还原", + "restore_title_from_hardware_wallet": "从硬件钱包还原", "restore_title_from_keys": "从密钥还原", "restore_title_from_seed": "从种子还原", "restore_title_from_seed_keys": "从种子/密钥还原", @@ -754,6 +767,7 @@ "unsupported_asset": "我们不支持针对该资产采取此操作。请创建或切换到支持的资产类型的钱包。", "uptime": "正常运行时间", "upto": "最高 ${value}", + "usb": "USB", "use": "切换使用", "use_card_info_three": "在线使用电子卡或使用非接触式支付方式。", "use_card_info_two": "预付账户中的资金转换为美元,不是数字货币。", diff --git a/scripts/android/pubspec_gen.sh b/scripts/android/pubspec_gen.sh index d238052fe..bc7985506 100755 --- a/scripts/android/pubspec_gen.sh +++ b/scripts/android/pubspec_gen.sh @@ -10,7 +10,7 @@ case $APP_ANDROID_TYPE in CONFIG_ARGS="--monero" ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana" + CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana --tron" ;; $HAVEN) CONFIG_ARGS="--haven" diff --git a/scripts/ios/app_config.sh b/scripts/ios/app_config.sh index 9f59d6632..ab7fbd422 100755 --- a/scripts/ios/app_config.sh +++ b/scripts/ios/app_config.sh @@ -28,7 +28,7 @@ case $APP_IOS_TYPE in CONFIG_ARGS="--monero" ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana" + CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana --tron" ;; $HAVEN) diff --git a/scripts/macos/app_config.sh b/scripts/macos/app_config.sh index bd1417c4b..a1143bb12 100755 --- a/scripts/macos/app_config.sh +++ b/scripts/macos/app_config.sh @@ -31,7 +31,7 @@ case $APP_MACOS_TYPE in $MONERO_COM) CONFIG_ARGS="--monero";; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana";; #--haven + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron";; #--haven esac cp -rf pubspec_description.yaml pubspec.yaml diff --git a/tool/configure.dart b/tool/configure.dart index ceb0c9ccc..f2391103d 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -8,6 +8,7 @@ const bitcoinCashOutputPath = 'lib/bitcoin_cash/bitcoin_cash.dart'; const nanoOutputPath = 'lib/nano/nano.dart'; const polygonOutputPath = 'lib/polygon/polygon.dart'; const solanaOutputPath = 'lib/solana/solana.dart'; +const tronOutputPath = 'lib/tron/tron.dart'; const walletTypesPath = 'lib/wallet_types.g.dart'; const pubspecDefaultPath = 'pubspec_default.yaml'; const pubspecOutputPath = 'pubspec.yaml'; @@ -23,6 +24,7 @@ Future main(List args) async { final hasBanano = args.contains('${prefix}banano'); final hasPolygon = args.contains('${prefix}polygon'); final hasSolana = args.contains('${prefix}solana'); + final hasTron = args.contains('${prefix}tron'); await generateBitcoin(hasBitcoin); await generateMonero(hasMonero); @@ -32,6 +34,7 @@ Future main(List args) async { await generateNano(hasNano); await generatePolygon(hasPolygon); await generateSolana(hasSolana); + await generateTron(hasTron); // await generateBanano(hasEthereum); await generatePubspec( @@ -44,6 +47,7 @@ Future main(List args) async { hasBitcoinCash: hasBitcoinCash, hasPolygon: hasPolygon, hasSolana: hasSolana, + hasTron: hasTron, ); await generateWalletTypes( hasMonero: hasMonero, @@ -55,6 +59,7 @@ Future main(List args) async { hasBitcoinCash: hasBitcoinCash, hasPolygon: hasPolygon, hasSolana: hasSolana, + hasTron: hasTron, ); } @@ -63,9 +68,13 @@ Future generateBitcoin(bool hasImplementation) async { const bitcoinCommonHeaders = """ import 'dart:typed_data'; import 'package:cw_core/node.dart'; +import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; +import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/unspent_transaction_output.dart'; +import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/transaction_priority.dart'; @@ -73,8 +82,8 @@ import 'package:cw_core/output_info.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/view_model/send/output.dart'; import 'package:hive/hive.dart'; +import 'package:ledger_flutter/ledger_flutter.dart'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as btc; import 'package:bip32/bip32.dart' as bip32; @@ -88,6 +97,7 @@ import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; +import 'package:cw_bitcoin/bitcoin_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; @@ -100,6 +110,7 @@ import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/litecoin_wallet_service.dart'; import 'package:cw_bitcoin/script_hash.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; +import 'package:cw_bitcoin/bitcoin_hardware_wallet_service.dart'; import 'package:mobx/mobx.dart'; """; const bitcoinCwPart = "part 'cw_bitcoin.dart';"; @@ -134,6 +145,7 @@ abstract class Bitcoin { }); WalletCredentials createBitcoinRestoreWalletFromWIFCredentials({required String name, required String password, required String wif, WalletInfo? walletInfo}); WalletCredentials createBitcoinNewWalletCredentials({required String name, WalletInfo? walletInfo}); + WalletCredentials createBitcoinHardwareWalletCredentials({required String name, required HardwareAccountData accountData, WalletInfo? walletInfo}); List getWordList(); Map getWalletKeys(Object wallet); List getTransactionPriorities(); @@ -182,6 +194,9 @@ abstract class Bitcoin { int getFeeAmountForPriority(Object wallet, TransactionPriority priority, int inputsCount, int outputsCount, {int? size}); int getEstimatedFeeWithFeeRate(Object wallet, int feeRate, int? amount, {int? outputsCount, int? size}); int getMaxCustomFeeRate(Object wallet); + + void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device); + Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}); } """; @@ -563,9 +578,11 @@ abstract class HavenAccountList { Future generateEthereum(bool hasImplementation) async { final outputFile = File(ethereumOutputPath); const ethereumCommonHeaders = """ +import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; +import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:cw_core/output_info.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_priority.dart'; @@ -574,6 +591,7 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:hive/hive.dart'; +import 'package:ledger_flutter/ledger_flutter.dart'; import 'package:web3dart/web3dart.dart'; """; @@ -584,6 +602,9 @@ import 'package:cw_evm/evm_chain_transaction_credentials.dart'; import 'package:cw_evm/evm_chain_transaction_info.dart'; import 'package:cw_evm/evm_chain_transaction_priority.dart'; import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart'; +import 'package:cw_evm/evm_chain_hardware_wallet_service.dart'; +import 'package:cw_evm/evm_ledger_credentials.dart'; +import 'package:cw_evm/evm_chain_wallet.dart'; import 'package:cw_ethereum/ethereum_client.dart'; import 'package:cw_ethereum/ethereum_wallet.dart'; @@ -600,6 +621,7 @@ abstract class Ethereum { WalletCredentials createEthereumNewWalletCredentials({required String name, WalletInfo? walletInfo}); WalletCredentials createEthereumRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password}); WalletCredentials createEthereumRestoreWalletFromPrivateKey({required String name, required String privateKey, required String password}); + WalletCredentials createEthereumHardwareWalletCredentials({required String name, required HardwareAccountData hwAccountData, WalletInfo? walletInfo}); String getAddress(WalletBase wallet); String getPrivateKey(WalletBase wallet); String getPublicKey(WalletBase wallet); @@ -633,6 +655,9 @@ abstract class Ethereum { void updateEtherscanUsageState(WalletBase wallet, bool isEnabled); Web3Client? getWeb3Client(WalletBase wallet); String getTokenAddress(CryptoCurrency asset); + + void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device); + Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}); } """; @@ -656,9 +681,11 @@ abstract class Ethereum { Future generatePolygon(bool hasImplementation) async { final outputFile = File(polygonOutputPath); const polygonCommonHeaders = """ +import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; +import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:cw_core/output_info.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_priority.dart'; @@ -667,16 +694,20 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:hive/hive.dart'; +import 'package:ledger_flutter/ledger_flutter.dart'; import 'package:web3dart/web3dart.dart'; """; const polygonCWHeaders = """ import 'package:cw_evm/evm_chain_formatter.dart'; import 'package:cw_evm/evm_chain_mnemonics.dart'; +import 'package:cw_evm/evm_chain_transaction_credentials.dart'; import 'package:cw_evm/evm_chain_transaction_info.dart'; import 'package:cw_evm/evm_chain_transaction_priority.dart'; -import 'package:cw_evm/evm_chain_transaction_credentials.dart'; import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart'; +import 'package:cw_evm/evm_chain_hardware_wallet_service.dart'; +import 'package:cw_evm/evm_ledger_credentials.dart'; +import 'package:cw_evm/evm_chain_wallet.dart'; import 'package:cw_polygon/polygon_client.dart'; import 'package:cw_polygon/polygon_wallet.dart'; @@ -693,6 +724,7 @@ abstract class Polygon { WalletCredentials createPolygonNewWalletCredentials({required String name, WalletInfo? walletInfo}); WalletCredentials createPolygonRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password}); WalletCredentials createPolygonRestoreWalletFromPrivateKey({required String name, required String privateKey, required String password}); + WalletCredentials createPolygonHardwareWalletCredentials({required String name, required HardwareAccountData hwAccountData, WalletInfo? walletInfo}); String getAddress(WalletBase wallet); String getPrivateKey(WalletBase wallet); String getPublicKey(WalletBase wallet); @@ -726,6 +758,9 @@ abstract class Polygon { void updatePolygonScanUsageState(WalletBase wallet, bool isEnabled); Web3Client? getWeb3Client(WalletBase wallet); String getTokenAddress(CryptoCurrency asset); + + void setLedger(WalletBase wallet, Ledger ledger, LedgerDevice device); + Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}); } """; @@ -1024,6 +1059,79 @@ abstract class Solana { await outputFile.writeAsString(output); } +Future generateTron(bool hasImplementation) async { + final outputFile = File(tronOutputPath); + const tronCommonHeaders = """ +import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/output_info.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:hive/hive.dart'; + +"""; + const tronCWHeaders = """ +import 'package:cw_evm/evm_chain_mnemonics.dart'; +import 'package:cw_tron/tron_transaction_credentials.dart'; +import 'package:cw_tron/tron_transaction_info.dart'; +import 'package:cw_tron/tron_wallet_creation_credentials.dart'; + +import 'package:cw_tron/tron_client.dart'; +import 'package:cw_tron/tron_token.dart'; +import 'package:cw_tron/tron_wallet.dart'; +import 'package:cw_tron/tron_wallet_service.dart'; + +"""; + const tronCwPart = "part 'cw_tron.dart';"; + const tronContent = """ +abstract class Tron { + List getTronWordList(String language); + WalletService createTronWalletService(Box walletInfoSource); + WalletCredentials createTronNewWalletCredentials({required String name, WalletInfo? walletInfo}); + WalletCredentials createTronRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password}); + WalletCredentials createTronRestoreWalletFromPrivateKey({required String name, required String privateKey, required String password}); + String getAddress(WalletBase wallet); + + Object createTronTransactionCredentials( + List outputs, { + required CryptoCurrency currency, + }); + + List getTronTokenCurrencies(WalletBase wallet); + Future addTronToken(WalletBase wallet, CryptoCurrency token, String contractAddress); + Future deleteTronToken(WalletBase wallet, CryptoCurrency token); + Future getTronToken(WalletBase wallet, String contractAddress); + + double getTransactionAmountRaw(TransactionInfo transactionInfo); + CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction); + String getTokenAddress(CryptoCurrency asset); + String getTronBase58Address(String hexAddress, WalletBase wallet); + + String? getTronNativeEstimatedFee(WalletBase wallet); + String? getTronTRC20EstimatedFee(WalletBase wallet); +} + """; + + const tronEmptyDefinition = 'Tron? tron;\n'; + const tronCWDefinition = 'Tron? tron = CWTron();\n'; + + final output = '$tronCommonHeaders\n' + + (hasImplementation ? '$tronCWHeaders\n' : '\n') + + (hasImplementation ? '$tronCwPart\n\n' : '\n') + + (hasImplementation ? tronCWDefinition : tronEmptyDefinition) + + '\n' + + tronContent; + + if (outputFile.existsSync()) { + await outputFile.delete(); + } + + await outputFile.writeAsString(output); +} + Future generatePubspec( {required bool hasMonero, required bool hasBitcoin, @@ -1033,7 +1141,8 @@ Future generatePubspec( required bool hasBanano, required bool hasBitcoinCash, required bool hasPolygon, - required bool hasSolana}) async { + required bool hasSolana, + required bool hasTron}) async { const cwCore = """ cw_core: path: ./cw_core @@ -1082,6 +1191,10 @@ Future generatePubspec( cw_evm: path: ./cw_evm """; + const cwTron = """ + cw_tron: + path: ./cw_tron + """; final inputFile = File(pubspecOutputPath); final inputText = await inputFile.readAsString(); final inputLines = inputText.split('\n'); @@ -1121,6 +1234,10 @@ Future generatePubspec( output += '\n$cwSolana'; } + if (hasTron) { + output += '\n$cwTron'; + } + if (hasHaven && !hasMonero) { output += '\n$cwSharedExternal\n$cwHaven'; } else if (hasHaven) { @@ -1152,7 +1269,8 @@ Future generateWalletTypes( required bool hasBanano, required bool hasBitcoinCash, required bool hasPolygon, - required bool hasSolana}) async { + required bool hasSolana, + required bool hasTron}) async { final walletTypesFile = File(walletTypesPath); if (walletTypesFile.existsSync()) { @@ -1191,6 +1309,10 @@ Future generateWalletTypes( outputContent += '\tWalletType.solana,\n'; } + if (hasTron) { + outputContent += '\tWalletType.tron,\n'; + } + if (hasNano) { outputContent += '\tWalletType.nano,\n'; } diff --git a/tool/generate_secrets_config.dart b/tool/generate_secrets_config.dart index 8745c2933..6aaa39b7c 100644 --- a/tool/generate_secrets_config.dart +++ b/tool/generate_secrets_config.dart @@ -6,6 +6,7 @@ import 'utils/utils.dart'; const configPath = 'tool/.secrets-config.json'; const evmChainsConfigPath = 'tool/.evm-secrets-config.json'; const solanaConfigPath = 'tool/.solana-secrets-config.json'; +const tronConfigPath = 'tool/.tron-secrets-config.json'; Future main(List args) async => generateSecretsConfig(args); @@ -20,9 +21,10 @@ Future generateSecretsConfig(List args) async { final configFile = File(configPath); final evmChainsConfigFile = File(evmChainsConfigPath); final solanaConfigFile = File(solanaConfigPath); + final tronConfigFile = File(tronConfigPath); final secrets = {}; - + secrets.addAll(extraInfo); secrets.removeWhere((key, dynamic value) { if (key.contains('--')) { @@ -78,4 +80,18 @@ Future generateSecretsConfig(List args) async { secretsJson = JsonEncoder.withIndent(' ').convert(secrets); await solanaConfigFile.writeAsString(secretsJson); + + secrets.clear(); + + SecretKey.tronSecrets.forEach((sec) { + if (secrets[sec.name] != null) { + return; + } + + secrets[sec.name] = sec.generate(); + }); + + secretsJson = JsonEncoder.withIndent(' ').convert(secrets); + + await tronConfigFile.writeAsString(secretsJson); } diff --git a/tool/import_secrets_config.dart b/tool/import_secrets_config.dart index 02061669b..b2f3ca691 100644 --- a/tool/import_secrets_config.dart +++ b/tool/import_secrets_config.dart @@ -10,6 +10,9 @@ const evmChainsOutputPath = 'cw_evm/lib/.secrets.g.dart'; const solanaConfigPath = 'tool/.solana-secrets-config.json'; const solanaOutputPath = 'cw_solana/lib/.secrets.g.dart'; + +const tronConfigPath = 'tool/.tron-secrets-config.json'; +const tronOutputPath = 'cw_tron/lib/.secrets.g.dart'; Future main(List args) async => importSecretsConfig(); Future importSecretsConfig() async { @@ -29,6 +32,11 @@ Future importSecretsConfig() async { final solanaOutput = solanaInput.keys.fold('', (String acc, String val) => acc + generateConst(val, solanaInput)); + final tronOutputFile = File(tronOutputPath); + final tronInput = json.decode(File(tronConfigPath).readAsStringSync()) as Map; + final tronOutput = + tronInput.keys.fold('', (String acc, String val) => acc + generateConst(val, tronInput)); + if (outputFile.existsSync()) { await outputFile.delete(); } @@ -46,4 +54,10 @@ Future importSecretsConfig() async { } await solanaOutputFile.writeAsString(solanaOutput); + + if (tronOutputFile.existsSync()) { + await tronOutputFile.delete(); + } + + await tronOutputFile.writeAsString(tronOutput); } diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index 5d5e61cec..542e91b38 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -50,6 +50,10 @@ class SecretKey { SecretKey('ankrApiKey', () => ''), ]; + static final tronSecrets = [ + SecretKey('tronGridApiKey', () => ''), + ]; + final String name; final String Function() generate; }