diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index 88dd2c1eb..4df215e13 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -6,9 +6,9 @@ on: workflow_dispatch: inputs: branch: - description: 'Branch name to build' + description: "Branch name to build" required: true - default: 'main' + default: "main" jobs: PR_test_build: @@ -104,22 +104,14 @@ jobs: - name: Build generated code run: | cd /opt/android/cake_wallet - cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. - cd cw_evm && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. - cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. - cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. - cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. - cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. - cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. - cd cw_ethereum && flutter pub get && cd .. - cd cw_polygon && flutter pub get && cd .. - flutter packages pub run build_runner build --delete-conflicting-outputs + ./model_generator.sh - name: Add secrets run: | cd /opt/android/cake_wallet touch lib/.secrets.g.dart touch cw_evm/lib/.secrets.g.dart + touch cw_solana/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 @@ -154,45 +146,50 @@ jobs: echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart + echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart - name: Rename app - run: echo -e "id=com.cakewallet.test\nname=${{ env.BRANCH_NAME }}" > /opt/android/cake_wallet/android/app.properties + run: | + hash=`sha512sum <<<"${{ env.BRANCH_NAME }}"` + substring=${hash:0:15} + echo substring + echo -e "id=com.cakewallet.test_$(substring)\nname=${{ env.BRANCH_NAME }}" > /opt/android/cake_wallet/android/app.properties - name: Build run: | cd /opt/android/cake_wallet - flutter build apk --release + flutter build apk --release --split-per-abi -# - name: Push to App Center -# run: | -# echo 'Installing App Center CLI tools' -# npm install -g appcenter-cli -# echo "Publishing test to App Center" -# appcenter distribute release \ -# --group "Testers" \ -# --file "/opt/android/cake_wallet/build/app/outputs/apk/release/app-release.apk" \ -# --release-notes ${{ env.BRANCH_NAME }} \ -# --app Cake-Labs/Cake-Wallet \ -# --token ${{ secrets.APP_CENTER_TOKEN }} \ -# --quiet + # - name: Push to App Center + # run: | + # echo 'Installing App Center CLI tools' + # npm install -g appcenter-cli + # echo "Publishing test to App Center" + # appcenter distribute release \ + # --group "Testers" \ + # --file "/opt/android/cake_wallet/build/app/outputs/apk/release/app-release.apk" \ + # --release-notes ${{ env.BRANCH_NAME }} \ + # --app Cake-Labs/Cake-Wallet \ + # --token ${{ secrets.APP_CENTER_TOKEN }} \ + # --quiet - name: Rename apk file run: | - cd /opt/android/cake_wallet/build/app/outputs/apk/release + cd /opt/android/cake_wallet/build/app/outputs/flutter-apk mkdir test-apk - cp app-release.apk test-apk/${{env.BRANCH_NAME}}.apk + cp app-arm64-v8a-release.apk test-apk/${{env.BRANCH_NAME}}.apk - name: Upload Artifact uses: kittaakos/upload-artifact-as-is@v0 with: - path: /opt/android/cake_wallet/build/app/outputs/apk/release/test-apk/ + path: /opt/android/cake_wallet/build/app/outputs/flutter-apk/test-apk/ - name: Send Test APK continue-on-error: true uses: adrey/slack-file-upload-action@1.0.5 with: token: ${{ secrets.SLACK_APP_TOKEN }} - path: /opt/android/cake_wallet/build/app/outputs/apk/release/app-release.apk + path: /opt/android/cake_wallet/build/app/outputs/flutter-apk/test-apk/${{env.BRANCH_NAME}}.apk channel: ${{ secrets.SLACK_APK_CHANNEL }} title: "${{ env.BRANCH_NAME }}.apk" filename: ${{ env.BRANCH_NAME }}.apk diff --git a/.gitignore b/.gitignore index f084f8d0d..6f2d0a182 100644 --- a/.gitignore +++ b/.gitignore @@ -86,14 +86,17 @@ cw_monero/cw_monero/android/.cxx/ **/*.g.dart android/key.properties +android/app/key.jks **/tool/.secrets-prod.json **/tool/.secrets-test.json **/tool/.secrets-config.json **/tool/.evm-secrets-config.json **/tool/.ethereum-secrets-config.json +**/tool/.solana-secrets-config.json **/lib/.secrets.g.dart **/cw_evm/lib/.secrets.g.dart +**/cw_solana/lib/.secrets.g.dart vendor/ @@ -128,6 +131,7 @@ lib/ethereum/ethereum.dart lib/bitcoin_cash/bitcoin_cash.dart lib/nano/nano.dart lib/polygon/polygon.dart +lib/solana/solana.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 180190914..eea9b5521 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -66,6 +66,7 @@ + [OPS['OP_HASH160'], hash, OPS['OP_EQUAL']]); -} - -Uint8List addressToOutputScript( - String address, bitcoin.NetworkType networkType) { +List addressToOutputScript(String address, bitcoin.BasedUtxoNetwork network) { try { - // FIXME: improve validation for p2sh addresses - // 3 for bitcoin - // m for litecoin - if (address.startsWith('3') || address.toLowerCase().startsWith('m')) { - return p2shAddressToOutputScript(address); - } - - return Address.addressToOutputScript(address, networkType); + return bitcoin.addressToOutputScript(address: address, network: network); } catch (err) { print(err); return Uint8List(0); diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index 676edb4a5..d8d908230 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -1,6 +1,9 @@ import 'dart:convert'; import 'package:bitbox/bitbox.dart' as bitbox; +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_bitcoin/script_hash.dart' as sh; + class BitcoinAddressRecord { BitcoinAddressRecord( this.address, { @@ -10,23 +13,41 @@ class BitcoinAddressRecord { int balance = 0, String name = '', bool isUsed = false, + required this.type, + String? scriptHash, + required this.network, }) : _txCount = txCount, _balance = balance, _name = name, - _isUsed = isUsed; + _isUsed = isUsed, + scriptHash = + scriptHash ?? (network != null ? sh.scriptHash(address, network: network) : null); - factory BitcoinAddressRecord.fromJSON(String jsonSource) { + factory BitcoinAddressRecord.fromJSON(String jsonSource, BasedUtxoNetwork? network) { final decoded = json.decode(jsonSource) as Map; - return BitcoinAddressRecord(decoded['address'] as String, - index: decoded['index'] as int, - isHidden: decoded['isHidden'] as bool? ?? false, - isUsed: decoded['isUsed'] as bool? ?? false, - txCount: decoded['txCount'] as int? ?? 0, - name: decoded['name'] as String? ?? '', - balance: decoded['balance'] as int? ?? 0); + return BitcoinAddressRecord( + decoded['address'] as String, + index: decoded['index'] as int, + isHidden: decoded['isHidden'] as bool? ?? false, + isUsed: decoded['isUsed'] as bool? ?? false, + txCount: decoded['txCount'] as int? ?? 0, + name: decoded['name'] as String? ?? '', + balance: decoded['balance'] as int? ?? 0, + type: decoded['type'] != null && decoded['type'] != '' + ? BitcoinAddressType.values + .firstWhere((type) => type.toString() == decoded['type'] as String) + : SegwitAddresType.p2wpkh, + scriptHash: decoded['scriptHash'] as String?, + network: (decoded['network'] as String?) == null + ? network + : BasedUtxoNetwork.fromName(decoded['network'] as String), + ); } + @override + bool operator ==(Object o) => o is BitcoinAddressRecord && address == o.address; + final String address; bool isHidden; final int index; @@ -34,6 +55,8 @@ class BitcoinAddressRecord { int _balance; String _name; bool _isUsed; + String? scriptHash; + BasedUtxoNetwork? network; int get txCount => _txCount; @@ -50,21 +73,28 @@ class BitcoinAddressRecord { void setAsUsed() => _isUsed = true; void setNewName(String label) => _name = label; - @override - bool operator ==(Object o) => o is BitcoinAddressRecord && address == o.address; - @override int get hashCode => address.hashCode; String get cashAddr => bitbox.Address.toCashAddress(address); + BitcoinAddressType type; + + String updateScriptHash(BasedUtxoNetwork network) { + scriptHash = sh.scriptHash(address, network: network); + return scriptHash!; + } + String toJSON() => json.encode({ 'address': address, 'index': index, 'isHidden': isHidden, + 'isUsed': isUsed, 'txCount': txCount, 'name': name, - 'isUsed': isUsed, 'balance': balance, + 'type': type.toString(), + 'scriptHash': scriptHash, + 'network': network?.value, }); } diff --git a/cw_bitcoin/lib/bitcoin_receive_page_option.dart b/cw_bitcoin/lib/bitcoin_receive_page_option.dart new file mode 100644 index 000000000..2d2339a41 --- /dev/null +++ b/cw_bitcoin/lib/bitcoin_receive_page_option.dart @@ -0,0 +1,42 @@ +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_core/receive_page_option.dart'; + +class BitcoinReceivePageOption implements ReceivePageOption { + static const p2wpkh = BitcoinReceivePageOption._('Segwit (P2WPKH) (Default)'); + static const p2sh = BitcoinReceivePageOption._('Segwit-Compatible (P2SH)'); + static const p2tr = BitcoinReceivePageOption._('Taproot (P2TR)'); + static const p2wsh = BitcoinReceivePageOption._('Segwit (P2WSH)'); + static const p2pkh = BitcoinReceivePageOption._('Legacy (P2PKH)'); + + const BitcoinReceivePageOption._(this.value); + + final String value; + + String toString() { + return value; + } + + static const all = [ + BitcoinReceivePageOption.p2wpkh, + BitcoinReceivePageOption.p2tr, + BitcoinReceivePageOption.p2wsh, + BitcoinReceivePageOption.p2sh, + BitcoinReceivePageOption.p2pkh + ]; + + factory BitcoinReceivePageOption.fromType(BitcoinAddressType type) { + switch (type) { + case SegwitAddresType.p2tr: + return BitcoinReceivePageOption.p2tr; + case SegwitAddresType.p2wsh: + return BitcoinReceivePageOption.p2wsh; + case P2pkhAddressType.p2pkh: + return BitcoinReceivePageOption.p2pkh; + case P2shAddressType.p2wpkhInP2sh: + return BitcoinReceivePageOption.p2sh; + case SegwitAddresType.p2wpkh: + default: + return BitcoinReceivePageOption.p2wpkh; + } + } +} diff --git a/cw_bitcoin/lib/bitcoin_unspent.dart b/cw_bitcoin/lib/bitcoin_unspent.dart index 9c198c27c..52edea091 100644 --- a/cw_bitcoin/lib/bitcoin_unspent.dart +++ b/cw_bitcoin/lib/bitcoin_unspent.dart @@ -6,10 +6,9 @@ class BitcoinUnspent extends Unspent { : bitcoinAddressRecord = addressRecord, super(addressRecord.address, hash, value, vout, null); - factory BitcoinUnspent.fromJSON( - BitcoinAddressRecord address, Map json) => - BitcoinUnspent(address, json['tx_hash'] as String, json['value'] as int, - json['tx_pos'] as int); + factory BitcoinUnspent.fromJSON(BitcoinAddressRecord address, Map json) => + BitcoinUnspent( + address, json['tx_hash'] as String, json['value'] as int, json['tx_pos'] as int); final BitcoinAddressRecord bitcoinAddressRecord; } diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 9cdb78f2d..3b3e9c636 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -1,3 +1,4 @@ +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/unspent_coins_info.dart'; @@ -17,36 +18,42 @@ part 'bitcoin_wallet.g.dart'; 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, - List? initialAddresses, - ElectrumBalance? initialBalance, - int initialRegularAddressIndex = 0, - int initialChangeAddressIndex = 0}) - : super( + BitcoinWalletBase({ + required String mnemonic, + required String password, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + required Uint8List seedBytes, + String? addressPageType, + BasedUtxoNetwork? networkParam, + List? initialAddresses, + ElectrumBalance? initialBalance, + Map? initialRegularAddressIndex, + Map? initialChangeAddressIndex, + }) : super( mnemonic: mnemonic, password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, - networkType: bitcoin.bitcoin, + networkType: networkParam == null + ? bitcoin.bitcoin + : networkParam == BitcoinNetwork.mainnet + ? bitcoin.bitcoin + : bitcoin.testnet, initialAddresses: initialAddresses, initialBalance: initialBalance, seedBytes: seedBytes, currency: CryptoCurrency.btc) { walletAddresses = BitcoinWalletAddresses( - walletInfo, - electrumClient: electrumClient, - initialAddresses: initialAddresses, - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex, - mainHd: hd, - sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType) - .derivePath("m/0'/1"), - networkType: networkType); + walletInfo, + electrumClient: electrumClient, + initialAddresses: initialAddresses, + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex, + mainHd: hd, + sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/1"), + network: networkParam ?? network, + ); autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; }); @@ -57,21 +64,26 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { required String password, required WalletInfo walletInfo, required Box unspentCoinsInfo, + String? addressPageType, + BasedUtxoNetwork? network, List? initialAddresses, ElectrumBalance? initialBalance, - int initialRegularAddressIndex = 0, - int initialChangeAddressIndex = 0 + Map? initialRegularAddressIndex, + Map? initialChangeAddressIndex, }) async { return BitcoinWallet( - mnemonic: mnemonic, - password: password, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfo, - initialAddresses: initialAddresses, - initialBalance: initialBalance, - seedBytes: await mnemonicToSeedBytes(mnemonic), - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex); + mnemonic: mnemonic, + password: password, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + initialAddresses: initialAddresses, + initialBalance: initialBalance, + seedBytes: await mnemonicToSeedBytes(mnemonic), + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex, + addressPageType: addressPageType, + networkParam: network, + ); } static Future open({ @@ -80,16 +92,21 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { required Box unspentCoinsInfo, required String password, }) async { - final snp = await ElectrumWallletSnapshot.load(name, walletInfo.type, password); + final snp = await ElectrumWalletSnapshot.load(name, walletInfo.type, password, + walletInfo.network != null ? BasedUtxoNetwork.fromName(walletInfo.network!) : null); + return BitcoinWallet( - mnemonic: snp.mnemonic, - password: password, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfo, - initialAddresses: snp.addresses, - initialBalance: snp.balance, - seedBytes: await mnemonicToSeedBytes(snp.mnemonic), - initialRegularAddressIndex: snp.regularAddressIndex, - initialChangeAddressIndex: snp.changeAddressIndex); + mnemonic: snp.mnemonic, + password: password, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + initialAddresses: snp.addresses, + initialBalance: snp.balance, + seedBytes: await mnemonicToSeedBytes(snp.mnemonic), + initialRegularAddressIndex: snp.regularAddressIndex, + initialChangeAddressIndex: snp.changeAddressIndex, + addressPageType: snp.addressPageType, + networkParam: snp.network, + ); } -} \ No newline at end of file +} diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 36d37127d..f12577492 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -1,6 +1,5 @@ -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; -import 'package:cw_bitcoin/bitcoin_address_record.dart'; -import 'package:cw_bitcoin/electrum.dart'; +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/wallet_info.dart'; @@ -11,24 +10,31 @@ part 'bitcoin_wallet_addresses.g.dart'; class BitcoinWalletAddresses = BitcoinWalletAddressesBase with _$BitcoinWalletAddresses; abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with Store { - BitcoinWalletAddressesBase(WalletInfo walletInfo, - {required bitcoin.HDWallet mainHd, - required bitcoin.HDWallet sideHd, - required bitcoin.NetworkType networkType, - required ElectrumClient electrumClient, - List? initialAddresses, - int initialRegularAddressIndex = 0, - int initialChangeAddressIndex = 0}) - : super(walletInfo, - initialAddresses: initialAddresses, - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex, - mainHd: mainHd, - sideHd: sideHd, - electrumClient: electrumClient, - networkType: networkType); + BitcoinWalletAddressesBase( + WalletInfo walletInfo, { + required super.mainHd, + required super.sideHd, + required super.network, + required super.electrumClient, + super.initialAddresses, + super.initialRegularAddressIndex, + super.initialChangeAddressIndex, + }) : super(walletInfo); @override - String getAddress({required int index, required bitcoin.HDWallet hd}) => - generateP2WPKHAddress(hd: hd, index: index, networkType: networkType); + String getAddress({required int index, required HDWallet hd, BitcoinAddressType? addressType}) { + if (addressType == P2pkhAddressType.p2pkh) + return generateP2PKHAddress(hd: hd, index: index, network: network); + + if (addressType == SegwitAddresType.p2tr) + return generateP2TRAddress(hd: hd, index: index, network: network); + + if (addressType == SegwitAddresType.p2wsh) + return generateP2WSHAddress(hd: hd, index: index, network: network); + + if (addressType == P2shAddressType.p2wpkhInP2sh) + return generateP2SHAddress(hd: hd, index: index, network: network); + + return generateP2WPKHAddress(hd: hd, index: index, network: network); + } } diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index 736ec1044..38e769d15 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -1,6 +1,7 @@ import 'dart:io'; +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; -import 'package:cw_bitcoin/bitcoin_mnemonic_is_incorrect_exception.dart'; +import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart'; import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_base.dart'; @@ -23,12 +24,17 @@ class BitcoinWalletService extends WalletService WalletType.bitcoin; @override - Future create(BitcoinNewWalletCredentials credentials) async { + Future create(BitcoinNewWalletCredentials credentials, {bool? isTestnet}) async { + final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet; + credentials.walletInfo?.network = network.value; + final wallet = await BitcoinWalletBase.create( - mnemonic: await generateMnemonic(), - password: credentials.password!, - walletInfo: credentials.walletInfo!, - unspentCoinsInfo: unspentCoinsInfoSource); + mnemonic: await generateMnemonic(), + password: credentials.password!, + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + network: network, + ); await wallet.save(); await wallet.init(); return wallet; @@ -92,20 +98,27 @@ class BitcoinWalletService extends WalletService restoreFromKeys(BitcoinRestoreWalletFromWIFCredentials credentials) async => + Future restoreFromKeys(BitcoinRestoreWalletFromWIFCredentials credentials, + {bool? isTestnet}) async => throw UnimplementedError(); @override - Future restoreFromSeed(BitcoinRestoreWalletFromSeedCredentials credentials) async { + Future restoreFromSeed(BitcoinRestoreWalletFromSeedCredentials credentials, + {bool? isTestnet}) async { if (!validateMnemonic(credentials.mnemonic)) { throw BitcoinMnemonicIsIncorrectException(); } + final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet; + credentials.walletInfo?.network = network.value; + final wallet = await BitcoinWalletBase.create( - password: credentials.password!, - mnemonic: credentials.mnemonic, - walletInfo: credentials.walletInfo!, - unspentCoinsInfo: unspentCoinsInfoSource); + password: credentials.password!, + mnemonic: credentials.mnemonic, + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + network: network, + ); await wallet.save(); await wallet.init(); return wallet; diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index a05c251fe..51a53e285 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -2,12 +2,12 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'package:bitcoin_flutter/bitcoin_flutter.dart'; +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/script_hash.dart'; import 'package:flutter/foundation.dart'; import 'package:rxdart/rxdart.dart'; -import 'package:collection/collection.dart'; +import 'package:http/http.dart' as http; String jsonrpcparams(List params) { final _params = params?.map((val) => '"${val.toString()}"')?.join(','); @@ -22,10 +22,7 @@ String jsonrpc( '{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${json.encode(params)}}\n'; class SocketTask { - SocketTask({ - required this.isSubscription, - this.completer, - this.subject}); + SocketTask({required this.isSubscription, this.completer, this.subject}); final Completer? completer; final BehaviorSubject? subject; @@ -51,8 +48,7 @@ class ElectrumClient { Timer? _aliveTimer; String unterminatedString; - Future connectToUri(Uri uri) async => - await connect(host: uri.host, port: uri.port); + Future connectToUri(Uri uri) async => await connect(host: uri.host, port: uri.port); Future connect({required String host, required int port}) async { try { @@ -104,21 +100,20 @@ class ElectrumClient { } if (isJSONStringCorrect(unterminatedString)) { - final response = - json.decode(unterminatedString) as Map; + final response = json.decode(unterminatedString) as Map; _handleResponse(response); unterminatedString = ''; } } on TypeError catch (e) { - if (!e.toString().contains('Map') && !e.toString().contains('Map')) { + if (!e.toString().contains('Map') && + !e.toString().contains('Map')) { return; } unterminatedString += message; if (isJSONStringCorrect(unterminatedString)) { - final response = - json.decode(unterminatedString) as Map; + final response = json.decode(unterminatedString) as Map; _handleResponse(response); // unterminatedString = null; unterminatedString = ''; @@ -142,8 +137,7 @@ class ElectrumClient { } } - Future> version() => - call(method: 'server.version').then((dynamic result) { + Future> version() => call(method: 'server.version').then((dynamic result) { if (result is List) { return result.map((dynamic val) => val.toString()).toList(); } @@ -178,11 +172,10 @@ class ElectrumClient { }); Future>> getListUnspentWithAddress( - String address, NetworkType networkType) => + String address, BasedUtxoNetwork network) => call( - method: 'blockchain.scripthash.listunspent', - params: [scriptHash(address, networkType: networkType)]) - .then((dynamic result) { + method: 'blockchain.scripthash.listunspent', + params: [scriptHash(address, network: network)]).then((dynamic result) { if (result is List) { return result.map((dynamic val) { if (val is Map) { @@ -229,8 +222,7 @@ class ElectrumClient { return []; }); - Future> getTransactionRaw( - {required String hash}) async => + Future> getTransactionRaw({required String hash}) async => callWithTimeout(method: 'blockchain.transaction.get', params: [hash, true], timeout: 10000) .then((dynamic result) { if (result is Map) { @@ -240,8 +232,7 @@ class ElectrumClient { return {}; }); - Future getTransactionHex( - {required String hash}) async => + Future getTransactionHex({required String hash}) async => callWithTimeout(method: 'blockchain.transaction.get', params: [hash, false], timeout: 10000) .then((dynamic result) { if (result is String) { @@ -252,29 +243,40 @@ class ElectrumClient { }); Future broadcastTransaction( - {required String transactionRaw}) async => - call(method: 'blockchain.transaction.broadcast', params: [transactionRaw]) - .then((dynamic result) { - if (result is String) { - return result; + {required String transactionRaw, BasedUtxoNetwork? network}) async { + if (network == BitcoinNetwork.testnet) { + return http + .post(Uri(scheme: 'https', host: 'blockstream.info', path: '/testnet/api/tx'), + headers: {'Content-Type': 'application/json; charset=utf-8'}, + body: transactionRaw) + .then((http.Response response) { + if (response.statusCode == 200) { + return response.body; } - return ''; + throw Exception('Failed to broadcast transaction: ${response.body}'); }); + } - Future> getMerkle( - {required String hash, required int height}) async => - await call( - method: 'blockchain.transaction.get_merkle', - params: [hash, height]) as Map; + return call(method: 'blockchain.transaction.broadcast', params: [transactionRaw]) + .then((dynamic result) { + if (result is String) { + return result; + } - Future> getHeader({required int height}) async => - await call(method: 'blockchain.block.get_header', params: [height]) + return ''; + }); + } + + Future> getMerkle({required String hash, required int height}) async => + await call(method: 'blockchain.transaction.get_merkle', params: [hash, height]) as Map; + Future> getHeader({required int height}) async => + await call(method: 'blockchain.block.get_header', params: [height]) as Map; + Future estimatefee({required int p}) => - call(method: 'blockchain.estimatefee', params: [p]) - .then((dynamic result) { + call(method: 'blockchain.estimatefee', params: [p]).then((dynamic result) { if (result is double) { return result; } @@ -314,20 +316,17 @@ class ElectrumClient { return []; }); - Future> feeRates() async { + Future> feeRates({BasedUtxoNetwork? network}) async { + if (network == BitcoinNetwork.testnet) { + return [1, 1, 1]; + } try { final topDoubleString = await estimatefee(p: 1); final middleDoubleString = await estimatefee(p: 5); final bottomDoubleString = await estimatefee(p: 100); - final top = - (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000) - .round(); - final middle = - (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000) - .round(); - final bottom = - (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000) - .round(); + final top = (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000).round(); + final middle = (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000).round(); + final bottom = (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000).round(); return [bottom, middle, top]; } catch (_) { @@ -335,6 +334,21 @@ class ElectrumClient { } } + // https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-headers-subscribe + // example response: + // { + // "height": 520481, + // "hex": "00000020890208a0ae3a3892aa047c5468725846577cfcd9b512b50000000000000000005dc2b02f2d297a9064ee103036c14d678f9afc7e3d9409cf53fd58b82e938e8ecbeca05a2d2103188ce804c4" + // } + Future getCurrentBlockChainTip() => + call(method: 'blockchain.headers.subscribe').then((result) { + if (result is Map) { + return result["height"] as int; + } + + return null; + }); + BehaviorSubject? scripthashUpdate(String scripthash) { _id += 1; return subscribe( @@ -344,16 +358,14 @@ class ElectrumClient { } BehaviorSubject? subscribe( - {required String id, - required String method, - List params = const []}) { + {required String id, required String method, List params = const []}) { try { final subscription = BehaviorSubject(); _regisrySubscription(id, subscription); socket!.write(jsonrpc(method: method, id: _id, params: params)); return subscription; - } catch(e) { + } catch (e) { print(e.toString()); return null; } @@ -370,9 +382,7 @@ class ElectrumClient { } Future callWithTimeout( - {required String method, - List params = const [], - int timeout = 4000}) async { + {required String method, List params = const [], int timeout = 4000}) async { try { final completer = Completer(); _id += 1; @@ -386,7 +396,7 @@ class ElectrumClient { }); return completer.future; - } catch(e) { + } catch (e) { print(e.toString()); } } @@ -397,8 +407,8 @@ class ElectrumClient { onConnectionStatusChange = null; } - void _registryTask(int id, Completer completer) => _tasks[id.toString()] = - SocketTask(completer: completer, isSubscription: false); + void _registryTask(int id, Completer completer) => + _tasks[id.toString()] = SocketTask(completer: completer, isSubscription: false); void _regisrySubscription(String id, BehaviorSubject subject) => _tasks[id] = SocketTask(subject: subject, isSubscription: true); @@ -419,8 +429,7 @@ class ElectrumClient { } } - void _methodHandler( - {required String method, required Map request}) { + void _methodHandler({required String method, required Map request}) { switch (method) { case 'blockchain.scripthash.subscribe': final params = request['params'] as List; @@ -451,8 +460,8 @@ class ElectrumClient { _methodHandler(method: method, request: response); return; } - - if (id != null){ + + if (id != null) { _finish(id, result); } } diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index bf5ec2c4f..cfea0e089 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -1,3 +1,4 @@ +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; import 'package:cw_bitcoin/address_from_output.dart'; @@ -10,13 +11,12 @@ import 'package:cw_core/wallet_type.dart'; class ElectrumTransactionBundle { ElectrumTransactionBundle(this.originalTransaction, - {required this.ins, - required this.confirmations, - this.time}); - final bitcoin.Transaction originalTransaction; - final List ins; + {required this.ins, required this.confirmations, this.time, required this.height}); + final BtcTransaction originalTransaction; + final List ins; final int? time; final int confirmations; + final int height; } class ElectrumTransactionInfo extends TransactionInfo { @@ -39,8 +39,7 @@ class ElectrumTransactionInfo extends TransactionInfo { this.confirmations = confirmations; } - factory ElectrumTransactionInfo.fromElectrumVerbose( - Map obj, WalletType type, + factory ElectrumTransactionInfo.fromElectrumVerbose(Map obj, WalletType type, {required List addresses, required int height}) { final addressesSet = addresses.map((addr) => addr.address).toSet(); final id = obj['txid'] as String; @@ -58,10 +57,8 @@ class ElectrumTransactionInfo extends TransactionInfo { for (dynamic vin in vins) { final vout = vin['vout'] as int; final out = vin['tx']['vout'][vout] as Map; - final outAddresses = - (out['scriptPubKey']['addresses'] as List?)?.toSet(); - inputsAmount += - stringDoubleToBitcoinAmount((out['value'] as double? ?? 0).toString()); + final outAddresses = (out['scriptPubKey']['addresses'] as List?)?.toSet(); + inputsAmount += stringDoubleToBitcoinAmount((out['value'] as double? ?? 0).toString()); if (outAddresses?.intersection(addressesSet).isNotEmpty ?? false) { direction = TransactionDirection.outgoing; @@ -69,11 +66,9 @@ class ElectrumTransactionInfo extends TransactionInfo { } for (dynamic out in vout) { - final outAddresses = - out['scriptPubKey']['addresses'] as List? ?? []; + final outAddresses = out['scriptPubKey']['addresses'] as List? ?? []; final ntrs = outAddresses.toSet().intersection(addressesSet); - final value = stringDoubleToBitcoinAmount( - (out['value'] as double? ?? 0.0).toString()); + final value = stringDoubleToBitcoinAmount((out['value'] as double? ?? 0.0).toString()); totalOutAmount += value; if ((direction == TransactionDirection.incoming && ntrs.isNotEmpty) || @@ -96,44 +91,50 @@ class ElectrumTransactionInfo extends TransactionInfo { } factory ElectrumTransactionInfo.fromElectrumBundle( - ElectrumTransactionBundle bundle, - WalletType type, - bitcoin.NetworkType networkType, - {required Set addresses, - required int height}) { + ElectrumTransactionBundle bundle, WalletType type, BasedUtxoNetwork network, + {required Set addresses, required int height}) { final date = bundle.time != null - ? DateTime.fromMillisecondsSinceEpoch(bundle.time! * 1000) - : DateTime.now(); + ? DateTime.fromMillisecondsSinceEpoch(bundle.time! * 1000) + : DateTime.now(); var direction = TransactionDirection.incoming; var amount = 0; var inputAmount = 0; var totalOutAmount = 0; - for (var i = 0; i < bundle.originalTransaction.ins.length; i++) { - final input = bundle.originalTransaction.ins[i]; + for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) { + final input = bundle.originalTransaction.inputs[i]; final inputTransaction = bundle.ins[i]; - final vout = input.index; - final outTransaction = inputTransaction.outs[vout!]; - final address = addressFromOutput(outTransaction.script!, networkType); - inputAmount += outTransaction.value!; - if (addresses.contains(address)) { + final outTransaction = inputTransaction.outputs[input.txIndex]; + inputAmount += outTransaction.amount.toInt(); + if (addresses.contains(addressFromOutputScript(outTransaction.scriptPubKey, network))) { direction = TransactionDirection.outgoing; } } - for (final out in bundle.originalTransaction.outs) { - totalOutAmount += out.value!; - final address = addressFromOutput(out.script!, networkType); - final addressExists = addresses.contains(address); + final receivedAmounts = []; + for (final out in bundle.originalTransaction.outputs) { + totalOutAmount += out.amount.toInt(); + final addressExists = addresses.contains(addressFromOutputScript(out.scriptPubKey, network)); + + if (addressExists) { + receivedAmounts.add(out.amount.toInt()); + } + if ((direction == TransactionDirection.incoming && addressExists) || (direction == TransactionDirection.outgoing && !addressExists)) { - amount += out.value!; + amount += out.amount.toInt(); } } + if (receivedAmounts.length == bundle.originalTransaction.outputs.length) { + // Self-send + direction = TransactionDirection.incoming; + amount = receivedAmounts.reduce((a, b) => a + b); + } + final fee = inputAmount - totalOutAmount; return ElectrumTransactionInfo(type, - id: bundle.originalTransaction.getId(), + id: bundle.originalTransaction.txId(), height: height, isPending: bundle.confirmations == 0, fee: fee, @@ -152,8 +153,8 @@ class ElectrumTransactionInfo extends TransactionInfo { if (addresses != null) { tx.outs.forEach((out) { try { - final p2pkh = bitcoin.P2PKH( - data: PaymentData(output: out.script), network: bitcoin.bitcoin); + final p2pkh = + bitcoin.P2PKH(data: PaymentData(output: out.script), network: bitcoin.bitcoin); exist = addresses.contains(p2pkh.data.address); if (exist) { @@ -163,9 +164,8 @@ class ElectrumTransactionInfo extends TransactionInfo { }); } - final date = timestamp != null - ? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000) - : DateTime.now(); + final date = + timestamp != null ? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000) : DateTime.now(); return ElectrumTransactionInfo(type, id: tx.getId(), @@ -178,8 +178,7 @@ class ElectrumTransactionInfo extends TransactionInfo { confirmations: confirmations); } - factory ElectrumTransactionInfo.fromJson( - Map data, WalletType type) { + factory ElectrumTransactionInfo.fromJson(Map data, WalletType type) { return ElectrumTransactionInfo(type, id: data['id'] as String, height: data['height'] as int, diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 6ca7f684d..fde2404a7 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -3,9 +3,10 @@ import 'dart:convert'; 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:collection/collection.dart'; -import 'package:cw_bitcoin/address_to_output_script.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/bitcoin_transaction_no_inputs_exception.dart'; @@ -18,6 +19,7 @@ import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_transaction_history.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; +import 'package:cw_bitcoin/litecoin_network.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/script_hash.dart'; import 'package:cw_bitcoin/utils.dart'; @@ -33,10 +35,10 @@ import 'package:cw_core/utils/file.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:flutter/foundation.dart'; -import 'package:hex/hex.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:rxdart/subjects.dart'; +import 'package:http/http.dart' as http; part 'electrum_wallet.g.dart'; @@ -73,6 +75,12 @@ abstract class ElectrumWalletBase } : {}), this.unspentCoinsInfo = unspentCoinsInfo, + this.network = networkType == bitcoin.bitcoin + ? BitcoinNetwork.mainnet + : networkType == litecoinNetwork + ? LitecoinNetwork.mainnet + : BitcoinNetwork.testnet, + this.isTestnet = networkType == bitcoin.testnet, super(walletInfo) { this.electrumClient = electrumClient ?? ElectrumClient(); this.walletInfo = walletInfo; @@ -106,13 +114,13 @@ abstract class ElectrumWalletBase @observable SyncStatus syncStatus; - List get scriptHashes => walletAddresses.addresses - .map((addr) => scriptHash(addr.address, networkType: networkType)) + List get scriptHashes => walletAddresses.addressesByReceiveType + .map((addr) => scriptHash(addr.address, network: network)) .toList(); - List get publicScriptHashes => walletAddresses.addresses + List get publicScriptHashes => walletAddresses.allAddresses .where((addr) => !addr.isHidden) - .map((addr) => scriptHash(addr.address, networkType: networkType)) + .map((addr) => scriptHash(addr.address, network: network)) .toList(); String get xpub => hd.base58!; @@ -121,6 +129,10 @@ abstract class ElectrumWalletBase String get seed => mnemonic; bitcoin.NetworkType networkType; + BasedUtxoNetwork network; + + @override + bool? isTestnet; @override BitcoinWalletKeys get keys => @@ -145,12 +157,11 @@ abstract class ElectrumWalletBase Future startSync() async { try { syncStatus = AttemptingSyncStatus(); - await walletAddresses.discoverAddresses(); await updateTransactions(); _subscribeForUpdates(); await updateUnspent(); await updateBalance(); - _feeRates = await electrumClient.feeRates(); + _feeRates = await electrumClient.feeRates(network: network); Timer.periodic( const Duration(minutes: 1), (timer) async => _feeRates = await electrumClient.feeRates()); @@ -181,188 +192,211 @@ abstract class ElectrumWalletBase } } - @override - Future createTransaction(Object credentials) async { - const minAmount = 546; - final transactionCredentials = credentials as BitcoinTransactionCredentials; - final inputs = []; - final outputs = transactionCredentials.outputs; - final hasMultiDestination = outputs.length > 1; + Future _estimateTxFeeAndInputsToUse( + int credentialsAmount, + bool sendAll, + List outputAddresses, + List outputs, + BitcoinTransactionCredentials transactionCredentials, + {int? inputsCount}) async { + final utxos = []; + List privateKeys = []; + + var leftAmount = credentialsAmount; var allInputsAmount = 0; final String? opReturnMemo = outputs.first.memo; + for (int i = 0; i < unspentCoins.length; i++) { + final utx = unspentCoins[i]; - if (unspentCoins.isEmpty) { - await updateUnspent(); - } - - for (final utx in unspentCoins) { if (utx.isSending) { allInputsAmount += utx.value; - inputs.add(utx); - } - } - - if (inputs.isEmpty) { - throw BitcoinTransactionNoInputsException(); - } - - final allAmountFee = transactionCredentials.feeRate != null - ? feeAmountWithFeeRate(transactionCredentials.feeRate!, inputs.length, outputs.length) - : feeAmountForPriority(transactionCredentials.priority!, inputs.length, outputs.length); - - final allAmount = allInputsAmount - allAmountFee; - - var credentialsAmount = 0; - var amount = 0; - var fee = 0; - - if (hasMultiDestination) { - if (outputs.any((item) => item.sendAll || item.formattedCryptoAmount! <= 0)) { - throw BitcoinTransactionWrongBalanceException(currency); - } - - credentialsAmount = outputs.fold(0, (acc, value) => acc + value.formattedCryptoAmount!); - - if (allAmount - credentialsAmount < minAmount) { - throw BitcoinTransactionWrongBalanceException(currency); - } - - amount = credentialsAmount; - - if (transactionCredentials.feeRate != null) { - fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount, - outputsCount: outputs.length + 1); - } else { - fee = calculateEstimatedFee(transactionCredentials.priority, amount, - outputsCount: outputs.length + 1); - } - } else { - final output = outputs.first; - credentialsAmount = !output.sendAll ? output.formattedCryptoAmount! : 0; - - if (credentialsAmount > allAmount) { - throw BitcoinTransactionWrongBalanceException(currency); - } - - amount = output.sendAll || allAmount - credentialsAmount < minAmount - ? allAmount - : credentialsAmount; - - if (output.sendAll || amount == allAmount) { - fee = allAmountFee; - } else if (transactionCredentials.feeRate != null) { - fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount); - } else { - fee = calculateEstimatedFee(transactionCredentials.priority, amount); - } - } - - if (fee == 0) { - throw BitcoinTransactionWrongBalanceException(currency); - } - - final totalAmount = amount + fee; - - if (totalAmount > balance[currency]!.confirmed || totalAmount > allInputsAmount) { - throw BitcoinTransactionWrongBalanceException(currency); - } - - final txb = bitcoin.TransactionBuilder(network: networkType); - final changeAddress = await walletAddresses.getChangeAddress(); - var leftAmount = totalAmount; - var totalInputAmount = 0; - - inputs.clear(); - - for (final utx in unspentCoins) { - if (utx.isSending) { leftAmount = leftAmount - utx.value; - totalInputAmount += utx.value; - inputs.add(utx); - if (leftAmount <= 0) { + 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); + + utxos.add( + UtxoWithAddress( + utxo: BitcoinUtxo( + txHash: utx.hash, + value: BigInt.from(utx.value), + vout: utx.vout, + scriptType: _getScriptType(address), + ), + ownerDetails: + UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: address), + ), + ); + + bool amountIsAcquired = !sendAll && leftAmount <= 0; + if ((inputsCount == null && amountIsAcquired) || inputsCount == i + 1) { break; } } } - if (inputs.isEmpty) { + if (utxos.isEmpty) { throw BitcoinTransactionNoInputsException(); } - if (amount <= 0 || totalInputAmount < totalAmount) { + var changeValue = allInputsAmount - credentialsAmount; + + if (!sendAll) { + if (changeValue > 0) { + final changeAddress = await walletAddresses.getChangeAddress(); + final address = _addressTypeFromStr(changeAddress, network); + outputAddresses.add(address); + outputs.add(BitcoinOutput(address: address, value: BigInt.from(changeValue))); + } + } + + final estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize( + utxos: utxos, outputs: outputs, network: network); + + final fee = transactionCredentials.feeRate != null + ? feeAmountWithFeeRate(transactionCredentials.feeRate!, 0, 0, size: estimatedSize) + : feeAmountForPriority(transactionCredentials.priority!, 0, 0, size: estimatedSize); + + if (fee == 0) { throw BitcoinTransactionWrongBalanceException(currency); } - txb.setVersion(1); - inputs.forEach((input) { - if (input.isP2wpkh) { - final p2wpkh = bitcoin - .P2WPKH( - data: generatePaymentData( - hd: input.bitcoinAddressRecord.isHidden - ? walletAddresses.sideHd - : walletAddresses.mainHd, - index: input.bitcoinAddressRecord.index), - network: networkType) - .data; + var amount = credentialsAmount; - txb.addInput(input.hash, input.vout, null, p2wpkh.output); - } else { - txb.addInput(input.hash, input.vout); + final lastOutput = outputs.last; + if (!sendAll) { + if (changeValue > fee) { + // Here, lastOutput is change, deduct the fee from it + outputs[outputs.length - 1] = + BitcoinOutput(address: lastOutput.address, value: lastOutput.value - BigInt.from(fee)); } - }); - - outputs.forEach((item) { - final outputAmount = hasMultiDestination ? item.formattedCryptoAmount : amount; - final outputAddress = item.isParsedAddress ? item.extractedAddress! : item.address; - txb.addOutput(addressToOutputScript(outputAddress, networkType), outputAmount!); - }); - - final estimatedSize = estimatedTransactionSize(inputs.length, outputs.length + 1); - var feeAmount = 0; - - if (transactionCredentials.feeRate != null) { - feeAmount = transactionCredentials.feeRate! * estimatedSize; } else { - feeAmount = feeRate(transactionCredentials.priority!) * estimatedSize; + // Here, if sendAll, the output amount equals to the input value - fee to fully spend every input on the transaction and have no amount for change + amount = allInputsAmount - fee; + outputs[outputs.length - 1] = + BitcoinOutput(address: lastOutput.address, value: BigInt.from(amount)); } - final changeValue = totalInputAmount - amount - feeAmount; + final totalAmount = amount + fee; - if (changeValue > minAmount) { - txb.addOutput(changeAddress, changeValue); + if (totalAmount > balance[currency]!.confirmed) { + throw BitcoinTransactionWrongBalanceException(currency); } - if (opReturnMemo != null) txb.addOutputData(opReturnMemo); + if (totalAmount > allInputsAmount) { + if (unspentCoins.where((utx) => utx.isSending).length == utxos.length) { + throw BitcoinTransactionWrongBalanceException(currency); + } else { + if (changeValue > fee) { + outputAddresses.removeLast(); + outputs.removeLast(); + } - for (var i = 0; i < inputs.length; i++) { - final input = inputs[i]; - final keyPair = generateKeyPair( - hd: input.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, - index: input.bitcoinAddressRecord.index, - network: networkType); - final witnessValue = input.isP2wpkh ? input.value : null; - - txb.sign(vin: i, keyPair: keyPair, witnessValue: witnessValue); + return _estimateTxFeeAndInputsToUse( + credentialsAmount, sendAll, outputAddresses, outputs, transactionCredentials, + inputsCount: utxos.length + 1); + } } - return PendingBitcoinTransaction(txb.build(), type, - electrumClient: electrumClient, amount: amount, fee: fee) - ..addListener((transaction) async { - transactionHistory.addOne(transaction); - await updateBalance(); + return EstimatedTxResult(utxos: utxos, privateKeys: privateKeys, fee: fee, amount: amount); + } + + @override + Future createTransaction(Object credentials) async { + try { + final outputs = []; + final outputAddresses = []; + final transactionCredentials = credentials as BitcoinTransactionCredentials; + final hasMultiDestination = transactionCredentials.outputs.length > 1; + final sendAll = !hasMultiDestination && transactionCredentials.outputs.first.sendAll; + + var credentialsAmount = 0; + + for (final out in transactionCredentials.outputs) { + final outputAddress = out.isParsedAddress ? out.extractedAddress! : out.address; + final address = _addressTypeFromStr(outputAddress, network); + + outputAddresses.add(address); + + if (hasMultiDestination) { + if (out.sendAll || out.formattedCryptoAmount! <= 0) { + throw BitcoinTransactionWrongBalanceException(currency); + } + + final outputAmount = out.formattedCryptoAmount!; + credentialsAmount += outputAmount; + + outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount))); + } else { + if (!sendAll) { + final outputAmount = out.formattedCryptoAmount!; + credentialsAmount += outputAmount; + outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount))); + } else { + // The value will be changed after estimating the Tx size and deducting the fee from the total + outputs.add(BitcoinOutput(address: address, value: BigInt.from(0))); + } + } + } + + final estimatedTx = await _estimateTxFeeAndInputsToUse( + credentialsAmount, sendAll, outputAddresses, outputs, transactionCredentials); + + final txb = BitcoinTransactionBuilder( + utxos: estimatedTx.utxos, + outputs: outputs, + fee: BigInt.from(estimatedTx.fee), + network: network); + + final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) { + final key = estimatedTx.privateKeys + .firstWhereOrNull((element) => element.getPublic().toHex() == publicKey); + + if (key == null) { + throw Exception("Cannot find private key"); + } + + if (utxo.utxo.isP2tr()) { + return key.signTapRoot(txDigest, sighash: sighash); + } else { + return key.signInput(txDigest, sigHash: sighash); + } }); + + // TODO: fix + if (opReturnMemo != null) txb.addOutputData(opReturnMemo); + + return PendingBitcoinTransaction(transaction, type, + electrumClient: electrumClient, + amount: estimatedTx.amount, + fee: estimatedTx.fee, + network: network) + ..addListener((transaction) async { + transactionHistory.addOne(transaction); + await updateBalance(); + }); + } catch (e) { + throw e; + } } String toJSON() => json.encode({ 'mnemonic': mnemonic, - 'account_index': walletAddresses.currentReceiveAddressIndex.toString(), - 'change_address_index': walletAddresses.currentChangeAddressIndex.toString(), - 'addresses': walletAddresses.addresses.map((addr) => addr.toJSON()).toList(), - 'balance': balance[currency]?.toJSON() + 'account_index': walletAddresses.currentReceiveAddressIndexByType, + 'change_address_index': walletAddresses.currentChangeAddressIndexByType, + 'addresses': walletAddresses.allAddresses.map((addr) => addr.toJSON()).toList(), + 'address_page_type': walletInfo.addressPageType == null + ? SegwitAddresType.p2wpkh.toString() + : walletInfo.addressPageType.toString(), + 'balance': balance[currency]?.toJSON(), + 'network_type': network == BitcoinNetwork.testnet ? 'testnet' : 'mainnet', }); int feeRate(TransactionPriority priority) { @@ -377,24 +411,29 @@ abstract class ElectrumWalletBase } } - int feeAmountForPriority( - BitcoinTransactionPriority priority, int inputsCount, int outputsCount) => - feeRate(priority) * estimatedTransactionSize(inputsCount, outputsCount); + int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount, int outputsCount, + {int? size}) => + feeRate(priority) * (size ?? estimatedTransactionSize(inputsCount, outputsCount)); - int feeAmountWithFeeRate(int feeRate, int inputsCount, int outputsCount) => - feeRate * estimatedTransactionSize(inputsCount, outputsCount); + int feeAmountWithFeeRate(int feeRate, int inputsCount, int outputsCount, {int? size}) => + feeRate * (size ?? estimatedTransactionSize(inputsCount, outputsCount)); @override - int calculateEstimatedFee(TransactionPriority? priority, int? amount, {int? outputsCount}) { + int calculateEstimatedFee(TransactionPriority? priority, int? amount, + {int? outputsCount, int? size}) { if (priority is BitcoinTransactionPriority) { return calculateEstimatedFeeWithFeeRate(feeRate(priority), amount, - outputsCount: outputsCount); + outputsCount: outputsCount, size: size); } return 0; } - int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount}) { + int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount, int? size}) { + if (size != null) { + return feeAmountWithFeeRate(feeRate, 0, 0, size: size); + } + int inputsCount = 0; if (amount != null) { @@ -462,9 +501,6 @@ abstract class ElectrumWalletBase await transactionHistory.changePassword(password); } - bitcoin.ECPair keyPairFor({required int index}) => - generateKeyPair(hd: hd, index: index, network: networkType); - @override Future rescan({required int height}) async => throw UnimplementedError(); @@ -478,20 +514,23 @@ abstract class ElectrumWalletBase Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); Future updateUnspent() async { - final unspent = await Future.wait(walletAddresses.addresses.map((address) => electrumClient - .getListUnspentWithAddress(address.address, networkType) - .then((unspent) => unspent.map((unspent) { + List updatedUnspentCoins = []; + + final addressesSet = walletAddresses.allAddresses.map((addr) => addr.address).toSet(); + + await Future.wait(walletAddresses.allAddresses.map((address) => electrumClient + .getListUnspentWithAddress(address.address, network) + .then((unspent) => Future.forEach>(unspent, (unspent) async { try { - return BitcoinUnspent.fromJSON(address, unspent); - } catch (_) { - return null; - } - }).whereNotNull()))); - unspentCoins = unspent.expand((e) => e).toList(); - unspentCoins.forEach((coin) async { - final tx = await fetchTransactionInfo(hash: coin.hash, height: 0); - coin.isChange = tx?.direction == TransactionDirection.outgoing; - }); + final coin = BitcoinUnspent.fromJSON(address, unspent); + final tx = await fetchTransactionInfo( + hash: coin.hash, height: 0, myAddresses: addressesSet); + coin.isChange = tx?.direction == TransactionDirection.outgoing; + updatedUnspentCoins.add(coin); + } catch (_) {} + })))); + + unspentCoins = updatedUnspentCoins; if (unspentCoinsInfo.isEmpty) { unspentCoins.forEach((coin) => _addCoinInfo(coin)); @@ -500,8 +539,10 @@ abstract class ElectrumWalletBase if (unspentCoins.isNotEmpty) { unspentCoins.forEach((coin) { - final coinInfoList = unspentCoinsInfo.values - .where((element) => element.walletId.contains(id) && element.hash.contains(coin.hash)); + final coinInfoList = unspentCoinsInfo.values.where((element) => + element.walletId.contains(id) && + element.hash.contains(coin.hash) && + element.vout == coin.vout); if (coinInfoList.isNotEmpty) { final coinInfo = coinInfoList.first; @@ -542,7 +583,8 @@ abstract class ElectrumWalletBase if (currentWalletUnspentCoins.isNotEmpty) { currentWalletUnspentCoins.forEach((element) { - final existUnspentCoins = unspentCoins.where((coin) => element.hash.contains(coin.hash)); + final existUnspentCoins = unspentCoins + .where((coin) => element.hash.contains(coin.hash) && element.vout == coin.vout); if (existUnspentCoins.isEmpty) { keys.add(element.key); @@ -560,92 +602,145 @@ abstract class ElectrumWalletBase Future getTransactionExpanded( {required String hash, required int height}) async { - final verboseTransaction = await electrumClient.getTransactionRaw(hash: hash); - final transactionHex = verboseTransaction['hex'] as String; - final original = bitcoin.Transaction.fromHex(transactionHex); - final ins = []; - final time = verboseTransaction['time'] as int?; - final confirmations = verboseTransaction['confirmations'] as int? ?? 0; + String transactionHex; + int? time; + int confirmations = 0; + if (network == BitcoinNetwork.testnet) { + // Testnet public electrum server does not support verbose transaction fetching + transactionHex = await electrumClient.getTransactionHex(hash: hash); - for (final vin in original.ins) { - final id = HEX.encode(vin.hash!.reversed.toList()); - final txHex = await electrumClient.getTransactionHex(hash: id); - final tx = bitcoin.Transaction.fromHex(txHex); + final status = json.decode( + (await http.get(Uri.parse("https://blockstream.info/testnet/api/tx/$hash/status"))).body); + + time = status["block_time"] as int?; + final tip = await electrumClient.getCurrentBlockChainTip() ?? 0; + confirmations = tip - (status["block_height"] as int? ?? 0); + } else { + final verboseTransaction = await electrumClient.getTransactionRaw(hash: hash); + + transactionHex = verboseTransaction['hex'] as String; + time = verboseTransaction['time'] as int?; + confirmations = verboseTransaction['confirmations'] as int? ?? 0; + } + + final original = bitcoin_base.BtcTransaction.fromRaw(transactionHex); + final ins = []; + + for (final vin in original.inputs) { + final txHex = await electrumClient.getTransactionHex(hash: vin.txId); + final tx = bitcoin_base.BtcTransaction.fromRaw(txHex); ins.add(tx); } - return ElectrumTransactionBundle(original, ins: ins, time: time, confirmations: confirmations); + return ElectrumTransactionBundle(original, + ins: ins, time: time, confirmations: confirmations, height: height); } Future fetchTransactionInfo( - {required String hash, required int height}) async { + {required String hash, + required int height, + required Set myAddresses, + bool? retryOnFailure}) async { try { - final tx = await getTransactionExpanded(hash: hash, height: height); - final addresses = walletAddresses.addresses.map((addr) => addr.address).toSet(); - return ElectrumTransactionInfo.fromElectrumBundle(tx, walletInfo.type, networkType, - addresses: addresses, height: height); - } catch (_) { + return ElectrumTransactionInfo.fromElectrumBundle( + await getTransactionExpanded(hash: hash, height: height), walletInfo.type, network, + addresses: myAddresses, height: height); + } catch (e) { + if (e is FormatException && retryOnFailure == true) { + await Future.delayed(const Duration(seconds: 2)); + return fetchTransactionInfo(hash: hash, height: height, myAddresses: myAddresses); + } return null; } } @override Future> fetchTransactions() async { - final addressHashes = {}; - final normalizedHistories = >[]; - final newTxCounts = {}; - - walletAddresses.addresses.forEach((addressRecord) { - final sh = scriptHash(addressRecord.address, networkType: networkType); - addressHashes[sh] = addressRecord; - newTxCounts[sh] = 0; - }); - try { - final histories = addressHashes.keys.map((scriptHash) => - electrumClient.getHistory(scriptHash).then((history) => {scriptHash: history})); - final historyResults = await Future.wait(histories); + final Map historiesWithDetails = {}; + final addressesSet = walletAddresses.allAddresses.map((addr) => addr.address).toSet(); + final currentHeight = await electrumClient.getCurrentBlockChainTip() ?? 0; - historyResults.forEach((history) { - history.entries.forEach((historyItem) { - if (historyItem.value.isNotEmpty) { - final address = addressHashes[historyItem.key]; - address?.setAsUsed(); - newTxCounts[historyItem.key] = historyItem.value.length; - normalizedHistories.addAll(historyItem.value); + await Future.wait(ADDRESS_TYPES.map((type) { + final addressesByType = walletAddresses.allAddresses.where((addr) => addr.type == type); + + return Future.wait(addressesByType.map((addressRecord) async { + final history = await _fetchAddressHistory(addressRecord, addressesSet, currentHeight); + + if (history.isNotEmpty) { + addressRecord.txCount = history.length; + historiesWithDetails.addAll(history); + + final matchedAddresses = + addressesByType.where((addr) => addr.isHidden == addressRecord.isHidden); + + final isLastUsedAddress = + history.isNotEmpty && addressRecord.address == matchedAddresses.last.address; + + if (isLastUsedAddress) { + await walletAddresses.discoverAddresses( + matchedAddresses.toList(), + addressRecord.isHidden, + (address, addressesSet) => + _fetchAddressHistory(address, addressesSet, currentHeight) + .then((history) => history.isNotEmpty ? address.address : null), + type: type); + } } - }); - }); - - for (var sh in addressHashes.keys) { - var balanceData = await electrumClient.getBalance(sh); - var addressRecord = addressHashes[sh]; - if (addressRecord != null) { - addressRecord.balance = balanceData['confirmed'] as int? ?? 0; - } - } - - addressHashes.forEach((sh, addressRecord) { - addressRecord.txCount = newTxCounts[sh] ?? 0; - }); - - final historiesWithDetails = await Future.wait(normalizedHistories.map((transaction) { - try { - return fetchTransactionInfo( - hash: transaction['tx_hash'] as String, height: transaction['height'] as int); - } catch (_) { - return Future.value(null); - } + })); })); - return historiesWithDetails.fold>( - {}, (acc, tx) { - if (tx == null) { - return acc; - } - acc[tx.id] = acc[tx.id]?.updated(tx) ?? tx; - return acc; - }); + return historiesWithDetails; + } catch (e) { + print(e.toString()); + return {}; + } + } + + Future> _fetchAddressHistory( + BitcoinAddressRecord addressRecord, Set addressesSet, int currentHeight) async { + try { + final Map historiesWithDetails = {}; + + final history = await electrumClient + .getHistory(addressRecord.scriptHash ?? addressRecord.updateScriptHash(network)); + + if (history.isNotEmpty) { + addressRecord.setAsUsed(); + + await Future.wait(history.map((transaction) async { + final txid = transaction['tx_hash'] as String; + final height = transaction['height'] as int; + final storedTx = transactionHistory.transactions[txid]; + + if (storedTx != null) { + if (height > 0) { + storedTx.height = height; + // the tx's block itself is the first confirmation so add 1 + storedTx.confirmations = currentHeight - height + 1; + storedTx.isPending = storedTx.confirmations == 0; + } + + historiesWithDetails[txid] = storedTx; + } else { + final tx = await fetchTransactionInfo( + hash: txid, height: height, myAddresses: addressesSet, retryOnFailure: true); + + if (tx != null) { + historiesWithDetails[txid] = tx; + + // Got a new transaction fetched, add it to the transaction history + // instead of waiting all to finish, and next time it will be faster + transactionHistory.addOne(tx); + await transactionHistory.save(); + } + } + + return Future.value(null); + })); + } + + return historiesWithDetails; } catch (e) { print(e.toString()); return {}; @@ -659,10 +754,8 @@ abstract class ElectrumWalletBase } _isTransactionUpdating = true; - final transactions = await fetchTransactions(); - transactionHistory.addMany(transactions); + await fetchTransactions(); walletAddresses.updateReceiveAddresses(); - await transactionHistory.save(); _isTransactionUpdating = false; } catch (e, stacktrace) { print(stacktrace); @@ -693,11 +786,11 @@ abstract class ElectrumWalletBase } Future _fetchBalances() async { - final addresses = walletAddresses.addresses.toList(); + final addresses = walletAddresses.allAddresses.toList(); final balanceFutures = >>[]; for (var i = 0; i < addresses.length; i++) { final addressRecord = addresses[i]; - final sh = scriptHash(addressRecord.address, networkType: networkType); + final sh = scriptHash(addressRecord.address, network: network); final balanceFuture = electrumClient.getBalance(sh); balanceFutures.add(balanceFuture); } @@ -706,6 +799,7 @@ abstract class ElectrumWalletBase unspentCoinsInfo.values.forEach((info) { unspentCoins.forEach((element) { if (element.hash == info.hash && + element.vout == info.vout && info.isFrozen && element.bitcoinAddressRecord.address == info.address && element.value == info.value) { @@ -743,10 +837,10 @@ abstract class ElectrumWalletBase String getChangeAddress() { const minCountOfHiddenAddresses = 5; final random = Random(); - var addresses = walletAddresses.addresses.where((addr) => addr.isHidden).toList(); + var addresses = walletAddresses.allAddresses.where((addr) => addr.isHidden).toList(); if (addresses.length < minCountOfHiddenAddresses) { - addresses = walletAddresses.addresses.toList(); + addresses = walletAddresses.allAddresses.toList(); } return addresses[random.nextInt(addresses.length)].address; @@ -758,9 +852,62 @@ abstract class ElectrumWalletBase @override String signMessage(String message, {String? address = null}) { final index = address != null - ? walletAddresses.addresses.firstWhere((element) => element.address == address).index + ? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index : null; final HD = index == null ? hd : hd.derive(index); return base64Encode(HD.signMessage(message)); } } + +class EstimateTxParams { + EstimateTxParams( + {required this.amount, + required this.feeRate, + required this.priority, + required this.outputsCount, + required this.size}); + + final int amount; + final int feeRate; + final TransactionPriority priority; + final int outputsCount; + final int size; +} + +class EstimatedTxResult { + EstimatedTxResult( + {required this.utxos, required this.privateKeys, required this.fee, required this.amount}); + + final List utxos; + final List privateKeys; + final int fee; + final int amount; +} + +BitcoinBaseAddress _addressTypeFromStr(String address, BasedUtxoNetwork network) { + if (P2pkhAddress.regex.hasMatch(address)) { + return P2pkhAddress.fromAddress(address: address, network: network); + } else if (P2shAddress.regex.hasMatch(address)) { + return P2shAddress.fromAddress(address: address, network: network); + } else if (P2wshAddress.regex.hasMatch(address)) { + return P2wshAddress.fromAddress(address: address, network: network); + } else if (P2trAddress.regex.hasMatch(address)) { + return P2trAddress.fromAddress(address: address, network: network); + } else { + return P2wpkhAddress.fromAddress(address: address, network: network); + } +} + +BitcoinAddressType _getScriptType(BitcoinBaseAddress type) { + if (type is P2pkhAddress) { + return P2pkhAddressType.p2pkh; + } else if (type is P2shAddress) { + return P2shAddressType.p2wpkhInP2sh; + } else if (type is P2wshAddress) { + return SegwitAddresType.p2wsh; + } else if (type is P2trAddress) { + return SegwitAddresType.p2tr; + } else { + return SegwitAddresType.p2wpkh; + } +} diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 850d58f40..5880f5a19 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -1,8 +1,8 @@ -import 'package:bitbox/bitbox.dart' as bitbox; +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:bitbox/bitbox.dart' as bitbox; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum.dart'; -import 'package:cw_bitcoin/script_hash.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -12,25 +12,41 @@ part 'electrum_wallet_addresses.g.dart'; class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses; +const List ADDRESS_TYPES = [ + SegwitAddresType.p2wpkh, + P2pkhAddressType.p2pkh, + SegwitAddresType.p2tr, + SegwitAddresType.p2wsh, + P2shAddressType.p2wpkhInP2sh, +]; + abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { - ElectrumWalletAddressesBase(WalletInfo walletInfo, - {required this.mainHd, - required this.sideHd, - required this.electrumClient, - required this.networkType, - List? initialAddresses, - int initialRegularAddressIndex = 0, - int initialChangeAddressIndex = 0}) - : addresses = ObservableList.of((initialAddresses ?? []).toSet()), + ElectrumWalletAddressesBase( + WalletInfo walletInfo, { + required this.mainHd, + required this.sideHd, + required this.electrumClient, + required this.network, + List? initialAddresses, + Map? initialRegularAddressIndex, + Map? initialChangeAddressIndex, + }) : _addresses = ObservableList.of((initialAddresses ?? []).toSet()), + addressesByReceiveType = + ObservableList.of(([]).toSet()), receiveAddresses = ObservableList.of((initialAddresses ?? []) .where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed) .toSet()), changeAddresses = ObservableList.of((initialAddresses ?? []) .where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed) .toSet()), - currentReceiveAddressIndex = initialRegularAddressIndex, - currentChangeAddressIndex = initialChangeAddressIndex, - super(walletInfo); + currentReceiveAddressIndexByType = initialRegularAddressIndex ?? {}, + currentChangeAddressIndexByType = initialChangeAddressIndex ?? {}, + _addressPageType = walletInfo.addressPageType != null + ? BitcoinAddressType.fromValue(walletInfo.addressPageType!) + : SegwitAddresType.p2wpkh, + super(walletInfo) { + updateAddressesByMatch(); + } static const defaultReceiveAddressesCount = 22; static const defaultChangeAddressesCount = 17; @@ -40,37 +56,48 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { static String toLegacy(String address) => bitbox.Address.toLegacyAddress(address); - final ObservableList addresses; + final ObservableList _addresses; + // Matched by addressPageType + late ObservableList addressesByReceiveType; final ObservableList receiveAddresses; final ObservableList changeAddresses; final ElectrumClient electrumClient; - final bitcoin.NetworkType networkType; + final BasedUtxoNetwork network; final bitcoin.HDWallet mainHd; final bitcoin.HDWallet sideHd; + @observable + BitcoinAddressType _addressPageType = SegwitAddresType.p2wpkh; + + @computed + BitcoinAddressType get addressPageType => _addressPageType; + + @computed + List get allAddresses => _addresses; + @override @computed String get address { - if (isEnabledAutoGenerateSubaddress) { - if (receiveAddresses.isEmpty) { - final newAddress = generateNewAddress(hd: mainHd).address; - return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(newAddress) : newAddress; - } - final receiveAddress = receiveAddresses.first.address; + String receiveAddress; - return walletInfo.type == WalletType.bitcoinCash - ? toCashAddr(receiveAddress) - : receiveAddress; + final typeMatchingReceiveAddresses = receiveAddresses.where(_isAddressPageTypeMatch); + + if ((isEnabledAutoGenerateSubaddress && receiveAddresses.isEmpty) || + typeMatchingReceiveAddresses.isEmpty) { + receiveAddress = generateNewAddress().address; } else { - final receiveAddress = (receiveAddresses.first.address != addresses.first.address && - previousAddressRecord != null) - ? previousAddressRecord!.address - : addresses.first.address; + final previousAddressMatchesType = + previousAddressRecord != null && previousAddressRecord!.type == addressPageType; - return walletInfo.type == WalletType.bitcoinCash - ? toCashAddr(receiveAddress) - : receiveAddress; + if (previousAddressMatchesType && + typeMatchingReceiveAddresses.first.address != addressesByReceiveType.first.address) { + receiveAddress = previousAddressRecord!.address; + } else { + receiveAddress = typeMatchingReceiveAddresses.first.address; + } } + + return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(receiveAddress) : receiveAddress; } @observable @@ -81,7 +108,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { if (addr.startsWith('bitcoincash:')) { addr = toLegacy(addr); } - final addressRecord = addresses.firstWhere((addressRecord) => addressRecord.address == addr); + final addressRecord = _addresses.firstWhere((addressRecord) => addressRecord.address == addr); previousAddressRecord = addressRecord; receiveAddresses.remove(addressRecord); @@ -89,16 +116,29 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @override - String get primaryAddress => getAddress(index: 0, hd: mainHd); + String get primaryAddress => getAddress(index: 0, hd: mainHd, addressType: addressPageType); - int currentReceiveAddressIndex; - int currentChangeAddressIndex; + Map currentReceiveAddressIndexByType; + + int get currentReceiveAddressIndex => + currentReceiveAddressIndexByType[_addressPageType.toString()] ?? 0; + + void set currentReceiveAddressIndex(int index) => + currentReceiveAddressIndexByType[_addressPageType.toString()] = index; + + Map currentChangeAddressIndexByType; + + int get currentChangeAddressIndex => + currentChangeAddressIndexByType[_addressPageType.toString()] ?? 0; + + void set currentChangeAddressIndex(int index) => + currentChangeAddressIndexByType[_addressPageType.toString()] = index; @observable BitcoinAddressRecord? previousAddressRecord; @computed - int get totalCountOfReceiveAddresses => addresses.fold(0, (acc, addressRecord) { + int get totalCountOfReceiveAddresses => addressesByReceiveType.fold(0, (acc, addressRecord) { if (!addressRecord.isHidden) { return acc + 1; } @@ -106,22 +146,21 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { }); @computed - int get totalCountOfChangeAddresses => addresses.fold(0, (acc, addressRecord) { + int get totalCountOfChangeAddresses => addressesByReceiveType.fold(0, (acc, addressRecord) { if (addressRecord.isHidden) { return acc + 1; } return acc; }); - Future discoverAddresses() async { - await _discoverAddresses(mainHd, false); - await _discoverAddresses(sideHd, true); - await updateAddressesInBox(); - } - @override Future init() async { await _generateInitialAddresses(); + await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); + await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh); + await _generateInitialAddresses(type: SegwitAddresType.p2tr); + await _generateInitialAddresses(type: SegwitAddresType.p2wsh); + updateAddressesByMatch(); updateReceiveAddresses(); updateChangeAddresses(); await updateAddressesInBox(); @@ -141,10 +180,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { if (changeAddresses.isEmpty) { final newAddresses = await _createNewAddresses(gap, - hd: sideHd, startIndex: totalCountOfChangeAddresses > 0 ? totalCountOfChangeAddresses - 1 : 0, isHidden: true); - _addAddresses(newAddresses); + addAddresses(newAddresses); } if (currentChangeAddressIndex >= changeAddresses.length) { @@ -157,19 +195,26 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return address; } - BitcoinAddressRecord generateNewAddress({bitcoin.HDWallet? hd, String? label}) { - final isHidden = hd == sideHd; + BitcoinAddressRecord generateNewAddress({String label = ''}) { + final newAddressIndex = addressesByReceiveType.fold( + 0, (int acc, addressRecord) => addressRecord.isHidden == false ? acc + 1 : acc); - final newAddressIndex = addresses.fold( - 0, (int acc, addressRecord) => isHidden == addressRecord.isHidden ? acc + 1 : acc); - - final address = BitcoinAddressRecord(getAddress(index: newAddressIndex, hd: hd ?? sideHd), - index: newAddressIndex, isHidden: isHidden, name: label ?? ''); - addresses.add(address); + final address = BitcoinAddressRecord( + getAddress(index: newAddressIndex, hd: mainHd, addressType: addressPageType), + index: newAddressIndex, + isHidden: false, + name: label, + type: addressPageType, + network: network, + ); + _addresses.add(address); + updateAddressesByMatch(); return address; } - String getAddress({required int index, required bitcoin.HDWallet hd}) => ''; + String getAddress( + {required int index, required bitcoin.HDWallet hd, BitcoinAddressType? addressType}) => + ''; @override Future updateAddressesInBox() async { @@ -187,126 +232,138 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { if (address.startsWith('bitcoincash:')) { address = toLegacy(address); } - final addressRecord = addresses.firstWhere((addressRecord) => addressRecord.address == address); + final addressRecord = + _addresses.firstWhere((addressRecord) => addressRecord.address == address); addressRecord.setNewName(label); - final index = addresses.indexOf(addressRecord); - addresses.remove(addressRecord); - addresses.insert(index, addressRecord); + final index = _addresses.indexOf(addressRecord); + _addresses.remove(addressRecord); + _addresses.insert(index, addressRecord); + } + + @action + void updateAddressesByMatch() { + addressesByReceiveType.clear(); + addressesByReceiveType.addAll(_addresses.where(_isAddressPageTypeMatch).toList()); } @action void updateReceiveAddresses() { receiveAddresses.removeRange(0, receiveAddresses.length); final newAddresses = - addresses.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed); + _addresses.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed); receiveAddresses.addAll(newAddresses); } @action void updateChangeAddresses() { changeAddresses.removeRange(0, changeAddresses.length); - final newAddresses = - addresses.where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed); + final newAddresses = _addresses.where((addressRecord) => + addressRecord.isHidden && + !addressRecord.isUsed && + // TODO: feature to change change address type. For now fixed to p2wpkh, the cheapest type + addressRecord.type == SegwitAddresType.p2wpkh); changeAddresses.addAll(newAddresses); } - Future _discoverAddresses(bitcoin.HDWallet hd, bool isHidden) async { - var hasAddrUse = true; - List addrs; - - if (addresses.isNotEmpty) { - - - if(!isHidden) { - final receiveAddressesList = addresses.where((addr) => !addr.isHidden).toList(); - validateSideHdAddresses(receiveAddressesList); - } - - addrs = addresses.where((addr) => addr.isHidden == isHidden).toList(); - } else { - addrs = await _createNewAddresses( - isHidden ? defaultChangeAddressesCount : defaultReceiveAddressesCount, - startIndex: 0, - hd: hd, - isHidden: isHidden); + @action + Future discoverAddresses(List addressList, bool isHidden, + Future Function(BitcoinAddressRecord, Set) getAddressHistory, + {BitcoinAddressType type = SegwitAddresType.p2wpkh}) async { + if (!isHidden) { + _validateSideHdAddresses(addressList.toList()); } - while (hasAddrUse) { - final addr = addrs.last.address; - hasAddrUse = await _hasAddressUsed(addr); + final newAddresses = await _createNewAddresses(gap, + startIndex: addressList.length, isHidden: isHidden, type: type); + addAddresses(newAddresses); - if (!hasAddrUse) { - break; - } + final addressesWithHistory = await Future.wait(newAddresses + .map((addr) => getAddressHistory(addr, _addresses.map((e) => e.address).toSet()))); + final isLastAddressUsed = addressesWithHistory.last == addressList.last.address; - final start = addrs.length; - final count = start + gap; - final batch = await _createNewAddresses(count, startIndex: start, hd: hd, isHidden: isHidden); - addrs.addAll(batch); - } - - if (addresses.length < addrs.length) { - _addAddresses(addrs); + if (isLastAddressUsed) { + discoverAddresses(addressList, isHidden, getAddressHistory, type: type); } } - Future _generateInitialAddresses() async { + Future _generateInitialAddresses( + {BitcoinAddressType type = SegwitAddresType.p2wpkh}) async { var countOfReceiveAddresses = 0; var countOfHiddenAddresses = 0; - addresses.forEach((addr) { - if (addr.isHidden) { - countOfHiddenAddresses += 1; - return; - } + _addresses.forEach((addr) { + if (addr.type == type) { + if (addr.isHidden) { + countOfHiddenAddresses += 1; + return; + } - countOfReceiveAddresses += 1; + countOfReceiveAddresses += 1; + } }); if (countOfReceiveAddresses < defaultReceiveAddressesCount) { final addressesCount = defaultReceiveAddressesCount - countOfReceiveAddresses; final newAddresses = await _createNewAddresses(addressesCount, - startIndex: countOfReceiveAddresses, hd: mainHd, isHidden: false); - addresses.addAll(newAddresses); + startIndex: countOfReceiveAddresses, isHidden: false, type: type); + addAddresses(newAddresses); } if (countOfHiddenAddresses < defaultChangeAddressesCount) { final addressesCount = defaultChangeAddressesCount - countOfHiddenAddresses; final newAddresses = await _createNewAddresses(addressesCount, - startIndex: countOfHiddenAddresses, hd: sideHd, isHidden: true); - addresses.addAll(newAddresses); + startIndex: countOfHiddenAddresses, isHidden: true, type: type); + addAddresses(newAddresses); } } Future> _createNewAddresses(int count, - {required bitcoin.HDWallet hd, int startIndex = 0, bool isHidden = false}) async { + {int startIndex = 0, bool isHidden = false, BitcoinAddressType? type}) async { final list = []; for (var i = startIndex; i < count + startIndex; i++) { - final address = - BitcoinAddressRecord(getAddress(index: i, hd: hd), index: i, isHidden: isHidden); + final address = BitcoinAddressRecord( + getAddress(index: i, hd: _getHd(isHidden), addressType: type ?? addressPageType), + index: i, + isHidden: isHidden, + type: type ?? addressPageType, + network: network, + ); list.add(address); } return list; } - void _addAddresses(Iterable addresses) { - final addressesSet = this.addresses.toSet(); + @action + void addAddresses(Iterable addresses) { + final addressesSet = this._addresses.toSet(); addressesSet.addAll(addresses); - this.addresses.removeRange(0, this.addresses.length); - this.addresses.addAll(addressesSet); + this._addresses.clear(); + this._addresses.addAll(addressesSet); + updateAddressesByMatch(); } - Future _hasAddressUsed(String address) async { - final sh = scriptHash(address, networkType: networkType); - final transactionHistory = await electrumClient.getHistory(sh); - return transactionHistory.isNotEmpty; - } - - void validateSideHdAddresses(List addrWithTransactions) { + void _validateSideHdAddresses(List addrWithTransactions) { addrWithTransactions.forEach((element) { - if (element.address != getAddress(index: element.index, hd: mainHd)) element.isHidden = true; + if (element.address != + getAddress(index: element.index, hd: mainHd, addressType: element.type)) + element.isHidden = true; }); } + + @action + Future setAddressType(BitcoinAddressType type) async { + _addressPageType = type; + updateAddressesByMatch(); + walletInfo.addressPageType = addressPageType.toString(); + await walletInfo.save(); + } + + bool _isAddressPageTypeMatch(BitcoinAddressRecord addressRecord) { + return _isAddressByType(addressRecord, addressPageType); + } + + bitcoin.HDWallet _getHd(bool isHidden) => isHidden ? sideHd : mainHd; + bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type; } diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 86d3e2fed..98c3753db 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -1,12 +1,13 @@ import 'dart:convert'; +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/utils/file.dart'; import 'package:cw_core/wallet_type.dart'; -class ElectrumWallletSnapshot { - ElectrumWallletSnapshot({ +class ElectrumWalletSnapshot { + ElectrumWalletSnapshot({ required this.name, required this.type, required this.password, @@ -14,19 +15,24 @@ class ElectrumWallletSnapshot { required this.addresses, required this.balance, required this.regularAddressIndex, - required this.changeAddressIndex}); + required this.changeAddressIndex, + required this.addressPageType, + required this.network, + }); final String name; final String password; final WalletType type; + final String addressPageType; + final BasedUtxoNetwork network; String mnemonic; List addresses; ElectrumBalance balance; - int regularAddressIndex; - int changeAddressIndex; + Map regularAddressIndex; + Map changeAddressIndex; - static Future load(String name, WalletType type, String password) async { + static Future load(String name, WalletType type, String password, BasedUtxoNetwork? network) async { final path = await pathForWallet(name: name, type: type); final jsonSource = await read(path: path, password: password); final data = json.decode(jsonSource) as Map; @@ -34,26 +40,39 @@ class ElectrumWallletSnapshot { final mnemonic = data['mnemonic'] as String; final addresses = addressesTmp .whereType() - .map((addr) => BitcoinAddressRecord.fromJSON(addr)) + .map((addr) => BitcoinAddressRecord.fromJSON(addr, network)) .toList(); final balance = ElectrumBalance.fromJSON(data['balance'] as String) ?? ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); - var regularAddressIndex = 0; - var changeAddressIndex = 0; + var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; + var changeAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; try { - regularAddressIndex = int.parse(data['account_index'] as String? ?? '0'); - changeAddressIndex = int.parse(data['change_address_index'] as String? ?? '0'); - } catch (_) {} + regularAddressIndexByType = { + SegwitAddresType.p2wpkh.toString(): int.parse(data['account_index'] as String? ?? '0') + }; + changeAddressIndexByType = { + SegwitAddresType.p2wpkh.toString(): + int.parse(data['change_address_index'] as String? ?? '0') + }; + } catch (_) { + try { + regularAddressIndexByType = data["account_index"] as Map? ?? {}; + changeAddressIndexByType = data["change_address_index"] as Map? ?? {}; + } catch (_) {} + } - return ElectrumWallletSnapshot( + return ElectrumWalletSnapshot( name: name, type: type, password: password, mnemonic: mnemonic, addresses: addresses, balance: balance, - regularAddressIndex: regularAddressIndex, - changeAddressIndex: changeAddressIndex); + regularAddressIndex: regularAddressIndexByType, + changeAddressIndex: changeAddressIndexByType, + addressPageType: data['address_page_type'] as String? ?? SegwitAddresType.p2wpkh.toString(), + network: data['network_type'] == 'testnet' ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet, + ); } } diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 222e95acc..d2379d5a5 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -1,3 +1,4 @@ +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -20,17 +21,18 @@ part 'litecoin_wallet.g.dart'; class LitecoinWallet = LitecoinWalletBase with _$LitecoinWallet; abstract class LitecoinWalletBase extends ElectrumWallet with Store { - LitecoinWalletBase( - {required String mnemonic, - required String password, - required WalletInfo walletInfo, - required Box unspentCoinsInfo, - required Uint8List seedBytes, - List? initialAddresses, - ElectrumBalance? initialBalance, - int initialRegularAddressIndex = 0, - int initialChangeAddressIndex = 0}) - : super( + LitecoinWalletBase({ + required String mnemonic, + required String password, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + required Uint8List seedBytes, + String? addressPageType, + List? initialAddresses, + ElectrumBalance? initialBalance, + Map? initialRegularAddressIndex, + Map? initialChangeAddressIndex, + }) : super( mnemonic: mnemonic, password: password, walletInfo: walletInfo, @@ -41,41 +43,42 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { seedBytes: seedBytes, currency: CryptoCurrency.ltc) { walletAddresses = LitecoinWalletAddresses( - walletInfo, - electrumClient: electrumClient, - initialAddresses: initialAddresses, - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex, - mainHd: hd, - sideHd: bitcoin.HDWallet - .fromSeed(seedBytes, network: networkType) - .derivePath("m/0'/1"), - networkType: networkType,); + walletInfo, + electrumClient: electrumClient, + initialAddresses: initialAddresses, + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex, + mainHd: hd, + sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/1"), + network: network, + ); autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; }); } - static Future create({ - required String mnemonic, - required String password, - required WalletInfo walletInfo, - required Box unspentCoinsInfo, - List? initialAddresses, - ElectrumBalance? initialBalance, - int initialRegularAddressIndex = 0, - int initialChangeAddressIndex = 0 - }) async { + static Future create( + {required String mnemonic, + required String password, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + String? addressPageType, + List? initialAddresses, + ElectrumBalance? initialBalance, + Map? initialRegularAddressIndex, + Map? initialChangeAddressIndex}) async { return LitecoinWallet( - mnemonic: mnemonic, - password: password, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfo, - initialAddresses: initialAddresses, - initialBalance: initialBalance, - seedBytes: await mnemonicToSeedBytes(mnemonic), - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex); + mnemonic: mnemonic, + password: password, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + initialAddresses: initialAddresses, + initialBalance: initialBalance, + seedBytes: await mnemonicToSeedBytes(mnemonic), + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex, + addressPageType: addressPageType, + ); } static Future open({ @@ -84,17 +87,20 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { required Box unspentCoinsInfo, required String password, }) async { - final snp = await ElectrumWallletSnapshot.load (name, walletInfo.type, password); + final snp = + await ElectrumWalletSnapshot.load(name, walletInfo.type, password, LitecoinNetwork.mainnet); return LitecoinWallet( - mnemonic: snp.mnemonic, - password: password, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfo, - initialAddresses: snp.addresses, - initialBalance: snp.balance, - seedBytes: await mnemonicToSeedBytes(snp.mnemonic), - initialRegularAddressIndex: snp.regularAddressIndex, - initialChangeAddressIndex: snp.changeAddressIndex); + mnemonic: snp.mnemonic, + password: password, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + initialAddresses: snp.addresses, + initialBalance: snp.balance, + seedBytes: await mnemonicToSeedBytes(snp.mnemonic), + initialRegularAddressIndex: snp.regularAddressIndex, + initialChangeAddressIndex: snp.changeAddressIndex, + addressPageType: snp.addressPageType, + ); } @override diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index a317fa9f2..993d17933 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -1,39 +1,28 @@ +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; -import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/utils.dart'; -import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; part 'litecoin_wallet_addresses.g.dart'; -class LitecoinWalletAddresses = LitecoinWalletAddressesBase - with _$LitecoinWalletAddresses; +class LitecoinWalletAddresses = LitecoinWalletAddressesBase with _$LitecoinWalletAddresses; -abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses - with Store { +abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Store { LitecoinWalletAddressesBase( - WalletInfo walletInfo, - {required bitcoin.HDWallet mainHd, - required bitcoin.HDWallet sideHd, - required bitcoin.NetworkType networkType, - required ElectrumClient electrumClient, - List? initialAddresses, - int initialRegularAddressIndex = 0, - int initialChangeAddressIndex = 0}) - : super( - walletInfo, - initialAddresses: initialAddresses, - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex, - mainHd: mainHd, - sideHd: sideHd, - electrumClient: electrumClient, - networkType: networkType); + WalletInfo walletInfo, { + required super.mainHd, + required super.sideHd, + required super.network, + required super.electrumClient, + super.initialAddresses, + super.initialRegularAddressIndex, + super.initialChangeAddressIndex, + }) : super(walletInfo); @override - String getAddress({required int index, required bitcoin.HDWallet hd}) => - generateP2WPKHAddress(hd: hd, index: index, networkType: networkType); -} \ No newline at end of file + String getAddress( + {required int index, required bitcoin.HDWallet hd, BitcoinAddressType? addressType}) => + generateP2WPKHAddress(hd: hd, index: index, network: network); +} diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index 69d1dfc7e..ee3b0e628 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:hive/hive.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; -import 'package:cw_bitcoin/bitcoin_mnemonic_is_incorrect_exception.dart'; +import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart'; import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart'; import 'package:cw_bitcoin/litecoin_wallet.dart'; import 'package:cw_core/wallet_service.dart'; @@ -25,7 +25,7 @@ class LitecoinWalletService extends WalletService< WalletType getType() => WalletType.litecoin; @override - Future create(BitcoinNewWalletCredentials credentials) async { + Future create(BitcoinNewWalletCredentials credentials, {bool? isTestnet}) async { final wallet = await LitecoinWalletBase.create( mnemonic: await generateMnemonic(), password: credentials.password!, @@ -94,14 +94,14 @@ class LitecoinWalletService extends WalletService< @override Future restoreFromKeys( - BitcoinRestoreWalletFromWIFCredentials credentials) async => + BitcoinRestoreWalletFromWIFCredentials credentials, {bool? isTestnet}) async => throw UnimplementedError(); @override Future restoreFromSeed( - BitcoinRestoreWalletFromSeedCredentials credentials) async { + BitcoinRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { if (!validateMnemonic(credentials.mnemonic)) { - throw BitcoinMnemonicIsIncorrectException(); + throw LitecoinMnemonicIsIncorrectException(); } final wallet = await LitecoinWalletBase.create( diff --git a/cw_bitcoin/lib/bitcoin_mnemonic_is_incorrect_exception.dart b/cw_bitcoin/lib/mnemonic_is_incorrect_exception.dart similarity index 50% rename from cw_bitcoin/lib/bitcoin_mnemonic_is_incorrect_exception.dart rename to cw_bitcoin/lib/mnemonic_is_incorrect_exception.dart index 8d0583ce5..779bd3ea2 100644 --- a/cw_bitcoin/lib/bitcoin_mnemonic_is_incorrect_exception.dart +++ b/cw_bitcoin/lib/mnemonic_is_incorrect_exception.dart @@ -3,3 +3,9 @@ class BitcoinMnemonicIsIncorrectException implements Exception { String toString() => 'Bitcoin mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; } + +class LitecoinMnemonicIsIncorrectException implements Exception { + @override + String toString() => + 'Litecoin mnemonic has incorrect format. Mnemonic should contain 24 words separated by space.'; +} diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index e2dc10bfb..fa413febd 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -1,5 +1,5 @@ import 'package:cw_bitcoin/bitcoin_commit_transaction_exception.dart'; -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; @@ -9,22 +9,21 @@ import 'package:cw_core/wallet_type.dart'; class PendingBitcoinTransaction with PendingTransaction { PendingBitcoinTransaction(this._tx, this.type, - {required this.electrumClient, - required this.amount, - required this.fee}) + {required this.electrumClient, required this.amount, required this.fee, this.network}) : _listeners = []; final WalletType type; - final bitcoin.Transaction _tx; + final BtcTransaction _tx; final ElectrumClient electrumClient; final int amount; final int fee; + final BasedUtxoNetwork? network; @override - String get id => _tx.getId(); + String get id => _tx.txId(); @override - String get hex => _tx.toHex(); + String get hex => _tx.serialize(); @override String get amountFormatted => bitcoinAmountToString(amount: amount); @@ -36,18 +35,16 @@ class PendingBitcoinTransaction with PendingTransaction { @override Future commit() async { - final result = - await electrumClient.broadcastTransaction(transactionRaw: _tx.toHex()); + final result = await electrumClient.broadcastTransaction(transactionRaw: hex, network: network); if (result.isEmpty) { throw BitcoinCommitTransactionException(); } - _listeners?.forEach((listener) => listener(transactionInfo())); + _listeners.forEach((listener) => listener(transactionInfo())); } - void addListener( - void Function(ElectrumTransactionInfo transaction) listener) => + void addListener(void Function(ElectrumTransactionInfo transaction) listener) => _listeners.add(listener); ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type, diff --git a/cw_bitcoin/lib/script_hash.dart b/cw_bitcoin/lib/script_hash.dart index 76a1bfcf0..620d3d28a 100644 --- a/cw_bitcoin/lib/script_hash.dart +++ b/cw_bitcoin/lib/script_hash.dart @@ -1,9 +1,8 @@ -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:crypto/crypto.dart'; -String scriptHash(String address, {required bitcoin.NetworkType networkType}) { - final outputScript = - bitcoin.Address.addressToOutputScript(address, networkType); +String scriptHash(String address, {required BasedUtxoNetwork network}) { + final outputScript = addressToOutputScript(address: address, network: network); final parts = sha256.convert(outputScript).toString().split(''); var res = ''; diff --git a/cw_bitcoin/lib/utils.dart b/cw_bitcoin/lib/utils.dart index 0d5a413b3..b156ccba3 100644 --- a/cw_bitcoin/lib/utils.dart +++ b/cw_bitcoin/lib/utils.dart @@ -1,55 +1,33 @@ import 'dart:typed_data'; +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:flutter/foundation.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; import 'package:hex/hex.dart'; -bitcoin.PaymentData generatePaymentData( - {required bitcoin.HDWallet hd, required int index}) => - PaymentData( - pubkey: Uint8List.fromList(HEX.decode(hd.derive(index).pubKey!))); +bitcoin.PaymentData generatePaymentData({required bitcoin.HDWallet hd, required int index}) => + PaymentData(pubkey: Uint8List.fromList(HEX.decode(hd.derive(index).pubKey!))); -bitcoin.ECPair generateKeyPair( - {required bitcoin.HDWallet hd, - required int index, - required bitcoin.NetworkType network}) => - bitcoin.ECPair.fromWIF(hd.derive(index).wif!, network: network); +ECPrivate generateECPrivate( + {required bitcoin.HDWallet hd, required int index, required BasedUtxoNetwork network}) => + ECPrivate.fromWif(hd.derive(index).wif!, netVersion: network.wifNetVer); String generateP2WPKHAddress( - {required bitcoin.HDWallet hd, - required int index, - required bitcoin.NetworkType networkType}) => - bitcoin - .P2WPKH( - data: PaymentData( - pubkey: - Uint8List.fromList(HEX.decode(hd.derive(index).pubKey!))), - network: networkType) - .data - .address!; + {required bitcoin.HDWallet hd, required int index, required BasedUtxoNetwork network}) => + ECPublic.fromHex(hd.derive(index).pubKey!).toP2wpkhAddress().toAddress(network); -String generateP2WPKHAddressByPath( - {required bitcoin.HDWallet hd, - required String path, - required bitcoin.NetworkType networkType}) => - bitcoin - .P2WPKH( - data: PaymentData( - pubkey: - Uint8List.fromList(HEX.decode(hd.derivePath(path).pubKey!))), - network: networkType) - .data - .address!; +String generateP2SHAddress( + {required bitcoin.HDWallet hd, required int index, required BasedUtxoNetwork network}) => + ECPublic.fromHex(hd.derive(index).pubKey!).toP2wpkhInP2sh().toAddress(network); + +String generateP2WSHAddress( + {required bitcoin.HDWallet hd, required int index, required BasedUtxoNetwork network}) => + ECPublic.fromHex(hd.derive(index).pubKey!).toP2wshAddress().toAddress(network); String generateP2PKHAddress( - {required bitcoin.HDWallet hd, - required int index, - required bitcoin.NetworkType networkType}) => - bitcoin - .P2PKH( - data: PaymentData( - pubkey: - Uint8List.fromList(HEX.decode(hd.derive(index).pubKey!))), - network: networkType) - .data - .address!; + {required bitcoin.HDWallet hd, required int index, required BasedUtxoNetwork network}) => + ECPublic.fromHex(hd.derive(index).pubKey!).toP2pkhAddress().toAddress(network); + +String generateP2TRAddress( + {required bitcoin.HDWallet hd, required int index, required BasedUtxoNetwork network}) => + ECPublic.fromHex(hd.derive(index).pubKey!).toTaprootAddress().toAddress(network); diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 3344cb807..25e6f269d 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -21,18 +21,18 @@ packages: dependency: transitive description: name: args - sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611" + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.2" asn1lib: dependency: transitive description: name: asn1lib - sha256: ab96a1cb3beeccf8145c52e449233fe68364c9641623acd3adad66f8184f1039 + sha256: c9c85fedbe2188b95133cbe960e16f5f448860f7133330e272edbbca5893ddc6 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.2" async: dependency: transitive description: @@ -75,6 +75,15 @@ packages: url: "https://github.com/cake-tech/bitbox-flutter.git" source: git version: "1.0.1" + bitcoin_base: + dependency: "direct main" + description: + path: "." + ref: cake-update-v1 + resolved-ref: "9611e9db77e92a8434e918cdfb620068f6fcb1aa" + url: "https://github.com/cake-tech/bitcoin_base.git" + source: git + version: "4.0.0" bitcoin_flutter: dependency: "direct main" description: @@ -84,6 +93,14 @@ packages: url: "https://github.com/cake-tech/bitcoin_flutter.git" source: git version: "2.1.0" + blockchain_utils: + dependency: "direct main" + description: + name: blockchain_utils + sha256: "9701dfaa74caad4daae1785f1ec4445cf7fb94e45620bc3a4aca1b9b281dc6c9" + url: "https://pub.dev" + source: hosted + version: "1.6.0" boolean_selector: dependency: transitive description: @@ -104,10 +121,10 @@ packages: dependency: transitive description: name: build - sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" build_config: dependency: transitive description: @@ -120,10 +137,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf" + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.0.1" build_resolvers: dependency: "direct dev" description: @@ -136,18 +153,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.8" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" + sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" url: "https://pub.dev" source: hosted - version: "7.2.7" + version: "7.2.10" built_collection: dependency: transitive description: @@ -160,10 +177,10 @@ packages: dependency: transitive description: name: built_value - sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725" + sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6 url: "https://pub.dev" source: hosted - version: "8.4.3" + version: "8.9.0" characters: dependency: transitive description: @@ -176,10 +193,10 @@ packages: dependency: transitive description: name: checked_yaml - sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" clock: dependency: transitive description: @@ -192,10 +209,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.10.0" collection: dependency: transitive description: @@ -216,18 +233,18 @@ packages: dependency: transitive description: name: crypto - sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" cryptography: dependency: "direct main" description: name: cryptography - sha256: e0e37f79665cd5c86e8897f9abe1accfe813c0cc5299dab22256e22fddc1fef8 + sha256: df156c5109286340817d21fa7b62f9140f17915077127dd70f8bd7a2a0997a35 url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.5.0" cw_core: dependency: "direct main" description: @@ -247,10 +264,10 @@ packages: dependency: transitive description: name: encrypt - sha256: "4fd4e4fdc21b9d7d4141823e1e6515cd94e7b8d84749504c232999fba25d9bbb" + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "5.0.3" fake_async: dependency: transitive description: @@ -263,10 +280,10 @@ packages: dependency: transitive description: name: ffi - sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" file: dependency: transitive description: @@ -292,10 +309,10 @@ packages: dependency: "direct main" description: name: flutter_mobx - sha256: "0da4add0016387a7bf309a0d0c41d36c6b3ae25ed7a176409267f166509e723e" + sha256: "4a5d062ff85ed3759f4aac6410ff0ffae32e324b2e71ca722ae1b37b32e865f4" url: "https://pub.dev" source: hosted - version: "2.0.6+5" + version: "2.2.0+2" flutter_test: dependency: "direct dev" description: flutter @@ -313,18 +330,18 @@ packages: dependency: transitive description: name: glob - sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" graphs: dependency: transitive description: name: graphs - sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.1" hex: dependency: transitive description: @@ -401,18 +418,18 @@ packages: dependency: transitive description: name: json_annotation - sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 url: "https://pub.dev" source: hosted - version: "4.8.0" + version: "4.8.1" logging: dependency: transitive description: name: logging - sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" matcher: dependency: transitive description: @@ -449,18 +466,26 @@ packages: dependency: "direct main" description: name: mobx - sha256: f1862bd92c6a903fab67338f27e2f731117c3cb9ea37cee1a487f9e4e0de314a + sha256: "74ee54012dc7c1b3276eaa960a600a7418ef5f9997565deb8fca1fd88fb36b78" url: "https://pub.dev" source: hosted - version: "2.1.3+1" + version: "2.3.0+1" mobx_codegen: dependency: "direct dev" description: name: mobx_codegen - sha256: "86122e410d8ea24dda0c69adb5c2a6ccadd5ce02ad46e144764e0d0184a06181" + sha256: d4beb9cea4b7b014321235f8fdc7c2193ee0fe1d1198e9da7403f8bc85c4407c url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.3.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" package_config: dependency: transitive description: @@ -481,26 +506,26 @@ packages: dependency: "direct main" description: name: path_provider - sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" path_provider_linux: dependency: transitive description: @@ -513,10 +538,10 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: @@ -529,26 +554,26 @@ packages: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.4" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.8" pointycastle: dependency: transitive description: name: pointycastle - sha256: db7306cf0249f838d1a24af52b5a5887c5bf7f31d8bb4e827d071dc0939ad346 + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" url: "https://pub.dev" source: hosted - version: "3.6.2" + version: "3.7.4" pool: dependency: transitive description: @@ -557,30 +582,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" - process: + provider: dependency: transitive description: - name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + name: provider + sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" url: "https://pub.dev" source: hosted - version: "4.2.4" + version: "6.1.1" pub_semver: dependency: transitive description: name: pub_semver - sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a" + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.3" rxdart: dependency: "direct main" description: @@ -593,18 +618,18 @@ packages: dependency: transitive description: name: shelf - sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" sky_engine: dependency: transitive description: flutter @@ -702,10 +727,10 @@ packages: dependency: transitive description: name: typed_data - sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" unorm_dart: dependency: "direct main" description: @@ -726,42 +751,42 @@ packages: dependency: transitive description: name: watcher - sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.0" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" win32: dependency: transitive description: name: win32 - sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 + sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "5.0.9" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "0.2.0+3" + version: "1.0.4" yaml: dependency: transitive description: name: yaml - sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" sdks: dart: ">=3.0.0 <4.0.0" - flutter: ">=3.7.0" + flutter: ">=3.10.0" diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index a50ff68ad..847b77773 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -30,6 +30,11 @@ dependencies: rxdart: ^0.27.5 unorm_dart: ^0.2.0 cryptography: ^2.0.5 + bitcoin_base: + git: + url: https://github.com/cake-tech/bitcoin_base.git + ref: cake-update-v1 + blockchain_utils: ^1.6.0 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 e79d21ba3..151bad93a 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -28,17 +28,18 @@ part 'bitcoin_cash_wallet.g.dart'; class BitcoinCashWallet = BitcoinCashWalletBase with _$BitcoinCashWallet; abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { - BitcoinCashWalletBase( - {required String mnemonic, - required String password, - required WalletInfo walletInfo, - required Box unspentCoinsInfo, - required Uint8List seedBytes, - List? initialAddresses, - ElectrumBalance? initialBalance, - int initialRegularAddressIndex = 0, - int initialChangeAddressIndex = 0}) - : super( + BitcoinCashWalletBase({ + required String mnemonic, + required String password, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + required Uint8List seedBytes, + String? addressPageType, + List? initialAddresses, + ElectrumBalance? initialBalance, + Map? initialRegularAddressIndex, + Map? initialChangeAddressIndex, + }) : super( mnemonic: mnemonic, password: password, walletInfo: walletInfo, @@ -48,40 +49,43 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { initialBalance: initialBalance, seedBytes: seedBytes, currency: CryptoCurrency.bch) { - walletAddresses = BitcoinCashWalletAddresses(walletInfo, - electrumClient: electrumClient, - initialAddresses: initialAddresses, - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex, - mainHd: hd, - sideHd: bitcoin.HDWallet.fromSeed(seedBytes) - .derivePath("m/44'/145'/0'/1"), - networkType: networkType); + walletAddresses = BitcoinCashWalletAddresses( + walletInfo, + electrumClient: electrumClient, + initialAddresses: initialAddresses, + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex, + mainHd: hd, + sideHd: bitcoin.HDWallet.fromSeed(seedBytes).derivePath("m/44'/145'/0'/1"), + network: network, + ); autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; }); } - static Future create( {required String mnemonic, required String password, required WalletInfo walletInfo, required Box unspentCoinsInfo, + String? addressPageType, List? initialAddresses, ElectrumBalance? initialBalance, - int initialRegularAddressIndex = 0, - int initialChangeAddressIndex = 0}) async { + Map? initialRegularAddressIndex, + Map? initialChangeAddressIndex}) async { return BitcoinCashWallet( - mnemonic: mnemonic, - password: password, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfo, - initialAddresses: initialAddresses, - initialBalance: initialBalance, - seedBytes: await Mnemonic.toSeed(mnemonic), - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex); + mnemonic: mnemonic, + password: password, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + initialAddresses: initialAddresses, + initialBalance: initialBalance, + seedBytes: await Mnemonic.toSeed(mnemonic), + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex, + addressPageType: addressPageType, + ); } static Future open({ @@ -90,17 +94,20 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { required Box unspentCoinsInfo, required String password, }) async { - final snp = await ElectrumWallletSnapshot.load(name, walletInfo.type, password); + final snp = await ElectrumWalletSnapshot.load( + name, walletInfo.type, password, BitcoinCashNetwork.mainnet); return BitcoinCashWallet( - mnemonic: snp.mnemonic, - password: password, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfo, - initialAddresses: snp.addresses, - initialBalance: snp.balance, - seedBytes: await Mnemonic.toSeed(snp.mnemonic), - initialRegularAddressIndex: snp.regularAddressIndex, - initialChangeAddressIndex: snp.changeAddressIndex); + mnemonic: snp.mnemonic, + password: password, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + initialAddresses: snp.addresses, + initialBalance: snp.balance, + seedBytes: await Mnemonic.toSeed(snp.mnemonic), + initialRegularAddressIndex: snp.regularAddressIndex, + initialChangeAddressIndex: snp.changeAddressIndex, + addressPageType: snp.addressPageType, + ); } @override @@ -273,20 +280,18 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { electrumClient: electrumClient, amount: amount, fee: fee); } - bitbox.ECPair generateKeyPair( - {required bitcoin.HDWallet hd, - required int index}) => + bitbox.ECPair generateKeyPair({required bitcoin.HDWallet hd, required int index}) => bitbox.ECPair.fromWIF(hd.derive(index).wif!); @override - int feeAmountForPriority( - BitcoinTransactionPriority priority, int inputsCount, int outputsCount) => + int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount, int outputsCount, + {int? size}) => feeRate(priority) * bitbox.BitcoinCash.getByteCount(inputsCount, outputsCount); - int feeAmountWithFeeRate(int feeRate, int inputsCount, int outputsCount) => + int feeAmountWithFeeRate(int feeRate, int inputsCount, int outputsCount, {int? size}) => feeRate * bitbox.BitcoinCash.getByteCount(inputsCount, outputsCount); - int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount}) { + int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount, int? size}) { int inputsCount = 0; int totalValue = 0; @@ -326,9 +331,10 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { @override String signMessage(String message, {String? address = null}) { final index = address != null - ? walletAddresses.addresses + ? walletAddresses.allAddresses .firstWhere((element) => element.address == AddressUtils.toLegacyAddress(address)) - .index : null; + .index + : null; final HD = index == null ? hd : hd.derive(index); return base64Encode(HD.signMessage(message)); } diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart index 1709c4d8f..8291ce2a5 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -1,6 +1,5 @@ +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; -import 'package:cw_bitcoin/bitcoin_address_record.dart'; -import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/wallet_info.dart'; @@ -11,24 +10,19 @@ part 'bitcoin_cash_wallet_addresses.g.dart'; class BitcoinCashWalletAddresses = BitcoinCashWalletAddressesBase with _$BitcoinCashWalletAddresses; abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses with Store { - BitcoinCashWalletAddressesBase(WalletInfo walletInfo, - {required bitcoin.HDWallet mainHd, - required bitcoin.HDWallet sideHd, - required bitcoin.NetworkType networkType, - required ElectrumClient electrumClient, - List? initialAddresses, - int initialRegularAddressIndex = 0, - int initialChangeAddressIndex = 0}) - : super(walletInfo, - initialAddresses: initialAddresses, - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex, - mainHd: mainHd, - sideHd: sideHd, - electrumClient: electrumClient, - networkType: networkType); + BitcoinCashWalletAddressesBase( + WalletInfo walletInfo, { + required super.mainHd, + required super.sideHd, + required super.network, + required super.electrumClient, + super.initialAddresses, + super.initialRegularAddressIndex, + super.initialChangeAddressIndex, + }) : super(walletInfo); @override - String getAddress({required int index, required bitcoin.HDWallet hd}) => - generateP2PKHAddress(hd: hd, index: index, networkType: networkType); + String getAddress( + {required int index, required bitcoin.HDWallet hd, BitcoinAddressType? addressType}) => + generateP2PKHAddress(hd: hd, index: index, network: network); } 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 f66e38ca7..df8e841f8 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart @@ -2,10 +2,7 @@ import 'dart:io'; import 'package:bip39/bip39.dart'; import 'package:cw_bitcoin_cash/cw_bitcoin_cash.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/unspent_coins_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; @@ -15,8 +12,7 @@ import 'package:collection/collection.dart'; import 'package:hive/hive.dart'; class BitcoinCashWalletService extends WalletService { + BitcoinCashRestoreWalletFromSeedCredentials, BitcoinCashRestoreWalletFromWIFCredentials> { BitcoinCashWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); final Box walletInfoSource; @@ -30,13 +26,9 @@ class BitcoinCashWalletService extends WalletService create( - credentials) async { - final strength = (credentials.seedPhraseLength == 12) - ? 128 - : (credentials.seedPhraseLength == 24) - ? 256 - : 128; + Future create(credentials, {bool? isTestnet}) async { + final strength = credentials.seedPhraseLength == 24 ? 256 : 128; + final wallet = await BitcoinCashWalletBase.create( mnemonic: await Mnemonic.generate(strength: strength), password: credentials.password!, @@ -49,21 +41,25 @@ class BitcoinCashWalletService extends WalletService openWallet(String name, String password) async { - final walletInfo = walletInfoSource.values.firstWhereOrNull( - (info) => info.id == WalletBase.idFor(name, getType()))!; + final walletInfo = walletInfoSource.values + .firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!; try { final wallet = await BitcoinCashWalletBase.open( - password: password, name: name, walletInfo: walletInfo, + password: password, + name: name, + walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource); await wallet.init(); saveBackup(name); return wallet; - } catch(_) { + } catch (_) { await restoreWalletFilesFromBackup(name); final wallet = await BitcoinCashWalletBase.open( - password: password, name: name, walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfoSource); + password: password, + name: name, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); await wallet.init(); return wallet; } @@ -71,17 +67,16 @@ class BitcoinCashWalletService extends WalletService 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()))!; + 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 rename(String currentName, String password, String newName) async { - final currentWalletInfo = walletInfoSource.values.firstWhereOrNull( - (info) => info.id == WalletBase.idFor(currentName, getType()))!; + final currentWalletInfo = walletInfoSource.values + .firstWhereOrNull((info) => info.id == WalletBase.idFor(currentName, getType()))!; final currentWallet = await BitcoinCashWalletBase.open( password: password, name: currentName, @@ -99,15 +94,14 @@ class BitcoinCashWalletService extends WalletService - restoreFromKeys(credentials) { + Future restoreFromKeys(credentials, {bool? isTestnet}) { // TODO: implement restoreFromKeys throw UnimplementedError('restoreFromKeys() is not implemented'); } @override - Future restoreFromSeed( - BitcoinCashRestoreWalletFromSeedCredentials credentials) async { + Future restoreFromSeed(BitcoinCashRestoreWalletFromSeedCredentials credentials, + {bool? isTestnet}) async { if (!validateMnemonic(credentials.mnemonic)) { throw BitcoinCashMnemonicIsIncorrectException(); } diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index 49a5efb15..9c098c0ff 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -29,7 +29,10 @@ dependencies: git: url: https://github.com/cake-tech/bitbox-flutter.git ref: master - bitcoin_base: ^3.0.1 + bitcoin_base: + git: + url: https://github.com/cake-tech/bitcoin_base.git + ref: cake-update-v1 diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 162ffb1d2..ce509015c 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -9,7 +9,9 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen required this.decimals, this.fullName, this.iconPath, - this.tag}) + this.tag, + this.enabled = false, + }) : super(title: title, raw: raw); final String name; @@ -17,6 +19,9 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen final String? fullName; final String? iconPath; final int decimals; + final bool enabled; + + set enabled(bool value) => this.enabled = value; static const all = [ CryptoCurrency.xmr, @@ -96,6 +101,8 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen CryptoCurrency.usdtPoly, CryptoCurrency.usdcEPoly, CryptoCurrency.kaspa, + CryptoCurrency.digibyte, + CryptoCurrency.usdtSol, ]; static const havenCurrencies = [ @@ -207,7 +214,9 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const banano = CryptoCurrency(title: 'BAN', fullName: 'Banano', raw: 86, name: 'banano', iconPath: 'assets/images/nano_icon.png', decimals: 29); static const usdtPoly = CryptoCurrency(title: 'USDT', tag: 'POLY', fullName: 'Tether USD (PoS)', raw: 87, name: 'usdtpoly', iconPath: 'assets/images/usdt_icon.png', decimals: 6); static const usdcEPoly = CryptoCurrency(title: 'USDC.E', tag: 'POLY', fullName: 'USD Coin (PoS)', raw: 88, name: 'usdcepoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6); - static const kaspa = CryptoCurrency(title: 'KAS', fullName: 'Kaspa', raw: 89, name: 'kaspa', iconPath: 'assets/images/kaspa_icon.png', decimals: 8); + static 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: 90, name: 'usdtsol', iconPath: 'assets/images/usdt_icon.png', decimals: 6); static final Map _rawCurrencyMap = @@ -238,7 +247,16 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen return CryptoCurrency._rawCurrencyMap[raw]!; } - static CryptoCurrency fromString(String name) { + // TODO: refactor this + static CryptoCurrency fromString(String name, {CryptoCurrency? walletCurrency}) { + try { + return CryptoCurrency.all.firstWhere((element) => + element.title.toLowerCase() == name && + (element.tag == null || + element.tag == walletCurrency?.title || + element.tag == walletCurrency?.tag)); + } catch (_) {} + if (CryptoCurrency._nameCurrencyMap[name.toLowerCase()] == null) { final s = 'Unexpected token: $name for CryptoCurrency fromString'; throw ArgumentError.value(name, 'name', s); diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart index ce0219f1f..58ee37669 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -21,6 +21,8 @@ CryptoCurrency currencyForWalletType(WalletType type) { return CryptoCurrency.banano; case WalletType.polygon: return CryptoCurrency.maticpoly; + case WalletType.solana: + return CryptoCurrency.sol; default: throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType'); } diff --git a/cw_core/lib/enumerate.dart b/cw_core/lib/enumerate.dart new file mode 100644 index 000000000..d92347e91 --- /dev/null +++ b/cw_core/lib/enumerate.dart @@ -0,0 +1,13 @@ +abstract class Enumerate { + String get value; + + @override + operator ==(other) { + if (identical(other, this)) return true; + if (other is! Enumerate) return false; + return other.runtimeType == runtimeType && value == other.value; + } + + @override + int get hashCode => value.hashCode; +} diff --git a/cw_core/lib/hive_type_ids.dart b/cw_core/lib/hive_type_ids.dart index 4d4d1a6a8..3fa2eb647 100644 --- a/cw_core/lib/hive_type_ids.dart +++ b/cw_core/lib/hive_type_ids.dart @@ -13,4 +13,5 @@ const ADDRESS_INFO_TYPE_ID = 11; const ERC20_TOKEN_TYPE_ID = 12; const NANO_ACCOUNT_TYPE_ID = 13; const POW_NODE_TYPE_ID = 14; -const DERIVATION_TYPE_TYPE_ID = 15; \ No newline at end of file +const DERIVATION_TYPE_TYPE_ID = 15; +const SPL_TOKEN_TYPE_ID = 16; diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 2c43dd21a..585bc3c38 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -70,15 +70,10 @@ class Node extends HiveObject with Keyable { Uri get uri { switch (type) { case WalletType.monero: - return Uri.http(uriRaw, ''); - case WalletType.bitcoin: - return createUriFromElectrumAddress(uriRaw); - case WalletType.litecoin: - return createUriFromElectrumAddress(uriRaw); case WalletType.haven: return Uri.http(uriRaw, ''); - case WalletType.ethereum: - return Uri.https(uriRaw, ''); + case WalletType.bitcoin: + case WalletType.litecoin: case WalletType.bitcoinCash: return createUriFromElectrumAddress(uriRaw); case WalletType.nano: @@ -88,7 +83,9 @@ class Node extends HiveObject with Keyable { } else { return Uri.http(uriRaw, ''); } + case WalletType.ethereum: case WalletType.polygon: + case WalletType.solana: return Uri.https(uriRaw, ''); default: throw Exception('Unexpected type ${type.toString()} for Node uri'); @@ -134,21 +131,17 @@ class Node extends HiveObject with Keyable { try { switch (type) { case WalletType.monero: - return requestMoneroNode(); - case WalletType.bitcoin: - return requestElectrumServer(); - case WalletType.litecoin: - return requestElectrumServer(); case WalletType.haven: return requestMoneroNode(); - case WalletType.ethereum: - return requestElectrumServer(); - case WalletType.bitcoinCash: - return requestElectrumServer(); case WalletType.nano: case WalletType.banano: return requestNanoNode(); + case WalletType.bitcoin: + case WalletType.litecoin: + case WalletType.bitcoinCash: + case WalletType.ethereum: case WalletType.polygon: + case WalletType.solana: return requestElectrumServer(); default: return false; diff --git a/cw_core/lib/pathForWallet.dart b/cw_core/lib/pathForWallet.dart index af4838ffa..cfc33ef21 100644 --- a/cw_core/lib/pathForWallet.dart +++ b/cw_core/lib/pathForWallet.dart @@ -1,6 +1,5 @@ import 'dart:io'; import 'package:cw_core/wallet_type.dart'; -import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; Future pathForWalletDir({required String name, required WalletType type}) async { diff --git a/cw_core/lib/receive_page_option.dart b/cw_core/lib/receive_page_option.dart new file mode 100644 index 000000000..786d07bc5 --- /dev/null +++ b/cw_core/lib/receive_page_option.dart @@ -0,0 +1,21 @@ +import 'package:cw_core/enumerate.dart'; + +class ReceivePageOption implements Enumerate { + static const mainnet = ReceivePageOption._('mainnet'); + static const anonPayInvoice = ReceivePageOption._('anonPayInvoice'); + static const anonPayDonationLink = ReceivePageOption._('anonPayDonationLink'); + + const ReceivePageOption._(this.value); + + final String value; + + String toString() { + return value; + } +} + +const ReceivePageOptions = [ + ReceivePageOption.mainnet, + ReceivePageOption.anonPayInvoice, + ReceivePageOption.anonPayDonationLink +]; diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index 09b423c14..49f1bdc94 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -88,4 +88,6 @@ abstract class WalletBase renameWalletFiles(String newWalletName); String signMessage(String message, {String? address = null}) => throw UnimplementedError(); + + bool? isTestnet; } diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index c4ccea00a..2a44175a7 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -148,6 +148,12 @@ class WalletInfo extends HiveObject { @HiveField(17) String? derivationPath; + @HiveField(18) + String? addressPageType; + + @HiveField(19) + String? network; + String get yatLastUsedAddress => yatLastUsedAddressRaw ?? ''; set yatLastUsedAddress(String address) { diff --git a/cw_core/lib/wallet_service.dart b/cw_core/lib/wallet_service.dart index 3b4908386..22981b9db 100644 --- a/cw_core/lib/wallet_service.dart +++ b/cw_core/lib/wallet_service.dart @@ -9,11 +9,11 @@ abstract class WalletService { WalletType getType(); - Future create(N credentials); + Future create(N credentials, {bool? isTestnet}); - Future restoreFromSeed(RFS credentials); + Future restoreFromSeed(RFS credentials, {bool? isTestnet}); - Future restoreFromKeys(RFK credentials); + Future restoreFromKeys(RFK credentials, {bool? isTestnet}); Future openWallet(String name, String password); diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index 20f0bdb19..a63ddf37c 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -14,6 +14,7 @@ const walletTypes = [ WalletType.nano, WalletType.banano, WalletType.polygon, + WalletType.solana, ]; @HiveType(typeId: WALLET_TYPE_TYPE_ID) @@ -46,7 +47,10 @@ enum WalletType { bitcoinCash, @HiveField(9) - polygon + polygon, + + @HiveField(10) + solana } int serializeToInt(WalletType type) { @@ -69,6 +73,8 @@ int serializeToInt(WalletType type) { return 7; case WalletType.polygon: return 8; + case WalletType.solana: + return 9; default: return -1; } @@ -94,6 +100,8 @@ WalletType deserializeFromInt(int raw) { return WalletType.bitcoinCash; case 8: return WalletType.polygon; + case 9: + return WalletType.solana; default: throw Exception('Unexpected token: $raw for WalletType deserializeFromInt'); } @@ -119,6 +127,8 @@ String walletTypeToString(WalletType type) { return 'Banano'; case WalletType.polygon: return 'Polygon'; + case WalletType.solana: + return 'Solana'; default: return ''; } @@ -144,6 +154,8 @@ String walletTypeToDisplayName(WalletType type) { return 'Banano (BAN)'; case WalletType.polygon: return 'Polygon (MATIC)'; + case WalletType.solana: + return 'Solana (SOL)'; default: return ''; } @@ -169,6 +181,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type) { return CryptoCurrency.banano; case WalletType.polygon: return CryptoCurrency.maticpoly; + case WalletType.solana: + return CryptoCurrency.sol; default: throw Exception( 'Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency'); diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index aacbd9ddd..678e57b54 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -5,34 +5,34 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "4897882604d919befd350648c7f91926a9d5de99e67b455bf0917cc2362f4bb8" + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 url: "https://pub.dev" source: hosted - version: "47.0.0" + version: "64.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "690e335554a8385bc9d787117d9eb52c0c03ee207a607e593de3c9d71b1cfe80" + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "6.2.0" args: dependency: transitive description: name: args - sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611" + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.2" asn1lib: dependency: transitive description: name: asn1lib - sha256: ab96a1cb3beeccf8145c52e449233fe68364c9641623acd3adad66f8184f1039 + sha256: "21afe4333076c02877d14f4a89df111e658a6d466cbfc802eb705eb91bd5adfd" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" async: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: build - sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" build_config: dependency: transitive description: @@ -69,34 +69,34 @@ packages: dependency: transitive description: name: build_daemon - sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf" + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.0.1" build_resolvers: dependency: "direct dev" description: name: build_resolvers - sha256: "687cf90a3951affac1bd5f9ecb5e3e90b60487f3d9cdc359bb310f8876bb02a6" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.0.10" + version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.8" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" + sha256: c9e32d21dd6626b5c163d48b037ce906bbe428bc23ab77bcd77bb21e593b6185 url: "https://pub.dev" source: hosted - version: "7.2.7" + version: "7.2.11" built_collection: dependency: transitive description: @@ -109,10 +109,10 @@ packages: dependency: transitive description: name: built_value - sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725" + sha256: c9aabae0718ec394e5bc3c7272e6bb0dc0b32201a08fe185ec1d8401d3e39309 url: "https://pub.dev" source: hosted - version: "8.4.3" + version: "8.8.1" characters: dependency: transitive description: @@ -125,10 +125,10 @@ packages: dependency: transitive description: name: checked_yaml - sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" clock: dependency: transitive description: @@ -141,10 +141,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.10.0" collection: dependency: transitive description: @@ -165,26 +165,26 @@ packages: dependency: transitive description: name: crypto - sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" dart_style: dependency: transitive description: name: dart_style - sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" + sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.4" encrypt: dependency: "direct main" description: name: encrypt - sha256: "4fd4e4fdc21b9d7d4141823e1e6515cd94e7b8d84749504c232999fba25d9bbb" + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "5.0.3" fake_async: dependency: transitive description: @@ -197,10 +197,10 @@ packages: dependency: transitive description: name: ffi - sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" file: dependency: "direct main" description: @@ -226,10 +226,10 @@ packages: dependency: "direct main" description: name: flutter_mobx - sha256: "0da4add0016387a7bf309a0d0c41d36c6b3ae25ed7a176409267f166509e723e" + sha256: "4a5d062ff85ed3759f4aac6410ff0ffae32e324b2e71ca722ae1b37b32e865f4" url: "https://pub.dev" source: hosted - version: "2.0.6+5" + version: "2.2.0+2" flutter_test: dependency: "direct dev" description: flutter @@ -247,18 +247,18 @@ packages: dependency: transitive description: name: glob - sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" graphs: dependency: transitive description: name: graphs - sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.1" hive: dependency: transitive description: @@ -327,18 +327,18 @@ packages: dependency: transitive description: name: json_annotation - sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 url: "https://pub.dev" source: hosted - version: "4.8.0" + version: "4.8.1" logging: dependency: transitive description: name: logging - sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" matcher: dependency: transitive description: @@ -375,18 +375,26 @@ packages: dependency: "direct main" description: name: mobx - sha256: f1862bd92c6a903fab67338f27e2f731117c3cb9ea37cee1a487f9e4e0de314a + sha256: "74ee54012dc7c1b3276eaa960a600a7418ef5f9997565deb8fca1fd88fb36b78" url: "https://pub.dev" source: hosted - version: "2.1.3+1" + version: "2.3.0+1" mobx_codegen: dependency: "direct dev" description: name: mobx_codegen - sha256: "86122e410d8ea24dda0c69adb5c2a6ccadd5ce02ad46e144764e0d0184a06181" + sha256: b26c7f9c20b38f0ea572c1ed3f29d8e027cb265538bbd1aed3ec198642cfca42 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.6.0+1" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" package_config: dependency: transitive description: @@ -407,26 +415,26 @@ packages: dependency: "direct main" description: name: path_provider - sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" path_provider_linux: dependency: transitive description: @@ -439,10 +447,10 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: @@ -455,26 +463,26 @@ packages: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.4" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.8" pointycastle: dependency: transitive description: name: pointycastle - sha256: db7306cf0249f838d1a24af52b5a5887c5bf7f31d8bb4e827d071dc0939ad346 + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" url: "https://pub.dev" source: hosted - version: "3.6.2" + version: "3.7.4" pool: dependency: transitive description: @@ -483,46 +491,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" - process: + provider: dependency: transitive description: - name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + name: provider + sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" url: "https://pub.dev" source: hosted - version: "4.2.4" + version: "6.1.1" pub_semver: dependency: transitive description: name: pub_semver - sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a" + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.3" shelf: dependency: transitive description: name: shelf - sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" sky_engine: dependency: transitive description: flutter @@ -540,18 +548,18 @@ packages: dependency: transitive description: name: source_gen - sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "1.2.6" + version: "1.5.0" source_helper: dependency: transitive description: name: source_helper - sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.4" source_span: dependency: transitive description: @@ -620,10 +628,10 @@ packages: dependency: transitive description: name: typed_data - sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" vector_math: dependency: transitive description: @@ -636,42 +644,42 @@ packages: dependency: transitive description: name: watcher - sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.0" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" win32: dependency: transitive description: name: win32 - sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 + sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "5.0.9" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "0.2.0+3" + version: "1.0.4" yaml: dependency: transitive description: name: yaml - sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" sdks: dart: ">=3.0.0 <4.0.0" - flutter: ">=3.7.0" + flutter: ">=3.10.0" diff --git a/cw_ethereum/lib/ethereum_wallet_service.dart b/cw_ethereum/lib/ethereum_wallet_service.dart index 5e8c22718..53c8bfea9 100644 --- a/cw_ethereum/lib/ethereum_wallet_service.dart +++ b/cw_ethereum/lib/ethereum_wallet_service.dart @@ -16,7 +16,7 @@ class EthereumWalletService extends EVMChainWalletService { WalletType getType() => WalletType.ethereum; @override - Future create(EVMChainNewWalletCredentials credentials) async { + Future create(EVMChainNewWalletCredentials credentials, {bool? isTestnet}) async { final strength = credentials.seedPhraseLength == 24 ? 256 : 128; final mnemonic = bip39.generateMnemonic(strength: strength); @@ -52,7 +52,6 @@ class EthereumWalletService extends EVMChainWalletService { saveBackup(name); return wallet; } catch (_) { - await restoreWalletFilesFromBackup(name); final wallet = await EthereumWallet.open( @@ -84,7 +83,8 @@ class EthereumWalletService extends EVMChainWalletService { } @override - Future restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials) async { + Future restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials, + {bool? isTestnet}) async { final wallet = EthereumWallet( password: credentials.password!, privateKey: credentials.privateKey, @@ -100,8 +100,8 @@ class EthereumWalletService extends EVMChainWalletService { } @override - Future restoreFromSeed( - EVMChainRestoreWalletFromSeedCredentials credentials) async { + Future restoreFromSeed(EVMChainRestoreWalletFromSeedCredentials credentials, + {bool? isTestnet}) async { if (!bip39.validateMnemonic(credentials.mnemonic)) { throw EthereumMnemonicIsIncorrectException(); } diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index e553cf478..2d58e95ab 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -146,7 +146,7 @@ abstract class EVMChainWalletBase privateKey: _hexPrivateKey, password: _password, ); - walletAddresses.address = _evmChainPrivateKey.address.toString(); + walletAddresses.address = _evmChainPrivateKey.address.hexEip55; await save(); } diff --git a/cw_evm/lib/evm_chain_wallet_addresses.dart b/cw_evm/lib/evm_chain_wallet_addresses.dart index d5d39f21d..4615d79ed 100644 --- a/cw_evm/lib/evm_chain_wallet_addresses.dart +++ b/cw_evm/lib/evm_chain_wallet_addresses.dart @@ -14,6 +14,7 @@ abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store { super(walletInfo); @override + @observable String address; @override diff --git a/cw_evm/lib/evm_chain_wallet_service.dart b/cw_evm/lib/evm_chain_wallet_service.dart index 988a38684..d77a3a81a 100644 --- a/cw_evm/lib/evm_chain_wallet_service.dart +++ b/cw_evm/lib/evm_chain_wallet_service.dart @@ -22,7 +22,7 @@ abstract class EVMChainWalletService extends WalletSer WalletType getType(); @override - Future create(EVMChainNewWalletCredentials credentials); + Future create(EVMChainNewWalletCredentials credentials, {bool? isTestnet}); @override Future openWallet(String name, String password); @@ -31,10 +31,10 @@ abstract class EVMChainWalletService extends WalletSer Future rename(String currentName, String password, String newName); @override - Future restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials); + Future restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials, {bool? isTestnet}); @override - Future restoreFromSeed(EVMChainRestoreWalletFromSeedCredentials credentials); + Future restoreFromSeed(EVMChainRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}); @override Future isWalletExit(String name) async => diff --git a/cw_haven/lib/haven_wallet_service.dart b/cw_haven/lib/haven_wallet_service.dart index dd7713e08..d4808c2d6 100644 --- a/cw_haven/lib/haven_wallet_service.dart +++ b/cw_haven/lib/haven_wallet_service.dart @@ -68,7 +68,7 @@ class HavenWalletService extends WalletService< WalletType getType() => WalletType.haven; @override - Future create(HavenNewWalletCredentials credentials) async { + Future create(HavenNewWalletCredentials credentials, {bool? isTestnet}) async { try { final path = await pathForWallet(name: credentials.name, type: getType()); await haven_wallet_manager.createWallet( @@ -174,7 +174,7 @@ class HavenWalletService extends WalletService< @override Future restoreFromKeys( - HavenRestoreWalletFromKeysCredentials credentials) async { + HavenRestoreWalletFromKeysCredentials credentials, {bool? isTestnet}) async { try { final path = await pathForWallet(name: credentials.name, type: getType()); await haven_wallet_manager.restoreFromKeys( @@ -198,7 +198,7 @@ class HavenWalletService extends WalletService< @override Future restoreFromSeed( - HavenRestoreWalletFromSeedCredentials credentials) async { + HavenRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { try { final path = await pathForWallet(name: credentials.name, type: getType()); await haven_wallet_manager.restoreFromSeed( diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 1a34e4bd6..ab4bfb0b0 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -576,15 +576,19 @@ abstract class MoneroWalletBase return; } - final height = _getHeightByDate(walletInfo.date); - - if (height > MIN_RESTORE_HEIGHT) { - monero_wallet.setRecoveringFromSeed(isRecovery: true); - monero_wallet.setRefreshFromBlockHeight(height: height); - return; + int height = 0; + try { + height = _getHeightByDate(walletInfo.date); + } catch (e, s) { + onError?.call(FlutterErrorDetails( + exception: e, + stack: s, + library: this.runtimeType.toString(), + )); } - throw Exception("height isn't > $MIN_RESTORE_HEIGHT!"); + monero_wallet.setRecoveringFromSeed(isRecovery: true); + monero_wallet.setRefreshFromBlockHeight(height: height); } int _getHeightDistance(DateTime date) { @@ -600,7 +604,7 @@ abstract class MoneroWalletBase final heightDistance = _getHeightDistance(date); if (nodeHeight <= 0) { - // the node returned 0 (an error state), so lets just restore from cache: + // the node returned 0 (an error state) throw Exception("nodeHeight is <= 0!"); } diff --git a/cw_monero/lib/monero_wallet_service.dart b/cw_monero/lib/monero_wallet_service.dart index 3dea7fc0e..1f33dbb3d 100644 --- a/cw_monero/lib/monero_wallet_service.dart +++ b/cw_monero/lib/monero_wallet_service.dart @@ -68,7 +68,7 @@ class MoneroWalletService extends WalletService WalletType.monero; @override - Future create(MoneroNewWalletCredentials credentials) async { + Future create(MoneroNewWalletCredentials credentials, {bool? isTestnet}) async { try { final path = await pathForWallet(name: credentials.name, type: getType()); @@ -203,7 +203,8 @@ class MoneroWalletService extends WalletService restoreFromKeys(MoneroRestoreWalletFromKeysCredentials credentials) async { + Future restoreFromKeys(MoneroRestoreWalletFromKeysCredentials credentials, + {bool? isTestnet}) async { try { final path = await pathForWallet(name: credentials.name, type: getType()); await monero_wallet_manager.restoreFromKeys( @@ -227,7 +228,8 @@ class MoneroWalletService extends WalletService restoreFromSeed(MoneroRestoreWalletFromSeedCredentials credentials) async { + Future restoreFromSeed(MoneroRestoreWalletFromSeedCredentials credentials, + {bool? isTestnet}) async { // Restore from Polyseed if (Polyseed.isValidSeed(credentials.mnemonic)) { return restoreFromPolyseed(credentials); diff --git a/cw_nano/lib/nano_wallet_service.dart b/cw_nano/lib/nano_wallet_service.dart index a76f0393d..7ab502d49 100644 --- a/cw_nano/lib/nano_wallet_service.dart +++ b/cw_nano/lib/nano_wallet_service.dart @@ -26,7 +26,7 @@ class NanoWalletService extends WalletService WalletType.nano; @override - Future create(NanoNewWalletCredentials credentials) async { + Future create(NanoNewWalletCredentials credentials, {bool? isTestnet}) async { // nano standard: DerivationType derivationType = DerivationType.nano; String seedKey = NanoSeeds.generateSeed(); @@ -79,7 +79,7 @@ class NanoWalletService extends WalletService restoreFromKeys(NanoRestoreWalletFromKeysCredentials credentials) async { + Future restoreFromKeys(NanoRestoreWalletFromKeysCredentials credentials, {bool? isTestnet}) async { if (credentials.seedKey.contains(' ')) { throw Exception("Invalid key!"); } else { @@ -113,7 +113,7 @@ class NanoWalletService extends WalletService restoreFromSeed(NanoRestoreWalletFromSeedCredentials credentials) async { + Future restoreFromSeed(NanoRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { if (credentials.mnemonic.contains(' ')) { if (!bip39.validateMnemonic(credentials.mnemonic)) { throw nm.NanoMnemonicIsIncorrectException(); diff --git a/cw_polygon/lib/polygon_wallet_service.dart b/cw_polygon/lib/polygon_wallet_service.dart index 0199a1552..59e14abbf 100644 --- a/cw_polygon/lib/polygon_wallet_service.dart +++ b/cw_polygon/lib/polygon_wallet_service.dart @@ -19,7 +19,7 @@ class PolygonWalletService extends EVMChainWalletService { WalletType getType() => WalletType.polygon; @override - Future create(EVMChainNewWalletCredentials credentials) async { + Future create(EVMChainNewWalletCredentials credentials, {bool? isTestnet}) async { final strength = credentials.seedPhraseLength == 24 ? 256 : 128; final mnemonic = bip39.generateMnemonic(strength: strength); @@ -62,7 +62,7 @@ class PolygonWalletService extends EVMChainWalletService { password: password, walletInfo: walletInfo, ); - + await wallet.init(); await wallet.save(); return wallet; @@ -70,8 +70,8 @@ class PolygonWalletService extends EVMChainWalletService { } @override - Future restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials) async { - + Future restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials, + {bool? isTestnet}) async { final wallet = PolygonWallet( password: credentials.password!, privateKey: credentials.privateKey, @@ -87,8 +87,8 @@ class PolygonWalletService extends EVMChainWalletService { } @override - Future restoreFromSeed( - EVMChainRestoreWalletFromSeedCredentials credentials) async { + Future restoreFromSeed(EVMChainRestoreWalletFromSeedCredentials credentials, + {bool? isTestnet}) async { if (!bip39.validateMnemonic(credentials.mnemonic)) { throw PolygonMnemonicIsIncorrectException(); } diff --git a/cw_solana/.gitignore b/cw_solana/.gitignore new file mode 100644 index 000000000..96486fd93 --- /dev/null +++ b/cw_solana/.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_solana/.metadata b/cw_solana/.metadata new file mode 100644 index 000000000..fa347fc6a --- /dev/null +++ b/cw_solana/.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_solana/CHANGELOG.md b/cw_solana/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/cw_solana/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/cw_solana/LICENSE b/cw_solana/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_solana/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_solana/README.md b/cw_solana/README.md new file mode 100644 index 000000000..02fe8ecab --- /dev/null +++ b/cw_solana/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_solana/analysis_options.yaml b/cw_solana/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_solana/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_solana/lib/cw_solana.dart b/cw_solana/lib/cw_solana.dart new file mode 100644 index 000000000..d04069b3b --- /dev/null +++ b/cw_solana/lib/cw_solana.dart @@ -0,0 +1,7 @@ +library cw_solana; + +/// A Calculator. +class Calculator { + /// Returns [value] plus 1. + int addOne(int value) => value + 1; +} diff --git a/cw_solana/lib/default_spl_tokens.dart b/cw_solana/lib/default_spl_tokens.dart new file mode 100644 index 000000000..7acad78e0 --- /dev/null +++ b/cw_solana/lib/default_spl_tokens.dart @@ -0,0 +1,107 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_solana/spl_token.dart'; + +class DefaultSPLTokens { + final List _defaultTokens = [ + SPLToken( + name: 'USDT Tether', + symbol: 'USDT', + mintAddress: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', + decimal: 6, + mint: 'usdtsol', + enabled: true, + ), + SPLToken( + name: 'USD Coin', + symbol: 'USDC', + mintAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + decimal: 6, + mint: 'usdcsol', + enabled: true, + ), + SPLToken( + name: 'Wrapped Ethereum (Sollet)', + symbol: 'soETH', + mintAddress: '2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk', + decimal: 6, + mint: 'soEth', + iconPath: 'assets/images/eth_icon.png', + ), + SPLToken( + name: 'Wrapped SOL', + symbol: 'WSOL', + mintAddress: 'So11111111111111111111111111111111111111112', + decimal: 9, + mint: 'WSOL', + iconPath: 'assets/images/sol_icon.png', + ), + SPLToken( + name: 'Wrapped Bitcoin (Sollet)', + symbol: 'BTC', + mintAddress: '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E', + decimal: 6, + mint: 'btcsol', + iconPath: 'assets/images/btc.png', + ), + SPLToken( + name: 'Bonk', + symbol: 'Bonk', + mintAddress: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263', + decimal: 5, + mint: 'Bonk', + iconPath: 'assets/images/bonk_icon.png', + ), + SPLToken( + name: 'Helium Network Token', + symbol: 'HNT', + mintAddress: 'hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux', + decimal: 8, + mint: 'hnt', + iconPath: 'assets/images/hnt_icon.png', + ), + SPLToken( + name: 'Pyth Network', + symbol: 'PYTH', + mintAddress: 'HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3', + decimal: 6, + mint: 'pyth', + ), + SPLToken( + name: 'Raydium', + symbol: 'RAY', + mintAddress: '4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R', + decimal: 6, + mint: 'ray', + iconPath: 'assets/images/ray_icon.png', + ), + SPLToken( + name: 'GMT', + symbol: 'GMT', + mintAddress: '7i5KKsX2weiTkry7jA4ZwSuXGhs5eJBEjY8vVxR4pfRx', + decimal: 6, + mint: 'ray', + iconPath: 'assets/images/gmt_icon.png', + ), + SPLToken( + name: 'AvocadoCoin', + symbol: 'AVDO', + mintAddress: 'EE5L8cMU4itTsCSuor7NLK6RZx6JhsBe8GGV3oaAHm3P', + decimal: 8, + mint: 'avdo', + iconPath: 'assets/images/avdo_icon.png', + ), + ]; + + List get initialSPLTokens => _defaultTokens.map((token) { + String? iconPath; + if (token.iconPath != null) return token; + + try { + iconPath = CryptoCurrency.all + .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase()) + .iconPath; + } catch (_) {} + + return SPLToken.copyWith(token, iconPath, 'SOL'); + }).toList(); +} diff --git a/cw_solana/lib/file.dart b/cw_solana/lib/file.dart new file mode 100644 index 000000000..8fd236ec3 --- /dev/null +++ b/cw_solana/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_solana/lib/pending_solana_transaction.dart b/cw_solana/lib/pending_solana_transaction.dart new file mode 100644 index 000000000..38347ed13 --- /dev/null +++ b/cw_solana/lib/pending_solana_transaction.dart @@ -0,0 +1,43 @@ +import 'package:cw_core/pending_transaction.dart'; +import 'package:solana/encoder.dart'; + +class PendingSolanaTransaction with PendingTransaction { + final double amount; + final SignedTx signedTransaction; + final String destinationAddress; + final Function sendTransaction; + final double fee; + + PendingSolanaTransaction({ + required this.fee, + required this.amount, + required this.signedTransaction, + required this.destinationAddress, + required this.sendTransaction, + }); + + @override + String get amountFormatted { + String stringifiedAmount = amount.toString(); + + if (stringifiedAmount.toString().length >= 6) { + stringifiedAmount = stringifiedAmount.substring(0, 6); + } + + return stringifiedAmount; + } + + @override + Future commit() async { + return await sendTransaction(); + } + + @override + String get feeFormatted => fee.toString(); + + @override + String get hex => signedTransaction.encode(); + + @override + String get id => ''; +} diff --git a/cw_solana/lib/solana_balance.dart b/cw_solana/lib/solana_balance.dart new file mode 100644 index 000000000..b1f0ef153 --- /dev/null +++ b/cw_solana/lib/solana_balance.dart @@ -0,0 +1,39 @@ +import 'dart:convert'; + +import 'package:cw_core/balance.dart'; + +class SolanaBalance extends Balance { + SolanaBalance(this.balance) : super(balance.toInt(), balance.toInt()); + + final double balance; + + @override + String get formattedAdditionalBalance => _balanceFormatted(); + + @override + String get formattedAvailableBalance => _balanceFormatted(); + + String _balanceFormatted() { + String stringBalance = balance.toString(); + if (stringBalance.toString().length >= 6) { + stringBalance = stringBalance.substring(0, 6); + } + return stringBalance; + } + + static SolanaBalance? fromJSON(String? jsonSource) { + if (jsonSource == null) { + return null; + } + + final decoded = json.decode(jsonSource) as Map; + + try { + return SolanaBalance(decoded['balance']); + } catch (e) { + return SolanaBalance(0.0); + } + } + + String toJSON() => json.encode({'balance': balance.toString()}); +} diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart new file mode 100644 index 000000000..ea4a9161a --- /dev/null +++ b/cw_solana/lib/solana_client.dart @@ -0,0 +1,478 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:math'; + +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_solana/pending_solana_transaction.dart'; +import 'package:cw_solana/solana_balance.dart'; +import 'package:cw_solana/solana_transaction_model.dart'; +import 'package:http/http.dart' as http; +import 'package:solana/dto.dart'; +import 'package:solana/encoder.dart'; +import 'package:solana/solana.dart'; +import '.secrets.g.dart' as secrets; + +class SolanaWalletClient { + final httpClient = http.Client(); + SolanaClient? _client; + + bool connect(Node node) { + try { + Uri? rpcUri; + String webSocketUrl; + bool isModifiedNodeUri = false; + + if (node.uriRaw == 'rpc.ankr.com') { + isModifiedNodeUri = true; + String ankrApiKey = secrets.ankrApiKey; + + rpcUri = Uri.https(node.uriRaw, '/solana/$ankrApiKey'); + webSocketUrl = 'wss://${node.uriRaw}/solana/ws/$ankrApiKey'; + } else { + webSocketUrl = 'wss://${node.uriRaw}'; + } + + _client = SolanaClient( + rpcUrl: isModifiedNodeUri ? rpcUri! : node.uri, + websocketUrl: Uri.parse(webSocketUrl), + timeout: const Duration(minutes: 2), + ); + return true; + } catch (e) { + return false; + } + } + + Future getBalance(String address) async { + try { + final balance = await _client!.rpcClient.getBalance(address); + + final solBalance = balance.value / lamportsPerSol; + + return solBalance; + } catch (_) { + return 0.0; + } + } + + Future getSPLTokenAccounts(String mintAddress, String publicKey) async { + try { + final tokenAccounts = await _client!.rpcClient.getTokenAccountsByOwner( + publicKey, + TokenAccountsFilter.byMint(mintAddress), + commitment: Commitment.confirmed, + encoding: Encoding.jsonParsed, + ); + return tokenAccounts; + } catch (e) { + return null; + } + } + + Future getSplTokenBalance(String mintAddress, String publicKey) async { + // Fetch the token accounts (a token can have multiple accounts for various uses) + final tokenAccounts = await getSPLTokenAccounts(mintAddress, publicKey); + + // Handle scenario where there is no token account + if (tokenAccounts == null || tokenAccounts.value.isEmpty) { + return null; + } + + // Sum the balances of all accounts with the specified mint address + double totalBalance = 0.0; + + for (var programAccount in tokenAccounts.value) { + final tokenAmountResult = + await _client!.rpcClient.getTokenAccountBalance(programAccount.pubkey); + + final balance = tokenAmountResult.value.uiAmountString; + + final balanceAsDouble = double.tryParse(balance ?? '0.0') ?? 0.0; + + totalBalance += balanceAsDouble; + } + + return SolanaBalance(totalBalance); + } + + Future getGasForMessage(String message) async { + try { + final gasPrice = await _client!.rpcClient.getFeeForMessage(message) ?? 0; + final fee = gasPrice / lamportsPerSol; + return fee; + } catch (_) { + return 0; + } + } + + /// Load the Address's transactions into the account + Future> fetchTransactions( + Ed25519HDPublicKey publicKey, { + String? splTokenSymbol, + int? splTokenDecimal, + }) async { + List transactions = []; + + try { + final response = await _client!.rpcClient.getTransactionsList( + publicKey, + commitment: Commitment.confirmed, + limit: 1000, + ); + + for (final tx in response) { + if (tx.transaction is ParsedTransaction) { + final parsedTx = (tx.transaction as ParsedTransaction); + final message = parsedTx.message; + + final fee = (tx.meta?.fee ?? 0) / lamportsPerSol; + + for (final instruction in message.instructions) { + if (instruction is ParsedInstruction) { + instruction.map( + system: (systemData) { + systemData.parsed.map( + transfer: (transferData) { + ParsedSystemTransferInformation transfer = transferData.info; + bool isOutgoingTx = transfer.source == publicKey.toBase58(); + + double amount = transfer.lamports.toDouble() / lamportsPerSol; + + transactions.add( + SolanaTransactionModel( + id: parsedTx.signatures.first, + from: transfer.source, + to: transfer.destination, + amount: amount, + isOutgoingTx: isOutgoingTx, + blockTimeInInt: tx.blockTime!, + fee: fee, + programId: SystemProgram.programId, + tokenSymbol: 'SOL', + ), + ); + }, + transferChecked: (_) {}, + unsupported: (_) {}, + ); + }, + splToken: (splTokenData) { + if (splTokenSymbol != null) { + splTokenData.parsed.map( + transfer: (transferData) { + SplTokenTransferInfo transfer = transferData.info; + bool isOutgoingTx = transfer.source == publicKey.toBase58(); + + double amount = (double.tryParse(transfer.amount) ?? 0.0) / + pow(10, splTokenDecimal ?? 9); + + transactions.add( + SolanaTransactionModel( + id: parsedTx.signatures.first, + fee: fee, + from: transfer.source, + to: transfer.destination, + amount: amount, + isOutgoingTx: isOutgoingTx, + programId: TokenProgram.programId, + blockTimeInInt: tx.blockTime!, + tokenSymbol: splTokenSymbol, + ), + ); + }, + transferChecked: (transferCheckedData) { + SplTokenTransferCheckedInfo transfer = transferCheckedData.info; + bool isOutgoingTx = transfer.source == publicKey.toBase58(); + double amount = + double.tryParse(transfer.tokenAmount.uiAmountString ?? '0.0') ?? 0.0; + + transactions.add( + SolanaTransactionModel( + id: parsedTx.signatures.first, + fee: fee, + from: transfer.source, + to: transfer.destination, + amount: amount, + isOutgoingTx: isOutgoingTx, + programId: TokenProgram.programId, + blockTimeInInt: tx.blockTime!, + tokenSymbol: splTokenSymbol, + ), + ); + }, + generic: (genericData) {}, + ); + } + }, + memo: (_) {}, + unsupported: (a) {}, + ); + } + } + } + } + + return transactions; + } catch (err) { + return []; + } + } + + Future> getSPLTokenTransfers( + String address, + String splTokenSymbol, + int splTokenDecimal, + Ed25519HDKeyPair ownerKeypair, + ) async { + final tokenMint = Ed25519HDPublicKey.fromBase58(address); + + ProgramAccount? associatedTokenAccount; + + try { + associatedTokenAccount = await _client!.getAssociatedTokenAccount( + mint: tokenMint, + owner: ownerKeypair.publicKey, + commitment: Commitment.confirmed, + ); + } catch (_) {} + + if (associatedTokenAccount == null) return []; + + final accountPublicKey = Ed25519HDPublicKey.fromBase58(associatedTokenAccount.pubkey); + + final tokenTransactions = await fetchTransactions( + accountPublicKey, + splTokenSymbol: splTokenSymbol, + splTokenDecimal: splTokenDecimal, + ); + + return tokenTransactions; + } + + void stop() {} + + SolanaClient? get getSolanaClient => _client; + + Future signSolanaTransaction({ + required String tokenTitle, + required int tokenDecimals, + String? tokenMint, + required double inputAmount, + required String destinationAddress, + required Ed25519HDKeyPair ownerKeypair, + List references = const [], + }) async { + const commitment = Commitment.confirmed; + + final latestBlockhash = + await _client!.rpcClient.getLatestBlockhash(commitment: commitment).value; + + final recentBlockhash = RecentBlockhash( + blockhash: latestBlockhash.blockhash, + feeCalculator: const FeeCalculator( + lamportsPerSignature: 500, + ), + ); + + if (tokenTitle == CryptoCurrency.sol.title) { + final pendingNativeTokenTransaction = await _signNativeTokenTransaction( + tokenTitle: tokenTitle, + tokenDecimals: tokenDecimals, + inputAmount: inputAmount, + destinationAddress: destinationAddress, + ownerKeypair: ownerKeypair, + recentBlockhash: recentBlockhash, + commitment: commitment, + ); + return pendingNativeTokenTransaction; + } else { + final pendingSPLTokenTransaction = _signSPLTokenTransaction( + tokenTitle: tokenTitle, + tokenDecimals: tokenDecimals, + tokenMint: tokenMint!, + inputAmount: inputAmount, + destinationAddress: destinationAddress, + ownerKeypair: ownerKeypair, + recentBlockhash: recentBlockhash, + commitment: commitment, + ); + return pendingSPLTokenTransaction; + } + } + + Future _signNativeTokenTransaction({ + required String tokenTitle, + required int tokenDecimals, + required double inputAmount, + required String destinationAddress, + required Ed25519HDKeyPair ownerKeypair, + required RecentBlockhash recentBlockhash, + required Commitment commitment, + }) async { + // Convert SOL to lamport + int lamports = (inputAmount * lamportsPerSol).toInt(); + + final instructions = [ + SystemInstruction.transfer( + fundingAccount: ownerKeypair.publicKey, + recipientAccount: Ed25519HDPublicKey.fromBase58(destinationAddress), + lamports: lamports, + ), + ]; + + final message = Message(instructions: instructions); + final signers = [ownerKeypair]; + + final signedTx = await _signTransactionInternal( + message: message, + signers: signers, + commitment: commitment, + recentBlockhash: recentBlockhash, + ); + + final fee = await _getFeeFromCompiledMessage( + message, + recentBlockhash, + signers.first.publicKey, + ); + + sendTx() async => await sendTransaction( + signedTransaction: signedTx, + commitment: commitment, + ); + + final pendingTransaction = PendingSolanaTransaction( + amount: inputAmount, + signedTransaction: signedTx, + destinationAddress: destinationAddress, + sendTransaction: sendTx, + fee: fee, + ); + + return pendingTransaction; + } + + Future _signSPLTokenTransaction({ + required String tokenTitle, + required int tokenDecimals, + required String tokenMint, + required double inputAmount, + required String destinationAddress, + required Ed25519HDKeyPair ownerKeypair, + required RecentBlockhash recentBlockhash, + required Commitment commitment, + }) async { + final destinationOwner = Ed25519HDPublicKey.fromBase58(destinationAddress); + final mint = Ed25519HDPublicKey.fromBase58(tokenMint); + + ProgramAccount? associatedRecipientAccount; + ProgramAccount? associatedSenderAccount; + + associatedRecipientAccount = await _client!.getAssociatedTokenAccount( + mint: mint, + owner: destinationOwner, + commitment: commitment, + ); + + associatedSenderAccount = await _client!.getAssociatedTokenAccount( + owner: ownerKeypair.publicKey, + mint: mint, + commitment: commitment, + ); + + // Throw an appropriate exception if the sender has no associated + // token account + if (associatedSenderAccount == null) { + throw NoAssociatedTokenAccountException(ownerKeypair.address, mint.toBase58()); + } + + try { + associatedRecipientAccount ??= await _client!.createAssociatedTokenAccount( + mint: mint, + owner: destinationOwner, + funder: ownerKeypair, + ); + } catch (e) { + throw Exception('Insufficient lamports balance to complete this transaction'); + } + + // Input by the user + final amount = (inputAmount * pow(10, tokenDecimals)).toInt(); + + final instruction = TokenInstruction.transfer( + source: Ed25519HDPublicKey.fromBase58(associatedSenderAccount.pubkey), + destination: Ed25519HDPublicKey.fromBase58(associatedRecipientAccount.pubkey), + owner: ownerKeypair.publicKey, + amount: amount, + ); + + final message = Message(instructions: [instruction]); + final signers = [ownerKeypair]; + + final signedTx = await _signTransactionInternal( + message: message, + signers: signers, + commitment: commitment, + recentBlockhash: recentBlockhash, + ); + + final fee = await _getFeeFromCompiledMessage( + message, + recentBlockhash, + signers.first.publicKey, + ); + + sendTx() async => await sendTransaction( + signedTransaction: signedTx, + commitment: commitment, + ); + + final pendingTransaction = PendingSolanaTransaction( + amount: inputAmount, + signedTransaction: signedTx, + destinationAddress: destinationAddress, + sendTransaction: sendTx, + fee: fee, + ); + return pendingTransaction; + } + + Future _getFeeFromCompiledMessage( + Message message, RecentBlockhash recentBlockhash, Ed25519HDPublicKey feePayer) async { + final compile = message.compile( + recentBlockhash: recentBlockhash.blockhash, + feePayer: feePayer, + ); + + final base64Message = base64Encode(compile.toByteArray().toList()); + + final fee = await getGasForMessage(base64Message); + return fee; + } + + Future _signTransactionInternal({ + required Message message, + required List signers, + required Commitment commitment, + required RecentBlockhash recentBlockhash, + }) async { + final signedTx = await signTransaction(recentBlockhash, message, signers); + + return signedTx; + } + + Future sendTransaction({ + required SignedTx signedTransaction, + required Commitment commitment, + }) async { + final signature = await _client!.rpcClient.sendTransaction( + signedTransaction.encode(), + preflightCommitment: commitment, + ); + + _client!.waitForSignatureStatus(signature, status: commitment); + + return signature; + } +} diff --git a/cw_solana/lib/solana_exceptions.dart b/cw_solana/lib/solana_exceptions.dart new file mode 100644 index 000000000..7409b0500 --- /dev/null +++ b/cw_solana/lib/solana_exceptions.dart @@ -0,0 +1,21 @@ +import 'package:cw_core/crypto_currency.dart'; + +class SolanaTransactionCreationException implements Exception { + final String exceptionMessage; + + SolanaTransactionCreationException(CryptoCurrency currency) + : exceptionMessage = 'Error creating ${currency.title} transaction.'; + + @override + String toString() => exceptionMessage; +} + +class SolanaTransactionWrongBalanceException implements Exception { + final String exceptionMessage; + + SolanaTransactionWrongBalanceException(CryptoCurrency currency) + : exceptionMessage = 'Wrong balance. Not enough ${currency.title} on your balance.'; + + @override + String toString() => exceptionMessage; +} diff --git a/cw_solana/lib/solana_mnemonics.dart b/cw_solana/lib/solana_mnemonics.dart new file mode 100644 index 000000000..21cbb613a --- /dev/null +++ b/cw_solana/lib/solana_mnemonics.dart @@ -0,0 +1,2058 @@ +class SolanaMnemonicIsIncorrectException implements Exception { + @override + String toString() => + 'Solana mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; +} + +class SolanaMnemonics { + static const englishWordlist = [ + 'abandon', + 'ability', + 'able', + 'about', + 'above', + 'absent', + 'absorb', + 'abstract', + 'absurd', + 'abuse', + 'access', + 'accident', + 'account', + 'accuse', + 'achieve', + 'acid', + 'acoustic', + 'acquire', + 'across', + 'act', + 'action', + 'actor', + 'actress', + 'actual', + 'adapt', + 'add', + 'addict', + 'address', + 'adjust', + 'admit', + 'adult', + 'advance', + 'advice', + 'aerobic', + 'affair', + 'afford', + 'afraid', + 'again', + 'age', + 'agent', + 'agree', + 'ahead', + 'aim', + 'air', + 'airport', + 'aisle', + 'alarm', + 'album', + 'alcohol', + 'alert', + 'alien', + 'all', + 'alley', + 'allow', + 'almost', + 'alone', + 'alpha', + 'already', + 'also', + 'alter', + 'always', + 'amateur', + 'amazing', + 'among', + 'amount', + 'amused', + 'analyst', + 'anchor', + 'ancient', + 'anger', + 'angle', + 'angry', + 'animal', + 'ankle', + 'announce', + 'annual', + 'another', + 'answer', + 'antenna', + 'antique', + 'anxiety', + 'any', + 'apart', + 'apology', + 'appear', + 'apple', + 'approve', + 'april', + 'arch', + 'arctic', + 'area', + 'arena', + 'argue', + 'arm', + 'armed', + 'armor', + 'army', + 'around', + 'arrange', + 'arrest', + 'arrive', + 'arrow', + 'art', + 'artefact', + 'artist', + 'artwork', + 'ask', + 'aspect', + 'assault', + 'asset', + 'assist', + 'assume', + 'asthma', + 'athlete', + 'atom', + 'attack', + 'attend', + 'attitude', + 'attract', + 'auction', + 'audit', + 'august', + 'aunt', + 'author', + 'auto', + 'autumn', + 'average', + 'avocado', + 'avoid', + 'awake', + 'aware', + 'away', + 'awesome', + 'awful', + 'awkward', + 'axis', + 'baby', + 'bachelor', + 'bacon', + 'badge', + 'bag', + 'balance', + 'balcony', + 'ball', + 'bamboo', + 'banana', + 'banner', + 'bar', + 'barely', + 'bargain', + 'barrel', + 'base', + 'basic', + 'basket', + 'battle', + 'beach', + 'bean', + 'beauty', + 'because', + 'become', + 'beef', + 'before', + 'begin', + 'behave', + 'behind', + 'believe', + 'below', + 'belt', + 'bench', + 'benefit', + 'best', + 'betray', + 'better', + 'between', + 'beyond', + 'bicycle', + 'bid', + 'bike', + 'bind', + 'biology', + 'bird', + 'birth', + 'bitter', + 'black', + 'blade', + 'blame', + 'blanket', + 'blast', + 'bleak', + 'bless', + 'blind', + 'blood', + 'blossom', + 'blouse', + 'blue', + 'blur', + 'blush', + 'board', + 'boat', + 'body', + 'boil', + 'bomb', + 'bone', + 'bonus', + 'book', + 'boost', + 'border', + 'boring', + 'borrow', + 'boss', + 'bottom', + 'bounce', + 'box', + 'boy', + 'bracket', + 'brain', + 'brand', + 'brass', + 'brave', + 'bread', + 'breeze', + 'brick', + 'bridge', + 'brief', + 'bright', + 'bring', + 'brisk', + 'broccoli', + 'broken', + 'bronze', + 'broom', + 'brother', + 'brown', + 'brush', + 'bubble', + 'buddy', + 'budget', + 'buffalo', + 'build', + 'bulb', + 'bulk', + 'bullet', + 'bundle', + 'bunker', + 'burden', + 'burger', + 'burst', + 'bus', + 'business', + 'busy', + 'butter', + 'buyer', + 'buzz', + 'cabbage', + 'cabin', + 'cable', + 'cactus', + 'cage', + 'cake', + 'call', + 'calm', + 'camera', + 'camp', + 'can', + 'canal', + 'cancel', + 'candy', + 'cannon', + 'canoe', + 'canvas', + 'canyon', + 'capable', + 'capital', + 'captain', + 'car', + 'carbon', + 'card', + 'cargo', + 'carpet', + 'carry', + 'cart', + 'case', + 'cash', + 'casino', + 'castle', + 'casual', + 'cat', + 'catalog', + 'catch', + 'category', + 'cattle', + 'caught', + 'cause', + 'caution', + 'cave', + 'ceiling', + 'celery', + 'cement', + 'census', + 'century', + 'cereal', + 'certain', + 'chair', + 'chalk', + 'champion', + 'change', + 'chaos', + 'chapter', + 'charge', + 'chase', + 'chat', + 'cheap', + 'check', + 'cheese', + 'chef', + 'cherry', + 'chest', + 'chicken', + 'chief', + 'child', + 'chimney', + 'choice', + 'choose', + 'chronic', + 'chuckle', + 'chunk', + 'churn', + 'cigar', + 'cinnamon', + 'circle', + 'citizen', + 'city', + 'civil', + 'claim', + 'clap', + 'clarify', + 'claw', + 'clay', + 'clean', + 'clerk', + 'clever', + 'click', + 'client', + 'cliff', + 'climb', + 'clinic', + 'clip', + 'clock', + 'clog', + 'close', + 'cloth', + 'cloud', + 'clown', + 'club', + 'clump', + 'cluster', + 'clutch', + 'coach', + 'coast', + 'coconut', + 'code', + 'coffee', + 'coil', + 'coin', + 'collect', + 'color', + 'column', + 'combine', + 'come', + 'comfort', + 'comic', + 'common', + 'company', + 'concert', + 'conduct', + 'confirm', + 'congress', + 'connect', + 'consider', + 'control', + 'convince', + 'cook', + 'cool', + 'copper', + 'copy', + 'coral', + 'core', + 'corn', + 'correct', + 'cost', + 'cotton', + 'couch', + 'country', + 'couple', + 'course', + 'cousin', + 'cover', + 'coyote', + 'crack', + 'cradle', + 'craft', + 'cram', + 'crane', + 'crash', + 'crater', + 'crawl', + 'crazy', + 'cream', + 'credit', + 'creek', + 'crew', + 'cricket', + 'crime', + 'crisp', + 'critic', + 'crop', + 'cross', + 'crouch', + 'crowd', + 'crucial', + 'cruel', + 'cruise', + 'crumble', + 'crunch', + 'crush', + 'cry', + 'crystal', + 'cube', + 'culture', + 'cup', + 'cupboard', + 'curious', + 'current', + 'curtain', + 'curve', + 'cushion', + 'custom', + 'cute', + 'cycle', + 'dad', + 'damage', + 'damp', + 'dance', + 'danger', + 'daring', + 'dash', + 'daughter', + 'dawn', + 'day', + 'deal', + 'debate', + 'debris', + 'decade', + 'december', + 'decide', + 'decline', + 'decorate', + 'decrease', + 'deer', + 'defense', + 'define', + 'defy', + 'degree', + 'delay', + 'deliver', + 'demand', + 'demise', + 'denial', + 'dentist', + 'deny', + 'depart', + 'depend', + 'deposit', + 'depth', + 'deputy', + 'derive', + 'describe', + 'desert', + 'design', + 'desk', + 'despair', + 'destroy', + 'detail', + 'detect', + 'develop', + 'device', + 'devote', + 'diagram', + 'dial', + 'diamond', + 'diary', + 'dice', + 'diesel', + 'diet', + 'differ', + 'digital', + 'dignity', + 'dilemma', + 'dinner', + 'dinosaur', + 'direct', + 'dirt', + 'disagree', + 'discover', + 'disease', + 'dish', + 'dismiss', + 'disorder', + 'display', + 'distance', + 'divert', + 'divide', + 'divorce', + 'dizzy', + 'doctor', + 'document', + 'dog', + 'doll', + 'dolphin', + 'domain', + 'donate', + 'donkey', + 'donor', + 'door', + 'dose', + 'double', + 'dove', + 'draft', + 'dragon', + 'drama', + 'drastic', + 'draw', + 'dream', + 'dress', + 'drift', + 'drill', + 'drink', + 'drip', + 'drive', + 'drop', + 'drum', + 'dry', + 'duck', + 'dumb', + 'dune', + 'during', + 'dust', + 'dutch', + 'duty', + 'dwarf', + 'dynamic', + 'eager', + 'eagle', + 'early', + 'earn', + 'earth', + 'easily', + 'east', + 'easy', + 'echo', + 'ecology', + 'economy', + 'edge', + 'edit', + 'educate', + 'effort', + 'egg', + 'eight', + 'either', + 'elbow', + 'elder', + 'electric', + 'elegant', + 'element', + 'elephant', + 'elevator', + 'elite', + 'else', + 'embark', + 'embody', + 'embrace', + 'emerge', + 'emotion', + 'employ', + 'empower', + 'empty', + 'enable', + 'enact', + 'end', + 'endless', + 'endorse', + 'enemy', + 'energy', + 'enforce', + 'engage', + 'engine', + 'enhance', + 'enjoy', + 'enlist', + 'enough', + 'enrich', + 'enroll', + 'ensure', + 'enter', + 'entire', + 'entry', + 'envelope', + 'episode', + 'equal', + 'equip', + 'era', + 'erase', + 'erode', + 'erosion', + 'error', + 'erupt', + 'escape', + 'essay', + 'essence', + 'estate', + 'eternal', + 'ethics', + 'evidence', + 'evil', + 'evoke', + 'evolve', + 'exact', + 'example', + 'excess', + 'exchange', + 'excite', + 'exclude', + 'excuse', + 'execute', + 'exercise', + 'exhaust', + 'exhibit', + 'exile', + 'exist', + 'exit', + 'exotic', + 'expand', + 'expect', + 'expire', + 'explain', + 'expose', + 'express', + 'extend', + 'extra', + 'eye', + 'eyebrow', + 'fabric', + 'face', + 'faculty', + 'fade', + 'faint', + 'faith', + 'fall', + 'false', + 'fame', + 'family', + 'famous', + 'fan', + 'fancy', + 'fantasy', + 'farm', + 'fashion', + 'fat', + 'fatal', + 'father', + 'fatigue', + 'fault', + 'favorite', + 'feature', + 'february', + 'federal', + 'fee', + 'feed', + 'feel', + 'female', + 'fence', + 'festival', + 'fetch', + 'fever', + 'few', + 'fiber', + 'fiction', + 'field', + 'figure', + 'file', + 'film', + 'filter', + 'final', + 'find', + 'fine', + 'finger', + 'finish', + 'fire', + 'firm', + 'first', + 'fiscal', + 'fish', + 'fit', + 'fitness', + 'fix', + 'flag', + 'flame', + 'flash', + 'flat', + 'flavor', + 'flee', + 'flight', + 'flip', + 'float', + 'flock', + 'floor', + 'flower', + 'fluid', + 'flush', + 'fly', + 'foam', + 'focus', + 'fog', + 'foil', + 'fold', + 'follow', + 'food', + 'foot', + 'force', + 'forest', + 'forget', + 'fork', + 'fortune', + 'forum', + 'forward', + 'fossil', + 'foster', + 'found', + 'fox', + 'fragile', + 'frame', + 'frequent', + 'fresh', + 'friend', + 'fringe', + 'frog', + 'front', + 'frost', + 'frown', + 'frozen', + 'fruit', + 'fuel', + 'fun', + 'funny', + 'furnace', + 'fury', + 'future', + 'gadget', + 'gain', + 'galaxy', + 'gallery', + 'game', + 'gap', + 'garage', + 'garbage', + 'garden', + 'garlic', + 'garment', + 'gas', + 'gasp', + 'gate', + 'gather', + 'gauge', + 'gaze', + 'general', + 'genius', + 'genre', + 'gentle', + 'genuine', + 'gesture', + 'ghost', + 'giant', + 'gift', + 'giggle', + 'ginger', + 'giraffe', + 'girl', + 'give', + 'glad', + 'glance', + 'glare', + 'glass', + 'glide', + 'glimpse', + 'globe', + 'gloom', + 'glory', + 'glove', + 'glow', + 'glue', + 'goat', + 'goddess', + 'gold', + 'good', + 'goose', + 'gorilla', + 'gospel', + 'gossip', + 'govern', + 'gown', + 'grab', + 'grace', + 'grain', + 'grant', + 'grape', + 'grass', + 'gravity', + 'great', + 'green', + 'grid', + 'grief', + 'grit', + 'grocery', + 'group', + 'grow', + 'grunt', + 'guard', + 'guess', + 'guide', + 'guilt', + 'guitar', + 'gun', + 'gym', + 'habit', + 'hair', + 'half', + 'hammer', + 'hamster', + 'hand', + 'happy', + 'harbor', + 'hard', + 'harsh', + 'harvest', + 'hat', + 'have', + 'hawk', + 'hazard', + 'head', + 'health', + 'heart', + 'heavy', + 'hedgehog', + 'height', + 'hello', + 'helmet', + 'help', + 'hen', + 'hero', + 'hidden', + 'high', + 'hill', + 'hint', + 'hip', + 'hire', + 'history', + 'hobby', + 'hockey', + 'hold', + 'hole', + 'holiday', + 'hollow', + 'home', + 'honey', + 'hood', + 'hope', + 'horn', + 'horror', + 'horse', + 'hospital', + 'host', + 'hotel', + 'hour', + 'hover', + 'hub', + 'huge', + 'human', + 'humble', + 'humor', + 'hundred', + 'hungry', + 'hunt', + 'hurdle', + 'hurry', + 'hurt', + 'husband', + 'hybrid', + 'ice', + 'icon', + 'idea', + 'identify', + 'idle', + 'ignore', + 'ill', + 'illegal', + 'illness', + 'image', + 'imitate', + 'immense', + 'immune', + 'impact', + 'impose', + 'improve', + 'impulse', + 'inch', + 'include', + 'income', + 'increase', + 'index', + 'indicate', + 'indoor', + 'industry', + 'infant', + 'inflict', + 'inform', + 'inhale', + 'inherit', + 'initial', + 'inject', + 'injury', + 'inmate', + 'inner', + 'innocent', + 'input', + 'inquiry', + 'insane', + 'insect', + 'inside', + 'inspire', + 'install', + 'intact', + 'interest', + 'into', + 'invest', + 'invite', + 'involve', + 'iron', + 'island', + 'isolate', + 'issue', + 'item', + 'ivory', + 'jacket', + 'jaguar', + 'jar', + 'jazz', + 'jealous', + 'jeans', + 'jelly', + 'jewel', + 'job', + 'join', + 'joke', + 'journey', + 'joy', + 'judge', + 'juice', + 'jump', + 'jungle', + 'junior', + 'junk', + 'just', + 'kangaroo', + 'keen', + 'keep', + 'ketchup', + 'key', + 'kick', + 'kid', + 'kidney', + 'kind', + 'kingdom', + 'kiss', + 'kit', + 'kitchen', + 'kite', + 'kitten', + 'kiwi', + 'knee', + 'knife', + 'knock', + 'know', + 'lab', + 'label', + 'labor', + 'ladder', + 'lady', + 'lake', + 'lamp', + 'language', + 'laptop', + 'large', + 'later', + 'latin', + 'laugh', + 'laundry', + 'lava', + 'law', + 'lawn', + 'lawsuit', + 'layer', + 'lazy', + 'leader', + 'leaf', + 'learn', + 'leave', + 'lecture', + 'left', + 'leg', + 'legal', + 'legend', + 'leisure', + 'lemon', + 'lend', + 'length', + 'lens', + 'leopard', + 'lesson', + 'letter', + 'level', + 'liar', + 'liberty', + 'library', + 'license', + 'life', + 'lift', + 'light', + 'like', + 'limb', + 'limit', + 'link', + 'lion', + 'liquid', + 'list', + 'little', + 'live', + 'lizard', + 'load', + 'loan', + 'lobster', + 'local', + 'lock', + 'logic', + 'lonely', + 'long', + 'loop', + 'lottery', + 'loud', + 'lounge', + 'love', + 'loyal', + 'lucky', + 'luggage', + 'lumber', + 'lunar', + 'lunch', + 'luxury', + 'lyrics', + 'machine', + 'mad', + 'magic', + 'magnet', + 'maid', + 'mail', + 'main', + 'major', + 'make', + 'mammal', + 'man', + 'manage', + 'mandate', + 'mango', + 'mansion', + 'manual', + 'maple', + 'marble', + 'march', + 'margin', + 'marine', + 'market', + 'marriage', + 'mask', + 'mass', + 'master', + 'match', + 'material', + 'math', + 'matrix', + 'matter', + 'maximum', + 'maze', + 'meadow', + 'mean', + 'measure', + 'meat', + 'mechanic', + 'medal', + 'media', + 'melody', + 'melt', + 'member', + 'memory', + 'mention', + 'menu', + 'mercy', + 'merge', + 'merit', + 'merry', + 'mesh', + 'message', + 'metal', + 'method', + 'middle', + 'midnight', + 'milk', + 'million', + 'mimic', + 'mind', + 'minimum', + 'minor', + 'minute', + 'miracle', + 'mirror', + 'misery', + 'miss', + 'mistake', + 'mix', + 'mixed', + 'mixture', + 'mobile', + 'model', + 'modify', + 'mom', + 'moment', + 'monitor', + 'monkey', + 'monster', + 'month', + 'moon', + 'moral', + 'more', + 'morning', + 'mosquito', + 'mother', + 'motion', + 'motor', + 'mountain', + 'mouse', + 'move', + 'movie', + 'much', + 'muffin', + 'mule', + 'multiply', + 'muscle', + 'museum', + 'mushroom', + 'music', + 'must', + 'mutual', + 'myself', + 'mystery', + 'myth', + 'naive', + 'name', + 'napkin', + 'narrow', + 'nasty', + 'nation', + 'nature', + 'near', + 'neck', + 'need', + 'negative', + 'neglect', + 'neither', + 'nephew', + 'nerve', + 'nest', + 'net', + 'network', + 'neutral', + 'never', + 'news', + 'next', + 'nice', + 'night', + 'noble', + 'noise', + 'nominee', + 'noodle', + 'normal', + 'north', + 'nose', + 'notable', + 'note', + 'nothing', + 'notice', + 'novel', + 'now', + 'nuclear', + 'number', + 'nurse', + 'nut', + 'oak', + 'obey', + 'object', + 'oblige', + 'obscure', + 'observe', + 'obtain', + 'obvious', + 'occur', + 'ocean', + 'october', + 'odor', + 'off', + 'offer', + 'office', + 'often', + 'oil', + 'okay', + 'old', + 'olive', + 'olympic', + 'omit', + 'once', + 'one', + 'onion', + 'online', + 'only', + 'open', + 'opera', + 'opinion', + 'oppose', + 'option', + 'orange', + 'orbit', + 'orchard', + 'order', + 'ordinary', + 'organ', + 'orient', + 'original', + 'orphan', + 'ostrich', + 'other', + 'outdoor', + 'outer', + 'output', + 'outside', + 'oval', + 'oven', + 'over', + 'own', + 'owner', + 'oxygen', + 'oyster', + 'ozone', + 'pact', + 'paddle', + 'page', + 'pair', + 'palace', + 'palm', + 'panda', + 'panel', + 'panic', + 'panther', + 'paper', + 'parade', + 'parent', + 'park', + 'parrot', + 'party', + 'pass', + 'patch', + 'path', + 'patient', + 'patrol', + 'pattern', + 'pause', + 'pave', + 'payment', + 'peace', + 'peanut', + 'pear', + 'peasant', + 'pelican', + 'pen', + 'penalty', + 'pencil', + 'people', + 'pepper', + 'perfect', + 'permit', + 'person', + 'pet', + 'phone', + 'photo', + 'phrase', + 'physical', + 'piano', + 'picnic', + 'picture', + 'piece', + 'pig', + 'pigeon', + 'pill', + 'pilot', + 'pink', + 'pioneer', + 'pipe', + 'pistol', + 'pitch', + 'pizza', + 'place', + 'planet', + 'plastic', + 'plate', + 'play', + 'please', + 'pledge', + 'pluck', + 'plug', + 'plunge', + 'poem', + 'poet', + 'point', + 'polar', + 'pole', + 'police', + 'pond', + 'pony', + 'pool', + 'popular', + 'portion', + 'position', + 'possible', + 'post', + 'potato', + 'pottery', + 'poverty', + 'powder', + 'power', + 'practice', + 'praise', + 'predict', + 'prefer', + 'prepare', + 'present', + 'pretty', + 'prevent', + 'price', + 'pride', + 'primary', + 'print', + 'priority', + 'prison', + 'private', + 'prize', + 'problem', + 'process', + 'produce', + 'profit', + 'program', + 'project', + 'promote', + 'proof', + 'property', + 'prosper', + 'protect', + 'proud', + 'provide', + 'public', + 'pudding', + 'pull', + 'pulp', + 'pulse', + 'pumpkin', + 'punch', + 'pupil', + 'puppy', + 'purchase', + 'purity', + 'purpose', + 'purse', + 'push', + 'put', + 'puzzle', + 'pyramid', + 'quality', + 'quantum', + 'quarter', + 'question', + 'quick', + 'quit', + 'quiz', + 'quote', + 'rabbit', + 'raccoon', + 'race', + 'rack', + 'radar', + 'radio', + 'rail', + 'rain', + 'raise', + 'rally', + 'ramp', + 'ranch', + 'random', + 'range', + 'rapid', + 'rare', + 'rate', + 'rather', + 'raven', + 'raw', + 'razor', + 'ready', + 'real', + 'reason', + 'rebel', + 'rebuild', + 'recall', + 'receive', + 'recipe', + 'record', + 'recycle', + 'reduce', + 'reflect', + 'reform', + 'refuse', + 'region', + 'regret', + 'regular', + 'reject', + 'relax', + 'release', + 'relief', + 'rely', + 'remain', + 'remember', + 'remind', + 'remove', + 'render', + 'renew', + 'rent', + 'reopen', + 'repair', + 'repeat', + 'replace', + 'report', + 'require', + 'rescue', + 'resemble', + 'resist', + 'resource', + 'response', + 'result', + 'retire', + 'retreat', + 'return', + 'reunion', + 'reveal', + 'review', + 'reward', + 'rhythm', + 'rib', + 'ribbon', + 'rice', + 'rich', + 'ride', + 'ridge', + 'rifle', + 'right', + 'rigid', + 'ring', + 'riot', + 'ripple', + 'risk', + 'ritual', + 'rival', + 'river', + 'road', + 'roast', + 'robot', + 'robust', + 'rocket', + 'romance', + 'roof', + 'rookie', + 'room', + 'rose', + 'rotate', + 'rough', + 'round', + 'route', + 'royal', + 'rubber', + 'rude', + 'rug', + 'rule', + 'run', + 'runway', + 'rural', + 'sad', + 'saddle', + 'sadness', + 'safe', + 'sail', + 'salad', + 'salmon', + 'salon', + 'salt', + 'salute', + 'same', + 'sample', + 'sand', + 'satisfy', + 'satoshi', + 'sauce', + 'sausage', + 'save', + 'say', + 'scale', + 'scan', + 'scare', + 'scatter', + 'scene', + 'scheme', + 'school', + 'science', + 'scissors', + 'scorpion', + 'scout', + 'scrap', + 'screen', + 'script', + 'scrub', + 'sea', + 'search', + 'season', + 'seat', + 'second', + 'secret', + 'section', + 'security', + 'seed', + 'seek', + 'segment', + 'select', + 'sell', + 'seminar', + 'senior', + 'sense', + 'sentence', + 'series', + 'service', + 'session', + 'settle', + 'setup', + 'seven', + 'shadow', + 'shaft', + 'shallow', + 'share', + 'shed', + 'shell', + 'sheriff', + 'shield', + 'shift', + 'shine', + 'ship', + 'shiver', + 'shock', + 'shoe', + 'shoot', + 'shop', + 'short', + 'shoulder', + 'shove', + 'shrimp', + 'shrug', + 'shuffle', + 'shy', + 'sibling', + 'sick', + 'side', + 'siege', + 'sight', + 'sign', + 'silent', + 'silk', + 'silly', + 'silver', + 'similar', + 'simple', + 'since', + 'sing', + 'siren', + 'sister', + 'situate', + 'six', + 'size', + 'skate', + 'sketch', + 'ski', + 'skill', + 'skin', + 'skirt', + 'skull', + 'slab', + 'slam', + 'sleep', + 'slender', + 'slice', + 'slide', + 'slight', + 'slim', + 'slogan', + 'slot', + 'slow', + 'slush', + 'small', + 'smart', + 'smile', + 'smoke', + 'smooth', + 'snack', + 'snake', + 'snap', + 'sniff', + 'snow', + 'soap', + 'soccer', + 'social', + 'sock', + 'soda', + 'soft', + 'solar', + 'soldier', + 'solid', + 'solution', + 'solve', + 'someone', + 'song', + 'soon', + 'sorry', + 'sort', + 'soul', + 'sound', + 'soup', + 'source', + 'south', + 'space', + 'spare', + 'spatial', + 'spawn', + 'speak', + 'special', + 'speed', + 'spell', + 'spend', + 'sphere', + 'spice', + 'spider', + 'spike', + 'spin', + 'spirit', + 'split', + 'spoil', + 'sponsor', + 'spoon', + 'sport', + 'spot', + 'spray', + 'spread', + 'spring', + 'spy', + 'square', + 'squeeze', + 'squirrel', + 'stable', + 'stadium', + 'staff', + 'stage', + 'stairs', + 'stamp', + 'stand', + 'start', + 'state', + 'stay', + 'steak', + 'steel', + 'stem', + 'step', + 'stereo', + 'stick', + 'still', + 'sting', + 'stock', + 'stomach', + 'stone', + 'stool', + 'story', + 'stove', + 'strategy', + 'street', + 'strike', + 'strong', + 'struggle', + 'student', + 'stuff', + 'stumble', + 'style', + 'subject', + 'submit', + 'subway', + 'success', + 'such', + 'sudden', + 'suffer', + 'sugar', + 'suggest', + 'suit', + 'summer', + 'sun', + 'sunny', + 'sunset', + 'super', + 'supply', + 'supreme', + 'sure', + 'surface', + 'surge', + 'surprise', + 'surround', + 'survey', + 'suspect', + 'sustain', + 'swallow', + 'swamp', + 'swap', + 'swarm', + 'swear', + 'sweet', + 'swift', + 'swim', + 'swing', + 'switch', + 'sword', + 'symbol', + 'symptom', + 'syrup', + 'system', + 'table', + 'tackle', + 'tag', + 'tail', + 'talent', + 'talk', + 'tank', + 'tape', + 'target', + 'task', + 'taste', + 'tattoo', + 'taxi', + 'teach', + 'team', + 'tell', + 'ten', + 'tenant', + 'tennis', + 'tent', + 'term', + 'test', + 'text', + 'thank', + 'that', + 'theme', + 'then', + 'theory', + 'there', + 'they', + 'thing', + 'this', + 'thought', + 'three', + 'thrive', + 'throw', + 'thumb', + 'thunder', + 'ticket', + 'tide', + 'tiger', + 'tilt', + 'timber', + 'time', + 'tiny', + 'tip', + 'tired', + 'tissue', + 'title', + 'toast', + 'tobacco', + 'today', + 'toddler', + 'toe', + 'together', + 'toilet', + 'token', + 'tomato', + 'tomorrow', + 'tone', + 'tongue', + 'tonight', + 'tool', + 'tooth', + 'top', + 'topic', + 'topple', + 'torch', + 'tornado', + 'tortoise', + 'toss', + 'total', + 'tourist', + 'toward', + 'tower', + 'town', + 'toy', + 'track', + 'trade', + 'traffic', + 'tragic', + 'train', + 'transfer', + 'trap', + 'trash', + 'travel', + 'tray', + 'treat', + 'tree', + 'trend', + 'trial', + 'tribe', + 'trick', + 'trigger', + 'trim', + 'trip', + 'trophy', + 'trouble', + 'truck', + 'true', + 'truly', + 'trumpet', + 'trust', + 'truth', + 'try', + 'tube', + 'tuition', + 'tumble', + 'tuna', + 'tunnel', + 'turkey', + 'turn', + 'turtle', + 'twelve', + 'twenty', + 'twice', + 'twin', + 'twist', + 'two', + 'type', + 'typical', + 'ugly', + 'umbrella', + 'unable', + 'unaware', + 'uncle', + 'uncover', + 'under', + 'undo', + 'unfair', + 'unfold', + 'unhappy', + 'uniform', + 'unique', + 'unit', + 'universe', + 'unknown', + 'unlock', + 'until', + 'unusual', + 'unveil', + 'update', + 'upgrade', + 'uphold', + 'upon', + 'upper', + 'upset', + 'urban', + 'urge', + 'usage', + 'use', + 'used', + 'useful', + 'useless', + 'usual', + 'utility', + 'vacant', + 'vacuum', + 'vague', + 'valid', + 'valley', + 'valve', + 'van', + 'vanish', + 'vapor', + 'various', + 'vast', + 'vault', + 'vehicle', + 'velvet', + 'vendor', + 'venture', + 'venue', + 'verb', + 'verify', + 'version', + 'very', + 'vessel', + 'veteran', + 'viable', + 'vibrant', + 'vicious', + 'victory', + 'video', + 'view', + 'village', + 'vintage', + 'violin', + 'virtual', + 'virus', + 'visa', + 'visit', + 'visual', + 'vital', + 'vivid', + 'vocal', + 'voice', + 'void', + 'volcano', + 'volume', + 'vote', + 'voyage', + 'wage', + 'wagon', + 'wait', + 'walk', + 'wall', + 'walnut', + 'want', + 'warfare', + 'warm', + 'warrior', + 'wash', + 'wasp', + 'waste', + 'water', + 'wave', + 'way', + 'wealth', + 'weapon', + 'wear', + 'weasel', + 'weather', + 'web', + 'wedding', + 'weekend', + 'weird', + 'welcome', + 'west', + 'wet', + 'whale', + 'what', + 'wheat', + 'wheel', + 'when', + 'where', + 'whip', + 'whisper', + 'wide', + 'width', + 'wife', + 'wild', + 'will', + 'win', + 'window', + 'wine', + 'wing', + 'wink', + 'winner', + 'winter', + 'wire', + 'wisdom', + 'wise', + 'wish', + 'witness', + 'wolf', + 'woman', + 'wonder', + 'wood', + 'wool', + 'word', + 'work', + 'world', + 'worry', + 'worth', + 'wrap', + 'wreck', + 'wrestle', + 'wrist', + 'write', + 'wrong', + 'yard', + 'year', + 'yellow', + 'you', + 'young', + 'youth', + 'zebra', + 'zero', + 'zone', + 'zoo' + ]; +} diff --git a/cw_solana/lib/solana_transaction_credentials.dart b/cw_solana/lib/solana_transaction_credentials.dart new file mode 100644 index 000000000..bd0c97f0b --- /dev/null +++ b/cw_solana/lib/solana_transaction_credentials.dart @@ -0,0 +1,12 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/output_info.dart'; + +class SolanaTransactionCredentials { + SolanaTransactionCredentials( + this.outputs, { + required this.currency, + }); + + final List outputs; + final CryptoCurrency currency; +} diff --git a/cw_solana/lib/solana_transaction_history.dart b/cw_solana/lib/solana_transaction_history.dart new file mode 100644 index 000000000..c03de19ad --- /dev/null +++ b/cw_solana/lib/solana_transaction_history.dart @@ -0,0 +1,78 @@ +import 'dart:convert'; +import 'dart:core'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_solana/file.dart'; +import 'package:cw_solana/solana_transaction_info.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cw_core/transaction_history.dart'; + +part 'solana_transaction_history.g.dart'; + +const transactionsHistoryFileName = 'solana_transactions.json'; + +class SolanaTransactionHistory = SolanaTransactionHistoryBase with _$SolanaTransactionHistory; + +abstract class SolanaTransactionHistoryBase extends TransactionHistoryBase + with Store { + SolanaTransactionHistoryBase({required this.walletInfo, required String password}) + : _password = password { + transactions = ObservableMap(); + } + + final WalletInfo walletInfo; + String _password; + + Future init() async => await _load(); + + @override + Future save() async { + try { + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + final path = '$dirPath/$transactionsHistoryFileName'; + 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) { + print('Error while saving solana transaction history: ${e.toString()}'); + print(s); + } + } + + @override + void addOne(SolanaTransactionInfo transaction) => transactions[transaction.id] = transaction; + + @override + void addMany(Map transactions) => + this.transactions.addAll(transactions); + + Future> _read() async { + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + final path = '$dirPath/$transactionsHistoryFileName'; + final content = await read(path: path, password: _password); + if (content.isEmpty) { + return {}; + } + return json.decode(content) as Map; + } + + Future _load() async { + try { + final content = await _read(); + final txs = content['transactions'] as Map? ?? {}; + + txs.entries.forEach((entry) { + final val = entry.value; + + if (val is Map) { + final tx = SolanaTransactionInfo.fromJson(val); + _update(tx); + } + }); + } catch (e) { + print(e); + } + } + + void _update(SolanaTransactionInfo transaction) => transactions[transaction.id] = transaction; +} diff --git a/cw_solana/lib/solana_transaction_info.dart b/cw_solana/lib/solana_transaction_info.dart new file mode 100644 index 000000000..1b7610e34 --- /dev/null +++ b/cw_solana/lib/solana_transaction_info.dart @@ -0,0 +1,78 @@ +import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_info.dart'; + +class SolanaTransactionInfo extends TransactionInfo { + SolanaTransactionInfo({ + required this.id, + required this.blockTime, + required this.to, + required this.from, + required this.direction, + required this.solAmount, + this.tokenSymbol = "SOL", + required this.isPending, + required this.txFee, + }) : amount = solAmount.toInt(); + + final String id; + final String? to; + final String? from; + final int amount; + final bool isPending; + final double solAmount; + final String tokenSymbol; + final DateTime blockTime; + final double txFee; + final TransactionDirection direction; + + String? _fiatAmount; + + @override + DateTime get date => blockTime; + + @override + String amountFormatted() { + String stringBalance = solAmount.toString(); + + if (stringBalance.toString().length >= 6) { + stringBalance = stringBalance.substring(0, 6); + } + return '$stringBalance $tokenSymbol'; + } + + @override + String fiatAmount() => _fiatAmount ?? ''; + + @override + void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); + + @override + String feeFormatted() => '${txFee.toString()} SOL'; + + factory SolanaTransactionInfo.fromJson(Map data) { + return SolanaTransactionInfo( + id: data['id'] as String, + solAmount: data['solAmount'], + direction: parseTransactionDirectionFromInt(data['direction'] as int), + blockTime: DateTime.fromMillisecondsSinceEpoch(data['blockTime'] as int), + isPending: data['isPending'] as bool, + tokenSymbol: data['tokenSymbol'] as String, + to: data['to'], + from: data['from'], + txFee: data['txFee'], + ); + } + + Map toJson() => { + 'id': id, + 'solAmount': solAmount, + 'direction': direction.index, + 'blockTime': blockTime.millisecondsSinceEpoch, + 'isPending': isPending, + 'tokenSymbol': tokenSymbol, + 'to': to, + 'from': from, + 'txFee': txFee, + }; +} diff --git a/cw_solana/lib/solana_transaction_model.dart b/cw_solana/lib/solana_transaction_model.dart new file mode 100644 index 000000000..c16c49258 --- /dev/null +++ b/cw_solana/lib/solana_transaction_model.dart @@ -0,0 +1,47 @@ +class SolanaTransactionModel { + final String id; + + final String from; + + final String to; + + final double amount; + + // If this is an outgoing transaction + final bool isOutgoingTx; + + // The Program ID of this transaction, e.g, System Program, Token Program... + final String programId; + + // The DateTime from the UNIX timestamp of the block where the transaction was included + final DateTime blockTime; + + // The Transaction fee + final double fee; + + // The token symbol + final String tokenSymbol; + + SolanaTransactionModel({ + required this.id, + required this.to, + required this.from, + required this.amount, + required this.programId, + required int blockTimeInInt, + this.isOutgoingTx = false, + required this.tokenSymbol, + required this.fee, + }) : blockTime = DateTime.fromMillisecondsSinceEpoch(blockTimeInInt * 1000); + + factory SolanaTransactionModel.fromJson(Map json) => SolanaTransactionModel( + id: json['id'], + blockTimeInInt: int.parse(json["timeStamp"]) * 1000, + from: json["from"], + to: json["to"], + amount: double.parse(json["value"]), + programId: json["programId"], + fee: json['fee'], + tokenSymbol: json['tokenSymbol'], + ); +} diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart new file mode 100644 index 000000000..f901a64be --- /dev/null +++ b/cw_solana/lib/solana_wallet.dart @@ -0,0 +1,510 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; +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_solana/default_spl_tokens.dart'; +import 'package:cw_solana/file.dart'; +import 'package:cw_solana/solana_balance.dart'; +import 'package:cw_solana/solana_client.dart'; +import 'package:cw_solana/solana_exceptions.dart'; +import 'package:cw_solana/solana_transaction_credentials.dart'; +import 'package:cw_solana/solana_transaction_history.dart'; +import 'package:cw_solana/solana_transaction_info.dart'; +import 'package:cw_solana/solana_transaction_model.dart'; +import 'package:cw_solana/solana_wallet_addresses.dart'; +import 'package:cw_solana/spl_token.dart'; +import 'package:hex/hex.dart'; +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:solana/metaplex.dart' as metaplex; +import 'package:solana/solana.dart'; +import 'package:web3dart/crypto.dart'; + +part 'solana_wallet.g.dart'; + +class SolanaWallet = SolanaWalletBase with _$SolanaWallet; + +abstract class SolanaWalletBase + extends WalletBase with Store { + SolanaWalletBase({ + required WalletInfo walletInfo, + String? mnemonic, + String? privateKey, + required String password, + SolanaBalance? initialBalance, + }) : syncStatus = const NotConnectedSyncStatus(), + _password = password, + _mnemonic = mnemonic, + _hexPrivateKey = privateKey, + _client = SolanaWalletClient(), + walletAddresses = SolanaWalletAddresses(walletInfo), + balance = ObservableMap.of( + {CryptoCurrency.sol: initialBalance ?? SolanaBalance(BigInt.zero.toDouble())}), + super(walletInfo) { + this.walletInfo = walletInfo; + transactionHistory = SolanaTransactionHistory(walletInfo: walletInfo, password: password); + + if (!CakeHive.isAdapterRegistered(SPLToken.typeId)) { + CakeHive.registerAdapter(SPLTokenAdapter()); + } + + _sharedPrefs.complete(SharedPreferences.getInstance()); + } + + final String _password; + final String? _mnemonic; + final String? _hexPrivateKey; + + // The Solana WalletPair + Ed25519HDKeyPair? _walletKeyPair; + + Ed25519HDKeyPair? get walletKeyPair => _walletKeyPair; + + // To access the privateKey bytes. + Ed25519HDKeyPairData? _keyPairData; + + late SolanaWalletClient _client; + + Timer? _transactionsUpdateTimer; + + late final Box splTokensBox; + + @override + WalletAddresses walletAddresses; + + @override + @observable + SyncStatus syncStatus; + + @override + @observable + late ObservableMap balance; + + Completer _sharedPrefs = Completer(); + + @override + Ed25519HDKeyPairData get keys { + if (_keyPairData == null) { + return Ed25519HDKeyPairData([], publicKey: const Ed25519HDPublicKey([])); + } + + return _keyPairData!; + } + + @override + String? get seed => _mnemonic; + + @override + String get privateKey => HEX.encode(_keyPairData!.bytes); + + Future init() async { + final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${SPLToken.boxName}"; + + splTokensBox = await CakeHive.openBox(boxName); + + // Create WalletPair using either the mnemonic or the privateKey + _walletKeyPair = await getWalletPair( + mnemonic: _mnemonic, + privateKey: _hexPrivateKey, + ); + + // Extract the keyPairData containing both the privateKey bytes and the publicKey hex. + _keyPairData = await _walletKeyPair!.extract(); + + walletInfo.address = _walletKeyPair!.address; + + await walletAddresses.init(); + await transactionHistory.init(); + await save(); + } + + Future getWalletPair({String? mnemonic, String? privateKey}) async { + assert(mnemonic != null || privateKey != null); + + if (privateKey != null) { + final privateKeyBytes = hexToBytes(privateKey); + return await Wallet.fromPrivateKeyBytes(privateKey: privateKeyBytes); + } + + return Wallet.fromMnemonic(mnemonic!, account: 0, change: 0); + } + + @override + int calculateEstimatedFee(TransactionPriority priority, int? amount) => 0; + + @override + Future changePassword(String password) => throw UnimplementedError("changePassword"); + + @override + void close() { + _client.stop(); + _transactionsUpdateTimer?.cancel(); + } + + @action + @override + Future connectToNode({required Node node}) async { + try { + syncStatus = ConnectingSyncStatus(); + + final isConnected = _client.connect(node); + + if (!isConnected) { + throw Exception("Solana Node connection failed"); + } + + try { + await Future.wait([ + _updateBalance(), + _updateNativeSOLTransactions(), + _updateSPLTokenTransactions(), + ]); + } catch (e) { + log(e.toString()); + } + + _setTransactionUpdateTimer(); + + syncStatus = ConnectedSyncStatus(); + } catch (e) { + syncStatus = FailedSyncStatus(); + } + } + + @override + Future createTransaction(Object credentials) async { + final solCredentials = credentials as SolanaTransactionCredentials; + + final outputs = solCredentials.outputs; + + final hasMultiDestination = outputs.length > 1; + + await _updateBalance(); + + final CryptoCurrency transactionCurrency = + balance.keys.firstWhere((element) => element.title == solCredentials.currency.title); + + final walletBalanceForCurrency = balance[transactionCurrency]!.balance; + + double totalAmount = 0.0; + + if (hasMultiDestination) { + if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { + throw SolanaTransactionWrongBalanceException(transactionCurrency); + } + + final totalAmountFromCredentials = + outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); + + totalAmount = totalAmountFromCredentials.toDouble(); + + if (walletBalanceForCurrency < totalAmount) { + throw SolanaTransactionWrongBalanceException(transactionCurrency); + } + } else { + final output = outputs.first; + + final totalOriginalAmount = double.parse(output.cryptoAmount ?? '0.0'); + + totalAmount = output.sendAll ? walletBalanceForCurrency : totalOriginalAmount; + + if (walletBalanceForCurrency < totalAmount) { + throw SolanaTransactionWrongBalanceException(transactionCurrency); + } + } + + String? tokenMint; + // Token Mint is only needed for transactions that are not native tokens(non-SOL transactions) + if (transactionCurrency.title != CryptoCurrency.sol.title) { + tokenMint = (transactionCurrency as SPLToken).mintAddress; + } + + final pendingSolanaTransaction = await _client.signSolanaTransaction( + tokenMint: tokenMint, + tokenTitle: transactionCurrency.title, + inputAmount: totalAmount, + ownerKeypair: _walletKeyPair!, + tokenDecimals: transactionCurrency.decimals, + destinationAddress: solCredentials.outputs.first.isParsedAddress + ? solCredentials.outputs.first.extractedAddress! + : solCredentials.outputs.first.address, + ); + + return pendingSolanaTransaction; + } + + @override + Future> fetchTransactions() async => {}; + + /// Fetches the native SOL transactions linked to the wallet Public Key + Future _updateNativeSOLTransactions() async { + final address = Ed25519HDPublicKey.fromBase58(_walletKeyPair!.address); + + final transactions = await _client.fetchTransactions(address); + + final Map result = {}; + + for (var transactionModel in transactions) { + result[transactionModel.id] = SolanaTransactionInfo( + id: transactionModel.id, + to: transactionModel.to, + from: transactionModel.from, + blockTime: transactionModel.blockTime, + direction: transactionModel.isOutgoingTx + ? TransactionDirection.outgoing + : TransactionDirection.incoming, + solAmount: transactionModel.amount, + isPending: false, + txFee: transactionModel.fee, + tokenSymbol: transactionModel.tokenSymbol, + ); + } + + transactionHistory.addMany(result); + + await transactionHistory.save(); + } + + /// Fetches the SPL Tokens transactions linked to the token account Public Key + Future _updateSPLTokenTransactions() async { + List splTokenTransactions = []; + + for (var token in balance.keys) { + if (token is SPLToken) { + final tokenTxs = await _client.getSPLTokenTransfers( + token.mintAddress, + token.symbol, + token.decimal, + _walletKeyPair!, + ); + + splTokenTransactions.addAll(tokenTxs); + } + } + + final Map result = {}; + + for (var transactionModel in splTokenTransactions) { + result[transactionModel.id] = SolanaTransactionInfo( + id: transactionModel.id, + to: transactionModel.to, + from: transactionModel.from, + blockTime: transactionModel.blockTime, + direction: transactionModel.isOutgoingTx + ? TransactionDirection.outgoing + : TransactionDirection.incoming, + solAmount: transactionModel.amount, + isPending: false, + txFee: transactionModel.fee, + tokenSymbol: transactionModel.tokenSymbol, + ); + } + + transactionHistory.addMany(result); + + await transactionHistory.save(); + } + + @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(); + } + + @action + @override + Future startSync() async { + try { + syncStatus = AttemptingSyncStatus(); + + await Future.wait([ + _updateBalance(), + _updateNativeSOLTransactions(), + _updateSPLTokenTransactions(), + ]); + + syncStatus = SyncedSyncStatus(); + } catch (e) { + syncStatus = FailedSyncStatus(); + } + } + + Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); + + String toJSON() => json.encode({ + 'mnemonic': _mnemonic, + 'private_key': privateKey, + 'balance': balance[currency]!.toJSON(), + }); + + 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 = SolanaBalance.fromJSON(data['balance'] as String) ?? SolanaBalance(0.0); + + return SolanaWallet( + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + privateKey: privateKey, + initialBalance: balance, + ); + } + + Future _updateBalance() async { + balance[currency] = await _fetchSOLBalance(); + await _fetchSPLTokensBalances(); + await save(); + } + + Future _fetchSOLBalance() async { + final balance = await _client.getBalance(_walletKeyPair!.address); + + return SolanaBalance(balance); + } + + Future _fetchSPLTokensBalances() async { + for (var token in splTokensBox.values) { + if (token.enabled) { + try { + final tokenBalance = + await _client.getSplTokenBalance(token.mintAddress, _walletKeyPair!.address) ?? + balance[token] ?? + SolanaBalance(0.0); + balance[token] = tokenBalance; + } catch (e) { + print('Error fetching spl token (${token.symbol}) balance ${e.toString()}'); + } + } else { + balance.remove(token); + } + } + } + + @override + Future? updateBalance() async => await _updateBalance(); + + List get splTokenCurrencies => splTokensBox.values.toList(); + + void addInitialTokens() { + final initialSPLTokens = DefaultSPLTokens().initialSPLTokens; + + for (var token in initialSPLTokens) { + splTokensBox.put(token.mintAddress, token); + } + } + + Future addSPLToken(SPLToken token) async { + await splTokensBox.put(token.mintAddress, token); + + if (token.enabled) { + final tokenBalance = + await _client.getSplTokenBalance(token.mintAddress, _walletKeyPair!.address) ?? + balance[token] ?? + SolanaBalance(0.0); + + balance[token] = tokenBalance; + } else { + balance.remove(token); + } + } + + Future deleteSPLToken(SPLToken token) async { + await token.delete(); + + balance.remove(token); + _updateBalance(); + } + + Future getSPLToken(String mintAddress) async { + // Convert SPL token mint address to public key + final mintPublicKey = Ed25519HDPublicKey.fromBase58(mintAddress); + + // Fetch token's metadata account + final token = await solanaClient!.rpcClient.getMetadata(mint: mintPublicKey); + + if (token == null) { + return null; + } + + return SPLToken.fromMetadata( + name: token.name, + mint: token.mint, + symbol: token.symbol, + mintAddress: mintAddress, + ); + } + + @override + Future renameWalletFiles(String newWalletName) async { + final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type); + final currentWalletFile = File(currentWalletPath); + + final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type); + final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName'); + + // Copies current wallet files into new wallet name's dir and files + if (currentWalletFile.existsSync()) { + final newWalletPath = await pathForWallet(name: newWalletName, type: type); + await currentWalletFile.copy(newWalletPath); + } + if (currentTransactionsFile.existsSync()) { + final newDirPath = await pathForWalletDir(name: newWalletName, type: type); + await currentTransactionsFile.copy('$newDirPath/$transactionsHistoryFileName'); + } + + // Delete old name's dir and files + await Directory(currentDirPath).delete(recursive: true); + } + + void _setTransactionUpdateTimer() { + if (_transactionsUpdateTimer?.isActive ?? false) { + _transactionsUpdateTimer!.cancel(); + } + + _transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 20), (_) { + _updateSPLTokenTransactions(); + _updateNativeSOLTransactions(); + _updateBalance(); + }); + } + + Future signSolanaMessage(String message) async { + // Convert the message to bytes + final messageBytes = utf8.encode(message); + + // Sign the message bytes with the wallet's private key + final signature = await _walletKeyPair!.sign(messageBytes); + + // Convert the signature to a hexadecimal string + final hex = bytesToHex(signature.bytes); + + return hex; + } + + SolanaClient? get solanaClient => _client.getSolanaClient; +} diff --git a/cw_solana/lib/solana_wallet_addresses.dart b/cw_solana/lib/solana_wallet_addresses.dart new file mode 100644 index 000000000..97a76fb99 --- /dev/null +++ b/cw_solana/lib/solana_wallet_addresses.dart @@ -0,0 +1,33 @@ +import 'package:cw_core/wallet_addresses.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:mobx/mobx.dart'; + +part 'solana_wallet_addresses.g.dart'; + +class SolanaWalletAddresses = SolanaWalletAddressesBase with _$SolanaWalletAddresses; + +abstract class SolanaWalletAddressesBase extends WalletAddresses with Store { + SolanaWalletAddressesBase(WalletInfo walletInfo) + : address = '', + super(walletInfo); + + @override + String address; + + @override + Future init() async { + address = walletInfo.address; + await updateAddressesInBox(); + } + + @override + Future updateAddressesInBox() async { + try { + addressesMap.clear(); + addressesMap[address] = ''; + await saveAddressesInBox(); + } catch (e) { + print(e.toString()); + } + } +} diff --git a/cw_solana/lib/solana_wallet_creation_credentials.dart b/cw_solana/lib/solana_wallet_creation_credentials.dart new file mode 100644 index 000000000..881c30abd --- /dev/null +++ b/cw_solana/lib/solana_wallet_creation_credentials.dart @@ -0,0 +1,29 @@ +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; + +class SolanaNewWalletCredentials extends WalletCredentials { + SolanaNewWalletCredentials({required String name, WalletInfo? walletInfo}) + : super(name: name, walletInfo: walletInfo); +} + +class SolanaRestoreWalletFromSeedCredentials extends WalletCredentials { + SolanaRestoreWalletFromSeedCredentials( + {required String name, + required String password, + required this.mnemonic, + WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String mnemonic; +} + +class SolanaRestoreWalletFromPrivateKey extends WalletCredentials { + SolanaRestoreWalletFromPrivateKey( + {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_solana/lib/solana_wallet_service.dart b/cw_solana/lib/solana_wallet_service.dart new file mode 100644 index 000000000..b3ff22e7e --- /dev/null +++ b/cw_solana/lib/solana_wallet_service.dart @@ -0,0 +1,120 @@ +import 'dart:io'; + +import 'package:bip39/bip39.dart' as bip39; +import 'package:collection/collection.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_solana/solana_mnemonics.dart'; +import 'package:cw_solana/solana_wallet.dart'; +import 'package:cw_solana/solana_wallet_creation_credentials.dart'; +import 'package:hive/hive.dart'; + +class SolanaWalletService extends WalletService { + SolanaWalletService(this.walletInfoSource); + + final Box walletInfoSource; + + @override + Future create(SolanaNewWalletCredentials credentials, {bool? isTestnet}) async { + final strength = credentials.seedPhraseLength == 24 ? 256 : 128; + + final mnemonic = bip39.generateMnemonic(strength: strength); + + final wallet = SolanaWallet( + walletInfo: credentials.walletInfo!, + mnemonic: mnemonic, + password: credentials.password!, + ); + + await wallet.init(); + wallet.addInitialTokens(); + return wallet; + } + + @override + WalletType getType() => WalletType.solana; + + @override + Future isWalletExit(String name) async => + File(await pathForWallet(name: name, type: getType())).existsSync(); + + @override + Future openWallet(String name, String password) async { + final walletInfo = + walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType())); + final wallet = await SolanaWalletBase.open( + name: name, + password: password, + walletInfo: walletInfo, + ); + + await wallet.init(); + await wallet.save(); + + return wallet; + } + + @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 restoreFromKeys(SolanaRestoreWalletFromPrivateKey credentials, + {bool? isTestnet}) async { + final wallet = SolanaWallet( + password: credentials.password!, + privateKey: credentials.privateKey, + walletInfo: credentials.walletInfo!, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + Future restoreFromSeed(SolanaRestoreWalletFromSeedCredentials credentials, + {bool? isTestnet}) async { + if (!bip39.validateMnemonic(credentials.mnemonic)) { + throw SolanaMnemonicIsIncorrectException(); + } + + final wallet = SolanaWallet( + 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 SolanaWalletBase.open( + password: password, name: currentName, walletInfo: currentWalletInfo); + + await currentWallet.renameWalletFiles(newName); + + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); + } +} diff --git a/cw_solana/lib/spl_token.dart b/cw_solana/lib/spl_token.dart new file mode 100644 index 000000000..0413990b1 --- /dev/null +++ b/cw_solana/lib/spl_token.dart @@ -0,0 +1,146 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/hive_type_ids.dart'; +import 'package:hive/hive.dart'; +import 'package:solana/metaplex.dart'; + +part 'spl_token.g.dart'; + +@HiveType(typeId: SPLToken.typeId) +class SPLToken extends CryptoCurrency with HiveObjectMixin { + @HiveField(0) + final String name; + + @HiveField(1) + final String symbol; + + @HiveField(2) + final String mintAddress; + + @HiveField(3) + final int decimal; + + @HiveField(4, defaultValue: false) + bool _enabled; + + @HiveField(5) + final String mint; + + @HiveField(6) + final String? iconPath; + + @HiveField(7) + final String? tag; + + SPLToken({ + required this.name, + required this.symbol, + required this.mintAddress, + required this.decimal, + required this.mint, + this.iconPath, + this.tag = 'SOL', + bool enabled = false, + }) : _enabled = enabled, + super( + name: mint.toLowerCase(), + title: symbol.toUpperCase(), + fullName: name, + tag: tag, + iconPath: iconPath, + decimals: decimal, + ); + + factory SPLToken.fromMetadata({ + required String name, + required String mint, + required String symbol, + required String mintAddress, + }) { + return SPLToken( + name: name, + symbol: symbol, + mintAddress: mintAddress, + decimal: 0, + mint: mint, + iconPath: '', + ); + } + + factory SPLToken.cryptoCurrency({ + required String name, + required String symbol, + required int decimals, + required String iconPath, + required String mint, + }) { + return SPLToken( + name: name, + symbol: symbol, + decimal: decimals, + mint: mint, + iconPath: iconPath, + mintAddress: '', + ); + } + + bool get enabled => _enabled; + + set enabled(bool value) => _enabled = value; + + SPLToken.copyWith(SPLToken other, String? icon, String? tag) + : name = other.name, + symbol = other.symbol, + mintAddress = other.mintAddress, + decimal = other.decimal, + _enabled = other.enabled, + mint = other.mint, + tag = other.tag, + iconPath = icon, + super( + title: other.symbol.toUpperCase(), + name: other.symbol.toLowerCase(), + decimals: other.decimal, + fullName: other.name, + tag: other.tag, + iconPath: icon, + ); + + static const typeId = SPL_TOKEN_TYPE_ID; + static const boxName = 'SPLTokens'; + + @override + bool operator ==(other) => + (other is SPLToken && other.mintAddress == mintAddress) || + (other is CryptoCurrency && other.title == title); + + @override + int get hashCode => mintAddress.hashCode; +} + +class NFT extends SPLToken { + final ImageInfo? imageInfo; + + NFT( + String mint, + String name, + String symbol, + String mintAddress, + int decimal, + String iconPath, + this.imageInfo, + ) : super( + name: name, + symbol: symbol, + mintAddress: mintAddress, + decimal: decimal, + mint: mint, + iconPath: iconPath, + ); +} + +class ImageInfo { + final String uri; + final OffChainMetadata? data; + + const ImageInfo(this.uri, this.data); +} diff --git a/cw_solana/pubspec.yaml b/cw_solana/pubspec.yaml new file mode 100644 index 000000000..c98b7492e --- /dev/null +++ b/cw_solana/pubspec.yaml @@ -0,0 +1,37 @@ +name: cw_solana +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 + solana: ^0.30.1 + cw_core: + path: ../cw_core + http: ^1.1.0 + hive: ^2.2.3 + bip39: ^1.0.6 + mobx: ^2.3.0+1 + shared_preferences: ^2.0.15 + web3dart: ^2.7.1 + bip32: ^2.0.0 + hex: ^0.2.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + build_runner: ^2.1.11 + mobx_codegen: ^2.0.7 + hive_generator: ^1.1.3 + +flutter: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg \ No newline at end of file diff --git a/cw_solana/test/cw_solana_test.dart b/cw_solana/test/cw_solana_test.dart new file mode 100644 index 000000000..42a5d8bdf --- /dev/null +++ b/cw_solana/test/cw_solana_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:cw_solana/cw_solana.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/ios/Podfile b/ios/Podfile index 027d48ceb..00b5fd2df 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '11.0' +platform :ios, '12.0' source 'https://github.com/CocoaPods/Specs.git' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. @@ -44,7 +44,7 @@ post_install do |installer| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0' config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ '$(inherited)', diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 2545e90ce..4f3aea7ec 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -7,7 +7,7 @@ PODS: - connectivity_plus (0.0.1): - Flutter - ReachabilitySwift - - CryptoSwift (1.7.1) + - CryptoSwift (1.8.1) - cw_haven (0.0.1): - cw_haven/Boost (= 0.0.1) - cw_haven/Haven (= 0.0.1) @@ -132,9 +132,9 @@ PODS: - permission_handler_apple (9.1.1): - Flutter - ReachabilitySwift (5.0.0) - - SDWebImage (5.16.0): - - SDWebImage/Core (= 5.16.0) - - SDWebImage/Core (5.16.0) + - SDWebImage (5.18.11): + - SDWebImage/Core (= 5.18.11) + - SDWebImage/Core (5.18.11) - sensitive_clipboard (0.0.1): - Flutter - share_plus (0.0.1): @@ -142,9 +142,9 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - SwiftProtobuf (1.22.0) + - SwiftProtobuf (1.25.2) - SwiftyGif (5.4.4) - - Toast (4.0.0) + - Toast (4.1.0) - uni_links (0.0.1): - Flutter - UnstoppableDomainsResolution (4.0.0): @@ -262,8 +262,8 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: barcode_scan2: 0af2bb63c81b4565aab6cd78278e4c0fa136dbb0 BigInt: f668a80089607f521586bbe29513d708491ef2f7 - connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e - CryptoSwift: d3d18dc357932f7e6d580689e065cf1f176007c1 + connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d + CryptoSwift: b9c701d6f5011df23794dbf7f2e480a77835d83d cw_haven: b3e54e1fbe7b8e6fda57a93206bc38f8e89b898a cw_monero: 4cf3b96f2da8e95e2ef7d6703dd4d2c509127b7d cw_shared_external: 2972d872b8917603478117c9957dfca611845a92 @@ -287,19 +287,19 @@ SPEC CHECKSUMS: path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 - SDWebImage: 2aea163b50bfcb569a2726b6a754c54a4506fcf6 + SDWebImage: a3ba0b8faac7228c3c8eadd1a55c9c9fe5e16457 sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986 share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 - SwiftProtobuf: 40bd808372cb8706108f22d28f8ab4a6b9bc6989 + SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1 SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f - Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 + Toast: ec33c32b8688982cecc6348adeae667c1b9938da uni_links: d97da20c7701486ba192624d99bffaaffcfc298a UnstoppableDomainsResolution: c3c67f4d0a5e2437cb00d4bd50c2e00d6e743841 url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812 wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 -PODFILE CHECKSUM: 09df1114e7c360f55770d35a79356bf5446e0100 +PODFILE CHECKSUM: fcb1b8418441a35b438585c9dd8374e722e6c6ca COCOAPODS: 1.12.1 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 1da5bc4bc..7a8b99b49 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -377,7 +377,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -523,7 +523,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -561,7 +561,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -607,9 +607,4 @@ /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; - SystemCapabilities = { - com.apple.BackgroundModes = { - enabled = 1; - }; - }; } diff --git a/ios/Runner/InfoBase.plist b/ios/Runner/InfoBase.plist index 2292d0b66..a7f208870 100644 --- a/ios/Runner/InfoBase.plist +++ b/ios/Runner/InfoBase.plist @@ -180,6 +180,16 @@ polygon-wallet + + CFBundleTypeRole + Viewer + CFBundleURLName + solana-wallet + CFBundleURLSchemes + + solana-wallet + + CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 889263f69..1f24250ac 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -1,182 +1,209 @@ part of 'bitcoin.dart'; class CWBitcoin extends Bitcoin { - @override - TransactionPriority getMediumTransactionPriority() => BitcoinTransactionPriority.medium; - - @override - WalletCredentials createBitcoinRestoreWalletFromSeedCredentials({ - required String name, - required String mnemonic, - required String password}) - => BitcoinRestoreWalletFromSeedCredentials(name: name, mnemonic: mnemonic, password: password); - - @override - WalletCredentials createBitcoinRestoreWalletFromWIFCredentials({ - required String name, - required String password, - required String wif, - WalletInfo? walletInfo}) - => BitcoinRestoreWalletFromWIFCredentials(name: name, password: password, wif: wif, walletInfo: walletInfo); - - @override - WalletCredentials createBitcoinNewWalletCredentials({ - required String name, - WalletInfo? walletInfo}) - => BitcoinNewWalletCredentials(name: name, walletInfo: walletInfo); - - @override - List getWordList() => wordlist; - - @override - Map getWalletKeys(Object wallet) { - final bitcoinWallet = wallet as ElectrumWallet; - final keys = bitcoinWallet.keys; - - return { - 'wif': keys.wif, - 'privateKey': keys.privateKey, - 'publicKey': keys.publicKey - }; - } - - @override - List getTransactionPriorities() - => BitcoinTransactionPriority.all; - - @override - List getLitecoinTransactionPriorities() - => LitecoinTransactionPriority.all; - - @override - TransactionPriority deserializeBitcoinTransactionPriority(int raw) - => BitcoinTransactionPriority.deserialize(raw: raw); - - @override - TransactionPriority deserializeLitecoinTransactionPriority(int raw) - => LitecoinTransactionPriority.deserialize(raw: raw); - - @override - int getFeeRate(Object wallet, TransactionPriority priority) { - final bitcoinWallet = wallet as ElectrumWallet; - return bitcoinWallet.feeRate(priority); - } - - @override - Future generateNewAddress(Object wallet, String label) async { - final bitcoinWallet = wallet as ElectrumWallet; - await bitcoinWallet.walletAddresses.generateNewAddress(label: label, hd: bitcoinWallet.hd); - await wallet.save(); - } - - @override - Future updateAddress(Object wallet,String address, String label) async { - final bitcoinWallet = wallet as ElectrumWallet; - bitcoinWallet.walletAddresses.updateAddress(address, label); - await wallet.save(); - } - - @override - Object createBitcoinTransactionCredentials(List outputs, {required TransactionPriority priority, int? feeRate}) - => BitcoinTransactionCredentials( - outputs.map((out) => OutputInfo( - fiatAmount: out.fiatAmount, - cryptoAmount: out.cryptoAmount, - address: out.address, - note: out.note, - sendAll: out.sendAll, - extractedAddress: out.extractedAddress, - isParsedAddress: out.isParsedAddress, - formattedCryptoAmount: out.formattedCryptoAmount, - memo: out.memo)) - .toList(), - priority: priority as BitcoinTransactionPriority, - feeRate: feeRate); - - @override - Object createBitcoinTransactionCredentialsRaw(List outputs, {TransactionPriority? priority, required int feeRate}) - => BitcoinTransactionCredentials( - outputs, - priority: priority != null ? priority as BitcoinTransactionPriority : null, - feeRate: feeRate); - - @override - List getAddresses(Object wallet) { - final bitcoinWallet = wallet as ElectrumWallet; - return bitcoinWallet.walletAddresses.addresses - .map((BitcoinAddressRecord addr) => addr.address) - .toList(); - } - - @override - @computed - List getSubAddresses(Object wallet) { - final electrumWallet = wallet as ElectrumWallet; - return electrumWallet.walletAddresses.addresses - .map((BitcoinAddressRecord addr) => ElectrumSubAddress( - id: addr.index, - name: addr.name, - address: electrumWallet.type == WalletType.bitcoinCash ? addr.cashAddr : addr.address, - txCount: addr.txCount, - balance: addr.balance, - isChange: addr.isHidden)) - .toList(); - } - - @override - String getAddress(Object wallet) { - final bitcoinWallet = wallet as ElectrumWallet; - return bitcoinWallet.walletAddresses.address; - } - - @override - String formatterBitcoinAmountToString({required int amount}) - => bitcoinAmountToString(amount: amount); - - @override - double formatterBitcoinAmountToDouble({required int amount}) - => bitcoinAmountToDouble(amount: amount); - - @override - int formatterStringDoubleToBitcoinAmount(String amount) - => stringDoubleToBitcoinAmount(amount); + @override + TransactionPriority getMediumTransactionPriority() => BitcoinTransactionPriority.medium; @override - String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate) - => (priority as BitcoinTransactionPriority).labelWithRate(rate); - - @override - List getUnspents(Object wallet) { - final bitcoinWallet = wallet as ElectrumWallet; - return bitcoinWallet.unspentCoins; - } - - Future updateUnspents(Object wallet) async { - final bitcoinWallet = wallet as ElectrumWallet; - await bitcoinWallet.updateUnspent(); - } - - WalletService createBitcoinWalletService(Box walletInfoSource, Box unspentCoinSource) { - return BitcoinWalletService(walletInfoSource, unspentCoinSource); - } - - WalletService createLitecoinWalletService(Box walletInfoSource, Box unspentCoinSource) { - return LitecoinWalletService(walletInfoSource, unspentCoinSource); - } - - @override - TransactionPriority getBitcoinTransactionPriorityMedium() - => BitcoinTransactionPriority.medium; + WalletCredentials createBitcoinRestoreWalletFromSeedCredentials( + {required String name, required String mnemonic, required String password}) => + BitcoinRestoreWalletFromSeedCredentials(name: name, mnemonic: mnemonic, password: password); @override - TransactionPriority getLitecoinTransactionPriorityMedium() - => LitecoinTransactionPriority.medium; + WalletCredentials createBitcoinRestoreWalletFromWIFCredentials( + {required String name, + required String password, + required String wif, + WalletInfo? walletInfo}) => + BitcoinRestoreWalletFromWIFCredentials( + name: name, password: password, wif: wif, walletInfo: walletInfo); @override - TransactionPriority getBitcoinTransactionPrioritySlow() - => BitcoinTransactionPriority.slow; - + WalletCredentials createBitcoinNewWalletCredentials( + {required String name, WalletInfo? walletInfo}) => + BitcoinNewWalletCredentials(name: name, walletInfo: walletInfo); + @override - TransactionPriority getLitecoinTransactionPrioritySlow() - => LitecoinTransactionPriority.slow; -} \ No newline at end of file + List getWordList() => wordlist; + + @override + Map getWalletKeys(Object wallet) { + final bitcoinWallet = wallet as ElectrumWallet; + final keys = bitcoinWallet.keys; + + return { + 'wif': keys.wif, + 'privateKey': keys.privateKey, + 'publicKey': keys.publicKey + }; + } + + @override + List getTransactionPriorities() => BitcoinTransactionPriority.all; + + @override + List getLitecoinTransactionPriorities() => LitecoinTransactionPriority.all; + + @override + TransactionPriority deserializeBitcoinTransactionPriority(int raw) => + BitcoinTransactionPriority.deserialize(raw: raw); + + @override + TransactionPriority deserializeLitecoinTransactionPriority(int raw) => + LitecoinTransactionPriority.deserialize(raw: raw); + + @override + int getFeeRate(Object wallet, TransactionPriority priority) { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.feeRate(priority); + } + + @override + Future generateNewAddress(Object wallet, String label) async { + final bitcoinWallet = wallet as ElectrumWallet; + await bitcoinWallet.walletAddresses.generateNewAddress(label: label); + await wallet.save(); + } + + @override + Future updateAddress(Object wallet, String address, String label) async { + final bitcoinWallet = wallet as ElectrumWallet; + bitcoinWallet.walletAddresses.updateAddress(address, label); + await wallet.save(); + } + + @override + Object createBitcoinTransactionCredentials(List outputs, + {required TransactionPriority priority, int? feeRate}) => + BitcoinTransactionCredentials( + outputs + .map((out) => OutputInfo( + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmount, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + formattedCryptoAmount: out.formattedCryptoAmount, + memo: out.memo)) + .toList(), + priority: priority as BitcoinTransactionPriority, + feeRate: feeRate); + + @override + Object createBitcoinTransactionCredentialsRaw(List outputs, + {TransactionPriority? priority, required int feeRate}) => + BitcoinTransactionCredentials(outputs, + priority: priority != null ? priority as BitcoinTransactionPriority : null, + feeRate: feeRate); + + @override + List getAddresses(Object wallet) { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.walletAddresses.addressesByReceiveType + .map((BitcoinAddressRecord addr) => addr.address) + .toList(); + } + + @override + @computed + List getSubAddresses(Object wallet) { + final electrumWallet = wallet as ElectrumWallet; + return electrumWallet.walletAddresses.addressesByReceiveType + .map((BitcoinAddressRecord addr) => ElectrumSubAddress( + id: addr.index, + name: addr.name, + address: electrumWallet.type == WalletType.bitcoinCash ? addr.cashAddr : addr.address, + txCount: addr.txCount, + balance: addr.balance, + isChange: addr.isHidden)) + .toList(); + } + + @override + String getAddress(Object wallet) { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.walletAddresses.address; + } + + @override + String formatterBitcoinAmountToString({required int amount}) => + bitcoinAmountToString(amount: amount); + + @override + double formatterBitcoinAmountToDouble({required int amount}) => + bitcoinAmountToDouble(amount: amount); + + @override + int formatterStringDoubleToBitcoinAmount(String amount) => stringDoubleToBitcoinAmount(amount); + + @override + String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate) => + (priority as BitcoinTransactionPriority).labelWithRate(rate); + + @override + List getUnspents(Object wallet) { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.unspentCoins; + } + + Future updateUnspents(Object wallet) async { + final bitcoinWallet = wallet as ElectrumWallet; + await bitcoinWallet.updateUnspent(); + } + + WalletService createBitcoinWalletService( + Box walletInfoSource, Box unspentCoinSource) { + return BitcoinWalletService(walletInfoSource, unspentCoinSource); + } + + WalletService createLitecoinWalletService( + Box walletInfoSource, Box unspentCoinSource) { + return LitecoinWalletService(walletInfoSource, unspentCoinSource); + } + + @override + TransactionPriority getBitcoinTransactionPriorityMedium() => BitcoinTransactionPriority.medium; + + @override + TransactionPriority getLitecoinTransactionPriorityMedium() => LitecoinTransactionPriority.medium; + + @override + TransactionPriority getBitcoinTransactionPrioritySlow() => BitcoinTransactionPriority.slow; + + @override + TransactionPriority getLitecoinTransactionPrioritySlow() => LitecoinTransactionPriority.slow; + + @override + Future setAddressType(Object wallet, dynamic option) async { + final bitcoinWallet = wallet as ElectrumWallet; + await bitcoinWallet.walletAddresses.setAddressType(option as BitcoinAddressType); + } + + @override + ReceivePageOption getSelectedAddressType(Object wallet) { + final bitcoinWallet = wallet as ElectrumWallet; + return BitcoinReceivePageOption.fromType(bitcoinWallet.walletAddresses.addressPageType); + } + + @override + List getBitcoinReceivePageOptions() => BitcoinReceivePageOption.all; + + @override + BitcoinAddressType getBitcoinAddressType(ReceivePageOption option) { + switch (option) { + case BitcoinReceivePageOption.p2pkh: + return P2pkhAddressType.p2pkh; + case BitcoinReceivePageOption.p2sh: + return P2shAddressType.p2wpkhInP2sh; + case BitcoinReceivePageOption.p2tr: + return SegwitAddresType.p2tr; + case BitcoinReceivePageOption.p2wsh: + return SegwitAddresType.p2wsh; + case BitcoinReceivePageOption.p2wpkh: + default: + return SegwitAddresType.p2wpkh; + } + } +} diff --git a/lib/bitcoin_cash/cw_bitcoin_cash.dart b/lib/bitcoin_cash/cw_bitcoin_cash.dart index 7dbb8614f..6e169209f 100644 --- a/lib/bitcoin_cash/cw_bitcoin_cash.dart +++ b/lib/bitcoin_cash/cw_bitcoin_cash.dart @@ -1,12 +1,6 @@ part of 'bitcoin_cash.dart'; class CWBitcoinCash extends BitcoinCash { - @override - String getMnemonic(int? strength) => Mnemonic.generate(); - - @override - Uint8List getSeedFromMnemonic(String seed) => Mnemonic.toSeed(seed); - @override String getCashAddrFormat(String address) => AddressUtils.getCashAddrFormat(address); diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 2ae9e3297..ad2c761a3 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -1,6 +1,7 @@ -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/core/validator.dart'; +import 'package:cake_wallet/solana/solana.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; @@ -9,7 +10,7 @@ class AddressValidator extends TextValidator { : super( errorMessage: S.current.error_text_address, useAdditionalValidation: type == CryptoCurrency.btc - ? bitcoin.Address.validateAddress + ? (String txt) => validateAddress(address: txt, network: BitcoinNetwork.mainnet) : null, pattern: getPattern(type), length: getLength(type)); @@ -25,7 +26,7 @@ class AddressValidator extends TextValidator { return '^[0-9a-zA-Z]{59}\$|^[0-9a-zA-Z]{92}\$|^[0-9a-zA-Z]{104}\$' '|^[0-9a-zA-Z]{105}\$|^addr1[0-9a-zA-Z]{98}\$'; case CryptoCurrency.btc: - return '^3[0-9a-zA-Z]{32}\$|^3[0-9a-zA-Z]{33}\$|^bc1[0-9a-zA-Z]{59}\$'; + return '^${P2pkhAddress.regex.pattern}\$|^${P2shAddress.regex.pattern}\$|^${P2wpkhAddress.regex.pattern}\$|${P2trAddress.regex.pattern}\$|^${P2wshAddress.regex.pattern}\$'; case CryptoCurrency.nano: return '[0-9a-zA-Z_]'; case CryptoCurrency.banano: @@ -90,7 +91,7 @@ class AddressValidator extends TextValidator { case CryptoCurrency.dai: case CryptoCurrency.dash: case CryptoCurrency.eos: - return '[0-9a-zA-Z]'; + return '[0-9a-zA-Z]'; case CryptoCurrency.bch: return '^(?!bitcoincash:)[0-9a-zA-Z]*\$|^(?!bitcoincash:)q|p[0-9a-zA-Z]{41}\$|^(?!bitcoincash:)q|p[0-9a-zA-Z]{42}\$|^bitcoincash:q|p[0-9a-zA-Z]{41}\$|^bitcoincash:q|p[0-9a-zA-Z]{42}\$'; case CryptoCurrency.bnb: @@ -130,6 +131,12 @@ class AddressValidator extends TextValidator { if (type is Erc20Token) { return [42]; } + + if (solana != null) { + final length = solana!.getValidationLength(type); + if (length != null) return length; + } + switch (type) { case CryptoCurrency.xmr: return null; @@ -192,11 +199,11 @@ class AddressValidator extends TextValidator { case CryptoCurrency.sc: return [76]; case CryptoCurrency.sol: + case CryptoCurrency.usdtSol: + case CryptoCurrency.usdcsol: return [32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44]; case CryptoCurrency.trx: return [34]; - case CryptoCurrency.usdcsol: - return [32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44]; case CryptoCurrency.usdt: return [34]; case CryptoCurrency.usdttrc20: @@ -250,9 +257,9 @@ class AddressValidator extends TextValidator { case CryptoCurrency.near: return [64]; case CryptoCurrency.btcln: - return null; + case CryptoCurrency.kaspa: default: - return []; + return null; } } @@ -263,12 +270,11 @@ class AddressValidator extends TextValidator { '|([^0-9a-zA-Z]|^)8[0-9a-zA-Z]{94}([^0-9a-zA-Z]|\$)' '|([^0-9a-zA-Z]|^)[0-9a-zA-Z]{106}([^0-9a-zA-Z]|\$)'; case CryptoCurrency.btc: - return '([^0-9a-zA-Z]|^)1[0-9a-zA-Z]{32}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)1[0-9a-zA-Z]{33}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)3[0-9a-zA-Z]{32}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)3[0-9a-zA-Z]{33}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)bc1[0-9a-zA-Z]{39}([^0-9a-zA-Z]|\$)' - '|([^0-9a-zA-Z]|^)bc1[0-9a-zA-Z]{59}([^0-9a-zA-Z]|\$)'; + return '([^0-9a-zA-Z]|^)([1mn][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2pkhAddress type + '|([^0-9a-zA-Z]|^)([23][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2shAddress type + '|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{25,39})([^0-9a-zA-Z]|\$)' //P2wpkhAddress type + '|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{40,80})([^0-9a-zA-Z]|\$)' //P2wshAddress type + '|([^0-9a-zA-Z]|^)((bc|tb)1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))([^0-9a-zA-Z]|\$)'; //P2trAddress type case CryptoCurrency.ltc: return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)' '|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)' @@ -286,8 +292,20 @@ class AddressValidator extends TextValidator { '|bitcoincash:q[0-9a-zA-Z]{42}([^0-9a-zA-Z]|\$)' '|([^0-9a-zA-Z]|^)q[0-9a-zA-Z]{41}([^0-9a-zA-Z]|\$)' '|([^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]|\$)'; default: + if (type.tag == CryptoCurrency.eth.title) { + return '0x[0-9a-zA-Z]{42}'; + } + if (type.tag == CryptoCurrency.maticpoly.tag) { + return '0x[0-9a-zA-Z]{42}'; + } + if (type.tag == CryptoCurrency.sol.title) { + return '([^0-9a-zA-Z]|^)[1-9A-HJ-NP-Za-km-z]{43,44}([^0-9a-zA-Z]|\$)'; + } + return null; } } -} \ No newline at end of file +} diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 9b5c4c8db..2ec5f293d 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -464,8 +464,6 @@ class BackupService { PreferencesKey.disableSellKey: _sharedPreferences.getBool(PreferencesKey.disableSellKey), PreferencesKey.defaultBuyProvider: _sharedPreferences.getInt(PreferencesKey.defaultBuyProvider), - PreferencesKey.isDarkThemeLegacy: - _sharedPreferences.getBool(PreferencesKey.isDarkThemeLegacy), PreferencesKey.currentPinLength: _sharedPreferences.getInt(PreferencesKey.currentPinLength), PreferencesKey.currentTransactionPriorityKeyLegacy: _sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy), diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index 8f65159e1..6d04055ba 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/haven/haven.dart'; 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:cw_core/wallet_type.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/nano/nano.dart'; @@ -37,6 +38,8 @@ class SeedValidator extends Validator { return nano!.getNanoWordList(language); case WalletType.polygon: return polygon!.getPolygonWordList(language); + case WalletType.solana: + return solana!.getSolanaWordList(language); default: return []; } diff --git a/lib/core/wallet_connect/chain_service.dart b/lib/core/wallet_connect/chain_service/chain_service.dart similarity index 100% rename from lib/core/wallet_connect/chain_service.dart rename to lib/core/wallet_connect/chain_service/chain_service.dart diff --git a/lib/core/wallet_connect/evm_chain_id.dart b/lib/core/wallet_connect/chain_service/eth/evm_chain_id.dart similarity index 86% rename from lib/core/wallet_connect/evm_chain_id.dart rename to lib/core/wallet_connect/chain_service/eth/evm_chain_id.dart index b71fb562e..0be21b1b2 100644 --- a/lib/core/wallet_connect/evm_chain_id.dart +++ b/lib/core/wallet_connect/chain_service/eth/evm_chain_id.dart @@ -1,4 +1,4 @@ -import 'package:cake_wallet/core/wallet_connect/evm_chain_service.dart'; +import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_service.dart'; enum EVMChainId { ethereum, diff --git a/lib/core/wallet_connect/evm_chain_service.dart b/lib/core/wallet_connect/chain_service/eth/evm_chain_service.dart similarity index 98% rename from lib/core/wallet_connect/evm_chain_service.dart rename to lib/core/wallet_connect/chain_service/eth/evm_chain_service.dart index 74bff7479..6f3c8fa98 100644 --- a/lib/core/wallet_connect/evm_chain_service.dart +++ b/lib/core/wallet_connect/chain_service/eth/evm_chain_service.dart @@ -3,7 +3,7 @@ import 'dart:developer'; import 'dart:typed_data'; import 'package:cake_wallet/core/wallet_connect/eth_transaction_model.dart'; -import 'package:cake_wallet/core/wallet_connect/evm_chain_id.dart'; +import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_id.dart'; import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; @@ -20,8 +20,8 @@ import 'package:eth_sig_util/util/utils.dart'; import 'package:http/http.dart' as http; import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; import 'package:web3dart/web3dart.dart'; -import 'chain_service.dart'; -import 'wallet_connect_key_service.dart'; +import '../chain_service.dart'; +import '../../wallet_connect_key_service.dart'; class EvmChainServiceImpl implements ChainService { final AppStore appStore; diff --git a/lib/core/wallet_connect/chain_service/solana/entities/solana_sign_message.dart b/lib/core/wallet_connect/chain_service/solana/entities/solana_sign_message.dart new file mode 100644 index 000000000..e462adbb5 --- /dev/null +++ b/lib/core/wallet_connect/chain_service/solana/entities/solana_sign_message.dart @@ -0,0 +1,28 @@ +class SolanaSignMessage { + final String pubkey; + final String message; + + SolanaSignMessage({ + required this.pubkey, + required this.message, + }); + + factory SolanaSignMessage.fromJson(Map json) { + return SolanaSignMessage( + pubkey: json['pubkey'] as String, + message: json['message'] as String, + ); + } + + Map toJson() { + return { + 'pubkey': pubkey, + 'message': message, + }; + } + + @override + String toString() { + return 'SolanaSignMessage(pubkey: $pubkey, message: $message)'; + } +} diff --git a/lib/core/wallet_connect/chain_service/solana/entities/solana_sign_transaction.dart b/lib/core/wallet_connect/chain_service/solana/entities/solana_sign_transaction.dart new file mode 100644 index 000000000..2cdf4697e --- /dev/null +++ b/lib/core/wallet_connect/chain_service/solana/entities/solana_sign_transaction.dart @@ -0,0 +1,106 @@ +class SolanaSignTransaction { + final String? feePayer; + final String? recentBlockhash; + final String transaction; + final List? instructions; + + SolanaSignTransaction({ + required this.feePayer, + required this.recentBlockhash, + required this.instructions, + required this.transaction, + }); + + factory SolanaSignTransaction.fromJson(Map json) { + return SolanaSignTransaction( + feePayer:json['feePayer'] !=null ? json['feePayer'] as String: null, + recentBlockhash: json['recentBlockhash']!=null? json['recentBlockhash'] as String: null, + instructions:json['instructions']!=null? (json['instructions'] as List) + .map((e) => SolanaInstruction.fromJson(e as Map)) + .toList(): null, + transaction: json['transaction'] as String, + ); + } + + Map toJson() { + return { + 'feePayer': feePayer, + 'recentBlockhash': recentBlockhash, + 'instructions': instructions, + 'transaction': transaction, + }; + } + + @override + String toString() { + return 'SolanaSignTransaction(feePayer: $feePayer, recentBlockhash: $recentBlockhash, instructions: $instructions, transaction: $transaction)'; + } +} + +class SolanaInstruction { + final String programId; + final List keys; + final List data; + + SolanaInstruction({ + required this.programId, + required this.keys, + required this.data, + }); + + factory SolanaInstruction.fromJson(Map json) { + return SolanaInstruction( + programId: json['programId'] as String, + keys: (json['keys'] as List) + .map((e) => SolanaKeyMetadata.fromJson(e as Map)) + .toList(), + data: (json['data'] as List).map((e) => e as int).toList(), + ); + } + + Map toJson() { + return { + 'programId': programId, + 'keys': keys, + 'data': data, + }; + } + + @override + String toString() { + return 'SolanaInstruction(programId: $programId, keys: $keys, data: $data)'; + } +} + +class SolanaKeyMetadata { + final String pubkey; + final bool isSigner; + final bool isWritable; + + SolanaKeyMetadata({ + required this.pubkey, + required this.isSigner, + required this.isWritable, + }); + + factory SolanaKeyMetadata.fromJson(Map json) { + return SolanaKeyMetadata( + pubkey: json['pubkey'] as String, + isSigner: json['isSigner'] as bool, + isWritable: json['isWritable'] as bool, + ); + } + + Map toJson() { + return { + 'pubkey': pubkey, + 'isSigner': isSigner, + 'isWritable': isWritable, + }; + } + + @override + String toString() { + return 'SolanaKeyMetadata(pubkey: $pubkey, isSigner: $isSigner, isWritable: $isWritable)'; + } +} diff --git a/lib/core/wallet_connect/chain_service/solana/solana_chain_id.dart b/lib/core/wallet_connect/chain_service/solana/solana_chain_id.dart new file mode 100644 index 000000000..bdc8a7d20 --- /dev/null +++ b/lib/core/wallet_connect/chain_service/solana/solana_chain_id.dart @@ -0,0 +1,27 @@ +import 'solana_chain_service.dart'; + +enum SolanaChainId { + mainnet, + testnet, + devnet, +} + +extension SolanaChainIdX on SolanaChainId { + String chain() { + String name = ''; + + switch (this) { + case SolanaChainId.mainnet: + name = '4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ'; + break; + case SolanaChainId.testnet: + name = '8E9rvCKLFQia2Y35HXjjpWzj8weVo44K'; + break; + case SolanaChainId.devnet: + name = ''; + break; + } + + return '${SolanaChainServiceImpl.namespace}:$name'; + } +} diff --git a/lib/core/wallet_connect/chain_service/solana/solana_chain_service.dart b/lib/core/wallet_connect/chain_service/solana/solana_chain_service.dart new file mode 100644 index 000000000..f5c696be6 --- /dev/null +++ b/lib/core/wallet_connect/chain_service/solana/solana_chain_service.dart @@ -0,0 +1,177 @@ +import 'dart:developer'; + +import 'package:cake_wallet/core/wallet_connect/chain_service/solana/entities/solana_sign_message.dart'; +import 'package:cake_wallet/core/wallet_connect/chain_service/solana/solana_chain_id.dart'; +import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_widget.dart'; +import 'package:cake_wallet/core/wallet_connect/models/connection_model.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_widget.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart'; +import 'package:solana/base58.dart'; +import 'package:solana/solana.dart'; +import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; +import '../chain_service.dart'; +import '../../wallet_connect_key_service.dart'; +import 'entities/solana_sign_transaction.dart'; + +class SolanaChainServiceImpl implements ChainService { + final BottomSheetService bottomSheetService; + final Web3Wallet wallet; + final WalletConnectKeyService wcKeyService; + + static const namespace = 'solana'; + static const solSignTransaction = 'solana_signTransaction'; + static const solSignMessage = 'solana_signMessage'; + + final SolanaChainId reference; + + final SolanaClient solanaClient; + + final Ed25519HDKeyPair? ownerKeyPair; + + SolanaChainServiceImpl({ + required this.reference, + required this.wcKeyService, + required this.bottomSheetService, + required this.wallet, + required this.ownerKeyPair, + required String webSocketUrl, + required Uri rpcUrl, + SolanaClient? solanaClient, + }) : solanaClient = solanaClient ?? + SolanaClient( + rpcUrl: rpcUrl, + websocketUrl: Uri.parse(webSocketUrl), + timeout: const Duration(minutes: 2), + ) { + for (final String event in getEvents()) { + wallet.registerEventEmitter(chainId: getChainId(), event: event); + } + wallet.registerRequestHandler( + chainId: getChainId(), + method: solSignTransaction, + handler: solanaSignTransaction, + ); + wallet.registerRequestHandler( + chainId: getChainId(), + method: solSignMessage, + handler: solanaSignMessage, + ); + } + + @override + String getNamespace() { + return namespace; + } + + @override + String getChainId() { + return reference.chain(); + } + + @override + List getEvents() { + return ['']; + } + + Future requestAuthorization(String? text) async { + // Show the bottom sheet + final bool? isApproved = await bottomSheetService.queueBottomSheet( + widget: Web3RequestModal( + child: ConnectionWidget( + title: S.current.signTransaction, + info: [ + ConnectionModel( + text: text, + ), + ], + ), + ), + ) as bool?; + + if (isApproved != null && isApproved == false) { + return 'User rejected signature'; + } + + return null; + } + + Future solanaSignTransaction(String topic, dynamic parameters) async { + log('received solana sign transaction request $parameters'); + + final solanaSignTx = + SolanaSignTransaction.fromJson(parameters as Map); + + final String? authError = await requestAuthorization('Confirm request to sign transaction?'); + + if (authError != null) { + return authError; + } + + try { + final message = + await solanaClient.rpcClient.getMessageFromEncodedTx(solanaSignTx.transaction); + + final sign = await ownerKeyPair?.signMessage( + message: message, + recentBlockhash: solanaSignTx.recentBlockhash ?? '', + ); + + if (sign == null) { + return ''; + } + + String signature = sign.signatures.first.toBase58(); + + print(signature); + print(signature.runtimeType); + + bottomSheetService.queueBottomSheet( + isModalDismissible: true, + widget: BottomSheetMessageDisplayWidget( + message: S.current.awaitDAppProcessing, + isError: false, + ), + ); + + return signature; + } catch (e) { + log('An error has occurred while signing transaction: ${e.toString()}'); + bottomSheetService.queueBottomSheet( + isModalDismissible: true, + widget: BottomSheetMessageDisplayWidget( + message: '${S.current.errorSigningTransaction}: ${e.toString()}', + ), + ); + return 'Failed'; + } + } + + Future solanaSignMessage(String topic, dynamic parameters) async { + log('received solana sign message request: $parameters'); + + final solanaSignMessage = SolanaSignMessage.fromJson(parameters as Map); + + final String? authError = await requestAuthorization('Confirm request to sign message?'); + + if (authError != null) { + return authError; + } + Signature? sign; + + try { + sign = await ownerKeyPair?.sign(base58decode(solanaSignMessage.message)); + } catch (e) { + print(e); + } + + if (sign == null) { + return ''; + } + + String signature = sign.toBase58(); + + return signature; + } +} diff --git a/lib/core/wallet_connect/wallet_connect_key_service.dart b/lib/core/wallet_connect/wallet_connect_key_service.dart index 33d721073..f05adad97 100644 --- a/lib/core/wallet_connect/wallet_connect_key_service.dart +++ b/lib/core/wallet_connect/wallet_connect_key_service.dart @@ -2,6 +2,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.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:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; @@ -13,7 +14,6 @@ abstract class WalletConnectKeyService { /// If the chain is not found, returns an empty list. /// - [chain]: The chain to get the keys for. List getKeysForChain(WalletBase wallet); - } class KeyServiceImpl implements WalletConnectKeyService { @@ -23,6 +23,8 @@ class KeyServiceImpl implements WalletConnectKeyService { return ethereum!.getPrivateKey(wallet); case WalletType.polygon: return polygon!.getPrivateKey(wallet); + case WalletType.solana: + return solana!.getPrivateKey(wallet); default: return ''; } @@ -34,6 +36,8 @@ class KeyServiceImpl implements WalletConnectKeyService { return ethereum!.getPublicKey(wallet); case WalletType.polygon: return polygon!.getPublicKey(wallet); + case WalletType.solana: + return solana!.getPublicKey(wallet); default: return ''; } @@ -53,6 +57,14 @@ class KeyServiceImpl implements WalletConnectKeyService { privateKey: _getPrivateKeyForWallet(wallet), publicKey: _getPublicKeyForWallet(wallet), ), + ChainKeyModel( + chains: [ + 'solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ', // main-net + 'solana:8E9rvCKLFQia2Y35HXjjpWzj8weVo44K', // test-net + ], + privateKey: _getPrivateKeyForWallet(wallet), + publicKey: _getPublicKeyForWallet(wallet), + ), ]; return keys; } diff --git a/lib/core/wallet_connect/web3wallet_service.dart b/lib/core/wallet_connect/web3wallet_service.dart index ee560a0e0..4c71abe48 100644 --- a/lib/core/wallet_connect/web3wallet_service.dart +++ b/lib/core/wallet_connect/web3wallet_service.dart @@ -2,23 +2,27 @@ import 'dart:async'; import 'dart:developer'; import 'dart:typed_data'; -import 'package:cake_wallet/core/wallet_connect/evm_chain_id.dart'; -import 'package:cake_wallet/core/wallet_connect/evm_chain_service.dart'; +import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_id.dart'; +import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_service.dart'; import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/core/wallet_connect/models/auth_request_model.dart'; import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart'; import 'package:cake_wallet/core/wallet_connect/models/session_request_model.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; +import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_request_widget.dart'; import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_widget.dart'; import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart'; import 'package:cake_wallet/store/app_store.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:eth_sig_util/eth_sig_util.dart'; import 'package:flutter/material.dart'; import 'package:mobx/mobx.dart'; import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; +import 'chain_service/solana/solana_chain_id.dart'; +import 'chain_service/solana/solana_chain_service.dart'; import 'wc_bottom_sheet_service.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; @@ -114,14 +118,34 @@ abstract class Web3WalletServiceBase with Store { final newAuthRequests = _web3Wallet.completeRequests.getAll(); auth.addAll(newAuthRequests); - for (final cId in EVMChainId.values) { - EvmChainServiceImpl( - reference: cId, - appStore: appStore, - wcKeyService: walletKeyService, - bottomSheetService: _bottomSheetHandler, - wallet: _web3Wallet, - ); + if (isEVMCompatibleChain(appStore.wallet!.type)) { + for (final cId in EVMChainId.values) { + EvmChainServiceImpl( + reference: cId, + appStore: appStore, + wcKeyService: walletKeyService, + bottomSheetService: _bottomSheetHandler, + wallet: _web3Wallet, + ); + } + } + + if (appStore.wallet!.type == WalletType.solana) { + for (final cId in SolanaChainId.values) { + final node = appStore.settingsStore.getCurrentNode(appStore.wallet!.type); + final rpcUri = node.uri; + final webSocketUri = 'wss://${node.uriRaw}/ws${node.uri.path}'; + + SolanaChainServiceImpl( + reference: cId, + rpcUrl: rpcUri, + webSocketUrl: webSocketUri, + wcKeyService: walletKeyService, + bottomSheetService: _bottomSheetHandler, + wallet: _web3Wallet, + ownerKeyPair: solana!.getWalletKeyPair(appStore.wallet!), + ); + } } } diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index 8548f079f..31a893ad6 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -2,7 +2,6 @@ import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hive/hive.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -55,7 +54,7 @@ class WalletCreationService { } } - Future create(WalletCredentials credentials) async { + Future create(WalletCredentials credentials, {bool? isTestnet}) async { checkIfExists(credentials.name); final password = generateWalletPassword(); credentials.password = password; @@ -63,7 +62,7 @@ class WalletCreationService { credentials.seedPhraseLength = settingsStore.seedPhraseLength.value; } await keyService.saveWalletPassword(password: password, walletName: credentials.name); - final wallet = await _service!.create(credentials); + final wallet = await _service!.create(credentials, isTestnet: isTestnet); if (wallet.type == WalletType.monero) { await sharedPreferences.setBool( @@ -73,12 +72,12 @@ class WalletCreationService { return wallet; } - Future restoreFromKeys(WalletCredentials credentials) async { + Future restoreFromKeys(WalletCredentials credentials, {bool? isTestnet}) async { checkIfExists(credentials.name); final password = generateWalletPassword(); credentials.password = password; await keyService.saveWalletPassword(password: password, walletName: credentials.name); - final wallet = await _service!.restoreFromKeys(credentials); + final wallet = await _service!.restoreFromKeys(credentials, isTestnet: isTestnet); if (wallet.type == WalletType.monero) { await sharedPreferences.setBool( @@ -88,12 +87,12 @@ class WalletCreationService { return wallet; } - Future restoreFromSeed(WalletCredentials credentials) async { + Future restoreFromSeed(WalletCredentials credentials, {bool? isTestnet}) async { checkIfExists(credentials.name); final password = generateWalletPassword(); credentials.password = password; await keyService.saveWalletPassword(password: password, walletName: credentials.name); - final wallet = await _service!.restoreFromSeed(credentials); + final wallet = await _service!.restoreFromSeed(credentials, isTestnet: isTestnet); if (wallet.type == WalletType.monero) { await sharedPreferences.setBool( diff --git a/lib/di.dart b/lib/di.dart index 9b474f25c..6f5535c5e 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -13,7 +13,7 @@ import 'package:cake_wallet/core/yat_service.dart'; import 'package:cake_wallet/entities/background_tasks.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; -import 'package:cake_wallet/entities/receive_page_option.dart'; +import 'package:cw_core/receive_page_option.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/ionia/ionia_anypay.dart'; @@ -21,6 +21,7 @@ import 'package:cake_wallet/ionia/ionia_gift_card.dart'; import 'package:cake_wallet/ionia/ionia_tip.dart'; import 'package:cake_wallet/polygon/polygon.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/buy/buy_options_page.dart'; import 'package:cake_wallet/src/screens/buy/webview_page.dart'; @@ -864,6 +865,8 @@ Future setup({ return nano!.createNanoWalletService(_walletInfoSource); case WalletType.polygon: return polygon!.createPolygonWalletService(_walletInfoSource); + case WalletType.solana: + return solana!.createSolanaWalletService(_walletInfoSource); default: throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService'); } @@ -1175,7 +1178,7 @@ Future setup({ getIt.registerFactoryParam>( (homeSettingsViewModel, arguments) => EditTokenPage( homeSettingsViewModel: homeSettingsViewModel, - erc20token: arguments['token'] as Erc20Token?, + token: arguments['token'] as CryptoCurrency?, initialContractAddress: arguments['contractAddress'] as String?, ), ); diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 68e76d423..fb3e9e80c 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -23,6 +23,10 @@ import 'package:collection/collection.dart'; const newCakeWalletMoneroUri = 'xmr-node.cakewallet.com:18081'; const cakeWalletBitcoinElectrumUri = 'electrum.cakewallet.com:50002'; +const publicBitcoinTestnetElectrumAddress = 'electrum.blockstream.info'; +const publicBitcoinTestnetElectrumPort = '60002'; +const publicBitcoinTestnetElectrumUri = + '$publicBitcoinTestnetElectrumAddress:$publicBitcoinTestnetElectrumPort'; const cakeWalletLitecoinElectrumUri = 'ltc-electrum.cakewallet.com:50002'; const havenDefaultNodeUri = 'nodes.havenprotocol.org:443'; const ethereumDefaultNodeUri = 'ethereum.publicnode.com'; @@ -30,6 +34,7 @@ const polygonDefaultNodeUri = 'polygon-bor.publicnode.com'; const cakeWalletBitcoinCashDefaultNodeUri = 'bitcoincash.stackwallet.com:50002'; const nanoDefaultNodeUri = 'rpc.nano.to'; const nanoDefaultPowNodeUri = 'rpc.nano.to'; +const solanaDefaultNodeUri = 'rpc.ankr.com'; Future defaultSettingsMigration( {required int version, @@ -186,10 +191,15 @@ Future defaultSettingsMigration( await rewriteSecureStoragePin(secureStorage: secureStorage); break; case 26: - /// commented out as it was a probable cause for some users to have white screen issues - /// maybe due to multiple access on Secure Storage at once - /// or long await time on start of the app - // await insecureStorageMigration(secureStorage: secureStorage, sharedPreferences: sharedPreferences); + + /// commented out as it was a probable cause for some users to have white screen issues + /// maybe due to multiple access on Secure Storage at once + /// or long await time on start of the app + // await insecureStorageMigration(secureStorage: secureStorage, sharedPreferences: sharedPreferences); + case 27: + await addSolanaNodeList(nodes: nodes); + await changeSolanaCurrentNodeToDefault( + sharedPreferences: sharedPreferences, nodes: nodes); break; default: break; @@ -328,6 +338,12 @@ Node? getBitcoinDefaultElectrumServer({required Box nodes}) { nodes.values.firstWhereOrNull((node) => node.type == WalletType.bitcoin); } +Node? getBitcoinTestnetDefaultElectrumServer({required Box nodes}) { + return nodes.values + .firstWhereOrNull((Node node) => node.uriRaw == publicBitcoinTestnetElectrumUri) ?? + nodes.values.firstWhereOrNull((node) => node.type == WalletType.bitcoin); +} + Node? getLitecoinDefaultElectrumServer({required Box nodes}) { return nodes.values .firstWhereOrNull((Node node) => node.uriRaw == cakeWalletLitecoinElectrumUri) ?? @@ -384,6 +400,11 @@ Node getMoneroDefaultNode({required Box nodes}) { } } +Node? getSolanaDefaultNode({required Box nodes}) { + return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == solanaDefaultNodeUri) ?? + nodes.values.firstWhereOrNull((node) => node.type == WalletType.solana); +} + Future insecureStorageMigration({ required SharedPreferences sharedPreferences, required FlutterSecureStorage secureStorage, @@ -492,8 +513,15 @@ Future rewriteSecureStoragePin({required FlutterSecureStorage secureStorag } Future changeBitcoinCurrentElectrumServerToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final server = getBitcoinDefaultElectrumServer(nodes: nodes); + {required SharedPreferences sharedPreferences, + required Box nodes, + bool? isTestnet}) async { + Node? server; + if (isTestnet == true) { + server = getBitcoinTestnetDefaultElectrumServer(nodes: nodes); + } else { + server = getBitcoinDefaultElectrumServer(nodes: nodes); + } final serverId = server?.key as int? ?? 0; await sharedPreferences.setInt(PreferencesKey.currentBitcoinElectrumSererIdKey, serverId); @@ -673,6 +701,7 @@ Future checkCurrentNodes( final currentNanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey); final currentBitcoinCashNodeId = sharedPreferences.getInt(PreferencesKey.currentBitcoinCashNodeIdKey); + final currentSolanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); final currentMoneroNode = nodeSource.values.firstWhereOrNull((node) => node.key == currentMoneroNodeId); final currentBitcoinElectrumServer = @@ -691,6 +720,8 @@ Future checkCurrentNodes( powNodeSource.values.firstWhereOrNull((node) => node.key == currentNanoPowNodeId); final currentBitcoinCashNodeServer = nodeSource.values.firstWhereOrNull((node) => node.key == currentBitcoinCashNodeId); + final currentSolanaNodeServer = + nodeSource.values.firstWhereOrNull((node) => node.key == currentSolanaNodeId); if (currentMoneroNode == null) { final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero); await nodeSource.add(newCakeWalletNode); @@ -750,6 +781,12 @@ Future checkCurrentNodes( await nodeSource.add(node); await sharedPreferences.setInt(PreferencesKey.currentPolygonNodeIdKey, node.key as int); } + + if (currentSolanaNodeServer == null) { + final node = Node(uri: solanaDefaultNodeUri, type: WalletType.solana); + await nodeSource.add(node); + await sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, node.key as int); + } } Future resetBitcoinElectrumServer( @@ -861,3 +898,20 @@ Future changePolygonCurrentNodeToDefault( await sharedPreferences.setInt(PreferencesKey.currentPolygonNodeIdKey, nodeId); } + +Future addSolanaNodeList({required Box nodes}) async { + final nodeList = await loadDefaultSolanaNodes(); + for (var node in nodeList) { + if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { + await nodes.add(node); + } + } +} + +Future changeSolanaCurrentNodeToDefault( + {required SharedPreferences sharedPreferences, required Box nodes}) async { + final node = getSolanaDefaultNode(nodes: nodes); + final nodeId = node?.key as int? ?? 0; + + await sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, nodeId); +} diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index aaac8a5c2..3c82a3f6c 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -149,6 +149,23 @@ Future> loadDefaultPolygonNodes() async { return nodes; } +Future> loadDefaultSolanaNodes() async { + final nodesRaw = await rootBundle.loadString('assets/solana_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.solana; + nodes.add(node); + } + } + + return nodes; +} + Future resetToDefault(Box nodeSource) async { final moneroNodes = await loadDefaultNodes(); final bitcoinElectrumServerList = await loadBitcoinElectrumServerList(); @@ -158,6 +175,7 @@ Future resetToDefault(Box nodeSource) async { final ethereumNodes = await loadDefaultEthereumNodes(); final nanoNodes = await loadDefaultNanoNodes(); final polygonNodes = await loadDefaultPolygonNodes(); + final solanaNodes = await loadDefaultSolanaNodes(); final nodes = moneroNodes + bitcoinElectrumServerList + @@ -166,7 +184,8 @@ Future resetToDefault(Box nodeSource) async { ethereumNodes + bitcoinCashElectrumServerList + nanoNodes + - polygonNodes; + polygonNodes + + solanaNodes; await nodeSource.clear(); await nodeSource.addAll(nodes); diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index 52bcc495b..bab0ef51d 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -51,7 +51,8 @@ class AddressResolver { } final match = RegExp(addressPattern).firstMatch(raw); - return match?.group(0)?.replaceAllMapped(RegExp('[^0-9a-zA-Z]|bitcoincash:|nano_'), (Match match) { + return match?.group(0)?.replaceAllMapped(RegExp('[^0-9a-zA-Z]|bitcoincash:|nano_'), + (Match match) { String group = match.group(0)!; if (group.startsWith('bitcoincash:') || group.startsWith('nano_')) { return group; @@ -68,25 +69,35 @@ class AddressResolver { return emailRegex.hasMatch(address); } - - Future resolve(BuildContext context, String text, String ticker) async { + // TODO: refactor this to take Crypto currency instead of ticker, or at least pass in the tag as well + Future resolve(BuildContext context, String text, String ticker) async { try { if (text.startsWith('@') && !text.substring(1).contains('@')) { - if(settingsStore.lookupsTwitter) { + if (settingsStore.lookupsTwitter) { final formattedName = text.substring(1); final twitterUser = await TwitterApi.lookupUserByName(userName: formattedName); final addressFromBio = extractAddressByType( - raw: twitterUser.description, type: CryptoCurrency.fromString(ticker)); + raw: twitterUser.description, + type: CryptoCurrency.fromString(ticker, walletCurrency: wallet.currency)); if (addressFromBio != null) { - return ParsedAddress.fetchTwitterAddress(address: addressFromBio, name: text); + return ParsedAddress.fetchTwitterAddress( + address: addressFromBio, + name: text, + profileImageUrl: twitterUser.profileImageUrl, + profileName: twitterUser.name); } final pinnedTweet = twitterUser.pinnedTweet?.text; if (pinnedTweet != null) { - final addressFromPinnedTweet = - extractAddressByType(raw: pinnedTweet, type: CryptoCurrency.fromString(ticker)); + final addressFromPinnedTweet = extractAddressByType( + raw: pinnedTweet, + type: CryptoCurrency.fromString(ticker, walletCurrency: wallet.currency)); if (addressFromPinnedTweet != null) { - return ParsedAddress.fetchTwitterAddress(address: addressFromPinnedTweet, name: text); + return ParsedAddress.fetchTwitterAddress( + address: addressFromPinnedTweet, + name: text, + profileImageUrl: twitterUser.profileImageUrl, + profileName: twitterUser.name); } } } @@ -100,17 +111,21 @@ class AddressResolver { final userName = subText.substring(0, hostNameIndex); final mastodonUser = - await MastodonAPI.lookupUserByUserName(userName: userName, apiHost: hostName); + await MastodonAPI.lookupUserByUserName(userName: userName, apiHost: hostName); if (mastodonUser != null) { - String? addressFromBio = - extractAddressByType(raw: mastodonUser.note, type: CryptoCurrency.fromString(ticker)); + String? addressFromBio = extractAddressByType( + raw: mastodonUser.note, type: CryptoCurrency.fromString(ticker)); if (addressFromBio != null) { - return ParsedAddress.fetchMastodonAddress(address: addressFromBio, name: text); + return ParsedAddress.fetchMastodonAddress( + address: addressFromBio, + name: text, + profileImageUrl: mastodonUser.profileImageUrl, + profileName: mastodonUser.username); } else { final pinnedPosts = - await MastodonAPI.getPinnedPosts(userId: mastodonUser.id, apiHost: hostName); + await MastodonAPI.getPinnedPosts(userId: mastodonUser.id, apiHost: hostName); if (pinnedPosts.isNotEmpty) { final userPinnedPostsText = pinnedPosts.map((item) => item.content).join('\n'); @@ -119,7 +134,10 @@ class AddressResolver { if (addressFromPinnedPost != null) { return ParsedAddress.fetchMastodonAddress( - address: addressFromPinnedPost, name: text); + address: addressFromPinnedPost, + name: text, + profileImageUrl: mastodonUser.profileImageUrl, + profileName: mastodonUser.username); } } } @@ -135,7 +153,7 @@ class AddressResolver { } } if (text.hasOnlyEmojis) { - if(settingsStore.lookupsYatService) { + if (settingsStore.lookupsYatService) { if (walletType != WalletType.haven) { final addresses = await yatService.fetchYatAddress(text, ticker); return ParsedAddress.fetchEmojiAddress(addresses: addresses, name: text); @@ -151,7 +169,7 @@ class AddressResolver { } if (unstoppableDomains.any((domain) => name.trim() == domain)) { - if(settingsStore.lookupsUnstoppableDomains) { + if (settingsStore.lookupsUnstoppableDomains) { final address = await fetchUnstoppableDomainAddress(text, ticker); return ParsedAddress.fetchUnstoppableDomainAddress(address: address, name: text); } @@ -167,7 +185,7 @@ class AddressResolver { } if (formattedName.contains(".")) { - if(settingsStore.lookupsOpenAlias) { + if (settingsStore.lookupsOpenAlias) { final txtRecord = await OpenaliasRecord.lookupOpenAliasRecord(formattedName); if (txtRecord != null) { final record = await OpenaliasRecord.fetchAddressAndName( @@ -186,7 +204,11 @@ class AddressResolver { String? addressFromBio = extractAddressByType( raw: nostrUserData.about, type: CryptoCurrency.fromString(ticker)); if (addressFromBio != null) { - return ParsedAddress.nostrAddress(address: addressFromBio, name: text); + return ParsedAddress.nostrAddress( + address: addressFromBio, + name: text, + profileImageUrl: nostrUserData.picture, + profileName: nostrUserData.name); } } } diff --git a/lib/entities/parsed_address.dart b/lib/entities/parsed_address.dart index d414a827d..fc8ab2440 100644 --- a/lib/entities/parsed_address.dart +++ b/lib/entities/parsed_address.dart @@ -1,7 +1,6 @@ import 'package:cake_wallet/entities/openalias_record.dart'; import 'package:cake_wallet/entities/yat_record.dart'; - enum ParseFrom { unstoppableDomains, openAlias, @@ -20,36 +19,37 @@ class ParsedAddress { required this.addresses, this.name = '', this.description = '', + this.profileImageUrl = '', + this.profileName = '', this.parseFrom = ParseFrom.notParsed, }); factory ParsedAddress.fetchEmojiAddress({ List? addresses, required String name, - }){ - if (addresses?.isEmpty ?? true) { - return ParsedAddress( - addresses: [name], parseFrom: ParseFrom.yatRecord); - } - return ParsedAddress( - addresses: addresses!.map((e) => e.address).toList(), - name: name, - parseFrom: ParseFrom.yatRecord, - ); + }) { + if (addresses?.isEmpty ?? true) { + return ParsedAddress(addresses: [name], parseFrom: ParseFrom.yatRecord); + } + return ParsedAddress( + addresses: addresses!.map((e) => e.address).toList(), + name: name, + parseFrom: ParseFrom.yatRecord, + ); } factory ParsedAddress.fetchUnstoppableDomainAddress({ String? address, required String name, - }){ - if (address?.isEmpty ?? true) { - return ParsedAddress(addresses: [name]); - } - return ParsedAddress( - addresses: [address!], - name: name, - parseFrom: ParseFrom.unstoppableDomains, - ); + }) { + if (address?.isEmpty ?? true) { + return ParsedAddress(addresses: [name]); + } + return ParsedAddress( + addresses: [address!], + name: name, + parseFrom: ParseFrom.unstoppableDomains, + ); } factory ParsedAddress.fetchOpenAliasAddress( @@ -65,7 +65,7 @@ class ParsedAddress { ); } - factory ParsedAddress.fetchFioAddress({required String address, required String name}){ + factory ParsedAddress.fetchFioAddress({required String address, required String name}) { return ParsedAddress( addresses: [address], name: name, @@ -73,23 +73,37 @@ class ParsedAddress { ); } - factory ParsedAddress.fetchTwitterAddress({required String address, required String name}){ + factory ParsedAddress.fetchTwitterAddress( + {required String address, + required String name, + required String profileImageUrl, + required String profileName, + String? description}) { return ParsedAddress( addresses: [address], name: name, + description: description ?? '', + profileImageUrl: profileImageUrl, + profileName: profileName, parseFrom: ParseFrom.twitter, ); } - factory ParsedAddress.fetchMastodonAddress({required String address, required String name}){ + factory ParsedAddress.fetchMastodonAddress( + {required String address, + required String name, + required String profileImageUrl, + required String profileName}) { return ParsedAddress( addresses: [address], name: name, - parseFrom: ParseFrom.mastodon + parseFrom: ParseFrom.mastodon, + profileImageUrl: profileImageUrl, + profileName: profileName, ); } - factory ParsedAddress.fetchContactAddress({required String address, required String name}){ + factory ParsedAddress.fetchContactAddress({required String address, required String name}) { return ParsedAddress( addresses: [address], name: name, @@ -105,17 +119,24 @@ class ParsedAddress { ); } - factory ParsedAddress.nostrAddress({required String address, required String name}) { + factory ParsedAddress.nostrAddress( + {required String address, + required String name, + required String profileImageUrl, + required String profileName}) { return ParsedAddress( addresses: [address], name: name, parseFrom: ParseFrom.nostr, + profileImageUrl: profileImageUrl, + profileName: profileName, ); } final List addresses; final String name; final String description; + final String profileImageUrl; + final String profileName; final ParseFrom parseFrom; - } diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 6df223f2e..3ef133e19 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -13,6 +13,7 @@ class PreferencesKey { static const currentBananoPowNodeIdKey = 'current_node_id_banano_pow'; static const currentFiatCurrencyKey = 'current_fiat_currency'; static const currentBitcoinCashNodeIdKey = 'current_node_id_bch'; + static const currentSolanaNodeIdKey = 'current_node_id_sol'; static const currentTransactionPriorityKeyLegacy = 'current_fee_priority'; static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; static const shouldSaveRecipientAddressKey = 'save_recipient_address'; @@ -27,7 +28,6 @@ class PreferencesKey { static const disableExchangeKey = 'disable_exchange'; static const exchangeStatusKey = 'exchange_status'; static const currentTheme = 'current_theme'; - static const isDarkThemeLegacy = 'dark_theme'; static const displayActionListModeKey = 'display_list_mode'; static const currentPinLength = 'current_pin_length'; static const currentLanguageCode = 'language_code'; diff --git a/lib/entities/priority_for_wallet_type.dart b/lib/entities/priority_for_wallet_type.dart index 70b072c55..5fc0b5566 100644 --- a/lib/entities/priority_for_wallet_type.dart +++ b/lib/entities/priority_for_wallet_type.dart @@ -21,12 +21,13 @@ List priorityForWalletType(WalletType type) { return ethereum!.getTransactionPriorities(); case WalletType.bitcoinCash: return bitcoinCash!.getTransactionPriorities(); - // no such thing for nano/banano: - case WalletType.nano: - case WalletType.banano: - return []; case WalletType.polygon: return polygon!.getTransactionPriorities(); + // no such thing for nano/banano/solana: + case WalletType.nano: + case WalletType.banano: + case WalletType.solana: + return []; default: return []; } diff --git a/lib/entities/provider_types.dart b/lib/entities/provider_types.dart index ed688590c..f9c2f1a82 100644 --- a/lib/entities/provider_types.dart +++ b/lib/entities/provider_types.dart @@ -66,7 +66,9 @@ class ProvidersHelper { case WalletType.bitcoinCash: return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood]; case WalletType.polygon: - return [ProviderType.askEachTime, ProviderType.dfx]; + return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx]; + case WalletType.solana: + return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood]; case WalletType.none: case WalletType.haven: return []; @@ -87,7 +89,14 @@ class ProvidersHelper { case WalletType.bitcoinCash: return [ProviderType.askEachTime, ProviderType.moonpaySell]; case WalletType.polygon: - return [ProviderType.askEachTime, ProviderType.dfx]; + return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx]; + case WalletType.solana: + return [ + ProviderType.askEachTime, + ProviderType.onramper, + ProviderType.robinhood, + ProviderType.moonpaySell, + ]; case WalletType.monero: case WalletType.nano: case WalletType.banano: diff --git a/lib/entities/receive_page_option.dart b/lib/entities/receive_page_option.dart deleted file mode 100644 index 3ee9abe96..000000000 --- a/lib/entities/receive_page_option.dart +++ /dev/null @@ -1,23 +0,0 @@ - -enum ReceivePageOption { - mainnet, - anonPayInvoice, - anonPayDonationLink; - - @override - String toString() { - String label = ''; - switch (this) { - case ReceivePageOption.mainnet: - label = 'Mainnet'; - break; - case ReceivePageOption.anonPayInvoice: - label = 'Trocador AnonPay Invoice'; - break; - case ReceivePageOption.anonPayDonationLink: - label = 'Trocador AnonPay Donation Link'; - break; - } - return label; - } -} diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart index 579c71288..52839d68a 100644 --- a/lib/ethereum/cw_ethereum.dart +++ b/lib/ethereum/cw_ethereum.dart @@ -120,12 +120,13 @@ class CWEthereum extends Ethereum { } @override - Future addErc20Token(WalletBase wallet, Erc20Token token) async => - await (wallet as EthereumWallet).addErc20Token(token); + Future addErc20Token(WalletBase wallet, CryptoCurrency token) async { + await (wallet as EthereumWallet).addErc20Token(token as Erc20Token); + } @override - Future deleteErc20Token(WalletBase wallet, Erc20Token token) async => - await (wallet as EthereumWallet).deleteErc20Token(token); + Future deleteErc20Token(WalletBase wallet, CryptoCurrency token) async => + await (wallet as EthereumWallet).deleteErc20Token(token as Erc20Token); @override Future getErc20Token(WalletBase wallet, String contractAddress) async { @@ -154,4 +155,6 @@ class CWEthereum extends Ethereum { Web3Client? getWeb3Client(WalletBase wallet) { return (wallet as EthereumWallet).getWeb3Client(); } + + String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress; } diff --git a/lib/main.dart b/lib/main.dart index 306b109a0..0c8a4c094 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -163,11 +163,11 @@ Future initializeAppConfigs() async { transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, - initialMigrationVersion: 26); + initialMigrationVersion: 27); } Future initialSetup( - {required SharedPreferences sharedPreferences, + {required SharedPreferences sharedPreferences, required Box nodes, required Box powNodes, required Box walletInfoSource, diff --git a/lib/mastodon/mastodon_user.dart b/lib/mastodon/mastodon_user.dart index f5a29f298..1832c083e 100644 --- a/lib/mastodon/mastodon_user.dart +++ b/lib/mastodon/mastodon_user.dart @@ -1,12 +1,14 @@ class MastodonUser { String id; String username; + String profileImageUrl; String acct; String note; MastodonUser({ required this.id, required this.username, + required this.profileImageUrl, required this.acct, required this.note, }); @@ -14,9 +16,10 @@ class MastodonUser { factory MastodonUser.fromJson(Map json) { return MastodonUser( id: json['id'] as String, - username: json['username'] as String, + username: json['username'] as String? ?? '', acct: json['acct'] as String, note: json['note'] as String, + profileImageUrl: json['avatar'] as String? ?? '' ); } } diff --git a/lib/palette.dart b/lib/palette.dart index 46e2e8165..eb0ff50e9 100644 --- a/lib/palette.dart +++ b/lib/palette.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; class Palette { static const Color green = Color.fromRGBO(39, 206, 80, 1.0); static const Color red = Color.fromRGBO(255, 51, 51, 1.0); - static const Color darkRed = Color.fromRGBO(204, 38, 38, 1.0); + static const Color darkRed = Color.fromRGBO(205, 0, 0, 1.0); static const Color blueAlice = Color.fromRGBO(229, 247, 255, 1.0); static const Color lightBlue = Color.fromRGBO(172, 203, 238, 1.0); static const Color lavender = Color.fromRGBO(237, 245, 252, 1.0); @@ -23,6 +23,7 @@ class Palette { static const Color cornflower = Color.fromRGBO(85, 147, 240, 1.0); static const Color royalBlue = Color.fromRGBO(43, 114, 221, 1.0); static const Color lightRed = Color.fromRGBO(227, 87, 87, 1.0); + static const Color veryLightRed = Color.fromRGBO(239, 156, 156, 1.0); static const Color persianRed = Color.fromRGBO(206, 55, 55, 1.0); static const Color blueCraiola = Color.fromRGBO(69, 110, 255, 1.0); static const Color blueGreyCraiola = Color.fromRGBO(106, 177, 207, 1.0); @@ -97,4 +98,8 @@ class PaletteDark { static const Color matrixGreen = Color.fromRGBO(18, 229, 90, 1.0); static const Color moneroOrange = Color.fromRGBO(255, 102, 0, 1.0); static const Color moneroCard = Color.fromRGBO(20, 21, 24, 1.0); + static const Color red = Color.fromRGBO(195, 0, 0, 1.0); + static const Color darkPurple = Color.fromRGBO(109, 14, 210, 1.0); + static const Color cakeBlue = Color.fromRGBO(0, 184, 250, 1.0); + static const Color darkBlue = Color.fromRGBO(0, 123, 168, 1.0); } diff --git a/lib/polygon/cw_polygon.dart b/lib/polygon/cw_polygon.dart index 6e5fbe2c6..0ee7457eb 100644 --- a/lib/polygon/cw_polygon.dart +++ b/lib/polygon/cw_polygon.dart @@ -119,12 +119,12 @@ class CWPolygon extends Polygon { } @override - Future addErc20Token(WalletBase wallet, Erc20Token token) async => - await (wallet as PolygonWallet).addErc20Token(token); + Future addErc20Token(WalletBase wallet, CryptoCurrency token) async => + await (wallet as PolygonWallet).addErc20Token(token as Erc20Token); @override - Future deleteErc20Token(WalletBase wallet, Erc20Token token) async => - await (wallet as PolygonWallet).deleteErc20Token(token); + Future deleteErc20Token(WalletBase wallet, CryptoCurrency token) async => + await (wallet as PolygonWallet).deleteErc20Token(token as Erc20Token); @override Future getErc20Token(WalletBase wallet, String contractAddress) async { @@ -153,4 +153,6 @@ class CWPolygon extends Polygon { Web3Client? getWeb3Client(WalletBase wallet) { return (wallet as PolygonWallet).getWeb3Client(); } + + String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress; } diff --git a/lib/reactions/fiat_rate_update.dart b/lib/reactions/fiat_rate_update.dart index 2b757ad44..fb1d4cd1a 100644 --- a/lib/reactions/fiat_rate_update.dart +++ b/lib/reactions/fiat_rate_update.dart @@ -4,9 +4,11 @@ import 'package:cake_wallet/entities/fiat_api_mode.dart'; 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/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; @@ -35,7 +37,7 @@ Future startFiatRateUpdate( torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly); } - Iterable? currencies; + Iterable? currencies; if (appStore.wallet!.type == WalletType.ethereum) { currencies = ethereum!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); @@ -46,6 +48,12 @@ Future startFiatRateUpdate( polygon!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); } + if (appStore.wallet!.type == WalletType.solana) { + currencies = + solana!.getSPLTokenCurrencies(appStore.wallet!).where((element) => element.enabled); + } + + if (currencies != null) { for (final currency in currencies) { () async { diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index ade9927ff..a2f2491f1 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -3,6 +3,8 @@ import 'package:cake_wallet/entities/fiat_api_mode.dart'; 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: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'; @@ -109,7 +111,7 @@ void startCurrentWalletChangeReaction( fiat: settingsStore.fiatCurrency, torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly); - Iterable? currencies; + Iterable? currencies; if (wallet.type == WalletType.ethereum) { currencies = ethereum!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); @@ -118,7 +120,11 @@ void startCurrentWalletChangeReaction( currencies = polygon!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); } - + if (wallet.type == WalletType.solana) { + currencies = + solana!.getSPLTokenCurrencies(appStore.wallet!).where((element) => element.enabled); + } + if (currencies != null) { for (final currency in currencies) { () async { diff --git a/lib/reactions/wallet_connect.dart b/lib/reactions/wallet_connect.dart index 4f5923e26..f4487123e 100644 --- a/lib/reactions/wallet_connect.dart +++ b/lib/reactions/wallet_connect.dart @@ -1,4 +1,5 @@ -import 'package:cake_wallet/core/wallet_connect/evm_chain_id.dart'; +import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_id.dart'; +import 'package:cake_wallet/core/wallet_connect/chain_service/solana/solana_chain_id.dart'; import 'package:cw_core/wallet_type.dart'; bool isEVMCompatibleChain(WalletType walletType) { @@ -11,12 +12,24 @@ bool isEVMCompatibleChain(WalletType walletType) { } } +bool isWalletConnectCompatibleChain(WalletType walletType) { + switch (walletType) { + case WalletType.polygon: + case WalletType.ethereum: + return true; + default: + return false; + } +} + String getChainNameSpaceAndIdBasedOnWalletType(WalletType walletType) { switch (walletType) { case WalletType.ethereum: return EVMChainId.ethereum.chain(); case WalletType.polygon: return EVMChainId.polygon.chain(); + case WalletType.solana: + return SolanaChainId.mainnet.chain(); default: return ''; } @@ -40,6 +53,8 @@ String getChainNameBasedOnWalletType(WalletType walletType) { return 'eth'; case WalletType.polygon: return 'polygon'; + case WalletType.solana: + return 'solana'; default: return ''; } @@ -51,6 +66,8 @@ String getTokenNameBasedOnWalletType(WalletType walletType) { return 'ETH'; case WalletType.polygon: return 'MATIC'; + case WalletType.solana: + return 'SOL'; default: return ''; } diff --git a/lib/router.dart b/lib/router.dart index b7b7c9a8e..ef7b7f31e 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -532,13 +532,19 @@ Route createRoute(RouteSettings settings) { builder: (_) => getIt.get(param1: title, param2: url)); case Routes.advancedPrivacySettings: - final type = settings.arguments as WalletType; + final args = settings.arguments as Map; + final type = args['type'] as WalletType; + final useTestnet = args['useTestnet'] as bool; + final toggleTestnet = args['toggleTestnet'] as Function(bool? val); return CupertinoPageRoute( builder: (_) => AdvancedPrivacySettingsPage( - getIt.get(param1: type), - getIt.get(param1: type, param2: false), - getIt.get())); + useTestnet, + toggleTestnet, + getIt.get(param1: type), + getIt.get(param1: type, param2: false), + getIt.get(), + )); case Routes.anonPayInvoicePage: final args = settings.arguments as List; diff --git a/lib/solana/cw_solana.dart b/lib/solana/cw_solana.dart new file mode 100644 index 000000000..a86d6b0c6 --- /dev/null +++ b/lib/solana/cw_solana.dart @@ -0,0 +1,118 @@ +part of 'solana.dart'; + +class CWSolana extends Solana { + @override + List getSolanaWordList(String language) => SolanaMnemonics.englishWordlist; + + WalletService createSolanaWalletService(Box walletInfoSource) => + SolanaWalletService(walletInfoSource); + + @override + WalletCredentials createSolanaNewWalletCredentials({ + required String name, + WalletInfo? walletInfo, + }) => + SolanaNewWalletCredentials(name: name, walletInfo: walletInfo); + + @override + WalletCredentials createSolanaRestoreWalletFromSeedCredentials({ + required String name, + required String mnemonic, + required String password, + }) => + SolanaRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic); + + @override + WalletCredentials createSolanaRestoreWalletFromPrivateKey({ + required String name, + required String privateKey, + required String password, + }) => + SolanaRestoreWalletFromPrivateKey(name: name, password: password, privateKey: privateKey); + + @override + String getAddress(WalletBase wallet) => (wallet as SolanaWallet).walletAddresses.address; + + @override + String getPrivateKey(WalletBase wallet) => (wallet as SolanaWallet).privateKey; + + @override + String getPublicKey(WalletBase wallet) => (wallet as SolanaWallet).keys.publicKey.toBase58(); + + @override + Ed25519HDKeyPair? getWalletKeyPair(WalletBase wallet) => (wallet as SolanaWallet).walletKeyPair; + + Object createSolanaTransactionCredentials( + List outputs, { + required CryptoCurrency currency, + }) => + SolanaTransactionCredentials( + 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, + ); + + Object createSolanaTransactionCredentialsRaw( + List outputs, { + required CryptoCurrency currency, + }) => + SolanaTransactionCredentials(outputs, currency: currency); + + @override + List getSPLTokenCurrencies(WalletBase wallet) { + final solanaWallet = wallet as SolanaWallet; + return solanaWallet.splTokenCurrencies; + } + + @override + Future addSPLToken(WalletBase wallet, CryptoCurrency token) async => + await (wallet as SolanaWallet).addSPLToken(token as SPLToken); + + @override + Future deleteSPLToken(WalletBase wallet, CryptoCurrency token) async => + await (wallet as SolanaWallet).deleteSPLToken(token as SPLToken); + + @override + Future getSPLToken(WalletBase wallet, String mintAddress) async { + final solanaWallet = wallet as SolanaWallet; + return await solanaWallet.getSPLToken(mintAddress); + } + + @override + CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) { + transaction as SolanaTransactionInfo; + if (transaction.tokenSymbol == CryptoCurrency.sol.title) { + return CryptoCurrency.sol; + } + + wallet as SolanaWallet; + return wallet.splTokenCurrencies + .firstWhere((element) => transaction.tokenSymbol == element.symbol); + } + + @override + double getTransactionAmountRaw(TransactionInfo transactionInfo) { + return (transactionInfo as SolanaTransactionInfo).solAmount.toDouble(); + } + + @override + String getTokenAddress(CryptoCurrency asset) => (asset as SPLToken).mintAddress; + + @override + List? getValidationLength(CryptoCurrency type) { + if (type is SPLToken) { + return [44]; + } + + return null; + } +} diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index 356c69c00..61e7d6176 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -224,7 +224,7 @@ class _DashboardPageView extends BasePage { .syncedBackgroundColor, ), child: Container( - padding: EdgeInsets.only(left: 32, right: 32), + padding: EdgeInsets.only(left: 24, right: 32), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: MainActions.all 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 a81a3f6e4..17a22a88f 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 @@ -37,6 +37,7 @@ class _DesktopWalletSelectionDropDownState extends State Image.asset( @@ -153,6 +154,8 @@ class _DesktopWalletSelectionDropDownState extends State { void initState() { super.initState(); - if (widget.erc20token != null) { - _contractAddressController.text = widget.erc20token!.contractAddress; - _tokenNameController.text = widget.erc20token!.name; - _tokenSymbolController.text = widget.erc20token!.symbol; - _tokenDecimalController.text = widget.erc20token!.decimal.toString(); + String? address; + + if (widget.token != null) { + address = widget.homeSettingsViewModel.getTokenAddressBasedOnWallet(widget.token!); + + _contractAddressController.text = address ?? ''; + _tokenNameController.text = widget.token!.name; + _tokenSymbolController.text = widget.token!.title; + _tokenDecimalController.text = widget.token!.decimals.toString(); } if (widget.initialContractAddress != null) { @@ -91,7 +96,7 @@ class _EditTokenPageBodyState extends State { } final contractAddress = _contractAddressController.text; - if (contractAddress.isNotEmpty && contractAddress != widget.erc20token?.contractAddress) { + if (contractAddress.isNotEmpty && contractAddress != address) { setState(() { _showDisclaimer = true; }); @@ -139,7 +144,9 @@ class _EditTokenPageBodyState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.normal, - color: Theme.of(context).extension()!.detailsTitlesColor, + color: Theme.of(context) + .extension()! + .detailsTitlesColor, ), ), ), @@ -172,12 +179,12 @@ class _EditTokenPageBodyState extends State { Expanded( child: PrimaryButton( onPressed: () async { - if (widget.erc20token != null) { - await widget.homeSettingsViewModel.deleteErc20Token(widget.erc20token!); + if (widget.token != null) { + await widget.homeSettingsViewModel.deleteToken(widget.token!); } Navigator.pop(context); }, - text: widget.erc20token != null ? S.of(context).delete : S.of(context).cancel, + text: widget.token != null ? S.of(context).delete : S.of(context).cancel, color: Colors.red, textColor: Colors.white, ), @@ -188,7 +195,7 @@ class _EditTokenPageBodyState extends State { onPressed: () async { if (_formKey.currentState!.validate() && (!_showDisclaimer || _disclaimerChecked)) { - await widget.homeSettingsViewModel.addErc20Token(Erc20Token( + await widget.homeSettingsViewModel.addToken(Erc20Token( name: _tokenNameController.text, symbol: _tokenSymbolController.text, contractAddress: _contractAddressController.text, @@ -214,14 +221,13 @@ class _EditTokenPageBodyState extends State { void _getTokenInfo() async { if (_contractAddressController.text.isNotEmpty) { - final token = - await widget.homeSettingsViewModel.getErc20Token(_contractAddressController.text); + final token = await widget.homeSettingsViewModel.getToken(_contractAddressController.text); if (token != null) { if (_tokenNameController.text.isEmpty) _tokenNameController.text = token.name; - if (_tokenSymbolController.text.isEmpty) _tokenSymbolController.text = token.symbol; + if (_tokenSymbolController.text.isEmpty) _tokenSymbolController.text = token.title; if (_tokenDecimalController.text.isEmpty) - _tokenDecimalController.text = token.decimal.toString(); + _tokenDecimalController.text = token.decimals.toString(); } } } diff --git a/lib/src/screens/dashboard/home_settings_page.dart b/lib/src/screens/dashboard/home_settings_page.dart index 618ba49ff..e841423c1 100644 --- a/lib/src/screens/dashboard/home_settings_page.dart +++ b/lib/src/screens/dashboard/home_settings_page.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/entities/sort_balance_types.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/widgets/cake_image_widget.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/themes/extensions/address_theme.dart'; @@ -117,7 +118,7 @@ class HomeSettingsPage extends BasePage { return SettingsSwitcherCell( title: "${token.name} " - "(${token.symbol})", + "(${token.title})", value: token.enabled, onValueChange: (_, bool value) { _homeSettingsViewModel.changeTokenAvailability(token, value); @@ -128,20 +129,16 @@ class HomeSettingsPage extends BasePage { 'token': token, }); }, - leading: token.iconPath != null - ? Container( - child: Image.asset( - token.iconPath!, - height: 30.0, - width: 30.0, - ), - ) - : Container( + leading: CakeImageWidget( + imageUrl: token.iconPath, + height: 40, + width: 40, + displayOnError: Container( height: 30.0, width: 30.0, child: Center( child: Text( - token.symbol.substring(0, min(token.symbol.length, 2)), + token.title.substring(0, min(token.title.length, 2)), style: TextStyle(fontSize: 11), ), ), @@ -149,7 +146,8 @@ class HomeSettingsPage extends BasePage { shape: BoxShape.circle, color: Colors.grey.shade400, ), - ), + ), + ), decoration: BoxDecoration( color: Theme.of(context).cardColor, borderRadius: BorderRadius.circular(30), diff --git a/lib/src/screens/dashboard/pages/address_page.dart b/lib/src/screens/dashboard/pages/address_page.dart index b4460edc9..0d7c4f11c 100644 --- a/lib/src/screens/dashboard/pages/address_page.dart +++ b/lib/src/screens/dashboard/pages/address_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart'; import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; import 'package:cake_wallet/di.dart'; @@ -5,7 +6,7 @@ import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/monero_accounts/monero_account_list_page.dart'; import 'package:cake_wallet/anonpay/anonpay_donation_link_info.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; -import 'package:cake_wallet/entities/receive_page_option.dart'; +import 'package:cw_core/receive_page_option.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/present_receive_option_picker.dart'; import 'package:cake_wallet/src/widgets/gradient_background.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; @@ -69,7 +70,7 @@ class AddressPage extends BasePage { size: 16, ); final _closeButton = - currentTheme.type == ThemeType.dark ? closeButtonImageDarkTheme : closeButtonImage; + currentTheme.type == ThemeType.dark ? closeButtonImageDarkTheme : closeButtonImage; bool isMobileView = responsiveLayoutUtil.shouldRenderMobileUI; @@ -163,11 +164,10 @@ class AddressPage extends BasePage { return SelectButton( text: addressListViewModel.buttonTitle, onTap: () async => dashboardViewModel.isAutoGenerateSubaddressesEnabled && - (WalletType.monero == addressListViewModel.wallet.type || - WalletType.haven == addressListViewModel.wallet.type) + (WalletType.monero == addressListViewModel.wallet.type || + WalletType.haven == addressListViewModel.wallet.type) ? await showPopUp( - context: context, - builder: (_) => getIt.get()) + context: context, builder: (_) => getIt.get()) : Navigator.of(context).pushNamed(Routes.receive), textColor: Theme.of(context).extension()!.textColor, color: Theme.of(context).extension()!.syncedBackgroundColor, @@ -230,6 +230,9 @@ class AddressPage extends BasePage { } break; default: + if (addressListViewModel.type == WalletType.bitcoin) { + addressListViewModel.setAddressType(bitcoin!.getBitcoinAddressType(option)); + } } }); diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index 453adccf5..bb3ec70dc 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/nft_listing_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/home_screen_account_widget.dart'; +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/src/screens/exchange_trade/information_page.dart'; import 'package:cake_wallet/src/widgets/introducing_card.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -333,15 +334,11 @@ class BalanceRowWidget extends StatelessWidget { child: Center( child: Column( children: [ - currency.iconPath != null - ? Container( - child: Image.asset( - currency.iconPath!, - height: 40.0, - width: 40.0, - ), - ) - : Container( + CakeImageWidget( + imageUrl: currency.iconPath, + height: 40, + width: 40, + displayOnError: Container( height: 30.0, width: 30.0, child: Center( @@ -355,6 +352,7 @@ class BalanceRowWidget extends StatelessWidget { color: Colors.grey.shade400, ), ), + ), const SizedBox(height: 10), Text( currency.title, diff --git a/lib/src/screens/dashboard/pages/nft_details_page.dart b/lib/src/screens/dashboard/pages/nft_details_page.dart index bb642fd4b..15d2a2b5c 100644 --- a/lib/src/screens/dashboard/pages/nft_details_page.dart +++ b/lib/src/screens/dashboard/pages/nft_details_page.dart @@ -2,7 +2,7 @@ import 'package:cake_wallet/entities/wallet_nft_response.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/menu_widget.dart'; -import 'package:cake_wallet/src/screens/dashboard/widgets/nft_image_tile_widget.dart'; +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/src/widgets/gradient_background.dart'; import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; @@ -94,7 +94,7 @@ class NFTDetailsPage extends BasePage { .syncedBackgroundColor, ), - child: NFTImageWidget( + child: CakeImageWidget( imageUrl: nftAsset.normalizedMetadata?.imageUrl, ), ), diff --git a/lib/src/screens/dashboard/widgets/home_screen_account_widget.dart b/lib/src/screens/dashboard/widgets/home_screen_account_widget.dart index fa036978f..df20c5c43 100644 --- a/lib/src/screens/dashboard/widgets/home_screen_account_widget.dart +++ b/lib/src/screens/dashboard/widgets/home_screen_account_widget.dart @@ -19,8 +19,8 @@ class HomeScreenAccountWidget extends StatelessWidget { builder: (_) => getIt.get()); }, behavior: HitTestBehavior.opaque, - child: Container( - height: 100.0, + child: Padding( + padding: EdgeInsets.only(top: 25, bottom: 25, left: 25, right: 0), child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index ed9b823ad..acd666025 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -18,23 +18,23 @@ class MenuWidget extends StatefulWidget { class MenuWidgetState extends State { MenuWidgetState() - : this.menuWidth = 0, - this.screenWidth = 0, - this.screenHeight = 0, - this.headerHeight = 120, - this.tileHeight = 60, - this.fromTopEdge = 50, - this.fromBottomEdge = 25, - this.moneroIcon = Image.asset('assets/images/monero_menu.png'), - this.bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png'), - this.litecoinIcon = Image.asset('assets/images/litecoin_menu.png'), - this.havenIcon = Image.asset('assets/images/haven_menu.png'), - this.ethereumIcon = Image.asset('assets/images/eth_icon.png'), - this.nanoIcon = Image.asset('assets/images/nano_icon.png'), - this.bananoIcon = Image.asset('assets/images/nano_icon.png'), + : this.menuWidth = 0, + this.screenWidth = 0, + this.screenHeight = 0, + this.headerHeight = 120, + this.tileHeight = 60, + this.fromTopEdge = 50, + this.fromBottomEdge = 25, + this.moneroIcon = Image.asset('assets/images/monero_menu.png'), + this.bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png'), + this.litecoinIcon = Image.asset('assets/images/litecoin_menu.png'), + this.havenIcon = Image.asset('assets/images/haven_menu.png'), + this.ethereumIcon = Image.asset('assets/images/eth_icon.png'), + this.nanoIcon = Image.asset('assets/images/nano_icon.png'), + 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.polygonIcon = Image.asset('assets/images/matic_icon.png'), + this.solanaIcon = Image.asset('assets/images/sol_icon.png'); final largeScreen = 731; @@ -56,7 +56,7 @@ class MenuWidgetState extends State { Image nanoIcon; Image bananoIcon; Image polygonIcon; - + Image solanaIcon; @override void initState() { @@ -224,6 +224,8 @@ class MenuWidgetState extends State { return bananoIcon; case WalletType.polygon: return polygonIcon; + case WalletType.solana: + return solanaIcon; default: throw Exception('No icon for ${type.toString()}'); } diff --git a/lib/src/screens/dashboard/widgets/nft_tile_widget.dart b/lib/src/screens/dashboard/widgets/nft_tile_widget.dart index e7391b970..4c4d214e7 100644 --- a/lib/src/screens/dashboard/widgets/nft_tile_widget.dart +++ b/lib/src/screens/dashboard/widgets/nft_tile_widget.dart @@ -1,9 +1,8 @@ import 'package:cake_wallet/entities/wallet_nft_response.dart'; import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/dashboard/widgets/nft_image_tile_widget.dart'; +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class NFTTileWidget extends StatelessWidget { @@ -38,7 +37,7 @@ class NFTTileWidget extends StatelessWidget { ), color: Theme.of(context).extension()!.syncedBackgroundColor, ), - child: NFTImageWidget( + child: CakeImageWidget( imageUrl: nftAsset.normalizedMetadata?.imageUrl, ), ), diff --git a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart index a4bd6c7b9..26478345e 100644 --- a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart +++ b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/entities/default_settings_migration.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/seed_phrase_length.dart'; @@ -11,6 +12,7 @@ import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model. 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/settings/choices_list_item.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -19,7 +21,7 @@ import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; class AdvancedPrivacySettingsPage extends BasePage { - AdvancedPrivacySettingsPage( + AdvancedPrivacySettingsPage(this.useTestnet, this.toggleUseTestnet, this.advancedPrivacySettingsViewModel, this.nodeViewModel, this.seedTypeViewModel); final AdvancedPrivacySettingsViewModel advancedPrivacySettingsViewModel; @@ -29,13 +31,16 @@ class AdvancedPrivacySettingsPage extends BasePage { @override String get title => S.current.privacy_settings; + final bool useTestnet; + final Function(bool? val) toggleUseTestnet; + @override - Widget body(BuildContext context) => AdvancedPrivacySettingsBody( + Widget body(BuildContext context) => AdvancedPrivacySettingsBody(useTestnet, toggleUseTestnet, advancedPrivacySettingsViewModel, nodeViewModel, seedTypeViewModel); } class AdvancedPrivacySettingsBody extends StatefulWidget { - const AdvancedPrivacySettingsBody( + const AdvancedPrivacySettingsBody(this.useTestnet, this.toggleUseTestnet, this.privacySettingsViewModel, this.nodeViewModel, this.seedTypeViewModel, {Key? key}) : super(key: key); @@ -44,6 +49,9 @@ class AdvancedPrivacySettingsBody extends StatefulWidget { final NodeCreateOrEditViewModel nodeViewModel; final SeedTypeViewModel seedTypeViewModel; + final bool useTestnet; + final Function(bool? val) toggleUseTestnet; + @override _AdvancedPrivacySettingsBodyState createState() => _AdvancedPrivacySettingsBodyState(); } @@ -52,9 +60,14 @@ class _AdvancedPrivacySettingsBodyState extends State(); + bool? testnetValue; @override Widget build(BuildContext context) { + if (testnetValue == null && widget.useTestnet != null) { + testnetValue = widget.useTestnet; + } + return Container( padding: EdgeInsets.only(top: 24), child: ScrollableWithBottomSection( @@ -125,6 +138,19 @@ class _AdvancedPrivacySettingsBodyState extends State WalletNameForm( - _walletNewVM, currentTheme.type == ThemeType.dark ? walletNameImage : walletNameLightImage, _seedTypeViewModel); + _walletNewVM, + currentTheme.type == ThemeType.dark ? walletNameImage : walletNameLightImage, + _seedTypeViewModel); } class WalletNameForm extends StatefulWidget { @@ -187,7 +189,6 @@ class _WalletNameFormState extends State { ), ), ), - if (_walletNewVM.hasLanguageSelector) ...[ if (_walletNewVM.hasSeedType) ...[ Observer( @@ -222,7 +223,7 @@ class _WalletNameFormState extends State { ), ), ) - ] + ], ], ), ), @@ -245,8 +246,11 @@ class _WalletNameFormState extends State { const SizedBox(height: 25), GestureDetector( onTap: () { - Navigator.of(context) - .pushNamed(Routes.advancedPrivacySettings, arguments: _walletNewVM.type); + Navigator.of(context).pushNamed(Routes.advancedPrivacySettings, arguments: { + "type": _walletNewVM.type, + "useTestnet": _walletNewVM.useTestnet, + "toggleTestnet": _walletNewVM.toggleUseTestnet + }); }, child: Text(S.of(context).advanced_settings), ), diff --git a/lib/src/screens/receive/anonpay_invoice_page.dart b/lib/src/screens/receive/anonpay_invoice_page.dart index fc835c72d..f33cdcc5b 100644 --- a/lib/src/screens/receive/anonpay_invoice_page.dart +++ b/lib/src/screens/receive/anonpay_invoice_page.dart @@ -4,7 +4,7 @@ import 'package:cake_wallet/anonpay/anonpay_donation_link_info.dart'; import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; -import 'package:cake_wallet/entities/receive_page_option.dart'; +import 'package:cw_core/receive_page_option.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/present_receive_option_picker.dart'; import 'package:cake_wallet/src/screens/receive/widgets/anonpay_input_form.dart'; diff --git a/lib/src/screens/receive/anonpay_receive_page.dart b/lib/src/screens/receive/anonpay_receive_page.dart index b602abde6..7d71e3a22 100644 --- a/lib/src/screens/receive/anonpay_receive_page.dart +++ b/lib/src/screens/receive/anonpay_receive_page.dart @@ -1,7 +1,7 @@ import 'package:cake_wallet/anonpay/anonpay_info_base.dart'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/entities/qr_view_data.dart'; -import 'package:cake_wallet/entities/receive_page_option.dart'; +import 'package:cw_core/receive_page_option.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index 899aacd19..fe5ac8487 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -210,8 +210,12 @@ class WalletRestorePage extends BasePage { const SizedBox(height: 25), GestureDetector( onTap: () { - Navigator.of(context).pushNamed(Routes.advancedPrivacySettings, - arguments: walletRestoreViewModel.type); + Navigator.of(context) + .pushNamed(Routes.advancedPrivacySettings, arguments: { + 'type': walletRestoreViewModel.type, + 'useTestnet': walletRestoreViewModel.useTestnet, + 'toggleTestnet': walletRestoreViewModel.toggleUseTestnet + }); }, child: Text(S.of(context).advanced_settings), ), diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 3746118d8..a3b7eaf85 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/core/auth_service.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/src/screens/dashboard/widgets/sync_indicator_icon.dart'; import 'package:cake_wallet/src/screens/send/widgets/send_card.dart'; import 'package:cake_wallet/src/widgets/add_template_button.dart'; @@ -14,6 +15,7 @@ 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/view_model/send/output.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'; @@ -419,7 +421,9 @@ class SendPage extends BasePage { amount: S.of(_dialogContext).send_amount, amountValue: sendViewModel.pendingTransaction!.amountFormatted, fiatAmountValue: sendViewModel.pendingTransactionFiatAmountFormatted, - fee: S.of(_dialogContext).send_fee, + fee: isEVMCompatibleChain(sendViewModel.walletType) + ? S.of(_dialogContext).send_estimated_fee + : S.of(_dialogContext).send_fee, feeValue: sendViewModel.pendingTransaction!.feeFormatted, feeFiatAmount: sendViewModel.pendingTransactionFeeFiatAmountFormatted, outputs: sendViewModel.outputs, @@ -439,10 +443,17 @@ class SendPage extends BasePage { } if (state is TransactionCommitted) { + String alertContent; + if (sendViewModel.walletType == WalletType.solana) { + alertContent = + '${S.of(_dialogContext).send_success(sendViewModel.selectedCryptoCurrency.toString())}. ${S.of(_dialogContext).waitFewSecondForTxUpdate}'; + } else { + alertContent = S.of(_dialogContext).send_success( + sendViewModel.selectedCryptoCurrency.toString()); + } return AlertWithOneAction( alertTitle: '', - alertContent: S.of(_dialogContext).send_success( - sendViewModel.selectedCryptoCurrency.toString()), + alertContent: alertContent, buttonText: S.of(_dialogContext).ok, buttonAction: () { Navigator.of(_dialogContext).pop(); diff --git a/lib/src/screens/send/widgets/extract_address_from_parsed.dart b/lib/src/screens/send/widgets/extract_address_from_parsed.dart index bb09d4ca3..eb997c11b 100644 --- a/lib/src/screens/send/widgets/extract_address_from_parsed.dart +++ b/lib/src/screens/send/widgets/extract_address_from_parsed.dart @@ -11,6 +11,8 @@ Future extractAddressFromParsed( var title = ''; var content = ''; var address = ''; + var profileImageUrl = ''; + var profileName = ''; switch (parsedAddress.parseFrom) { case ParseFrom.unstoppableDomains: @@ -37,16 +39,22 @@ Future extractAddressFromParsed( title = S.of(context).address_detected; content = S.of(context).extracted_address_content('${parsedAddress.name} (Twitter)'); address = parsedAddress.addresses.first; + profileImageUrl = parsedAddress.profileImageUrl; + profileName = parsedAddress.profileName; break; case ParseFrom.mastodon: title = S.of(context).address_detected; content = S.of(context).extracted_address_content('${parsedAddress.name} (Mastodon)'); address = parsedAddress.addresses.first; + profileImageUrl = parsedAddress.profileImageUrl; + profileName = parsedAddress.profileName; break; case ParseFrom.nostr: title = S.of(context).address_detected; content = S.of(context).extracted_address_content('${parsedAddress.name} (Nostr NIP-05)'); address = parsedAddress.addresses.first; + profileImageUrl = parsedAddress.profileImageUrl; + profileName = parsedAddress.profileName; break; case ParseFrom.yatRecord: if (parsedAddress.name.isEmpty) { @@ -95,6 +103,8 @@ Future extractAddressFromParsed( return AlertWithOneAction( alertTitle: title, + headerTitleText: profileName.isEmpty ? null : profileName, + headerImageProfileUrl: profileImageUrl.isEmpty ? null : profileImageUrl, alertContent: content, buttonText: S.of(context).ok, buttonAction: () => Navigator.of(context).pop()); diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 5f15c9c4d..6bd2d81e9 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -321,7 +321,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin Navigator.of(context).pushNamed(Routes.walletConnectConnectionsListing), ), diff --git a/lib/src/screens/settings/other_settings_page.dart b/lib/src/screens/settings/other_settings_page.dart index c88804147..fcf683050 100644 --- a/lib/src/screens/settings/other_settings_page.dart +++ b/lib/src/screens/settings/other_settings_page.dart @@ -26,7 +26,7 @@ class OtherSettingsPage extends BasePage { padding: EdgeInsets.only(top: 10), child: Column( children: [ - if (!_otherSettingsViewModel.changeRepresentativeEnabled) + if (_otherSettingsViewModel.displayTransactionPriority) SettingsPickerCell( title: S.current.settings_fee_priority, items: priorityForWalletType(_otherSettingsViewModel.walletType), diff --git a/lib/src/screens/wallet_connect/widgets/pairing_item_widget.dart b/lib/src/screens/wallet_connect/widgets/pairing_item_widget.dart index 063de8ec3..0d425f904 100644 --- a/lib/src/screens/wallet_connect/widgets/pairing_item_widget.dart +++ b/lib/src/screens/wallet_connect/widgets/pairing_item_widget.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/receive_page_theme.dart'; import 'package:flutter/material.dart'; @@ -26,12 +27,11 @@ class PairingItemWidget extends StatelessWidget { '$year-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}'; return ListTile( - leading: CircleAvatar( - backgroundImage: (metadata.icons.isNotEmpty - ? NetworkImage(metadata.icons[0]) - : const AssetImage( - 'assets/images/default_icon.png', - )) as ImageProvider, + leading: CakeImageWidget( + imageUrl: metadata.icons.isNotEmpty ? metadata.icons[0]: null, + displayOnError: CircleAvatar( + backgroundImage: AssetImage('assets/images/default_icon.png'), + ), ), title: Text( metadata.name, diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index 717bb0a94..b57473cba 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -103,6 +103,7 @@ class WalletListBodyState extends State { final bitcoinCashIcon = Image.asset('assets/images/bch_icon.png', height: 24, width: 24); 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 scrollController = ScrollController(); final double tileHeight = 60; Flushbar? _progressBar; @@ -313,6 +314,8 @@ class WalletListBodyState extends State { return nanoIcon; case WalletType.polygon: return polygonIcon; + case WalletType.solana: + return solanaIcon; default: return nonWalletTypeIcon; } diff --git a/lib/src/widgets/alert_with_one_action.dart b/lib/src/widgets/alert_with_one_action.dart index c06114f5b..7ad0ac1af 100644 --- a/lib/src/widgets/alert_with_one_action.dart +++ b/lib/src/widgets/alert_with_one_action.dart @@ -7,7 +7,9 @@ class AlertWithOneAction extends BaseAlertDialog { required this.alertContent, required this.buttonText, required this.buttonAction, - this.alertBarrierDismissible = true + this.alertBarrierDismissible = true, + this.headerTitleText, + this.headerImageProfileUrl }); final String alertTitle; @@ -15,6 +17,8 @@ class AlertWithOneAction extends BaseAlertDialog { final String buttonText; final VoidCallback buttonAction; final bool alertBarrierDismissible; + final String? headerTitleText; + final String? headerImageProfileUrl; @override String get titleText => alertTitle; @@ -25,6 +29,12 @@ class AlertWithOneAction extends BaseAlertDialog { @override bool get barrierDismissible => alertBarrierDismissible; + @override + String? get headerImageUrl => headerImageProfileUrl; + + @override + String? get headerText => headerTitleText; + @override Widget actionButtons(BuildContext context) { return Container( diff --git a/lib/src/widgets/base_alert_dialog.dart b/lib/src/widgets/base_alert_dialog.dart index 02a1f6ad0..b251e4b45 100644 --- a/lib/src/widgets/base_alert_dialog.dart +++ b/lib/src/widgets/base_alert_dialog.dart @@ -5,19 +5,34 @@ import 'package:cake_wallet/themes/extensions/alert_theme.dart'; import 'package:flutter/material.dart'; class BaseAlertDialog extends StatelessWidget { + String? get headerText => ''; + String get titleText => ''; + String get contentText => ''; + String get leftActionButtonText => ''; + String get rightActionButtonText => ''; + bool get isDividerExists => false; + VoidCallback get actionLeft => () {}; + VoidCallback get actionRight => () {}; + bool get barrierDismissible => true; + Color? get leftActionButtonTextColor => null; + Color? get rightActionButtonTextColor => null; + Color? get leftActionButtonColor => null; + Color? get rightActionButtonColor => null; + String? get headerImageUrl => null; + Widget title(BuildContext context) { return Text( titleText, @@ -32,6 +47,23 @@ class BaseAlertDialog extends StatelessWidget { ); } + Widget headerTitle(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 10.0), + child: Text( + headerText!, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 25, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none, + ), + ), + ); + } + Widget content(BuildContext context) { return Text( contentText, @@ -48,17 +80,17 @@ class BaseAlertDialog extends StatelessWidget { Widget actionButtons(BuildContext context) { return Container( - height: 60, - child: Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ + height: 60, + child: Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ Expanded( child: TextButton( onPressed: actionLeft, style: TextButton.styleFrom( - backgroundColor: leftActionButtonColor ?? - Theme.of(context).dialogBackgroundColor, + backgroundColor: + leftActionButtonColor ?? Theme.of(context).dialogBackgroundColor, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.zero))), child: Text( @@ -79,8 +111,8 @@ class BaseAlertDialog extends StatelessWidget { child: TextButton( onPressed: actionRight, style: TextButton.styleFrom( - backgroundColor: rightActionButtonColor ?? - Theme.of(context).dialogBackgroundColor, + backgroundColor: + rightActionButtonColor ?? Theme.of(context).dialogBackgroundColor, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.zero))), child: Text( @@ -90,8 +122,7 @@ class BaseAlertDialog extends StatelessWidget { fontSize: 15, fontFamily: 'Lato', fontWeight: FontWeight.w600, - color: rightActionButtonTextColor ?? - Theme.of(context).primaryColor, + color: rightActionButtonTextColor ?? Theme.of(context).primaryColor, decoration: TextDecoration.none, ), )), @@ -100,6 +131,24 @@ class BaseAlertDialog extends StatelessWidget { )); } + Widget headerImage(BuildContext context, String imageUrl) { + return Positioned( + top: -50, + left: 0, + right: 0, + child: CircleAvatar( + radius: 50, + backgroundColor: Colors.white, + child: ClipOval( + child: Image.network( + imageUrl, + fit: BoxFit.cover, + ), + ), + ), + ); + } + @override Widget build(BuildContext context) { return GestureDetector( @@ -109,43 +158,51 @@ class BaseAlertDialog extends StatelessWidget { child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0), child: Container( - decoration: BoxDecoration( - color: - Theme.of(context).extension()!.backdropColor), + decoration: + BoxDecoration(color: Theme.of(context).extension()!.backdropColor), child: Center( child: GestureDetector( onTap: () => null, - child: ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(30)), - child: Container( - width: 300, - color: Theme.of(context).dialogBackgroundColor, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: EdgeInsets.fromLTRB(24, 20, 24, 0), - child: title(context), - ), - isDividerExists - ? Padding( - padding: EdgeInsets.only(top: 16, bottom: 8), - child: const HorizontalSectionDivider(), - ) - : Offstage(), - Padding( - padding: EdgeInsets.fromLTRB(24, 8, 24, 32), - child: content(context), - ) - ], - ), - const HorizontalSectionDivider(), - actionButtons(context) - ], - ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: Theme.of(context).dialogBackgroundColor), + width: 300, + child: Stack( + clipBehavior: Clip.none, + children: [ + if (headerImageUrl != null) headerImage(context, headerImageUrl!), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (headerImageUrl != null) const SizedBox(height: 50), + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (headerText != null) headerTitle(context), + Padding( + padding: EdgeInsets.fromLTRB(24, 20, 24, 0), + child: title(context), + ), + isDividerExists + ? Padding( + padding: EdgeInsets.only(top: 16, bottom: 8), + child: const HorizontalSectionDivider(), + ) + : Offstage(), + Padding( + padding: EdgeInsets.fromLTRB(24, 8, 24, 32), + child: content(context), + ) + ], + ), + const HorizontalSectionDivider(), + ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(30)), + child: actionButtons(context)) + ], + ), + ], ), ), ), diff --git a/lib/src/screens/dashboard/widgets/nft_image_tile_widget.dart b/lib/src/widgets/cake_image_widget.dart similarity index 51% rename from lib/src/screens/dashboard/widgets/nft_image_tile_widget.dart rename to lib/src/widgets/cake_image_widget.dart index d34ff02cb..14c62ad34 100644 --- a/lib/src/screens/dashboard/widgets/nft_image_tile_widget.dart +++ b/lib/src/widgets/cake_image_widget.dart @@ -2,25 +2,45 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -class NFTImageWidget extends StatelessWidget { - const NFTImageWidget({ +class CakeImageWidget extends StatelessWidget { + CakeImageWidget({ required this.imageUrl, - }); + Widget? displayOnError, + this.height, + this.width, + }) : _displayOnError = displayOnError ?? Icon(Icons.error); final String? imageUrl; + final double? height; + final double? width; + final Widget? _displayOnError; @override Widget build(BuildContext context) { try { - if (imageUrl == null) return Icon(Icons.error); + if (imageUrl == null) return _displayOnError!; + + if (imageUrl!.contains('assets/images')) { + return Image.asset( + imageUrl!, + height: height, + width: width, + ); + } if (imageUrl!.contains('.svg')) { - return SvgPicture.network(imageUrl!); + return SvgPicture.network( + imageUrl!, + height: height, + width: width, + ); } return Image.network( imageUrl!, fit: BoxFit.cover, + height: height, + width: width, loadingBuilder: (BuildContext _, Widget child, ImageChunkEvent? loadingProgress) { if (loadingProgress == null) { return child; @@ -31,7 +51,7 @@ class NFTImageWidget extends StatelessWidget { errorBuilder: (_, __, ___) => Icon(Icons.error), ); } catch (_) { - return Icon(Icons.error); + return _displayOnError!; } } } diff --git a/lib/src/widgets/scollable_with_bottom_section.dart b/lib/src/widgets/scollable_with_bottom_section.dart index 53f56716c..2487e6130 100644 --- a/lib/src/widgets/scollable_with_bottom_section.dart +++ b/lib/src/widgets/scollable_with_bottom_section.dart @@ -14,37 +14,28 @@ class ScrollableWithBottomSection extends StatefulWidget { final EdgeInsets? bottomSectionPadding; @override - ScrollableWithBottomSectionState createState() => - ScrollableWithBottomSectionState(); + ScrollableWithBottomSectionState createState() => ScrollableWithBottomSectionState(); } -class ScrollableWithBottomSectionState - extends State { +class ScrollableWithBottomSectionState extends State { @override Widget build(BuildContext context) { - return LayoutBuilder(builder: (context, constraints) { - return SingleChildScrollView( - // physics: - // const AlwaysScrollableScrollPhysics(), // const NeverScrollableScrollPhysics(), // - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.heightConstraints().maxHeight), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: widget.contentPadding ?? - EdgeInsets.only(left: 20, right: 20), - child: widget.content, - ), - Padding( - padding: widget.bottomSectionPadding ?? - EdgeInsets.only(bottom: 20, right: 20, left: 20), - child: widget.bottomSection) - ], + return Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: widget.contentPadding ?? EdgeInsets.only(left: 20, right: 20), + child: widget.content, + ), ), ), - ); - }); + Padding( + padding: widget.bottomSectionPadding?.copyWith(top: 10) ?? + EdgeInsets.only(top: 10, bottom: 20, right: 20, left: 20), + child: widget.bottomSection, + ), + ], + ); } } diff --git a/lib/store/app_store.dart b/lib/store/app_store.dart index a5a2b95e0..7d61abfc5 100644 --- a/lib/store/app_store.dart +++ b/lib/store/app_store.dart @@ -41,7 +41,7 @@ abstract class AppStoreBase with Store { this.wallet = wallet; this.wallet!.setExceptionHandler(ExceptionHandler.onError); - if (isEVMCompatibleChain(wallet.type)) { + if (isWalletConnectCompatibleChain(wallet.type)) { await getIt.get().onDispose(); getIt.get().create(); await getIt.get().init(); diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 253adf3ea..6c91d73f3 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -19,6 +19,7 @@ import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; import 'package:cake_wallet/view_model/settings/sync_mode.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/themes/theme_list.dart'; @@ -789,12 +790,16 @@ abstract class SettingsStoreBase with Store { final exchangeStatus = ExchangeApiMode.deserialize( raw: sharedPreferences.getInt(PreferencesKey.exchangeStatusKey) ?? ExchangeApiMode.enabled.raw); - final legacyTheme = (sharedPreferences.getBool(PreferencesKey.isDarkThemeLegacy) ?? false) - ? ThemeType.dark.index - : ThemeType.bright.index; + final bool isNewInstall = sharedPreferences.getBool(PreferencesKey.isNewInstall) ?? true; + final int defaultTheme; + if (isNewInstall) { + defaultTheme = isMoneroOnly ? ThemeList.moneroDarkTheme.raw : ThemeList.brightTheme.raw; + } else { + defaultTheme = ThemeType.bright.index; + } final savedTheme = initialTheme ?? ThemeList.deserialize( - raw: sharedPreferences.getInt(PreferencesKey.currentTheme) ?? legacyTheme); + raw: sharedPreferences.getInt(PreferencesKey.currentTheme) ?? defaultTheme); final actionListDisplayMode = ObservableList(); actionListDisplayMode.addAll(deserializeActionlistDisplayModes( sharedPreferences.getInt(PreferencesKey.displayActionListModeKey) ?? defaultActionsMode)); @@ -838,6 +843,7 @@ abstract class SettingsStoreBase with Store { final polygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey); final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final nanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey); + final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); @@ -847,6 +853,7 @@ abstract class SettingsStoreBase with Store { final bitcoinCashElectrumServer = nodeSource.get(bitcoinCashElectrumServerId); final nanoNode = nodeSource.get(nanoNodeId); final nanoPowNode = powNodeSource.get(nanoPowNodeId); + final solanaNode = nodeSource.get(solanaNodeId); final packageInfo = await PackageInfo.fromPlatform(); final deviceName = await _getDeviceName() ?? ''; final shouldShowYatPopup = sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? true; @@ -903,6 +910,10 @@ abstract class SettingsStoreBase with Store { powNodes[WalletType.nano] = nanoPowNode; } + if (solanaNode != null) { + nodes[WalletType.solana] = solanaNode; + } + final savedSyncMode = SyncMode.all.firstWhere((element) { return element.type.index == (sharedPreferences.getInt(PreferencesKey.syncModeKey) ?? 0); }); @@ -1146,11 +1157,9 @@ abstract class SettingsStoreBase with Store { exchangeStatus = ExchangeApiMode.deserialize( raw: sharedPreferences.getInt(PreferencesKey.exchangeStatusKey) ?? ExchangeApiMode.enabled.raw); - final legacyTheme = (sharedPreferences.getBool(PreferencesKey.isDarkThemeLegacy) ?? false) - ? ThemeType.dark.index - : ThemeType.bright.index; currentTheme = ThemeList.deserialize( - raw: sharedPreferences.getInt(PreferencesKey.currentTheme) ?? legacyTheme); + raw: sharedPreferences.getInt(PreferencesKey.currentTheme) ?? + (isMoneroOnly ? ThemeList.moneroDarkTheme.raw : ThemeList.brightTheme.raw)); actionlistDisplayMode = ObservableList(); actionlistDisplayMode.addAll(deserializeActionlistDisplayModes( sharedPreferences.getInt(PreferencesKey.displayActionListModeKey) ?? defaultActionsMode)); @@ -1190,6 +1199,7 @@ abstract class SettingsStoreBase with Store { final ethereumNodeId = sharedPreferences.getInt(PreferencesKey.currentEthereumNodeIdKey); final polygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey); final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); + final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); @@ -1198,7 +1208,7 @@ abstract class SettingsStoreBase with Store { final polygonNode = nodeSource.get(polygonNodeId); final bitcoinCashNode = nodeSource.get(bitcoinCashElectrumServerId); final nanoNode = nodeSource.get(nanoNodeId); - + final solanaNode = nodeSource.get(solanaNodeId); if (moneroNode != null) { nodes[WalletType.monero] = moneroNode; } @@ -1231,6 +1241,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.nano] = nanoNode; } + if (solanaNode != null) { + nodes[WalletType.solana] = solanaNode; + } + // MIGRATED: useTOTP2FA = await SecureKey.getBool( @@ -1358,6 +1372,9 @@ abstract class SettingsStoreBase with Store { case WalletType.polygon: await _sharedPreferences.setInt(PreferencesKey.currentPolygonNodeIdKey, node.key as int); break; + case WalletType.solana: + await _sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, node.key as int); + break; default: break; } diff --git a/lib/themes/cake_dark_theme.dart b/lib/themes/cake_dark_theme.dart new file mode 100644 index 000000000..262ee1d64 --- /dev/null +++ b/lib/themes/cake_dark_theme.dart @@ -0,0 +1,24 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/themes/monero_dark_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/themes/extensions/menu_theme.dart'; + +class CakeDarkTheme extends MoneroDarkTheme { + CakeDarkTheme({required int raw}) : super(raw: raw); + + @override + String get title => S.current.cake_dark_theme; + @override + Color get primaryColor => PaletteDark.cakeBlue; + + @override + CakeMenuTheme get menuTheme => super.menuTheme.copyWith( + headerFirstGradientColor: PaletteDark.darkBlue, + headerSecondGradientColor: containerColor, + backgroundColor: containerColor, + subnameTextColor: Colors.grey, + dividerColor: colorScheme.secondaryContainer, + iconColor: Colors.white, + settingActionsIconColor: colorScheme.secondaryContainer); +} \ No newline at end of file diff --git a/lib/themes/monero_dark_theme.dart b/lib/themes/monero_dark_theme.dart index 4931d74bc..1478ba8c5 100644 --- a/lib/themes/monero_dark_theme.dart +++ b/lib/themes/monero_dark_theme.dart @@ -95,12 +95,12 @@ class MoneroDarkTheme extends DarkTheme { @override CakeMenuTheme get menuTheme => super.menuTheme.copyWith( - headerFirstGradientColor: containerColor, + headerFirstGradientColor: primaryColor, headerSecondGradientColor: containerColor, backgroundColor: containerColor, subnameTextColor: Colors.grey, dividerColor: colorScheme.secondaryContainer, - iconColor: colorScheme.secondaryContainer, + iconColor: Colors.white, settingActionsIconColor: colorScheme.secondaryContainer); @override diff --git a/lib/themes/red_dark_theme.dart b/lib/themes/red_dark_theme.dart new file mode 100644 index 000000000..d378fc66a --- /dev/null +++ b/lib/themes/red_dark_theme.dart @@ -0,0 +1,13 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/themes/monero_dark_theme.dart'; +import 'package:flutter/material.dart'; + +class RedDarkTheme extends MoneroDarkTheme { + RedDarkTheme({required int raw}) : super(raw: raw); + + @override + String get title => S.current.red_dark_theme; + @override + Color get primaryColor => PaletteDark.red; +} \ No newline at end of file diff --git a/lib/themes/red_light_theme.dart b/lib/themes/red_light_theme.dart new file mode 100644 index 000000000..47a995a11 --- /dev/null +++ b/lib/themes/red_light_theme.dart @@ -0,0 +1,13 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/themes/monero_light_theme.dart'; +import 'package:flutter/material.dart'; + +class RedLightTheme extends MoneroLightTheme { + RedLightTheme({required int raw}) : super(raw: raw); + + @override + String get title => S.current.red_light_theme; + @override + Color get primaryColor => Palette.darkRed; +} \ No newline at end of file diff --git a/lib/themes/theme_list.dart b/lib/themes/theme_list.dart index cb65dc2b9..3ca7346b3 100644 --- a/lib/themes/theme_list.dart +++ b/lib/themes/theme_list.dart @@ -3,23 +3,29 @@ import 'package:cake_wallet/themes/dark_theme.dart'; import 'package:cake_wallet/themes/light_theme.dart'; import 'package:cake_wallet/themes/monero_light_theme.dart'; import 'package:cake_wallet/themes/monero_dark_theme.dart'; +import 'package:cake_wallet/themes/cake_dark_theme.dart'; import 'package:cake_wallet/themes/matrix_green_theme.dart'; import 'package:cake_wallet/themes/bitcoin_dark_theme.dart'; import 'package:cake_wallet/themes/bitcoin_light_theme.dart'; import 'package:cake_wallet/themes/high_contrast_theme.dart'; +import 'package:cake_wallet/themes/red_dark_theme.dart'; +import 'package:cake_wallet/themes/red_light_theme.dart'; import 'package:cake_wallet/themes/theme_base.dart'; class ThemeList { static final all = [ - brightTheme, - lightTheme, + cakeDarkTheme, darkTheme, + lightTheme, moneroDarkTheme, moneroLightTheme, - matrixGreenTheme, + brightTheme, bitcoinDarkTheme, bitcoinLightTheme, - highContrastTheme + matrixGreenTheme, + redDarkTheme, + redLightTheme, + highContrastTheme, ]; static final lightTheme = LightTheme(raw: 0); @@ -31,6 +37,9 @@ class ThemeList { static final bitcoinDarkTheme = BitcoinDarkTheme(raw: 6); static final bitcoinLightTheme = BitcoinLightTheme(raw: 7); static final highContrastTheme = HighContrastTheme(raw: 8); + static final redLightTheme = RedLightTheme(raw: 9); + static final redDarkTheme = RedDarkTheme(raw: 10); + static final cakeDarkTheme = CakeDarkTheme(raw: 11); static ThemeBase deserialize({required int raw}) { switch (raw) { @@ -52,6 +61,12 @@ class ThemeList { return bitcoinLightTheme; case 8: return highContrastTheme; + case 9: + return redLightTheme; + case 10: + return redDarkTheme; + case 11: + return cakeDarkTheme; default: throw Exception( 'Unexpected token raw: $raw for deserialization of ThemeBase'); diff --git a/lib/twitter/twitter_api.dart b/lib/twitter/twitter_api.dart index 24121c9c0..5acb00e2a 100644 --- a/lib/twitter/twitter_api.dart +++ b/lib/twitter/twitter_api.dart @@ -12,7 +12,7 @@ class TwitterApi { static Future lookupUserByName({required String userName}) async { final queryParams = { - 'user.fields': 'description', + 'user.fields': 'description,profile_image_url', 'expansions': 'pinned_tweet_id', 'tweet.fields': 'note_tweet' }; @@ -32,7 +32,10 @@ class TwitterApi { } final Map responseJSON = jsonDecode(response.body) as Map; - if (responseJSON['errors'] != null) { + if (responseJSON['errors'] != null && + !responseJSON['errors'][0]['detail'] + .toString() + .contains("Could not find tweet with pinned_tweet_id")) { throw Exception(responseJSON['errors'][0]['detail']); } @@ -40,20 +43,24 @@ class TwitterApi { } static Tweet? _getPinnedTweet(Map responseJSON) { - final tweetId = responseJSON['data']['pinned_tweet_id'] as String?; - if (tweetId == null || responseJSON['includes'] == null) return null; + try { + final tweetId = responseJSON['data']['pinned_tweet_id'] as String?; + if (tweetId == null || responseJSON['includes'] == null) return null; - final tweetIncludes = List.from(responseJSON['includes']['tweets'] as List); - final pinnedTweetData = tweetIncludes.firstWhere( - (tweet) => tweet['id'] == tweetId, - orElse: () => null, - ) as Map?; + final tweetIncludes = List.from(responseJSON['includes']['tweets'] as List); + final pinnedTweetData = tweetIncludes.firstWhere( + (tweet) => tweet['id'] == tweetId, + orElse: () => null, + ) as Map?; - if (pinnedTweetData == null) return null; + if (pinnedTweetData == null) return null; - final pinnedTweetText = - (pinnedTweetData['note_tweet']?['text'] ?? pinnedTweetData['text']) as String; + final pinnedTweetText = + (pinnedTweetData['note_tweet']?['text'] ?? pinnedTweetData['text']) as String; - return Tweet(id: tweetId, text: pinnedTweetText); + return Tweet(id: tweetId, text: pinnedTweetText); + } catch (e) { + return null; + } } } diff --git a/lib/twitter/twitter_user.dart b/lib/twitter/twitter_user.dart index c0eb5431c..01db25684 100644 --- a/lib/twitter/twitter_user.dart +++ b/lib/twitter/twitter_user.dart @@ -4,20 +4,25 @@ class TwitterUser { required this.username, required this.name, required this.description, + required this.profileImageUrl, this.pinnedTweet}); final String id; final String username; final String name; final String description; + final String profileImageUrl; final Tweet? pinnedTweet; factory TwitterUser.fromJson(Map json, [Tweet? pinnedTweet]) { + final profileImageUrl = json['data']['profile_image_url'] as String? ?? ''; + final scaledProfileImageUrl = profileImageUrl.replaceFirst('normal', '200x200'); return TwitterUser( id: json['data']['id'] as String, - username: json['data']['username'] as String, + username: json['data']['username'] as String? ?? '', name: json['data']['name'] as String, description: json['data']['description'] as String? ?? '', + profileImageUrl: scaledProfileImageUrl, pinnedTweet: pinnedTweet, ); } diff --git a/lib/view_model/advanced_privacy_settings_view_model.dart b/lib/view_model/advanced_privacy_settings_view_model.dart index 75d7a9eb4..b78d831a4 100644 --- a/lib/view_model/advanced_privacy_settings_view_model.dart +++ b/lib/view_model/advanced_privacy_settings_view_model.dart @@ -27,8 +27,25 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { final SettingsStore _settingsStore; - bool get hasSeedPhraseLengthOption => - type == WalletType.bitcoinCash || type == WalletType.ethereum; + bool get hasSeedPhraseLengthOption { + // convert to switch case so that it give a syntax error when adding a new wallet type + // thus we don't forget about it + switch (type) { + case WalletType.ethereum: + case WalletType.bitcoinCash: + case WalletType.polygon: + case WalletType.solana: + return true; + case WalletType.monero: + case WalletType.none: + case WalletType.bitcoin: + case WalletType.litecoin: + case WalletType.haven: + case WalletType.nano: + case WalletType.banano: + return false; + } + } bool get hasSeedTypeOption => type == WalletType.monero; diff --git a/lib/view_model/anon_invoice_page_view_model.dart b/lib/view_model/anon_invoice_page_view_model.dart index 53e8473a0..187eea375 100644 --- a/lib/view_model/anon_invoice_page_view_model.dart +++ b/lib/view_model/anon_invoice_page_view_model.dart @@ -4,7 +4,7 @@ import 'package:cake_wallet/anonpay/anonpay_request.dart'; import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; -import 'package:cake_wallet/entities/receive_page_option.dart'; +import 'package:cw_core/receive_page_option.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/currency.dart'; diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index e2c0382b0..eee53516e 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -18,15 +18,15 @@ import 'package:mobx/mobx.dart'; part 'balance_view_model.g.dart'; class BalanceRecord { - const BalanceRecord({ - required this.availableBalance, - required this.additionalBalance, - required this.frozenBalance, - required this.fiatAvailableBalance, - required this.fiatAdditionalBalance, - required this.fiatFrozenBalance, - required this.asset, - required this.formattedAssetTitle}); + const BalanceRecord( + {required this.availableBalance, + required this.additionalBalance, + required this.frozenBalance, + required this.fiatAvailableBalance, + required this.fiatAdditionalBalance, + required this.fiatFrozenBalance, + required this.asset, + required this.formattedAssetTitle}); final String fiatAdditionalBalance; final String fiatAvailableBalance; final String fiatFrozenBalance; @@ -41,12 +41,10 @@ class BalanceViewModel = BalanceViewModelBase with _$BalanceViewModel; abstract class BalanceViewModelBase with Store { BalanceViewModelBase( - {required this.appStore, - required this.settingsStore, - required this.fiatConvertationStore}) - : isReversing = false, - isShowCard = appStore.wallet!.walletInfo.isShowIntroCakePayCard, - wallet = appStore.wallet! { + {required this.appStore, required this.settingsStore, required this.fiatConvertationStore}) + : isReversing = false, + isShowCard = appStore.wallet!.walletInfo.isShowIntroCakePayCard, + wallet = appStore.wallet! { reaction((_) => appStore.wallet, _onWalletChange); } @@ -60,8 +58,7 @@ abstract class BalanceViewModelBase with Store { bool isReversing; @observable - WalletBase, TransactionInfo> - wallet; + WalletBase, TransactionInfo> wallet; @computed double get price { @@ -82,7 +79,8 @@ abstract class BalanceViewModelBase with Store { bool get isFiatDisabled => settingsStore.fiatApiMode == FiatApiMode.disabled; @computed - bool get isHomeScreenSettingsEnabled => isEVMCompatibleChain(wallet.type); + bool get isHomeScreenSettingsEnabled => + isEVMCompatibleChain(wallet.type) || wallet.type == WalletType.solana; @computed bool get hasAccounts => wallet.type == WalletType.monero; @@ -97,7 +95,7 @@ abstract class BalanceViewModelBase with Store { String get asset { final typeFormatted = walletTypeToString(appStore.wallet!.type); - switch(wallet.type) { + switch (wallet.type) { case WalletType.haven: return '$typeFormatted Assets'; default: @@ -120,13 +118,14 @@ abstract class BalanceViewModelBase with Store { @computed String get availableBalanceLabel { - switch(wallet.type) { + switch (wallet.type) { case WalletType.monero: case WalletType.haven: case WalletType.ethereum: case WalletType.polygon: case WalletType.nano: case WalletType.banano: + case WalletType.solana: return S.current.xmr_available_balance; default: return S.current.confirmed; @@ -135,11 +134,12 @@ abstract class BalanceViewModelBase with Store { @computed String get additionalBalanceLabel { - switch(wallet.type) { + switch (wallet.type) { case WalletType.monero: case WalletType.haven: case WalletType.ethereum: case WalletType.polygon: + case WalletType.solana: return S.current.xmr_full_balance; case WalletType.nano: case WalletType.banano: @@ -228,15 +228,17 @@ abstract class BalanceViewModelBase with Store { Map get balances { return wallet.balance.map((key, value) { if (displayMode == BalanceDisplayMode.hiddenBalance) { - return MapEntry(key, BalanceRecord( - availableBalance: '---', - additionalBalance: '---', - frozenBalance: '---', - fiatAdditionalBalance: isFiatDisabled ? '' : '---', - fiatAvailableBalance: isFiatDisabled ? '' : '---', - fiatFrozenBalance: isFiatDisabled ? '' : '---', - asset: key, - formattedAssetTitle: _formatterAsset(key))); + return MapEntry( + key, + BalanceRecord( + availableBalance: '---', + additionalBalance: '---', + frozenBalance: '---', + fiatAdditionalBalance: isFiatDisabled ? '' : '---', + fiatAvailableBalance: isFiatDisabled ? '' : '---', + fiatFrozenBalance: isFiatDisabled ? '' : '---', + asset: key, + formattedAssetTitle: _formatterAsset(key))); } final fiatCurrency = settingsStore.fiatCurrency; final price = fiatConvertationStore.prices[key] ?? 0; @@ -245,25 +247,23 @@ abstract class BalanceViewModelBase with Store { // throw Exception('Price is null for: $key'); // } - final additionalFiatBalance = isFiatDisabled ? '' : (fiatCurrency.toString() - + ' ' - + _getFiatBalance( - price: price, - cryptoAmount: value.formattedAdditionalBalance)); + final additionalFiatBalance = isFiatDisabled + ? '' + : (fiatCurrency.toString() + + ' ' + + _getFiatBalance(price: price, cryptoAmount: value.formattedAdditionalBalance)); - final availableFiatBalance = isFiatDisabled ? '' : (fiatCurrency.toString() - + ' ' - + _getFiatBalance( - price: price, - cryptoAmount: value.formattedAvailableBalance)); - - - final frozenFiatBalance = isFiatDisabled ? '' : (fiatCurrency.toString() - + ' ' - + _getFiatBalance( - price: price, - cryptoAmount: getFormattedFrozenBalance(value))); + final availableFiatBalance = isFiatDisabled + ? '' + : (fiatCurrency.toString() + + ' ' + + _getFiatBalance(price: price, cryptoAmount: value.formattedAvailableBalance)); + final frozenFiatBalance = isFiatDisabled + ? '' + : (fiatCurrency.toString() + + ' ' + + _getFiatBalance(price: price, cryptoAmount: getFormattedFrozenBalance(value))); return MapEntry( key, @@ -276,12 +276,22 @@ abstract class BalanceViewModelBase with Store { fiatFrozenBalance: frozenFiatBalance, asset: key, formattedAssetTitle: _formatterAsset(key))); - }); + }); } @computed - bool get hasAdditionalBalance => !isEVMCompatibleChain(wallet.type); - + bool get hasAdditionalBalance => _hasAdditionBalanceForWalletType(wallet.type); + + bool _hasAdditionBalanceForWalletType(WalletType type) { + switch (type) { + case WalletType.ethereum: + case WalletType.polygon: + case WalletType.solana: + return false; + default: + return true; + } + } @computed List get formattedBalances { @@ -358,9 +368,7 @@ abstract class BalanceViewModelBase with Store { @action void _onWalletChange( - WalletBase, - TransactionInfo>? - wallet) { + WalletBase, TransactionInfo>? wallet) { if (wallet == null) { return; } @@ -371,7 +379,7 @@ abstract class BalanceViewModelBase with Store { } @action - Future disableIntroCakePayCard () async { + Future disableIntroCakePayCard() async { const cardDisplayStatus = false; wallet.walletInfo.showIntroCakePayCard = cardDisplayStatus; await wallet.walletInfo.save(); @@ -401,6 +409,6 @@ abstract class BalanceViewModelBase with Store { } } - String getFormattedFrozenBalance(Balance walletBalance) => walletBalance.formattedUnAvailableBalance; + String getFormattedFrozenBalance(Balance walletBalance) => + walletBalance.formattedUnAvailableBalance; } - diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index a794c2262..da5eb0373 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -148,17 +148,21 @@ abstract class DashboardViewModelBase with Store { monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id) .toList(); - transactions = ObservableList.of(_accountTransactions.map((transaction) => - TransactionListItem( - transaction: transaction, - balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore))); + final sortedTransactions = [..._accountTransactions]; + sortedTransactions.sort((a, b) => a.date.compareTo(b.date)); + + transactions = ObservableList.of(sortedTransactions.map((transaction) => TransactionListItem( + transaction: transaction, + balanceViewModel: balanceViewModel, + settingsStore: appStore.settingsStore))); } else { - transactions = ObservableList.of(wallet.transactionHistory.transactions.values.map( - (transaction) => TransactionListItem( - transaction: transaction, - balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore))); + final sortedTransactions = [...wallet.transactionHistory.transactions.values]; + sortedTransactions.sort((a, b) => a.date.compareTo(b.date)); + + transactions = ObservableList.of(sortedTransactions.map((transaction) => TransactionListItem( + transaction: transaction, + balanceViewModel: balanceViewModel, + settingsStore: appStore.settingsStore))); } // TODO: nano sub-account generation is disabled: diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart index fc2c27a7c..6d31a5af8 100644 --- a/lib/view_model/dashboard/home_settings_view_model.dart +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -3,6 +3,7 @@ import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/sort_balance_types.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/store/settings_store.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -16,14 +17,14 @@ class HomeSettingsViewModel = HomeSettingsViewModelBase with _$HomeSettingsViewM abstract class HomeSettingsViewModelBase with Store { HomeSettingsViewModelBase(this._settingsStore, this._balanceViewModel) - : tokens = ObservableSet() { + : tokens = ObservableSet() { _updateTokensList(); } final SettingsStore _settingsStore; final BalanceViewModel _balanceViewModel; - final ObservableSet tokens; + final ObservableSet tokens; @observable String searchText = ''; @@ -43,7 +44,7 @@ abstract class HomeSettingsViewModelBase with Store { @action void setPinNativeToken(bool value) => _settingsStore.pinNativeTokenAtTop = value; - Future addErc20Token(Erc20Token token) async { + Future addToken(CryptoCurrency token) async { if (_balanceViewModel.wallet.type == WalletType.ethereum) { await ethereum!.addErc20Token(_balanceViewModel.wallet, token); } @@ -52,23 +53,31 @@ abstract class HomeSettingsViewModelBase with Store { await polygon!.addErc20Token(_balanceViewModel.wallet, token); } + if (_balanceViewModel.wallet.type == WalletType.solana) { + await solana!.addSPLToken(_balanceViewModel.wallet, token); + } + _updateTokensList(); _updateFiatPrices(token); } - Future deleteErc20Token(Erc20Token token) async { + Future deleteToken(CryptoCurrency token) async { if (_balanceViewModel.wallet.type == WalletType.ethereum) { - await ethereum!.deleteErc20Token(_balanceViewModel.wallet, token); + await ethereum!.deleteErc20Token(_balanceViewModel.wallet, token as Erc20Token); } if (_balanceViewModel.wallet.type == WalletType.polygon) { - await polygon!.deleteErc20Token(_balanceViewModel.wallet, token); + await polygon!.deleteErc20Token(_balanceViewModel.wallet, token as Erc20Token); + } + + if (_balanceViewModel.wallet.type == WalletType.solana) { + await solana!.deleteSPLToken(_balanceViewModel.wallet, token); } _updateTokensList(); } - Future getErc20Token(String contractAddress) async { + Future getToken(String contractAddress) async { if (_balanceViewModel.wallet.type == WalletType.ethereum) { return await ethereum!.getErc20Token(_balanceViewModel.wallet, contractAddress); } @@ -77,12 +86,16 @@ abstract class HomeSettingsViewModelBase with Store { return await polygon!.getErc20Token(_balanceViewModel.wallet, contractAddress); } + if (_balanceViewModel.wallet.type == WalletType.solana) { + return await solana!.getSPLToken(_balanceViewModel.wallet, contractAddress); + } + return null; } CryptoCurrency get nativeToken => _balanceViewModel.wallet.currency; - void _updateFiatPrices(Erc20Token token) async { + void _updateFiatPrices(CryptoCurrency token) async { try { _balanceViewModel.fiatConvertationStore.prices[token] = await FiatConversionService.fetchPrice( @@ -92,20 +105,27 @@ abstract class HomeSettingsViewModelBase with Store { } catch (_) {} } - void changeTokenAvailability(Erc20Token token, bool value) async { + void changeTokenAvailability(CryptoCurrency token, bool value) async { token.enabled = value; + if (_balanceViewModel.wallet.type == WalletType.ethereum) { - ethereum!.addErc20Token(_balanceViewModel.wallet, token); + ethereum!.addErc20Token(_balanceViewModel.wallet, token as Erc20Token); } + if (_balanceViewModel.wallet.type == WalletType.polygon) { - polygon!.addErc20Token(_balanceViewModel.wallet, token); + polygon!.addErc20Token(_balanceViewModel.wallet, token as Erc20Token); } + + if (_balanceViewModel.wallet.type == WalletType.solana) { + solana!.addSPLToken(_balanceViewModel.wallet, token); + } + _refreshTokensList(); } @action void _updateTokensList() { - int _sortFunc(Erc20Token e1, Erc20Token e2) { + int _sortFunc(CryptoCurrency e1, CryptoCurrency e2) { int index1 = _balanceViewModel.formattedBalances.indexWhere((element) => element.asset == e1); int index2 = _balanceViewModel.formattedBalances.indexWhere((element) => element.asset == e2); @@ -138,6 +158,14 @@ abstract class HomeSettingsViewModelBase with Store { .toList() ..sort(_sortFunc)); } + + if (_balanceViewModel.wallet.type == WalletType.solana) { + tokens.addAll(solana! + .getSPLTokenCurrencies(_balanceViewModel.wallet) + .where((element) => _matchesSearchText(element)) + .toList() + ..sort(_sortFunc)); + } } @action @@ -153,10 +181,32 @@ abstract class HomeSettingsViewModelBase with Store { _updateTokensList(); } - bool _matchesSearchText(Erc20Token asset) { + bool _matchesSearchText(CryptoCurrency asset) { + final address = getTokenAddressBasedOnWallet(asset); + + // The homes settings would only be displayed for either of Ethereum, Polygon or Solana Wallets. + if (address == null) return false; + return searchText.isEmpty || asset.fullName!.toLowerCase().contains(searchText.toLowerCase()) || asset.title.toLowerCase().contains(searchText.toLowerCase()) || - asset.contractAddress == searchText; + address == searchText; + } + + String? getTokenAddressBasedOnWallet(CryptoCurrency asset) { + if (_balanceViewModel.wallet.type == WalletType.solana) { + return solana!.getTokenAddress(asset); + } + + if (_balanceViewModel.wallet.type == WalletType.ethereum) { + return ethereum!.getTokenAddress(asset); + } + + if (_balanceViewModel.wallet.type == WalletType.polygon) { + 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). + return null; } } diff --git a/lib/view_model/dashboard/receive_option_view_model.dart b/lib/view_model/dashboard/receive_option_view_model.dart index 0eaa2a5f0..1e4726eee 100644 --- a/lib/view_model/dashboard/receive_option_view_model.dart +++ b/lib/view_model/dashboard/receive_option_view_model.dart @@ -1,4 +1,5 @@ -import 'package:cake_wallet/entities/receive_page_option.dart'; +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; @@ -9,11 +10,20 @@ class ReceiveOptionViewModel = ReceiveOptionViewModelBase with _$ReceiveOptionVi abstract class ReceiveOptionViewModelBase with Store { ReceiveOptionViewModelBase(this._wallet, this.initialPageOption) - : selectedReceiveOption = initialPageOption ?? ReceivePageOption.mainnet, + : selectedReceiveOption = initialPageOption ?? + (_wallet.type == WalletType.bitcoin + ? bitcoin!.getSelectedAddressType(_wallet) + : ReceivePageOption.mainnet), _options = [] { final walletType = _wallet.type; - _options = - walletType == WalletType.haven ? [ReceivePageOption.mainnet] : ReceivePageOption.values; + _options = walletType == WalletType.haven + ? [ReceivePageOption.mainnet] + : walletType == WalletType.bitcoin + ? [ + ...bitcoin!.getBitcoinReceivePageOptions(), + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ] + : ReceivePageOptions; } final WalletBase _wallet; diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index d8c4776b7..99de14a18 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -4,6 +4,7 @@ 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/solana/solana.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -105,6 +106,14 @@ class TransactionListItem extends ActionListItem with Keyable { nano!.getTransactionAmountRaw(transaction).toString(), nanoUtil!.rawPerNano)), price: price); break; + case WalletType.solana: + final asset = solana!.assetOfTransaction(balanceViewModel.wallet, transaction); + final price = balanceViewModel.fiatConvertationStore.prices[asset]; + amount = calculateFiatAmountRaw( + cryptoAmount: solana!.getTransactionAmountRaw(transaction), + 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 ad95ad44e..b2894b43d 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -171,9 +171,14 @@ abstract class ExchangeTradeViewModelBase with Store { wallet.currency == CryptoCurrency.maticpoly && tradesStore.trade!.from.tag == CryptoCurrency.maticpoly.tag; + bool _isSplToken() => + wallet.currency == CryptoCurrency.sol && + tradesStore.trade!.from.tag == CryptoCurrency.sol.title; + return tradesStore.trade!.from == wallet.currency || tradesStore.trade!.provider == ExchangeProviderDescription.xmrto || _isEthToken() || - _isPolygonToken(); + _isPolygonToken() || + _isSplToken(); } } diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 649f6f821..605b0728c 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -628,6 +628,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with depositCurrency = CryptoCurrency.maticpoly; receiveCurrency = CryptoCurrency.xmr; break; + case WalletType.solana: + depositCurrency = CryptoCurrency.sol; + receiveCurrency = CryptoCurrency.xmr; + break; default: break; } 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 0fb9a83c6..e323268a0 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 @@ -65,6 +65,8 @@ abstract class NodeCreateOrEditViewModelBase with Store { bool get hasAuthCredentials => _walletType == WalletType.monero || _walletType == WalletType.haven; + bool get hasTestnetSupport => _walletType == WalletType.bitcoin; + String get uri { var uri = address; 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 0cd4d7491..9c2d2611e 100644 --- a/lib/view_model/node_list/node_list_view_model.dart +++ b/lib/view_model/node_list/node_list_view_model.dart @@ -52,7 +52,11 @@ abstract class NodeListViewModelBase with Store { switch (_appStore.wallet!.type) { case WalletType.bitcoin: - node = getBitcoinDefaultElectrumServer(nodes: _nodeSource)!; + if (_appStore.wallet!.isTestnet == true) { + node = getBitcoinTestnetDefaultElectrumServer(nodes: _nodeSource)!; + } else { + node = getBitcoinDefaultElectrumServer(nodes: _nodeSource)!; + } break; case WalletType.monero: node = getMoneroDefaultNode(nodes: _nodeSource); @@ -75,6 +79,9 @@ abstract class NodeListViewModelBase with Store { case WalletType.polygon: node = getPolygonDefaultNode(nodes: _nodeSource)!; break; + case WalletType.solana: + node = getSolanaDefaultNode(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 c8637c4be..31f0bfdd2 100644 --- a/lib/view_model/restore/restore_from_qr_vm.dart +++ b/lib/view_model/restore/restore_from_qr_vm.dart @@ -3,6 +3,7 @@ import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; 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/view_model/restore/restore_mode.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:hive/hive.dart'; @@ -75,6 +76,9 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store case WalletType.polygon: return polygon!.createPolygonRestoreWalletFromPrivateKey( name: name, password: password, privateKey: restoreWallet.privateKey!); + case WalletType.solana: + return solana!.createSolanaRestoreWalletFromPrivateKey( + name: name, password: password, privateKey: restoreWallet.privateKey!); default: throw Exception('Unexpected type: ${restoreWallet.type.toString()}'); } @@ -102,6 +106,9 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store case WalletType.polygon: return polygon!.createPolygonRestoreWalletFromSeedCredentials( name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); + case WalletType.solana: + return solana!.createSolanaRestoreWalletFromSeedCredentials( + 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 bfc9b7980..925c08cca 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -32,6 +32,7 @@ class WalletRestoreFromQRCode { 'bitcoincash': WalletType.bitcoinCash, 'bitcoincash-wallet': WalletType.bitcoinCash, 'bitcoincash_wallet': WalletType.bitcoinCash, + 'solana-wallet': WalletType.solana, }; static bool _containsAssetSpecifier(String code) => _extractWalletType(code) != null; @@ -175,6 +176,14 @@ class WalletRestoreFromQRCode { return WalletRestoreMode.seed; } + if (type == WalletType.solana && 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 371b6fb9b..c881284b3 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -150,9 +150,8 @@ abstract class OutputBase with Store { @computed String get estimatedFeeFiatAmount { try { - final currency = isEVMCompatibleChain(_wallet.type) - ? _wallet.currency - : cryptoCurrencyHandler(); + final currency = + isEVMCompatibleChain(_wallet.type) ? _wallet.currency : cryptoCurrencyHandler(); final fiat = calculateFiatAmountRaw( price: _fiatConversationStore.prices[currency]!, cryptoAmount: estimatedFee); return fiat; @@ -223,7 +222,6 @@ abstract class OutputBase with Store { final crypto = double.parse(fiatAmount.replaceAll(',', '.')) / _fiatConversationStore.prices[cryptoCurrencyHandler()]!; final cryptoAmountTmp = _cryptoNumberFormat.format(crypto); - if (cryptoAmount != cryptoAmountTmp) { cryptoAmount = cryptoAmountTmp; } @@ -255,6 +253,9 @@ abstract class OutputBase with Store { case WalletType.polygon: maximumFractionDigits = 12; break; + case WalletType.solana: + 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 007c4b8c0..f79fbddc7 100644 --- a/lib/view_model/send/send_template_view_model.dart +++ b/lib/view_model/send/send_template_view_model.dart @@ -52,7 +52,8 @@ abstract class SendTemplateViewModelBase with Store { bool get hasMultiRecipient => _wallet.type != WalletType.haven && _wallet.type != WalletType.ethereum && - _wallet.type != WalletType.polygon; + _wallet.type != WalletType.polygon && + _wallet.type != WalletType.solana; @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 ea0ae3e87..779a0187b 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/wallet_contact.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/store/app_store.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'; @@ -44,7 +45,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor void onWalletChange(wallet) { currencies = wallet.balance.keys.toList(); selectedCryptoCurrency = wallet.currency; - hasMultipleTokens = isEVMCompatibleChain(wallet.type); + hasMultipleTokens = isEVMCompatibleChain(wallet.type) || wallet.type == WalletType.solana; } SendViewModelBase( @@ -57,7 +58,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor ) : state = InitialExecutionState(), currencies = appStore.wallet!.balance.keys.toList(), selectedCryptoCurrency = appStore.wallet!.currency, - hasMultipleTokens = isEVMCompatibleChain(appStore.wallet!.type), + hasMultipleTokens = isEVMCompatibleChain(appStore.wallet!.type) || + appStore.wallet!.type == WalletType.solana, outputs = ObservableList(), _settingsStore = appStore.settingsStore, fiatFromSettings = appStore.settingsStore.fiatCurrency, @@ -100,6 +102,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor @computed bool get isBatchSending => outputs.length > 1; + bool get shouldDisplaySendALL => walletType != WalletType.solana; + @computed String get pendingTransactionFiatAmount { if (pendingTransaction == null) { @@ -298,6 +302,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor state = ExecutedSuccessfullyState(); return pendingTransaction; } catch (e) { + print('Failed with ${e.toString()}'); state = FailureState(e.toString()); return null; } @@ -353,7 +358,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor Object _credentials() { final priority = _settingsStore.priority[wallet.type]; - if (priority == null && wallet.type != WalletType.nano) { + if (priority == null && wallet.type != WalletType.nano && wallet.type != WalletType.solana) { throw Exception('Priority is null for wallet type: ${wallet.type}'); } @@ -379,6 +384,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor case WalletType.polygon: return polygon!.createPolygonTransactionCredentials(outputs, priority: priority!, currency: selectedCryptoCurrency); + case WalletType.solana: + return solana! + .createSolanaTransactionCredentials(outputs, currency: selectedCryptoCurrency); default: throw Exception('Unexpected wallet type: ${wallet.type}'); } diff --git a/lib/view_model/settings/other_settings_view_model.dart b/lib/view_model/settings/other_settings_view_model.dart index e44eb8fc7..263532d29 100644 --- a/lib/view_model/settings/other_settings_view_model.dart +++ b/lib/view_model/settings/other_settings_view_model.dart @@ -14,15 +14,14 @@ import 'package:package_info/package_info.dart'; part 'other_settings_view_model.g.dart'; -class OtherSettingsViewModel = OtherSettingsViewModelBase - with _$OtherSettingsViewModel; +class OtherSettingsViewModel = OtherSettingsViewModelBase with _$OtherSettingsViewModel; abstract class OtherSettingsViewModelBase with Store { OtherSettingsViewModelBase(this._settingsStore, this._wallet) : walletType = _wallet.type, currentVersion = '' { - PackageInfo.fromPlatform().then( - (PackageInfo packageInfo) => currentVersion = packageInfo.version); + PackageInfo.fromPlatform() + .then((PackageInfo packageInfo) => currentVersion = packageInfo.version); final priority = _settingsStore.priority[_wallet.type]; final priorities = priorityForWalletType(_wallet.type); @@ -33,8 +32,7 @@ abstract class OtherSettingsViewModelBase with Store { } final WalletType walletType; - final WalletBase, - TransactionInfo> _wallet; + final WalletBase, TransactionInfo> _wallet; @observable String currentVersion; @@ -57,12 +55,14 @@ abstract class OtherSettingsViewModelBase with Store { _wallet.type == WalletType.nano || _wallet.type == WalletType.banano; @computed - bool get isEnabledBuyAction => - !_settingsStore.disableBuy && _wallet.type != WalletType.haven; + bool get displayTransactionPriority => + !(changeRepresentativeEnabled || _wallet.type == WalletType.solana); @computed - bool get isEnabledSellAction => - !_settingsStore.disableSell && _wallet.type != WalletType.haven; + bool get isEnabledBuyAction => !_settingsStore.disableBuy && _wallet.type != WalletType.haven; + + @computed + bool get isEnabledSellAction => !_settingsStore.disableSell && _wallet.type != WalletType.haven; List get availableBuyProvidersTypes { return ProvidersHelper.getAvailableBuyProviderTypes(walletType); @@ -72,12 +72,10 @@ abstract class OtherSettingsViewModelBase with Store { ProvidersHelper.getAvailableSellProviderTypes(walletType); ProviderType get buyProviderType => - _settingsStore.defaultBuyProviders[walletType] ?? - ProviderType.askEachTime; + _settingsStore.defaultBuyProviders[walletType] ?? ProviderType.askEachTime; ProviderType get sellProviderType => - _settingsStore.defaultSellProviders[walletType] ?? - ProviderType.askEachTime; + _settingsStore.defaultSellProviders[walletType] ?? ProviderType.askEachTime; String getDisplayPriority(dynamic priority) { final _priority = priority as TransactionPriority; @@ -114,7 +112,6 @@ abstract class OtherSettingsViewModelBase with Store { _settingsStore.defaultBuyProviders[walletType] = buyProviderType; @action - ProviderType onSellProviderTypeSelected( - ProviderType sellProviderType) => + ProviderType onSellProviderTypeSelected(ProviderType sellProviderType) => _settingsStore.defaultSellProviders[walletType] = sellProviderType; } diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index a3bf281ca..1b1ceb814 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -54,6 +54,9 @@ abstract class TransactionDetailsViewModelBase with Store { case WalletType.polygon: _addPolygonListItems(tx, dateFormat); break; + case WalletType.solana: + _addSolanaListItems(tx, dateFormat); + break; default: break; } @@ -116,7 +119,7 @@ abstract class TransactionDetailsViewModelBase with Store { case WalletType.monero: return 'https://monero.com/tx/${txId}'; case WalletType.bitcoin: - return 'https://mempool.space/tx/${txId}'; + return 'https://mempool.space/${wallet.isTestnet == true ? "testnet/" : ""}tx/${txId}'; case WalletType.litecoin: return 'https://blockchair.com/litecoin/transaction/${txId}'; case WalletType.bitcoinCash: @@ -131,6 +134,8 @@ abstract class TransactionDetailsViewModelBase with Store { return 'https://bananolooker.com/block/${txId}'; case WalletType.polygon: return 'https://polygonscan.com/tx/${txId}'; + case WalletType.solana: + return 'https://solscan.io/tx/${txId}'; default: return ''; } @@ -155,6 +160,8 @@ abstract class TransactionDetailsViewModelBase with Store { return S.current.view_transaction_on + 'bananolooker.com'; case WalletType.polygon: return S.current.view_transaction_on + 'polygonscan.com'; + case WalletType.solana: + return S.current.view_transaction_on + 'solscan.io'; default: return ''; } @@ -281,4 +288,21 @@ abstract class TransactionDetailsViewModelBase with Store { items.addAll(_items); } + + void _addSolanaListItems(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: tx.to!), + if (tx.from != null) + StandartListItem(title: S.current.transaction_details_source_address, value: tx.from!), + ]; + + items.addAll(_items); + } } diff --git a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart index 6380bb07e..3b90aff41 100644 --- a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart +++ b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart @@ -15,7 +15,8 @@ class UnspentCoinsListViewModel = UnspentCoinsListViewModelBase with _$UnspentCo abstract class UnspentCoinsListViewModelBase with Store { UnspentCoinsListViewModelBase( {required this.wallet, required Box unspentCoinsInfo}) - : _unspentCoinsInfo = unspentCoinsInfo { + : _unspentCoinsInfo = unspentCoinsInfo, + _items = ObservableList() { _updateUnspentCoinsInfo(); _updateUnspents(); } @@ -23,7 +24,8 @@ abstract class UnspentCoinsListViewModelBase with Store { WalletBase wallet; final Box _unspentCoinsInfo; - final ObservableList _items = ObservableList(); + @observable + ObservableList _items; @computed ObservableList get items => _items; 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 9270d1d44..a2aab5251 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 @@ -7,6 +7,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; +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'; @@ -159,6 +160,21 @@ class PolygonURI extends PaymentURI { } } +class SolanaURI extends PaymentURI { + SolanaURI({required String amount, required String address}) + : super(amount: amount, address: address); + + @override + String toString() { + var base = 'solana:' + address; + if (amount.isNotEmpty) { + base += '?amount=${amount.replaceAll(',', '.')}'; + } + + return base; + } +} + abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewModel with Store { WalletAddressListViewModelBase({ required AppStore appStore, @@ -257,6 +273,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo return PolygonURI(amount: amount, address: address.address); } + if (wallet.type == WalletType.solana) { + return SolanaURI(amount: amount, address: address.address); + } + throw Exception('Unexpected type: ${type.toString()}'); } @@ -326,6 +346,12 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); } + if (wallet.type == WalletType.solana) { + final primaryAddress = solana!.getAddress(wallet); + + addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); + } + if (searchText.isNotEmpty) { return ObservableList.of(addressList.where((item) { if (item is WalletAddressListItem) { @@ -362,9 +388,6 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo wallet.type == WalletType.bitcoin || wallet.type == WalletType.litecoin; - // wallet.type == WalletType.nano || - // wallet.type == WalletType.banano; TODO: nano accounts are disabled for now - @computed bool get isElectrumWallet => wallet.type == WalletType.bitcoin || @@ -383,16 +406,17 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo void setAddress(WalletAddressListItem address) => wallet.walletAddresses.address = address.address; + @action + Future setAddressType(dynamic option) async { + if (wallet.type == WalletType.bitcoin) { + await bitcoin!.setAddressType(wallet, option); + } + } + void _init() { _baseItems = []; - if (wallet.type == WalletType.monero || - wallet.type == - WalletType - .haven /*|| - wallet.type == WalletType.nano || - wallet.type == WalletType.banano*/ - ) { + if (wallet.type == WalletType.monero || wallet.type == WalletType.haven) { _baseItems.add(WalletAccountListHeader()); } diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 45306905c..4a1e054d6 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -23,6 +23,12 @@ abstract class WalletCreationVMBase with Store { : state = InitialExecutionState(), name = ''; + @observable + bool _useTestnet = false; + + @computed + bool get useTestnet => _useTestnet; + @observable String name; @@ -94,4 +100,9 @@ abstract class WalletCreationVMBase with Store { Future processFromRestoredWallet( WalletCredentials credentials, RestoredWallet restoreWallet) => throw UnimplementedError(); + + @action + void toggleUseTestnet(bool? value) { + _useTestnet = value ?? !_useTestnet; + } } diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index f931fec19..d88316a04 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -20,9 +20,7 @@ abstract class WalletKeysViewModelBase with Store { WalletKeysViewModelBase(this._appStore) : title = _appStore.wallet!.type == WalletType.bitcoin || _appStore.wallet!.type == WalletType.litecoin || - _appStore.wallet!.type == WalletType.bitcoinCash || - _appStore.wallet!.type == WalletType.ethereum || - _appStore.wallet!.type == WalletType.polygon + _appStore.wallet!.type == WalletType.bitcoinCash ? S.current.wallet_seed : S.current.wallet_keys, _restoreHeight = _appStore.wallet!.walletInfo.restoreHeight, @@ -110,7 +108,8 @@ abstract class WalletKeysViewModelBase with Store { ]); } - if (isEVMCompatibleChain(_appStore.wallet!.type)) { + if (isEVMCompatibleChain(_appStore.wallet!.type) || + _appStore.wallet!.type == WalletType.solana) { items.addAll([ if (_appStore.wallet!.privateKey != null) StandartListItem(title: S.current.private_key, value: _appStore.wallet!.privateKey!), @@ -165,6 +164,8 @@ abstract class WalletKeysViewModelBase with Store { return 'banano-wallet'; case WalletType.polygon: return 'polygon-wallet'; + case WalletType.solana: + return 'solana-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 d4a0c4c00..8b19108ec 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -1,5 +1,6 @@ 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:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/monero/monero.dart'; @@ -36,19 +37,21 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { bool get hasLanguageSelector => type == WalletType.monero || type == WalletType.haven; int get seedPhraseWordsLength { - switch (type) { - case WalletType.monero: - if(advancedPrivacySettingsViewModel.isPolySeed) { - return 16; - } - return 25; - case WalletType.ethereum: - case WalletType.bitcoinCash: - return advancedPrivacySettingsViewModel.seedPhraseLength.value; - default: - return 24; - } + switch (type) { + case WalletType.monero: + if (advancedPrivacySettingsViewModel.isPolySeed) { + return 16; + } + return 25; + case WalletType.solana: + case WalletType.polygon: + case WalletType.ethereum: + case WalletType.bitcoinCash: + return advancedPrivacySettingsViewModel.seedPhraseLength.value; + default: + return 24; } + } bool get hasSeedType => type == WalletType.monero; @@ -64,8 +67,8 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { case WalletType.litecoin: return bitcoin!.createBitcoinNewWalletCredentials(name: name); case WalletType.haven: - return haven!.createHavenNewWalletCredentials( - name: name, language: options!.first as String); + return haven! + .createHavenNewWalletCredentials(name: name, language: options!.first as String); case WalletType.ethereum: return ethereum!.createEthereumNewWalletCredentials(name: name); case WalletType.bitcoinCash: @@ -74,6 +77,8 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { return nano!.createNanoNewWalletCredentials(name: name); case WalletType.polygon: return polygon!.createPolygonNewWalletCredentials(name: name); + case WalletType.solana: + return solana!.createSolanaNewWalletCredentials(name: name); default: throw Exception('Unexpected type: ${type.toString()}'); } @@ -82,6 +87,6 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { @override Future process(WalletCredentials credentials) async { walletCreationService.changeWalletType(type: type); - return walletCreationService.create(credentials); + return walletCreationService.create(credentials, isTestnet: useTestnet); } } diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index 8d1e3b223..93ca813d6 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/ethereum/ethereum.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:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/store/app_store.dart'; @@ -28,11 +29,11 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { {required WalletType type}) : hasSeedLanguageSelector = type == WalletType.monero || type == WalletType.haven, hasBlockchainHeightLanguageSelector = type == WalletType.monero || type == WalletType.haven, - hasRestoreFromPrivateKey = - type == WalletType.ethereum || + hasRestoreFromPrivateKey = type == WalletType.ethereum || type == WalletType.polygon || type == WalletType.nano || - type == WalletType.banano, + type == WalletType.banano || + type == WalletType.solana, isButtonEnabled = false, mode = WalletRestoreMode.seed, super(appStore, walletInfoSource, walletCreationService, type: type, isRecovery: true) { @@ -45,6 +46,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { break; case WalletType.nano: case WalletType.banano: + case WalletType.solana: availableModes = [WalletRestoreMode.seed, WalletRestoreMode.keys]; break; default: @@ -98,22 +100,21 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { name: name, height: height, mnemonic: seed, password: password); case WalletType.ethereum: return ethereum!.createEthereumRestoreWalletFromSeedCredentials( - name: name, - mnemonic: seed, - password: password); + name: name, mnemonic: seed, password: password); case WalletType.bitcoinCash: return bitcoinCash!.createBitcoinCashRestoreWalletFromSeedCredentials( - name: name, - mnemonic: seed, - password: password); + name: name, mnemonic: seed, password: password); case WalletType.nano: return nano!.createNanoRestoreWalletFromSeedCredentials( + name: name, mnemonic: seed, password: password, derivationType: derivationType); + case WalletType.polygon: + return polygon!.createPolygonRestoreWalletFromSeedCredentials( name: name, mnemonic: seed, password: password, - derivationType: derivationType); - case WalletType.polygon: - return polygon!.createPolygonRestoreWalletFromSeedCredentials( + ); + case WalletType.solana: + return solana!.createSolanaRestoreWalletFromSeedCredentials( name: name, mnemonic: seed, password: password, @@ -160,16 +161,22 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { case WalletType.nano: return nano!.createNanoRestoreWalletFromKeysCredentials( - name: name, - password: password, - seedKey: options['private_key'] as String, - derivationType: options["derivationType"] as DerivationType); + name: name, + password: password, + seedKey: options['private_key'] as String, + derivationType: options["derivationType"] as DerivationType); case WalletType.polygon: return polygon!.createPolygonRestoreWalletFromPrivateKey( name: name, password: password, privateKey: options['private_key'] as String, ); + case WalletType.solana: + return solana!.createSolanaRestoreWalletFromPrivateKey( + name: name, + password: password, + privateKey: options['private_key'] as String, + ); default: break; } @@ -187,10 +194,8 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { switch (type) { case WalletType.nano: - return nanoUtil!.compareDerivationMethods( - mnemonic: mnemonic, - privateKey: seedKey, - node: node); + return nanoUtil! + .compareDerivationMethods(mnemonic: mnemonic, privateKey: seedKey, node: node); default: break; } @@ -202,9 +207,9 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { @override Future process(WalletCredentials credentials) async { if (mode == WalletRestoreMode.keys) { - return walletCreationService.restoreFromKeys(credentials); + return walletCreationService.restoreFromKeys(credentials, isTestnet: useTestnet); } - return walletCreationService.restoreFromSeed(credentials); + return walletCreationService.restoreFromSeed(credentials, isTestnet: useTestnet); } } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 05996a674..75a78404f 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,7 +5,7 @@ import FlutterMacOS import Foundation -import connectivity_plus_macos +import connectivity_plus import cw_monero import device_info_plus import devicelocale diff --git a/macos/Podfile b/macos/Podfile index fe5678c70..16db2b54c 100644 --- a/macos/Podfile +++ b/macos/Podfile @@ -41,4 +41,20 @@ post_install do |installer| config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '12.0' end end + + installer.aggregate_targets.each do |target| + target.xcconfigs.each do |variant, xcconfig| + xcconfig_path = target.client_root + target.xcconfig_relative_path(variant) + IO.write(xcconfig_path, IO.read(xcconfig_path).gsub("DT_TOOLCHAIN_DIR", "TOOLCHAIN_DIR")) + end + end + + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + if config.base_configuration_reference.is_a? Xcodeproj::Project::Object::PBXFileReference + xcconfig_path = config.base_configuration_reference.real_path + IO.write(xcconfig_path, IO.read(xcconfig_path).gsub("DT_TOOLCHAIN_DIR", "TOOLCHAIN_DIR")) + end + end + end end diff --git a/macos/Podfile.lock b/macos/Podfile.lock index fcbe1d733..106a8a652 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - connectivity_plus_macos (0.0.1): + - connectivity_plus (0.0.1): - FlutterMacOS - ReachabilitySwift - cw_monero (0.0.1): @@ -51,7 +51,7 @@ PODS: - FlutterMacOS DEPENDENCIES: - - connectivity_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus_macos/macos`) + - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) - cw_monero (from `Flutter/ephemeral/.symlinks/plugins/cw_monero/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - devicelocale (from `Flutter/ephemeral/.symlinks/plugins/devicelocale/macos`) @@ -73,8 +73,8 @@ SPEC REPOS: - ReachabilitySwift EXTERNAL SOURCES: - connectivity_plus_macos: - :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus_macos/macos + connectivity_plus: + :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos cw_monero: :path: Flutter/ephemeral/.symlinks/plugins/cw_monero/macos device_info_plus: @@ -105,8 +105,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos SPEC CHECKSUMS: - connectivity_plus_macos: f6e86fd000e971d361e54b5afcadc8c8fa773308 - cw_monero: f8b7f104508efba2591548e76b5c058d05cba3f0 + connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 + cw_monero: ec03de55a19c4a2b174ea687e0f4202edc716fa4 device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f devicelocale: 9f0f36ac651cabae2c33f32dcff4f32b61c38225 flutter_inappwebview_macos: 9600c9df9fdb346aaa8933812009f8d94304203d @@ -123,6 +123,6 @@ SPEC CHECKSUMS: url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 -PODFILE CHECKSUM: 5107934592df7813b33d744aebc8ddc6b5a5445f +PODFILE CHECKSUM: 65ec1541137fb5b35d00490dec1bb48d4d9586bb COCOAPODS: 1.12.1 diff --git a/model_generator.sh b/model_generator.sh index a2b016bb0..8a6098621 100755 --- a/model_generator.sh +++ b/model_generator.sh @@ -5,6 +5,7 @@ cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +cd cw_solana && 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 f38482ec3..d4bf981cd 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -55,7 +55,7 @@ dependencies: basic_utils: ^5.6.1 get_it: ^7.2.0 # connectivity: ^3.0.3 - connectivity_plus: ^2.3.5 + connectivity_plus: ^5.0.2 keyboard_actions: ^4.0.1 another_flushbar: ^1.12.29 archive: ^3.3.0 @@ -106,6 +106,11 @@ dependencies: flutter_svg: ^2.0.9 polyseed: ^0.0.2 nostr_tools: ^1.0.9 + solana: ^0.30.1 + bitcoin_base: + git: + url: https://github.com/cake-tech/bitcoin_base.git + ref: cake-update-v1 dev_dependencies: flutter_test: @@ -152,6 +157,7 @@ flutter: - assets/nano_node_list.yml - assets/nano_pow_node_list.yml - assets/polygon_node_list.yml + - assets/solana_node_list.yml - assets/text/ - assets/faq/ - assets/animation/ diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 409fb415c..b2803407d 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -84,6 +84,7 @@ "buy_with": "اشتر بواسطة", "by_cake_pay": "عن طريق Cake Pay", "cake_2fa_preset": " كعكة 2FA مسبقا", + "cake_dark_theme": "موضوع الكعكة الظلام", "cake_pay_account_note": "قم بالتسجيل باستخدام عنوان بريد إلكتروني فقط لمشاهدة البطاقات وشرائها. حتى أن بعضها متوفر بسعر مخفض!", "cake_pay_learn_more": "شراء واسترداد بطاقات الهدايا على الفور في التطبيق!\nاسحب من اليسار إلى اليمين لمعرفة المزيد.", "cake_pay_subtitle": "شراء بطاقات هدايا مخفضة السعر (الولايات المتحدة فقط)", @@ -429,6 +430,7 @@ "provider_error": "خطأ ${provider}", "public_key": "مفتاح عمومي", "purchase_gift_card": "شراء بطاقة هدايا", + "purple_dark_theme": "موضوع الظلام الأرجواني", "qr_fullscreen": "انقر لفتح ال QR بملء الشاشة", "qr_payment_amount": "يحتوي هذا ال QR على مبلغ الدفع. هل تريد تغير المبلغ فوق القيمة الحالية؟", "question_to_disable_2fa": "هل أنت متأكد أنك تريد تعطيل Cake 2FA؟ لن تكون هناك حاجة إلى رمز 2FA للوصول إلى المحفظة ووظائف معينة.", @@ -440,6 +442,8 @@ "reconnect": "أعد الاتصال", "reconnect_alert_text": "هل أنت متأكد من رغبتك في إعادة الاتصال؟", "reconnection": "إعادة الاتصال", + "red_dark_theme": "موضوع الظلام الأحمر", + "red_light_theme": "موضوع الضوء الأحمر", "redeemed": "استردت", "refund_address": "عنوان إعادة المال", "reject": "ﺾﻓﺮﻳ", @@ -719,6 +723,7 @@ "use_card_info_two": "يتم تحويل الأموال إلى الدولار الأمريكي عند الاحتفاظ بها في الحساب المدفوع مسبقًا ، وليس بالعملات الرقمية.", "use_ssl": "استخدم SSL", "use_suggested": "استخدام المقترح", + "use_testnet": "استخدم testnet", "variable_pair_not_supported": "هذا الزوج المتغير غير مدعوم في التبادلات المحددة", "verification": "تَحَقّق", "verify_with_2fa": "تحقق مع Cake 2FA", @@ -728,6 +733,7 @@ "view_key_private": "مفتاح العرض (خاص)", "view_key_public": "مفتاح العرض (عام)", "view_transaction_on": "عرض العملية على", + "waitFewSecondForTxUpdate": "ﺕﻼﻣﺎﻌﻤﻟﺍ ﻞﺠﺳ ﻲﻓ ﺔﻠﻣﺎﻌﻤﻟﺍ ﺲﻜﻌﻨﺗ ﻰﺘﺣ ﻥﺍﻮﺛ ﻊﻀﺒﻟ ﺭﺎﻈﺘﻧﻻﺍ ﻰﺟﺮﻳ", "wallet_keys": "سييد المحفظة / المفاتيح", "wallet_list_create_new_wallet": "إنشاء محفظة جديدة", "wallet_list_edit_wallet": "تحرير المحفظة", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 39e9b0d2a..abdb3d032 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -84,6 +84,7 @@ "buy_with": "Купуване чрез", "by_cake_pay": "от Cake Pay", "cake_2fa_preset": "Торта 2FA Preset", + "cake_dark_theme": "Торта тъмна тема", "cake_pay_account_note": "Регистрайте се само с един имейл, за да виждате и купувате карти. За някои има дори и отстъпка!", "cake_pay_learn_more": "Купете и използвайте гифткарти директно в приложението!\nПлъзнете отляво надясно, за да научите още.", "cake_pay_subtitle": "Купете гифткарти на намалени цени (само за САЩ)", @@ -429,6 +430,7 @@ "provider_error": "Грешка на ${provider} ", "public_key": "Публичен ключ", "purchase_gift_card": "Купуване на Gift Card", + "purple_dark_theme": "Лилава тъмна тема", "qr_fullscreen": "Натиснете, за да отворите QR кода на цял екран", "qr_payment_amount": "Този QR код съдържа сума за плащане. Искате ли да промените стойността?", "question_to_disable_2fa": "Сигурни ли сте, че искате да деактивирате Cake 2FA? Вече няма да е необходим 2FA код за достъп до портфейла и определени функции.", @@ -440,6 +442,8 @@ "reconnect": "Reconnect", "reconnect_alert_text": "Сигурни ли сте, че искате да се свържете отново?", "reconnection": "Свързване отново", + "red_dark_theme": "Червена тъмна тема", + "red_light_theme": "Тема на червената светлина", "redeemed": "Използвани", "refund_address": "Refund address", "reject": "Отхвърляне", @@ -719,6 +723,7 @@ "use_card_info_two": "Средствата се обръщат в USD, когато биват запазени в предплатената карта, а не в дигитална валута.", "use_ssl": "Използване на SSL", "use_suggested": "Използване на предложеното", + "use_testnet": "Използвайте TestNet", "variable_pair_not_supported": "Този variable pair не се поддържа от избраната борса", "verification": "Потвърждаване", "verify_with_2fa": "Проверете с Cake 2FA", @@ -728,6 +733,7 @@ "view_key_private": "View key (таен)", "view_key_public": "View key (публичен)", "view_transaction_on": "Вижте транзакция на ", + "waitFewSecondForTxUpdate": "Моля, изчакайте няколко секунди, докато транзакцията се отрази в историята на транзакциите", "wallet_keys": "Seed/keys на портфейла", "wallet_list_create_new_wallet": "Създаване на нов портфейл", "wallet_list_edit_wallet": "Редактиране на портфейла", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index f2a25134d..c5cc6a78c 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -84,6 +84,7 @@ "buy_with": "Nakoupit pomocí", "by_cake_pay": "od Cake Pay", "cake_2fa_preset": "Předvolba Cake 2FA", + "cake_dark_theme": "Dort tmavé téma", "cake_pay_account_note": "Přihlaste se svou e-mailovou adresou pro zobrazení a nákup karet. Některé jsou dostupné ve slevě!", "cake_pay_learn_more": "Okamžitý nákup a uplatnění dárkových karet v aplikaci!\nPřejeďte prstem zleva doprava pro další informace.", "cake_pay_subtitle": "Kupte si zlevněné dárkové karty (pouze USA)", @@ -429,6 +430,7 @@ "provider_error": "${provider} chyba", "public_key": "Veřejný klíč", "purchase_gift_card": "Objednat dárkovou kartu", + "purple_dark_theme": "Fialové temné téma", "qr_fullscreen": "Poklepáním otevřete QR kód na celé obrazovce", "qr_payment_amount": "Tento QR kód obsahuje i částku. Chcete přepsat současnou hodnotu?", "question_to_disable_2fa": "Opravdu chcete deaktivovat Cake 2FA? Pro přístup k peněžence a některým funkcím již nebude potřeba kód 2FA.", @@ -440,6 +442,8 @@ "reconnect": "Znovu připojit", "reconnect_alert_text": "Opravdu se chcete znovu připojit?", "reconnection": "Znovu připojit", + "red_dark_theme": "Červené temné téma", + "red_light_theme": "Téma červeného světla", "redeemed": "Uplatněné", "refund_address": "Adresa pro vrácení", "reject": "Odmítnout", @@ -719,6 +723,7 @@ "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.", "use_ssl": "Použít SSL", "use_suggested": "Použít doporučený", + "use_testnet": "Použijte testNet", "variable_pair_not_supported": "Tento pár s tržním kurzem není ve zvolené směnárně podporován", "verification": "Ověření", "verify_with_2fa": "Ověřte pomocí Cake 2FA", @@ -728,6 +733,7 @@ "view_key_private": "Klíč pro zobrazení (soukromý)", "view_key_public": "Klíč pro zobrazení (veřejný)", "view_transaction_on": "Zobrazit transakci na ", + "waitFewSecondForTxUpdate": "Počkejte několik sekund, než se transakce projeví v historii transakcí", "wallet_keys": "Seed/klíče peněženky", "wallet_list_create_new_wallet": "Vytvořit novou peněženku", "wallet_list_edit_wallet": "Upravit peněženku", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 9358f924d..4a140777a 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -84,6 +84,7 @@ "buy_with": "Kaufen mit", "by_cake_pay": "von Cake Pay", "cake_2fa_preset": "Kuchen 2FA-Voreinstellung", + "cake_dark_theme": "Cake Dark Thema", "cake_pay_account_note": "Melden Sie sich nur mit einer E-Mail-Adresse an, um Karten anzuzeigen und zu kaufen. Einige sind sogar mit Rabatt erhältlich!", "cake_pay_learn_more": "Kaufen und lösen Sie Geschenkkarten sofort in der App ein!\nWischen Sie von links nach rechts, um mehr zu erfahren.", "cake_pay_subtitle": "Kaufen Sie ermäßigte Geschenkkarten (nur USA)", @@ -409,8 +410,8 @@ "placeholder_transactions": "Ihre Transaktionen werden hier angezeigt", "please_fill_totp": "Bitte geben Sie den 8-stelligen Code ein, der auf Ihrem anderen Gerät vorhanden ist", "please_make_selection": "Bitte treffen Sie unten eine Auswahl zum Erstellen oder Wiederherstellen Ihrer Wallet.", - "Please_reference_document": "Weitere Informationen finden Sie in den Dokumenten unten.", "please_reference_document": "Bitte verweisen Sie auf die folgenden Dokumente, um weitere Informationen zu erhalten.", + "Please_reference_document": "Weitere Informationen finden Sie in den Dokumenten unten.", "please_select": "Bitte auswählen:", "please_select_backup_file": "Bitte wählen Sie die Sicherungsdatei und geben Sie das Sicherungskennwort ein.", "please_try_to_connect_to_another_node": "Bitte versuchen Sie, sich mit einem anderen Knoten zu verbinden", @@ -430,6 +431,7 @@ "provider_error": "${provider}-Fehler", "public_key": "Öffentlicher Schlüssel", "purchase_gift_card": "Geschenkkarte kaufen", + "purple_dark_theme": "Lila dunkle Thema", "qr_fullscreen": "Tippen Sie hier, um den QR-Code im Vollbildmodus zu öffnen", "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", "question_to_disable_2fa": "Sind Sie sicher, dass Sie Cake 2FA deaktivieren möchten? Für den Zugriff auf die Wallet und bestimmte Funktionen wird kein 2FA-Code mehr benötigt.", @@ -441,6 +443,8 @@ "reconnect": "Erneut verbinden", "reconnect_alert_text": "Sind Sie sicher, dass Sie sich neu verbinden möchten?", "reconnection": "Neu verbinden", + "red_dark_theme": "Red Dark Thema", + "red_light_theme": "Rotlichtthema", "redeemed": "Versilbert", "refund_address": "Rückerstattungsadresse", "reject": "Ablehnen", @@ -721,6 +725,7 @@ "use_card_info_two": "Guthaben werden auf dem Prepaid-Konto in USD umgerechnet, nicht in digitale Währung.", "use_ssl": "SSL verwenden", "use_suggested": "Vorgeschlagen verwenden", + "use_testnet": "TESTNET verwenden", "variable_pair_not_supported": "Dieses Variablenpaar wird von den ausgewählten Börsen nicht unterstützt", "verification": "Verifizierung", "verify_with_2fa": "Verifizieren Sie mit Cake 2FA", @@ -730,6 +735,7 @@ "view_key_private": "View Key (geheim)", "view_key_public": "View Key (öffentlich)", "view_transaction_on": "Anzeigen der Transaktion auf ", + "waitFewSecondForTxUpdate": "Bitte warten Sie einige Sekunden, bis die Transaktion im Transaktionsverlauf angezeigt wird", "waiting_payment_confirmation": "Warte auf Zahlungsbestätigung", "wallet_keys": "Wallet-Seed/-Schlüssel", "wallet_list_create_new_wallet": "Neue Wallet erstellen", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index acf1cc8e0..323a0478d 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -84,6 +84,7 @@ "buy_with": "Buy with", "by_cake_pay": "by Cake Pay", "cake_2fa_preset": "Cake 2FA Preset", + "cake_dark_theme": "Cake Dark Theme", "cake_pay_account_note": "Sign up with just an email address to see and purchase cards. Some are even available at a discount!", "cake_pay_learn_more": "Instantly purchase and redeem gift cards in the app!\nSwipe left to right to learn more.", "cake_pay_subtitle": "Buy discounted gift cards (USA only)", @@ -429,6 +430,7 @@ "provider_error": "${provider} error", "public_key": "Public key", "purchase_gift_card": "Purchase Gift Card", + "purple_dark_theme": "Purple Dark Theme", "qr_fullscreen": "Tap to open full screen QR code", "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", "question_to_disable_2fa": "Are you sure that you want to disable Cake 2FA? A 2FA code will no longer be needed to access the wallet and certain functions.", @@ -440,6 +442,8 @@ "reconnect": "Reconnect", "reconnect_alert_text": "Are you sure you want to reconnect?", "reconnection": "Reconnection", + "red_dark_theme": "Red Dark Theme", + "red_light_theme": "Red Light Theme", "redeemed": "Redeemed", "refund_address": "Refund address", "reject": "Reject", @@ -719,6 +723,7 @@ "use_card_info_two": "Funds are converted to USD when they're held in the prepaid account, not in digital currencies.", "use_ssl": "Use SSL", "use_suggested": "Use Suggested", + "use_testnet": "Use Testnet", "variable_pair_not_supported": "This variable pair is not supported with the selected exchanges", "verification": "Verification", "verify_with_2fa": "Verify with Cake 2FA", @@ -728,6 +733,7 @@ "view_key_private": "View key (private)", "view_key_public": "View key (public)", "view_transaction_on": "View Transaction on ", + "waitFewSecondForTxUpdate": "Kindly wait for a few seconds for transaction to reflect in transactions history", "wallet_keys": "Wallet seed/keys", "wallet_list_create_new_wallet": "Create New Wallet", "wallet_list_edit_wallet": "Edit wallet", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 4a9efbe9f..4691dd70c 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -84,6 +84,7 @@ "buy_with": "Compra con", "by_cake_pay": "por Cake Pay", "cake_2fa_preset": "Pastel 2FA preestablecido", + "cake_dark_theme": "Tema oscuro del pastel", "cake_pay_account_note": "Regístrese con solo una dirección de correo electrónico para ver y comprar tarjetas. ¡Algunas incluso están disponibles con descuento!", "cake_pay_learn_more": "¡Compre y canjee tarjetas de regalo al instante en la aplicación!\nDeslice el dedo de izquierda a derecha para obtener más información.", "cake_pay_subtitle": "Compre tarjetas de regalo con descuento (solo EE. UU.)", @@ -430,6 +431,7 @@ "provider_error": "${provider} error", "public_key": "Clave pública", "purchase_gift_card": "Comprar tarjeta de regalo", + "purple_dark_theme": "Tema morado oscuro", "qr_fullscreen": "Toque para abrir el código QR en pantalla completa", "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", "question_to_disable_2fa": "¿Está seguro de que desea deshabilitar Cake 2FA? Ya no se necesitará un código 2FA para acceder a la billetera y a ciertas funciones.", @@ -441,6 +443,8 @@ "reconnect": "Volver a conectar", "reconnect_alert_text": "¿Estás seguro de reconectar?", "reconnection": "Reconexión", + "red_dark_theme": "Tema rojo oscuro", + "red_light_theme": "Tema de la luz roja", "redeemed": "Redimido", "refund_address": "Dirección de reembolso", "reject": "Rechazar", @@ -720,6 +724,7 @@ "use_card_info_two": "Los fondos se convierten a USD cuando se mantienen en la cuenta prepaga, no en monedas digitales.", "use_ssl": "Utilice SSL", "use_suggested": "Usar sugerido", + "use_testnet": "Use TestNet", "variable_pair_not_supported": "Este par de variables no es compatible con los intercambios seleccionados", "verification": "Verificación", "verify_with_2fa": "Verificar con Cake 2FA", @@ -729,6 +734,7 @@ "view_key_private": "View clave (privado)", "view_key_public": "View clave (público)", "view_transaction_on": "View Transaction on ", + "waitFewSecondForTxUpdate": "Espere unos segundos para que la transacción se refleje en el historial de transacciones.", "wallet_keys": "Billetera semilla/claves", "wallet_list_create_new_wallet": "Crear nueva billetera", "wallet_list_edit_wallet": "Editar billetera", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index d4547f2da..3257c57a2 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -84,6 +84,7 @@ "buy_with": "Acheter avec", "by_cake_pay": "par Cake Pay", "cake_2fa_preset": "Cake 2FA prédéfini", + "cake_dark_theme": "Thème sombre du gâteau", "cake_pay_account_note": "Inscrivez-vous avec juste une adresse e-mail pour voir et acheter des cartes. Certaines sont même disponibles à prix réduit !", "cake_pay_learn_more": "Achetez et utilisez instantanément des cartes-cadeaux dans l'application !\nBalayer de gauche à droite pour en savoir plus.", "cake_pay_subtitle": "Achetez des cartes-cadeaux à prix réduit (États-Unis uniquement)", @@ -429,6 +430,7 @@ "provider_error": "Erreur de ${provider}", "public_key": "Clef publique", "purchase_gift_card": "Acheter une carte-cadeau", + "purple_dark_theme": "THÈME PURPLE DARK", "qr_fullscreen": "Appuyez pour ouvrir le QR code en mode plein écran", "qr_payment_amount": "Ce QR code contient un montant de paiement. Voulez-vous remplacer la valeur actuelle ?", "question_to_disable_2fa": "Êtes-vous sûr de vouloir désactiver Cake 2FA ? Un code 2FA ne sera plus nécessaire pour accéder au portefeuille (wallet) et à certaines fonctions.", @@ -440,6 +442,8 @@ "reconnect": "Reconnecter", "reconnect_alert_text": "Êtes vous certain de vouloir vous reconnecter ?", "reconnection": "Reconnexion", + "red_dark_theme": "Thème rouge noir", + "red_light_theme": "Thème de la lumière rouge", "redeemed": "Converties", "refund_address": "Adresse de Remboursement", "reject": "Rejeter", @@ -719,6 +723,7 @@ "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.", "use_ssl": "Utiliser SSL", "use_suggested": "Suivre la suggestion", + "use_testnet": "Utiliser TestNet", "variable_pair_not_supported": "Cette paire variable n'est pas prise en charge avec les échanges sélectionnés", "verification": "Vérification", "verify_with_2fa": "Vérifier avec Cake 2FA", @@ -728,6 +733,7 @@ "view_key_private": "Clef d'audit (view key) (privée)", "view_key_public": "Clef d'audit (view key) (publique)", "view_transaction_on": "Voir la Transaction sur ", + "waitFewSecondForTxUpdate": "Veuillez attendre quelques secondes pour que la transaction soit reflétée dans l'historique des transactions.", "wallet_keys": "Phrase secrète (seed)/Clefs du portefeuille (wallet)", "wallet_list_create_new_wallet": "Créer un Nouveau Portefeuille (Wallet)", "wallet_list_edit_wallet": "Modifier le portefeuille", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index e08665385..3994fd14c 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -84,6 +84,7 @@ "buy_with": "Saya da", "by_cake_pay": "da Cake Pay", "cake_2fa_preset": "Cake 2FA saiti", + "cake_dark_theme": "Cake Dark Jigo", "cake_pay_account_note": "Yi rajista tare da adireshin imel kawai don gani da siyan katunan. Wasu ma suna samuwa a rangwame!", "cake_pay_learn_more": "Nan take siya ku kwaso katunan kyaututtuka a cikin app!\nTake hagu zuwa dama don ƙarin koyo.", "cake_pay_subtitle": "Sayi katunan kyauta masu rahusa (Amurka kawai)", @@ -431,6 +432,7 @@ "provider_error": "${provider} kuskure", "public_key": "Maɓallin jama'a", "purchase_gift_card": "Katin Kyautar Sayi", + "purple_dark_theme": "M duhu jigo", "qr_fullscreen": "Matsa don buɗe lambar QR na cikakken allo", "qr_payment_amount": "Wannan QR code yana da adadin kuɗi. Kuna so ku overwrite wannan adadi?", "question_to_disable_2fa": "Ka tabbata cewa kana son kashe cake 2fa? Ba za a sake buƙatar lambar 2FA ba don samun damar yin walat da takamaiman ayyuka.", @@ -442,6 +444,8 @@ "reconnect": "Sake haɗawa", "reconnect_alert_text": "Shin kun tabbata kuna son sake haɗawa?", "reconnection": "Sake haɗawa", + "red_dark_theme": "Ja duhu taken", + "red_light_theme": "Ja mai haske", "redeemed": "An fanshi", "refund_address": "Adireshin maidowa", "reject": "Ƙi", @@ -721,6 +725,7 @@ "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.", "use_ssl": "Yi amfani da SSL", "use_suggested": "Amfani da Shawarwari", + "use_testnet": "Amfani da gwaji", "variable_pair_not_supported": "Ba a samun goyan bayan wannan m biyu tare da zaɓaɓɓun musayar", "verification": "tabbatar", "verify_with_2fa": "Tabbatar da Cake 2FA", @@ -730,6 +735,7 @@ "view_key_private": "Duba maɓallin (maɓallin kalmar sirri)", "view_key_public": "Maɓallin Duba (maɓallin jama'a)", "view_transaction_on": "Dubo aikace-aikacen akan", + "waitFewSecondForTxUpdate": "Da fatan za a jira ƴan daƙiƙa don ciniki don yin tunani a tarihin ma'amala", "wallet_keys": "Iri/maɓalli na walat", "wallet_list_create_new_wallet": "Ƙirƙiri Sabon Wallet", "wallet_list_edit_wallet": "Gyara walat", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index f7f1519e8..8e0f18972 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -84,6 +84,7 @@ "buy_with": "के साथ खरीदें", "by_cake_pay": "केकपे द्वारा", "cake_2fa_preset": "केक 2एफए प्रीसेट", + "cake_dark_theme": "केक डार्क थीम", "cake_pay_account_note": "कार्ड देखने और खरीदने के लिए केवल एक ईमेल पते के साथ साइन अप करें। कुछ छूट पर भी उपलब्ध हैं!", "cake_pay_learn_more": "ऐप में उपहार कार्ड तुरंत खरीदें और रिडीम करें!\nअधिक जानने के लिए बाएं से दाएं स्वाइप करें।", "cake_pay_subtitle": "रियायती उपहार कार्ड खरीदें (केवल यूएसए)", @@ -430,6 +431,7 @@ "provider_error": "${provider} त्रुटि", "public_key": "सार्वजनिक कुंजी", "purchase_gift_card": "गिफ्ट कार्ड खरीदें", + "purple_dark_theme": "पर्पल डार्क थीम", "qr_fullscreen": "फ़ुल स्क्रीन क्यूआर कोड खोलने के लिए टैप करें", "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", "question_to_disable_2fa": "क्या आप सुनिश्चित हैं कि आप Cake 2FA को अक्षम करना चाहते हैं? वॉलेट और कुछ कार्यों तक पहुँचने के लिए अब 2FA कोड की आवश्यकता नहीं होगी।", @@ -442,6 +444,8 @@ "reconnect": "रिकनेक्ट", "reconnect_alert_text": "क्या आप पुन: कनेक्ट होना सुनिश्चित करते हैं?", "reconnection": "पुनर्संयोजन", + "red_dark_theme": "लाल डार्क थीम", + "red_light_theme": "लाल प्रकाश थीम", "redeemed": "रिडीम किया गया", "refund_address": "वापसी का पता", "reject": "अस्वीकार करना", @@ -721,6 +725,7 @@ "use_card_info_two": "डिजिटल मुद्राओं में नहीं, प्रीपेड खाते में रखे जाने पर निधियों को यूएसडी में बदल दिया जाता है।", "use_ssl": "उपयोग SSL", "use_suggested": "सुझाए गए का प्रयोग करें", + "use_testnet": "टेस्टनेट का उपयोग करें", "variable_pair_not_supported": "यह परिवर्तनीय जोड़ी चयनित एक्सचेंजों के साथ समर्थित नहीं है", "verification": "सत्यापन", "verify_with_2fa": "केक 2FA के साथ सत्यापित करें", @@ -730,6 +735,7 @@ "view_key_private": "कुंजी देखें(निजी)", "view_key_public": "कुंजी देखें (जनता)", "view_transaction_on": "View Transaction on ", + "waitFewSecondForTxUpdate": "लेन-देन इतिहास में लेन-देन प्रतिबिंबित होने के लिए कृपया कुछ सेकंड प्रतीक्षा करें", "wallet_keys": "बटुआ बीज / चाबियाँ", "wallet_list_create_new_wallet": "नया बटुआ बनाएँ", "wallet_list_edit_wallet": "बटुआ संपादित करें", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 12907a17e..b3a80ffc7 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -84,6 +84,7 @@ "buy_with": "Kupite s", "by_cake_pay": "od Cake Paya", "cake_2fa_preset": "Cake 2FA Preset", + "cake_dark_theme": "TOKA DARKA TEMA", "cake_pay_account_note": "Prijavite se samo s adresom e-pošte da biste vidjeli i kupili kartice. Neke su čak dostupne uz popust!", "cake_pay_learn_more": "Azonnal vásárolhat és válthat be ajándékutalványokat az alkalmazásban!\nTovábbi információért csúsztassa balról jobbra az ujját.", "cake_pay_subtitle": "Kupite darovne kartice s popustom (samo SAD)", @@ -429,6 +430,7 @@ "provider_error": "${provider} greška", "public_key": "Javni ključ", "purchase_gift_card": "Kupnja darovne kartice", + "purple_dark_theme": "Ljubičasta tamna tema", "qr_fullscreen": "Dodirnite za otvaranje QR koda preko cijelog zaslona", "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", "question_to_disable_2fa": "Jeste li sigurni da želite onemogućiti Cake 2FA? 2FA kod više neće biti potreban za pristup novčaniku i određenim funkcijama.", @@ -440,6 +442,8 @@ "reconnect": "Ponovno povezivanje", "reconnect_alert_text": "Jeste li sigurni da se želite ponovno povezati?", "reconnection": "Ponovno povezivanje", + "red_dark_theme": "Crvena tamna tema", + "red_light_theme": "Tema crvenog svjetla", "redeemed": "otkupljeno", "refund_address": "Adresa za povrat", "reject": "Odbiti", @@ -719,6 +723,7 @@ "use_card_info_two": "Sredstva se pretvaraju u USD kada se drže na prepaid računu, a ne u digitalnim valutama.", "use_ssl": "Koristi SSL", "use_suggested": "Koristite predloženo", + "use_testnet": "Koristite TestNet", "variable_pair_not_supported": "Ovaj par varijabli nije podržan s odabranim burzama", "verification": "Potvrda", "verify_with_2fa": "Provjerite s Cake 2FA", @@ -728,6 +733,7 @@ "view_key_private": "View key (privatni)", "view_key_public": "View key (javni)", "view_transaction_on": "View Transaction on ", + "waitFewSecondForTxUpdate": "Pričekajte nekoliko sekundi da se transakcija prikaže u povijesti transakcija", "wallet_keys": "Pristupni izraz/ključ novčanika", "wallet_list_create_new_wallet": "Izradi novi novčanik", "wallet_list_edit_wallet": "Uredi novčanik", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index ba8f3f623..b9cd89b99 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -84,6 +84,7 @@ "buy_with": "Beli dengan", "by_cake_pay": "oleh Cake Pay", "cake_2fa_preset": "Preset Kue 2FA", + "cake_dark_theme": "Tema Kue Gelap", "cake_pay_account_note": "Daftar hanya dengan alamat email untuk melihat dan membeli kartu. Beberapa di antaranya bahkan tersedia dengan diskon!", "cake_pay_learn_more": "Beli dan tukar kartu hadiah secara instan di aplikasi!\nGeser ke kanan untuk informasi lebih lanjut.", "cake_pay_subtitle": "Beli kartu hadiah dengan harga diskon (hanya USA)", @@ -431,6 +432,7 @@ "provider_error": "${provider} error", "public_key": "Kunci publik", "purchase_gift_card": "Beli Kartu Hadiah", + "purple_dark_theme": "Tema gelap ungu", "qr_fullscreen": "Tap untuk membuka layar QR code penuh", "qr_payment_amount": "QR code ini berisi jumlah pembayaran. Apakah Anda ingin menimpa nilai saat ini?", "question_to_disable_2fa": "Apakah Anda yakin ingin menonaktifkan Cake 2FA? Kode 2FA tidak lagi diperlukan untuk mengakses dompet dan fungsi tertentu.", @@ -442,6 +444,8 @@ "reconnect": "Sambungkan kembali", "reconnect_alert_text": "Apakah Anda yakin ingin menyambungkan kembali?", "reconnection": "Koneksi kembali", + "red_dark_theme": "Tema gelap merah", + "red_light_theme": "Tema lampu merah", "redeemed": "Ditukar", "refund_address": "Alamat pengembalian", "reject": "Menolak", @@ -722,6 +726,7 @@ "use_card_info_two": "Dana dikonversi ke USD ketika disimpan dalam akun pra-bayar, bukan dalam mata uang digital.", "use_ssl": "Gunakan SSL", "use_suggested": "Gunakan yang Disarankan", + "use_testnet": "Gunakan TestNet", "variable_pair_not_supported": "Pasangan variabel ini tidak didukung dengan bursa yang dipilih", "verification": "Verifikasi", "verify_with_2fa": "Verifikasi dengan Cake 2FA", @@ -731,6 +736,7 @@ "view_key_private": "Kunci tampilan (privat)", "view_key_public": "Kunci tampilan (publik)", "view_transaction_on": "Lihat Transaksi di ", + "waitFewSecondForTxUpdate": "Mohon tunggu beberapa detik hingga transaksi terlihat di riwayat transaksi", "wallet_keys": "Seed/kunci dompet", "wallet_list_create_new_wallet": "Buat Dompet Baru", "wallet_list_edit_wallet": "Edit dompet", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index fb447bd24..921bab78d 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -84,6 +84,7 @@ "buy_with": "Acquista con", "by_cake_pay": "da Cake Pay", "cake_2fa_preset": "Torta 2FA Preset", + "cake_dark_theme": "Tema oscuro della torta", "cake_pay_account_note": "Iscriviti con solo un indirizzo email per vedere e acquistare le carte. Alcune sono anche disponibili con uno sconto!", "cake_pay_learn_more": "Acquista e riscatta istantaneamente carte regalo nell'app!\nScorri da sinistra a destra per saperne di più.", "cake_pay_subtitle": "Acquista buoni regalo scontati (solo USA)", @@ -431,6 +432,7 @@ "provider_error": "${provider} errore", "public_key": "Chiave pubblica", "purchase_gift_card": "Acquista carta regalo", + "purple_dark_theme": "Tema oscuro viola", "qr_fullscreen": "Tocca per aprire il codice QR a schermo intero", "qr_payment_amount": "Questo codice QR contiene l'ammontare del pagamento. Vuoi sovrascrivere il varlore attuale?", "question_to_disable_2fa": "Sei sicuro di voler disabilitare Cake 2FA? Non sarà più necessario un codice 2FA per accedere al portafoglio e ad alcune funzioni.", @@ -442,6 +444,8 @@ "reconnect": "Riconnetti", "reconnect_alert_text": "Sei sicuro di volerti riconnettere?", "reconnection": "Riconnessione", + "red_dark_theme": "Red Dark Theme", + "red_light_theme": "Tema della luce rossa", "redeemed": "Redento", "refund_address": "Indirizzo di rimborso", "reject": "Rifiutare", @@ -721,6 +725,7 @@ "use_card_info_two": "I fondi vengono convertiti in USD quando sono detenuti nel conto prepagato, non in valute digitali.", "use_ssl": "Usa SSL", "use_suggested": "Usa suggerito", + "use_testnet": "Usa TestNet", "variable_pair_not_supported": "Questa coppia di variabili non è supportata con gli scambi selezionati", "verification": "Verifica", "verify_with_2fa": "Verifica con Cake 2FA", @@ -730,6 +735,7 @@ "view_key_private": "Chiave di visualizzazione (privata)", "view_key_public": "Chiave di visualizzazione (pubblica)", "view_transaction_on": "View Transaction on ", + "waitFewSecondForTxUpdate": "Attendi qualche secondo affinché la transazione venga riflessa nella cronologia delle transazioni", "waiting_payment_confirmation": "In attesa di conferma del pagamento", "wallet_keys": "Seme Portafoglio /chiavi", "wallet_list_create_new_wallet": "Crea Nuovo Portafoglio", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 361792b0f..c4d0299e6 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -84,6 +84,7 @@ "buy_with": "で購入", "by_cake_pay": "by Cake Pay", "cake_2fa_preset": "ケーキ 2FA プリセット", + "cake_dark_theme": "ケーキ暗いテーマ", "cake_pay_account_note": "メールアドレスだけでサインアップして、カードを表示して購入できます。割引価格で利用できるカードもあります!", "cake_pay_learn_more": "アプリですぐにギフトカードを購入して引き換えましょう!\n左から右にスワイプして詳細をご覧ください。", "cake_pay_subtitle": "割引ギフトカードを購入する (米国のみ)", @@ -430,6 +431,7 @@ "provider_error": "${provider} エラー", "public_key": "公開鍵", "purchase_gift_card": "ギフトカードを購入", + "purple_dark_theme": "紫色の暗いテーマ", "qr_fullscreen": "タップして全画面QRコードを開く", "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", "question_to_disable_2fa": "Cake 2FA を無効にしてもよろしいですか?ウォレットと特定の機能にアクセスするために 2FA コードは必要なくなります。", @@ -441,6 +443,8 @@ "reconnect": "再接続", "reconnect_alert_text": "再接続しますか?", "reconnection": "再接続", + "red_dark_theme": "赤い暗いテーマ", + "red_light_theme": "赤色光のテーマ", "redeemed": "償還", "refund_address": "払い戻し住所", "reject": "拒否する", @@ -720,6 +724,7 @@ "use_card_info_two": "デジタル通貨ではなく、プリペイドアカウントで保持されている場合、資金は米ドルに変換されます。", "use_ssl": "SSLを使用する", "use_suggested": "推奨を使用", + "use_testnet": "テストネットを使用します", "variable_pair_not_supported": "この変数ペアは、選択した取引所ではサポートされていません", "verification": "検証", "verify_with_2fa": "Cake 2FA で検証する", @@ -729,6 +734,7 @@ "view_key_private": "ビューキー (プライベート)", "view_key_public": "ビューキー (パブリック)", "view_transaction_on": "View Transaction on ", + "waitFewSecondForTxUpdate": "取引履歴に取引が反映されるまで数秒お待ちください。", "wallet_keys": "ウォレットシード/キー", "wallet_list_create_new_wallet": "新しいウォレットを作成", "wallet_list_edit_wallet": "ウォレットを編集する", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index a294b5b62..b59110990 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -84,6 +84,7 @@ "buy_with": "구매", "by_cake_pay": "Cake Pay로", "cake_2fa_preset": "케이크 2FA 프리셋", + "cake_dark_theme": "케이크 다크 테마", "cake_pay_account_note": "이메일 주소로 가입하면 카드를 보고 구매할 수 있습니다. 일부는 할인된 가격으로 사용 가능합니다!", "cake_pay_learn_more": "앱에서 즉시 기프트 카드를 구매하고 사용하세요!\n자세히 알아보려면 왼쪽에서 오른쪽으로 스와이프하세요.", "cake_pay_subtitle": "할인된 기프트 카드 구매(미국만 해당)", @@ -409,8 +410,8 @@ "placeholder_transactions": "거래가 여기에 표시됩니다", "please_fill_totp": "다른 기기에 있는 8자리 코드를 입력하세요.", "please_make_selection": "아래에서 선택하십시오 지갑 만들기 또는 복구.", - "Please_reference_document": "자세한 내용은 아래 문서를 참조하십시오.", "please_reference_document": "자세한 내용은 아래 문서를 참조하십시오.", + "Please_reference_document": "자세한 내용은 아래 문서를 참조하십시오.", "please_select": "선택 해주세요:", "please_select_backup_file": "백업 파일을 선택하고 백업 암호를 입력하십시오.", "please_try_to_connect_to_another_node": "다른 노드에 연결을 시도하십시오", @@ -430,6 +431,7 @@ "provider_error": "${provider} 오류", "public_key": "공개 키", "purchase_gift_card": "기프트 카드 구매", + "purple_dark_theme": "보라색 어두운 테마", "qr_fullscreen": "전체 화면 QR 코드를 열려면 탭하세요.", "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", "question_to_disable_2fa": "Cake 2FA를 비활성화하시겠습니까? 지갑 및 특정 기능에 액세스하는 데 더 이상 2FA 코드가 필요하지 않습니다.", @@ -441,6 +443,8 @@ "reconnect": "다시 연결", "reconnect_alert_text": "다시 연결 하시겠습니까?", "reconnection": "재 연결", + "red_dark_theme": "빨간 어두운 테마", + "red_light_theme": "빨간불 테마", "redeemed": "구함", "refund_address": "환불 주소", "reject": "거부하다", @@ -720,6 +724,7 @@ "use_card_info_two": "디지털 화폐가 아닌 선불 계정에 보유하면 자금이 USD로 변환됩니다.", "use_ssl": "SSL 사용", "use_suggested": "추천 사용", + "use_testnet": "TestNet을 사용하십시오", "variable_pair_not_supported": "이 변수 쌍은 선택한 교환에서 지원되지 않습니다.", "verification": "검증", "verify_with_2fa": "케이크 2FA로 확인", @@ -729,6 +734,7 @@ "view_key_private": "키보기(은밀한)", "view_key_public": "키보기 (공공의)", "view_transaction_on": "View Transaction on ", + "waitFewSecondForTxUpdate": "거래 내역에 거래가 반영될 때까지 몇 초 정도 기다려 주세요.", "wallet_keys": "지갑 시드 / 키", "wallet_list_create_new_wallet": "새 월렛 만들기", "wallet_list_edit_wallet": "지갑 수정", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 9abfee29d..9a30ac5c5 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -84,6 +84,7 @@ "buy_with": "အတူဝယ်ပါ။", "by_cake_pay": "Cake Pay ဖြင့်", "cake_2fa_preset": "ကိတ်မုန့် 2FA ကြိုတင်သတ်မှတ်", + "cake_dark_theme": "ကိတ်မုန့် Dark Theme", "cake_pay_account_note": "ကတ်များကြည့်ရှုဝယ်ယူရန် အီးမေးလ်လိပ်စာတစ်ခုဖြင့် စာရင်းသွင်းပါ။ အချို့ကို လျှော့ဈေးဖြင့်ပင် ရနိုင်သည်။", "cake_pay_learn_more": "အက်ပ်ရှိ လက်ဆောင်ကတ်များကို ချက်ချင်းဝယ်ယူပြီး ကူပွန်ဖြင့် လဲလှယ်ပါ။\nပိုမိုလေ့လာရန် ဘယ်မှညာသို့ ပွတ်ဆွဲပါ။", "cake_pay_subtitle": "လျှော့စျေးလက်ဆောင်ကတ်များဝယ်ပါ (USA သာ)", @@ -429,6 +430,7 @@ "provider_error": "${provider} အမှား", "public_key": "အများသူငှာသော့", "purchase_gift_card": "လက်ဆောင်ကတ်ဝယ်ပါ။", + "purple_dark_theme": "ခရမ်းရောင် Drwing Theme", "qr_fullscreen": "မျက်နှာပြင်အပြည့် QR ကုဒ်ကိုဖွင့်ရန် တို့ပါ။", "qr_payment_amount": "ဤ QR ကုဒ်တွင် ငွေပေးချေမှုပမာဏတစ်ခုပါရှိသည်။ လက်ရှိတန်ဖိုးကို ထပ်ရေးလိုပါသလား။", "question_to_disable_2fa": "Cake 2FA ကို ပိတ်လိုသည်မှာ သေချာပါသလား။ ပိုက်ဆံအိတ်နှင့် အချို့သောလုပ်ဆောင်ချက်များကို အသုံးပြုရန်အတွက် 2FA ကုဒ်တစ်ခု မလိုအပ်တော့ပါ။", @@ -440,6 +442,8 @@ "reconnect": "ပြန်လည်ချိတ်ဆက်ပါ။", "reconnect_alert_text": "ပြန်လည်ချိတ်ဆက်လိုသည်မှာ သေချာပါသလား။ ?", "reconnection": "ပြန်လည်ချိတ်ဆက်မှု", + "red_dark_theme": "အနီရောင်မှောင်မိုက်ဆောင်ပုဒ်", + "red_light_theme": "အနီရောင်အလင်းအကြောင်းအရာ", "redeemed": "ရွေးနှုတ်ခဲ့သည်။", "refund_address": "ပြန်အမ်းငွေလိပ်စာ", "reject": "ငြင်းပယ်ပါ။", @@ -719,6 +723,7 @@ "use_card_info_two": "ဒစ်ဂျစ်တယ်ငွေကြေးများဖြင့်မဟုတ်ဘဲ ကြိုတင်ငွေပေးချေသည့်အကောင့်တွင် သိမ်းထားသည့်အခါ ရန်ပုံငွေများကို USD သို့ ပြောင်းလဲပါသည်။", "use_ssl": "SSL ကိုသုံးပါ။", "use_suggested": "အကြံပြုထားသည်ကို အသုံးပြုပါ။", + "use_testnet": "testnet ကိုသုံးပါ", "variable_pair_not_supported": "ရွေးချယ်ထားသော ဖလှယ်မှုများဖြင့် ဤပြောင်းလဲနိုင်သောအတွဲကို ပံ့ပိုးမထားပါ။", "verification": "စိစစ်ခြင်း။", "verify_with_2fa": "Cake 2FA ဖြင့် စစ်ဆေးပါ။", @@ -728,6 +733,7 @@ "view_key_private": "သော့ကိုကြည့်ရန် (သီးသန့်)", "view_key_public": "သော့ကိုကြည့်ရန် (အများပြည်သူ)", "view_transaction_on": "ငွေလွှဲခြင်းကို ဖွင့်ကြည့်ပါ။", + "waitFewSecondForTxUpdate": "ငွေပေးငွေယူ မှတ်တမ်းတွင် ရောင်ပြန်ဟပ်ရန် စက္ကန့်အနည်းငယ်စောင့်ပါ။", "wallet_keys": "ပိုက်ဆံအိတ် အစေ့/သော့များ", "wallet_list_create_new_wallet": "Wallet အသစ်ဖန်တီးပါ။", "wallet_list_edit_wallet": "ပိုက်ဆံအိတ်ကို တည်းဖြတ်ပါ။", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 7c4f8aa37..215f9148f 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -84,6 +84,7 @@ "buy_with": "Koop met", "by_cake_pay": "door Cake Pay", "cake_2fa_preset": "Taart 2FA Voorinstelling", + "cake_dark_theme": "Cake Dark Theme", "cake_pay_account_note": "Meld u aan met alleen een e-mailadres om kaarten te bekijken en te kopen. Sommige zijn zelfs met korting verkrijgbaar!", "cake_pay_learn_more": "Koop en wissel cadeaubonnen direct in de app in!\nSwipe van links naar rechts voor meer informatie.", "cake_pay_subtitle": "Koop cadeaubonnen met korting (alleen VS)", @@ -429,6 +430,7 @@ "provider_error": "${provider} fout", "public_key": "Publieke sleutel", "purchase_gift_card": "Cadeaubon kopen", + "purple_dark_theme": "Paars donker thema", "qr_fullscreen": "Tik om de QR-code op volledig scherm te openen", "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", "question_to_disable_2fa": "Weet je zeker dat je Cake 2FA wilt uitschakelen? Er is geen 2FA-code meer nodig om toegang te krijgen tot de portemonnee en bepaalde functies.", @@ -440,6 +442,8 @@ "reconnect": "Sluit", "reconnect_alert_text": "Weet u zeker dat u opnieuw verbinding wilt maken?", "reconnection": "Reconnection", + "red_dark_theme": "Rood donker thema", + "red_light_theme": "Rood licht thema", "redeemed": "Verzilverd", "refund_address": "Adres voor terugbetaling", "reject": "Afwijzen", @@ -719,6 +723,7 @@ "use_card_info_two": "Tegoeden worden omgezet naar USD wanneer ze op de prepaid-rekening staan, niet in digitale valuta.", "use_ssl": "Gebruik SSL", "use_suggested": "Gebruik aanbevolen", + "use_testnet": "Gebruik testnet", "variable_pair_not_supported": "Dit variabelenpaar wordt niet ondersteund met de geselecteerde uitwisselingen", "verification": "Verificatie", "verify_with_2fa": "Controleer met Cake 2FA", @@ -728,6 +733,7 @@ "view_key_private": "Bekijk sleutel (privaat)", "view_key_public": "Bekijk sleutel (openbaar)", "view_transaction_on": "View Transaction on ", + "waitFewSecondForTxUpdate": "Wacht een paar seconden totdat de transactie wordt weergegeven in de transactiegeschiedenis", "waiting_payment_confirmation": "In afwachting van betalingsbevestiging", "wallet_keys": "Portemonnee zaad/sleutels", "wallet_list_create_new_wallet": "Maak een nieuwe portemonnee", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 252a2b82a..648cb4e83 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -84,6 +84,7 @@ "buy_with": "Kup za pomocą", "by_cake_pay": "przez Cake Pay", "cake_2fa_preset": "Ciasto 2FA Preset", + "cake_dark_theme": "Cake Dark Temat", "cake_pay_account_note": "Zarejestruj się, używając tylko adresu e-mail, aby przeglądać i kupować karty. Niektóre są nawet dostępne ze zniżką!", "cake_pay_learn_more": "Kupuj i wykorzystuj karty podarunkowe od razu w aplikacji!\nPrzesuń od lewej do prawej, aby dowiedzieć się więcej.", "cake_pay_subtitle": "Kup karty upominkowe ze zniżką (tylko USA)", @@ -429,6 +430,7 @@ "provider_error": "${provider} pomyłka", "public_key": "Klucz publiczny", "purchase_gift_card": "Kup kartę podarunkową", + "purple_dark_theme": "Purple Dark Temat", "qr_fullscreen": "Dotknij, aby otworzyć pełnoekranowy kod QR", "qr_payment_amount": "Ten kod QR zawiera kwotę do zapłaty. Czy chcesz nadpisać obecną wartość?", "question_to_disable_2fa": "Czy na pewno chcesz wyłączyć Cake 2FA? Kod 2FA nie będzie już potrzebny do uzyskania dostępu do portfela i niektórych funkcji.", @@ -440,6 +442,8 @@ "reconnect": "Połącz ponownie", "reconnect_alert_text": "Czy na pewno ponownie się ponownie połączysz?", "reconnection": "Ponowne łączenie", + "red_dark_theme": "Czerwony Mroczny motyw", + "red_light_theme": "Motyw czerwony światło", "redeemed": "wykupione", "refund_address": "Adres do zwrotu", "reject": "Odrzucić", @@ -719,6 +723,7 @@ "use_card_info_two": "Środki są przeliczane na USD, gdy są przechowywane na koncie przedpłaconym, a nie w walutach cyfrowych.", "use_ssl": "Użyj SSL", "use_suggested": "Użyj sugerowane", + "use_testnet": "Użyj testne", "variable_pair_not_supported": "Ta para zmiennych nie jest obsługiwana na wybranych giełdach", "verification": "Weryfikacja", "verify_with_2fa": "Sprawdź za pomocą Cake 2FA", @@ -728,6 +733,7 @@ "view_key_private": "Prywatny Klucz Wglądu", "view_key_public": "Publiczny Klucz Wglądu", "view_transaction_on": "Zobacz transakcje na ", + "waitFewSecondForTxUpdate": "Poczekaj kilka sekund, aż transakcja zostanie odzwierciedlona w historii transakcji", "wallet_keys": "Klucze portfela", "wallet_list_create_new_wallet": "Utwórz nowy portfel", "wallet_list_edit_wallet": "Edytuj portfel", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 6230eba93..3bb1aa66b 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -84,6 +84,7 @@ "buy_with": "Compre com", "by_cake_pay": "por Cake Pay", "cake_2fa_preset": "Predefinição de bolo 2FA", + "cake_dark_theme": "Bolo tema escuro", "cake_pay_account_note": "Inscreva-se com apenas um endereço de e-mail para ver e comprar cartões. Alguns estão até com desconto!", "cake_pay_learn_more": "Compre e resgate vales-presente instantaneamente no app!\nDeslize da esquerda para a direita para saber mais.", "cake_pay_subtitle": "Compre vales-presente com desconto (somente nos EUA)", @@ -431,6 +432,7 @@ "provider_error": "${provider} erro", "public_key": "Chave pública", "purchase_gift_card": "Comprar vale-presente", + "purple_dark_theme": "Tema escuro roxo", "qr_fullscreen": "Toque para abrir o código QR em tela cheia", "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", "question_to_disable_2fa": "Tem certeza de que deseja desativar o Cake 2FA? Um código 2FA não será mais necessário para acessar a carteira e certas funções.", @@ -442,6 +444,8 @@ "reconnect": "Reconectar", "reconnect_alert_text": "Você tem certeza de que deseja reconectar?", "reconnection": "Reconectar", + "red_dark_theme": "Tema escuro vermelho", + "red_light_theme": "Tema da luz vermelha", "redeemed": "Resgatado", "refund_address": "Endereço de reembolso", "reject": "Rejeitar", @@ -721,6 +725,7 @@ "use_card_info_two": "Os fundos são convertidos para USD quando mantidos na conta pré-paga, não em moedas digitais.", "use_ssl": "Use SSL", "use_suggested": "Uso sugerido", + "use_testnet": "Use testNet", "variable_pair_not_supported": "Este par de variáveis não é compatível com as trocas selecionadas", "verification": "Verificação", "verify_with_2fa": "Verificar com Cake 2FA", @@ -730,6 +735,7 @@ "view_key_private": "Chave de visualização (privada)", "view_key_public": "Chave de visualização (pública)", "view_transaction_on": "View Transaction on ", + "waitFewSecondForTxUpdate": "Aguarde alguns segundos para que a transação seja refletida no histórico de transações", "waiting_payment_confirmation": "Aguardando confirmação de pagamento", "wallet_keys": "Semente/chaves da carteira", "wallet_list_create_new_wallet": "Criar nova carteira", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 91ab48598..93f1b1e7b 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -84,6 +84,7 @@ "buy_with": "Купить с помощью", "by_cake_pay": "от Cake Pay", "cake_2fa_preset": "Торт 2FA Preset", + "cake_dark_theme": "Тейт темная тема", "cake_pay_account_note": "Зарегистрируйтесь, указав только адрес электронной почты, чтобы просматривать и покупать карты. Некоторые даже доступны со скидкой!", "cake_pay_learn_more": "Мгновенно покупайте и используйте подарочные карты в приложении!\nПроведите по экрану слева направо, чтобы узнать больше.", "cake_pay_subtitle": "Покупайте подарочные карты со скидкой (только для США)", @@ -430,6 +431,7 @@ "provider_error": "${provider} ошибка", "public_key": "Публичный ключ", "purchase_gift_card": "Купить подарочную карту", + "purple_dark_theme": "Пурпурная темная тема", "qr_fullscreen": "Нажмите, чтобы открыть полноэкранный QR-код", "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", "question_to_disable_2fa": "Вы уверены, что хотите отключить Cake 2FA? Код 2FA больше не потребуется для доступа к кошельку и некоторым функциям.", @@ -441,6 +443,8 @@ "reconnect": "Переподключиться", "reconnect_alert_text": "Вы хотите переподключиться?", "reconnection": "Переподключение", + "red_dark_theme": "Красная темная тема", + "red_light_theme": "Тема красного света", "redeemed": "искуплен", "refund_address": "Адрес возврата", "reject": "Отклонять", @@ -720,6 +724,7 @@ "use_card_info_two": "Средства конвертируются в доллары США, когда они хранятся на предоплаченном счете, а не в цифровых валютах.", "use_ssl": "Использовать SSL", "use_suggested": "Использовать предложенный", + "use_testnet": "Используйте Testnet", "variable_pair_not_supported": "Эта пара переменных не поддерживается выбранными биржами.", "verification": "Проверка", "verify_with_2fa": "Подтвердить с помощью Cake 2FA", @@ -729,6 +734,7 @@ "view_key_private": "Приватный ключ просмотра", "view_key_public": "Публичный ключ просмотра", "view_transaction_on": "View Transaction on ", + "waitFewSecondForTxUpdate": "Пожалуйста, подождите несколько секунд, чтобы транзакция отразилась в истории транзакций.", "wallet_keys": "Мнемоническая фраза/ключи кошелька", "wallet_list_create_new_wallet": "Создать новый кошелёк", "wallet_list_edit_wallet": "Изменить кошелек", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index d6b5f8f0d..5b42380e8 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -84,6 +84,7 @@ "buy_with": "ซื้อด้วย", "by_cake_pay": "โดย Cake Pay", "cake_2fa_preset": "เค้ก 2FA ที่ตั้งไว้ล่วงหน้า", + "cake_dark_theme": "ธีมเค้กมืด", "cake_pay_account_note": "ลงทะเบียนด้วยอีเมลเพียงอย่างเดียวเพื่อดูและซื้อบัตร บางบัตรอาจมีส่วนลด!", "cake_pay_learn_more": "ซื้อและเบิกบัตรของขวัญในแอพพลิเคชันทันที!\nกระแทกขวาไปซ้ายเพื่อเรียนรู้เพิ่มเติม", "cake_pay_subtitle": "ซื้อบัตรของขวัญราคาถูก (สำหรับสหรัฐอเมริกาเท่านั้น)", @@ -429,6 +430,7 @@ "provider_error": "ข้อผิดพลาด ${provider}", "public_key": "คีย์สาธารณะ", "purchase_gift_card": "ซื้อบัตรของขวัญ", + "purple_dark_theme": "ธีมสีม่วงเข้ม", "qr_fullscreen": "แตะเพื่อเปิดหน้าจอ QR code แบบเต็มจอ", "qr_payment_amount": "QR code นี้มีจำนวนการชำระเงิน คุณต้องการเขียนทับค่าปัจจุบันหรือไม่?", "question_to_disable_2fa": "คุณแน่ใจหรือไม่ว่าต้องการปิดการใช้งาน Cake 2FA ไม่จำเป็นต้องใช้รหัส 2FA ในการเข้าถึงกระเป๋าเงินและฟังก์ชั่นบางอย่างอีกต่อไป", @@ -440,6 +442,8 @@ "reconnect": "เชื่อมต่อใหม่", "reconnect_alert_text": "คุณแน่ใจหรือไม่ว่าต้องการเชื่อมต่อใหม่?", "reconnection": "เชื่อมต่อใหม่", + "red_dark_theme": "ธีมสีแดงเข้ม", + "red_light_theme": "ธีมแสงสีแดง", "redeemed": "แลกของขวัญ", "refund_address": "ที่อยู่สำหรับส่งคืน", "reject": "ปฏิเสธ", @@ -719,6 +723,7 @@ "use_card_info_two": "เงินจะถูกแปลงค่าเป็นดอลลาร์สหรัฐเมื่อถือไว้ในบัญชีสำรองเงิน ไม่ใช่สกุลเงินดิจิตอล", "use_ssl": "ใช้ SSL", "use_suggested": "ใช้ที่แนะนำ", + "use_testnet": "ใช้ testnet", "variable_pair_not_supported": "คู่ความสัมพันธ์ที่เปลี่ยนแปลงได้นี้ไม่สนับสนุนกับหุ้นที่เลือก", "verification": "การตรวจสอบ", "verify_with_2fa": "ตรวจสอบกับ Cake 2FA", @@ -728,6 +733,7 @@ "view_key_private": "คีย์มุมมอง (ส่วนตัว)", "view_key_public": "คีย์มุมมอง (สาธารณะ)", "view_transaction_on": "ดูการทำธุรกรรมบน ", + "waitFewSecondForTxUpdate": "กรุณารอสักครู่เพื่อให้ธุรกรรมปรากฏในประวัติการทำธุรกรรม", "wallet_keys": "ซีดของกระเป๋า/คีย์", "wallet_list_create_new_wallet": "สร้างกระเป๋าใหม่", "wallet_list_edit_wallet": "แก้ไขกระเป๋าสตางค์", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index fcc1db761..ec5afbe9a 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -84,6 +84,7 @@ "buy_with": "Bumili ka", "by_cake_pay": "sa pamamagitan ng cake pay", "cake_2fa_preset": "Cake 2fa preset", + "cake_dark_theme": "Cake madilim na tema", "cake_pay_account_note": "Mag -sign up na may isang email address lamang upang makita at bumili ng mga kard. Ang ilan ay magagamit kahit sa isang diskwento!", "cake_pay_learn_more": "Agad na bumili at tubusin ang mga kard ng regalo sa app!\nMag -swipe pakaliwa sa kanan upang matuto nang higit pa.", "cake_pay_subtitle": "Bumili ng mga diskwento na gift card (USA lamang)", @@ -429,6 +430,7 @@ "provider_error": "${provider} error", "public_key": "Pampublikong susi", "purchase_gift_card": "Bumili ng Gift Card", + "purple_dark_theme": "Purple Madilim na Tema", "qr_fullscreen": "Tapikin upang buksan ang buong screen QR code", "qr_payment_amount": "Ang QR code na ito ay naglalaman ng isang halaga ng pagbabayad. Nais mo bang i -overwrite ang kasalukuyang halaga?", "question_to_disable_2fa": "Sigurado ka bang nais mong huwag paganahin ang cake 2fa? Ang isang 2FA code ay hindi na kinakailangan upang ma -access ang pitaka at ilang mga pag -andar.", @@ -440,6 +442,8 @@ "reconnect": "Kumonekta muli", "reconnect_alert_text": "Sigurado ka bang nais mong muling kumonekta?", "reconnection": "Pag -ugnay muli", + "red_dark_theme": "Red Madilim na Tema", + "red_light_theme": "Red light tema", "redeemed": "Tinubos", "refund_address": "Refund address", "reject": "Tanggihan", @@ -719,6 +723,7 @@ "use_card_info_two": "Ang mga pondo ay na -convert sa USD kapag gaganapin sila sa prepaid account, hindi sa mga digital na pera.", "use_ssl": "Gumamit ng SSL", "use_suggested": "Gumamit ng iminungkahing", + "use_testnet": "Gumamit ng testnet", "variable_pair_not_supported": "Ang variable na pares na ito ay hindi suportado sa mga napiling palitan", "verification": "Pag -verify", "verify_with_2fa": "Mag -verify sa cake 2FA", @@ -728,6 +733,7 @@ "view_key_private": "Tingnan ang Key (Pribado)", "view_key_public": "Tingnan ang Key (Publiko)", "view_transaction_on": "Tingnan ang transaksyon sa", + "waitFewSecondForTxUpdate": "Mangyaring maghintay ng ilang segundo para makita ang transaksyon sa history ng mga transaksyon", "wallet_keys": "Mga buto/susi ng pitaka", "wallet_list_create_new_wallet": "Lumikha ng bagong pitaka", "wallet_list_edit_wallet": "I -edit ang Wallet", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 8018a804e..ced3e8161 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -84,6 +84,7 @@ "buy_with": "Şunun ile al: ", "by_cake_pay": "Cake Pay tarafından", "cake_2fa_preset": "Kek 2FA Ön Ayarı", + "cake_dark_theme": "Kek Koyu Tema", "cake_pay_account_note": "Kartları görmek ve satın almak için sadece bir e-posta adresiyle kaydolun. Hatta bazıları indirimli olarak bile mevcut!", "cake_pay_learn_more": "Uygulamada anında hediye kartları satın alın ve harcayın!\nDaha fazla öğrenmek için soldan sağa kaydır.", "cake_pay_subtitle": "İndirimli hediye kartları satın alın (yalnızca ABD)", @@ -429,6 +430,7 @@ "provider_error": "${provider} hatası", "public_key": "Genel Anahtar", "purchase_gift_card": "Hediye Kartı Satın Al", + "purple_dark_theme": "Mor karanlık tema", "qr_fullscreen": "QR kodunu tam ekranda açmak için dokun", "qr_payment_amount": "Bu QR kodu ödeme tutarını içeriyor. Geçerli miktarın üzerine yazmak istediğine emin misin?", "question_to_disable_2fa": "Cake 2FA'yı devre dışı bırakmak istediğinizden emin misiniz? M-cüzdana ve belirli işlevlere erişmek için artık 2FA koduna gerek kalmayacak.", @@ -440,6 +442,8 @@ "reconnect": "Yeniden Bağlan", "reconnect_alert_text": "Yeniden bağlanmak istediğinden emin misin?", "reconnection": "Yeniden bağlantı", + "red_dark_theme": "Kırmızı Karanlık Tema", + "red_light_theme": "Kırmızı Işık Teması", "redeemed": "Kullanılmış", "refund_address": "İade adresi", "reject": "Reddetmek", @@ -719,6 +723,7 @@ "use_card_info_two": "Paralar, dijital para birimlerinde değil, ön ödemeli hesapta tutulduğunda USD'ye dönüştürülür.", "use_ssl": "SSL kullan", "use_suggested": "Önerileni Kullan", + "use_testnet": "TestNet kullanın", "variable_pair_not_supported": "Bu değişken paritesi seçilen borsalarda desteklenmemekte", "verification": "Doğrulama", "verify_with_2fa": "Cake 2FA ile Doğrulayın", @@ -728,6 +733,7 @@ "view_key_private": "İzleme anahtarı (özel)", "view_key_public": "İzleme anahtarı (genel)", "view_transaction_on": "İşlemi şurada görüntüle ", + "waitFewSecondForTxUpdate": "İşlemin işlem geçmişine yansıması için lütfen birkaç saniye bekleyin", "wallet_keys": "Cüzdan tohumu/anahtarları", "wallet_list_create_new_wallet": "Yeni Cüzdan Oluştur", "wallet_list_edit_wallet": "Cüzdanı düzenle", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 492794008..da62e75d3 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -84,6 +84,7 @@ "buy_with": "Купити за допомогою", "by_cake_pay": "від Cake Pay", "cake_2fa_preset": "Торт 2FA Preset", + "cake_dark_theme": "Темна тема торта", "cake_pay_account_note": "Зареєструйтеся, використовуючи лише адресу електронної пошти, щоб переглядати та купувати картки. Деякі навіть доступні зі знижкою!", "cake_pay_learn_more": "Миттєво купуйте та активуйте подарункові картки в додатку!\nПроведіть пальцем зліва направо, щоб дізнатися більше.", "cake_pay_subtitle": "Купуйте подарункові картки зі знижкою (тільки для США)", @@ -429,6 +430,7 @@ "provider_error": "${provider} помилка", "public_key": "Публічний ключ", "purchase_gift_card": "Придбати подарункову картку", + "purple_dark_theme": "Фіолетова темна тема", "qr_fullscreen": "Торкніться, щоб відкрити QR-код на весь екран", "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", "question_to_disable_2fa": "Ви впевнені, що хочете вимкнути Cake 2FA? Код 2FA більше не потрібен для доступу до гаманця та певних функцій.", @@ -440,6 +442,8 @@ "reconnect": "Перепідключитися", "reconnect_alert_text": "Ви хочете перепідключитися?", "reconnection": "Перепідключення", + "red_dark_theme": "Червона темна тема", + "red_light_theme": "Тема червоного світла", "redeeded": "Викуплено", "redeemed": "Викуплений", "refund_address": "Адреса повернення коштів", @@ -720,6 +724,7 @@ "use_card_info_two": "Кошти конвертуються в долари США, якщо вони зберігаються на передплаченому рахунку, а не в цифрових валютах.", "use_ssl": "Використати SSL", "use_suggested": "Використати запропоноване", + "use_testnet": "Використовуйте тестову мережу", "variable_pair_not_supported": "Ця пара змінних не підтримується вибраними біржами", "verification": "Перевірка", "verify_with_2fa": "Перевірте за допомогою Cake 2FA", @@ -729,6 +734,7 @@ "view_key_private": "Приватний ключ перегляду", "view_key_public": "Публічний ключ перегляду", "view_transaction_on": "View Transaction on ", + "waitFewSecondForTxUpdate": "Будь ласка, зачекайте кілька секунд, поки транзакція відобразиться в історії транзакцій", "wallet_keys": "Мнемонічна фраза/ключі гаманця", "wallet_list_create_new_wallet": "Створити новий гаманець", "wallet_list_edit_wallet": "Редагувати гаманець", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 1ec3e6f94..b636d8156 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -84,6 +84,7 @@ "buy_with": "کے ساتھ خریدیں۔", "by_cake_pay": "Cake پے کے ذریعے", "cake_2fa_preset": "کیک 2FA پیش سیٹ", + "cake_dark_theme": "کیک ڈارک تھیم", "cake_pay_account_note": "کارڈز دیکھنے اور خریدنے کے لیے صرف ایک ای میل ایڈریس کے ساتھ سائن اپ کریں۔ کچھ رعایت پر بھی دستیاب ہیں!", "cake_pay_learn_more": "ایپ میں فوری طور پر گفٹ کارڈز خریدیں اور بھنائیں!\\nمزید جاننے کے لیے بائیں سے دائیں سوائپ کریں۔", "cake_pay_subtitle": "رعایتی گفٹ کارڈز خریدیں (صرف امریکہ)", @@ -431,6 +432,7 @@ "provider_error": "${provider} خرابی۔", "public_key": "عوامی کلید", "purchase_gift_card": "گفٹ کارڈ خریدیں۔", + "purple_dark_theme": "ارغوانی ڈارک تھیم", "qr_fullscreen": "فل سکرین QR کوڈ کھولنے کے لیے تھپتھپائیں۔", "qr_payment_amount": "اس QR کوڈ میں ادائیگی کی رقم شامل ہے۔ کیا آپ موجودہ قدر کو اوور رائٹ کرنا چاہتے ہیں؟", "question_to_disable_2fa": "کیا آپ واقعی کیک 2FA کو غیر فعال کرنا چاہتے ہیں؟ بٹوے اور بعض افعال تک رسائی کے لیے اب 2FA کوڈ کی ضرورت نہیں ہوگی۔", @@ -442,6 +444,8 @@ "reconnect": "دوبارہ جڑیں۔", "reconnect_alert_text": "کیا آپ واقعی دوبارہ جڑنا چاہتے ہیں؟", "reconnection": "دوبارہ رابطہ", + "red_dark_theme": "ریڈ ڈارک تھیم", + "red_light_theme": "ریڈ لائٹ تھیم", "redeemed": "چھڑایا", "refund_address": "رقم کی واپسی کا پتہ", "reject": "ﺎﻧﺮﮐ ﺩﺭ", @@ -721,6 +725,7 @@ "use_card_info_two": "رقوم کو امریکی ڈالر میں تبدیل کیا جاتا ہے جب پری پیڈ اکاؤنٹ میں رکھا جاتا ہے، ڈیجیٹل کرنسیوں میں نہیں۔", "use_ssl": "SSL استعمال کریں۔", "use_suggested": "تجویز کردہ استعمال کریں۔", + "use_testnet": "ٹیسٹ نیٹ استعمال کریں", "variable_pair_not_supported": "یہ متغیر جوڑا منتخب ایکسچینجز کے ساتھ تعاون یافتہ نہیں ہے۔", "verification": "تصدیق", "verify_with_2fa": "کیک 2FA سے تصدیق کریں۔", @@ -730,6 +735,7 @@ "view_key_private": "کلید دیکھیں (نجی)", "view_key_public": "کلید دیکھیں (عوامی)", "view_transaction_on": "لین دین دیکھیں آن", + "waitFewSecondForTxUpdate": "۔ﮟﯾﺮﮐ ﺭﺎﻈﺘﻧﺍ ﺎﮐ ﮉﻨﮑﯿﺳ ﺪﻨﭼ ﻡﺮﮐ ﮦﺍﺮﺑ ﮯﯿﻟ ﮯﮐ ﮯﻧﺮﮐ ﯽﺳﺎﮑﻋ ﯽﮐ ﻦﯾﺩ ﻦﯿﻟ ﮟﯿﻣ ﺦﯾﺭﺎﺗ ﯽﮐ ﻦ", "wallet_keys": "بٹوے کے بیج / چابیاں", "wallet_list_create_new_wallet": "نیا والیٹ بنائیں", "wallet_list_edit_wallet": "بٹوے میں ترمیم کریں۔", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 7146b7a06..79b4620e5 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -84,6 +84,7 @@ "buy_with": "Rà pẹ̀lú", "by_cake_pay": "láti ọwọ́ Cake Pay", "cake_2fa_preset": "Cake 2FA Tito", + "cake_dark_theme": "Akara oyinbo dudu koko", "cake_pay_account_note": "Ẹ fi àdírẹ́sì ímeèlì nìkan forúkọ sílẹ̀ k'ẹ́ rí àti ra àwọn káàdì. Ẹ lè fi owó tó kéré jù ra àwọn káàdì kan!", "cake_pay_learn_more": "Láìpẹ́ ra àti lo àwọn káàdí ìrajà t'á lò nínú irú kan ìtajà nínú áàpù!\nẸ tẹ̀ òsì de ọ̀tún láti kọ́ jù.", "cake_pay_subtitle": "Ra àwọn káàdì ìrajà t'á lò nínú ìtajà kan fún owó tí kò pọ̀ (USA nìkan)", @@ -430,6 +431,7 @@ "provider_error": "Àṣìṣe ${provider}", "public_key": "Kọ́kọ́rọ́ tó kò àdáni", "purchase_gift_card": "Ra káàdì ìrajà t'á lò nínú irú kan ìtajà", + "purple_dark_theme": "Akọle dudu dudu", "qr_fullscreen": "Àmì ìlujá túbọ̀ máa tóbi tí o bá tẹ̀", "qr_payment_amount": "Iye owó t'á ránṣé wà nínú àmì ìlujá yìí. Ṣé ẹ fẹ́ pààrọ̀ ẹ̀?", "question_to_disable_2fa": "Ṣe o wa daadaa pe o fẹ ko 2FA Cake? Ko si itumọ ti a yoo nilo lati ranse si iwe iwe naa ati eyikeyi iṣẹ ti o ni.", @@ -441,6 +443,8 @@ "reconnect": "Ṣe àtúnse", "reconnect_alert_text": "Ṣó dá ẹ lójú pé ẹ fẹ́ ṣe àtúnse?", "reconnection": "Àtúnṣe", + "red_dark_theme": "Akọle dudu pupa", + "red_light_theme": "Akori ina pupa", "redeemed": "Ó lílò", "refund_address": "Àdírẹ́sì t'ẹ́ gba owó sí", "reject": "Kọ", @@ -720,6 +724,7 @@ "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.", "use_ssl": "Lo SSL", "use_suggested": "Lo àbá", + "use_testnet": "Lo tele", "variable_pair_not_supported": "A kì í ṣe k'á fi àwọn ilé pàṣípààrọ̀ yìí ṣe pàṣípààrọ̀ irú owó méji yìí", "verification": "Ìjẹ́rìísí", "verify_with_2fa": "Ṣeẹda pẹlu Cake 2FA", @@ -729,6 +734,7 @@ "view_key_private": "Kọ́kọ́rọ́ ìwò (àdáni)", "view_key_public": "Kọ́kọ́rọ́ ìwò (kò àdáni)", "view_transaction_on": "Wo pàṣípààrọ̀ lórí ", + "waitFewSecondForTxUpdate": "Fi inurere duro fun awọn iṣeju diẹ fun idunadura lati ṣe afihan ninu itan-akọọlẹ iṣowo", "wallet_keys": "Hóró/kọ́kọ́rọ́ àpamọ́wọ́", "wallet_list_create_new_wallet": "Ṣe àpamọ́wọ́ títun", "wallet_list_edit_wallet": "Ṣatunkọ apamọwọ", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index c990eca65..8ec69939b 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -84,6 +84,7 @@ "buy_with": "一起购买", "by_cake_pay": "通过 Cake Pay", "cake_2fa_preset": "蛋糕 2FA 预设", + "cake_dark_theme": "蛋糕黑暗主题", "cake_pay_account_note": "只需使用電子郵件地址註冊即可查看和購買卡片。有些甚至可以打折!", "cake_pay_learn_more": "立即在应用中购买和兑换礼品卡!\n从左向右滑动以了解详情。", "cake_pay_subtitle": "购买打折礼品卡(仅限美国)", @@ -429,6 +430,7 @@ "provider_error": "${provider} 错误", "public_key": "公钥", "purchase_gift_card": "购买礼品卡", + "purple_dark_theme": "紫色的黑暗主题", "qr_fullscreen": "点击打开全屏二维码", "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", "question_to_disable_2fa": "您确定要禁用 Cake 2FA 吗?访问钱包和某些功能将不再需要 2FA 代码。", @@ -440,6 +442,8 @@ "reconnect": "重新连接", "reconnect_alert_text": "您确定要重新连接吗?", "reconnection": "重新连接", + "red_dark_theme": "红色的黑暗主题", + "red_light_theme": "红灯主题", "redeemed": "赎回", "refund_address": "退款地址", "reject": "拒绝", @@ -719,6 +723,7 @@ "use_card_info_two": "预付账户中的资金转换为美元,不是数字货币。", "use_ssl": "使用SSL", "use_suggested": "使用建议", + "use_testnet": "使用TestNet", "variable_pair_not_supported": "所选交易所不支持此变量对", "verification": "验证", "verify_with_2fa": "用 Cake 2FA 验证", @@ -728,6 +733,7 @@ "view_key_private": "View 密钥(私钥)", "view_key_public": "View 密钥(公钥)", "view_transaction_on": "View Transaction on ", + "waitFewSecondForTxUpdate": "请等待几秒钟,交易才会反映在交易历史记录中", "wallet_keys": "钱包种子/密钥", "wallet_list_create_new_wallet": "创建新钱包", "wallet_list_edit_wallet": "编辑钱包", diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index f659239e7..fa3701fa7 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -15,15 +15,15 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.10.3" -MONERO_COM_BUILD_NUMBER=75 +MONERO_COM_VERSION="1.11.0" +MONERO_COM_BUILD_NUMBER=77 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.13.3" -CAKEWALLET_BUILD_NUMBER=192 +CAKEWALLET_VERSION="4.14.0" +CAKEWALLET_BUILD_NUMBER=196 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/android/pubspec_gen.sh b/scripts/android/pubspec_gen.sh index 4b89c4afa..d238052fe 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" + CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana" ;; $HAVEN) CONFIG_ARGS="--haven" diff --git a/scripts/android/shell.nix b/scripts/android/shell.nix new file mode 100644 index 000000000..b89da09c0 --- /dev/null +++ b/scripts/android/shell.nix @@ -0,0 +1,16 @@ +{ pkgs ? import {} }: + +pkgs.mkShell { + buildInputs = [ + pkgs.curl + pkgs.unzip + pkgs.automake + pkgs.file + pkgs.pkg-config + pkgs.git + pkgs.libtool + pkgs.ncurses5 + pkgs.openjdk8 + pkgs.clang + ]; +} diff --git a/scripts/ios/app_config.sh b/scripts/ios/app_config.sh index 81752a015..9f59d6632 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" + CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana" ;; $HAVEN) diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index bb4ca77f8..31f0b9548 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -13,13 +13,13 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.10.3" -MONERO_COM_BUILD_NUMBER=73 +MONERO_COM_VERSION="1.11.0" +MONERO_COM_BUILD_NUMBER=75 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.13.3" -CAKEWALLET_BUILD_NUMBER=212 +CAKEWALLET_VERSION="4.14.0" +CAKEWALLET_BUILD_NUMBER=215 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/macos/app_config.sh b/scripts/macos/app_config.sh index cda367b9c..bd1417c4b 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";; #--haven + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana";; #--haven esac cp -rf pubspec_description.yaml pubspec.yaml diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index 49edd9acb..4dec47f40 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -16,13 +16,13 @@ if [ -n "$1" ]; then fi MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.0.3" -MONERO_COM_BUILD_NUMBER=5 +MONERO_COM_VERSION="1.1.0" +MONERO_COM_BUILD_NUMBER=7 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.6.3" -CAKEWALLET_BUILD_NUMBER=53 +CAKEWALLET_VERSION="1.7.0" +CAKEWALLET_BUILD_NUMBER=55 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then diff --git a/tool/configure.dart b/tool/configure.dart index 408a6f6b1..3c1587a98 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -7,6 +7,7 @@ const ethereumOutputPath = 'lib/ethereum/ethereum.dart'; 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 walletTypesPath = 'lib/wallet_types.g.dart'; const pubspecDefaultPath = 'pubspec_default.yaml'; const pubspecOutputPath = 'pubspec.yaml'; @@ -21,6 +22,7 @@ Future main(List args) async { final hasNano = args.contains('${prefix}nano'); final hasBanano = args.contains('${prefix}banano'); final hasPolygon = args.contains('${prefix}polygon'); + final hasSolana = args.contains('${prefix}solana'); await generateBitcoin(hasBitcoin); await generateMonero(hasMonero); @@ -29,6 +31,7 @@ Future main(List args) async { await generateBitcoinCash(hasBitcoinCash); await generateNano(hasNano); await generatePolygon(hasPolygon); + await generateSolana(hasSolana); // await generateBanano(hasEthereum); await generatePubspec( @@ -40,6 +43,7 @@ Future main(List args) async { hasBanano: hasBanano, hasBitcoinCash: hasBitcoinCash, hasPolygon: hasPolygon, + hasSolana: hasSolana, ); await generateWalletTypes( hasMonero: hasMonero, @@ -50,12 +54,14 @@ Future main(List args) async { hasBanano: hasBanano, hasBitcoinCash: hasBitcoinCash, hasPolygon: hasPolygon, + hasSolana: hasSolana, ); } Future generateBitcoin(bool hasImplementation) async { final outputFile = File(bitcoinOutputPath); const bitcoinCommonHeaders = """ +import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; @@ -65,8 +71,10 @@ import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:hive/hive.dart';"""; +import 'package:hive/hive.dart'; +import 'package:bitcoin_base/bitcoin_base.dart';"""; const bitcoinCWHeaders = """ +import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; @@ -134,6 +142,11 @@ abstract class Bitcoin { TransactionPriority getLitecoinTransactionPriorityMedium(); TransactionPriority getBitcoinTransactionPrioritySlow(); TransactionPriority getLitecoinTransactionPrioritySlow(); + + Future setAddressType(Object wallet, dynamic option); + ReceivePageOption getSelectedAddressType(Object wallet); + List getBitcoinReceivePageOptions(); + BitcoinAddressType getBitcoinAddressType(ReceivePageOption option); } """; @@ -577,13 +590,14 @@ abstract class Ethereum { int formatterEthereumParseAmount(String amount); double formatterEthereumAmountToDouble({TransactionInfo? transaction, BigInt? amount, int exponent = 18}); List getERC20Currencies(WalletBase wallet); - Future addErc20Token(WalletBase wallet, Erc20Token token); - Future deleteErc20Token(WalletBase wallet, Erc20Token token); + Future addErc20Token(WalletBase wallet, CryptoCurrency token); + Future deleteErc20Token(WalletBase wallet, CryptoCurrency token); Future getErc20Token(WalletBase wallet, String contractAddress); CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction); void updateEtherscanUsageState(WalletBase wallet, bool isEnabled); Web3Client? getWeb3Client(WalletBase wallet); + String getTokenAddress(CryptoCurrency asset); } """; @@ -669,13 +683,14 @@ abstract class Polygon { int formatterPolygonParseAmount(String amount); double formatterPolygonAmountToDouble({TransactionInfo? transaction, BigInt? amount, int exponent = 18}); List getERC20Currencies(WalletBase wallet); - Future addErc20Token(WalletBase wallet, Erc20Token token); - Future deleteErc20Token(WalletBase wallet, Erc20Token token); + Future addErc20Token(WalletBase wallet, CryptoCurrency token); + Future deleteErc20Token(WalletBase wallet, CryptoCurrency token); Future getErc20Token(WalletBase wallet, String contractAddress); CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction); void updatePolygonScanUsageState(WalletBase wallet, bool isEnabled); Web3Client? getWeb3Client(WalletBase wallet); + String getTokenAddress(CryptoCurrency asset); } """; @@ -716,10 +731,6 @@ import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; const bitcoinCashCwPart = "part 'cw_bitcoin_cash.dart';"; const bitcoinCashContent = """ abstract class BitcoinCash { - String getMnemonic(int? strength); - - Uint8List getSeedFromMnemonic(String seed); - String getCashAddrFormat(String address); WalletService createBitcoinCashWalletService( @@ -885,6 +896,86 @@ abstract class NanoUtil { await outputFile.writeAsString(output); } +Future generateSolana(bool hasImplementation) async { + final outputFile = File(solanaOutputPath); + const solanaCommonHeaders = """ +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'; +import 'package:solana/solana.dart'; + +"""; + const solanaCWHeaders = """ +import 'package:cw_solana/spl_token.dart'; +import 'package:cw_solana/solana_wallet.dart'; +import 'package:cw_solana/solana_mnemonics.dart'; +import 'package:cw_solana/solana_wallet_service.dart'; +import 'package:cw_solana/solana_transaction_info.dart'; +import 'package:cw_solana/solana_transaction_credentials.dart'; +import 'package:cw_solana/solana_wallet_creation_credentials.dart'; +"""; + const solanaCwPart = "part 'cw_solana.dart';"; + const solanaContent = """ +abstract class Solana { + List getSolanaWordList(String language); + WalletService createSolanaWalletService(Box walletInfoSource); + WalletCredentials createSolanaNewWalletCredentials( + {required String name, WalletInfo? walletInfo}); + WalletCredentials createSolanaRestoreWalletFromSeedCredentials( + {required String name, required String mnemonic, required String password}); + WalletCredentials createSolanaRestoreWalletFromPrivateKey( + {required String name, required String privateKey, required String password}); + + String getAddress(WalletBase wallet); + String getPrivateKey(WalletBase wallet); + String getPublicKey(WalletBase wallet); + Ed25519HDKeyPair? getWalletKeyPair(WalletBase wallet); + + Object createSolanaTransactionCredentials( + List outputs, { + required CryptoCurrency currency, + }); + + Object createSolanaTransactionCredentialsRaw( + List outputs, { + required CryptoCurrency currency, + }); + List getSPLTokenCurrencies(WalletBase wallet); + Future addSPLToken(WalletBase wallet, CryptoCurrency token); + Future deleteSPLToken(WalletBase wallet, CryptoCurrency token); + Future getSPLToken(WalletBase wallet, String contractAddress); + + CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction); + double getTransactionAmountRaw(TransactionInfo transactionInfo); + String getTokenAddress(CryptoCurrency asset); + List? getValidationLength(CryptoCurrency type); +} + + """; + + const solanaEmptyDefinition = 'Solana? solana;\n'; + const solanaCWDefinition = 'Solana? solana = CWSolana();\n'; + + final output = '$solanaCommonHeaders\n' + + (hasImplementation ? '$solanaCWHeaders\n' : '\n') + + (hasImplementation ? '$solanaCwPart\n\n' : '\n') + + (hasImplementation ? solanaCWDefinition : solanaEmptyDefinition) + + '\n' + + solanaContent; + + if (outputFile.existsSync()) { + await outputFile.delete(); + } + + await outputFile.writeAsString(output); +} + Future generatePubspec( {required bool hasMonero, required bool hasBitcoin, @@ -893,7 +984,8 @@ Future generatePubspec( required bool hasNano, required bool hasBanano, required bool hasBitcoinCash, - required bool hasPolygon}) async { + required bool hasPolygon, + required bool hasSolana}) async { const cwCore = """ cw_core: path: ./cw_core @@ -934,6 +1026,10 @@ Future generatePubspec( cw_polygon: path: ./cw_polygon """; + const cwSolana = """ + cw_solana: + path: ./cw_solana + """; const cwEVM = """ cw_evm: path: ./cw_evm @@ -972,6 +1068,10 @@ Future generatePubspec( output += '\n$cwPolygon'; } + if (hasSolana) { + output += '\n$cwSolana'; + } + if (hasHaven && !hasMonero) { output += '\n$cwSharedExternal\n$cwHaven'; } else if (hasHaven) { @@ -1002,7 +1102,8 @@ Future generateWalletTypes( required bool hasNano, required bool hasBanano, required bool hasBitcoinCash, - required bool hasPolygon}) async { + required bool hasPolygon, + required bool hasSolana}) async { final walletTypesFile = File(walletTypesPath); if (walletTypesFile.existsSync()) { @@ -1037,6 +1138,10 @@ Future generateWalletTypes( outputContent += '\tWalletType.polygon,\n'; } + if (hasSolana) { + outputContent += '\tWalletType.solana,\n'; + } + if (hasNano) { outputContent += '\tWalletType.nano,\n'; } diff --git a/tool/generate_secrets_config.dart b/tool/generate_secrets_config.dart index 58e7b8839..8745c2933 100644 --- a/tool/generate_secrets_config.dart +++ b/tool/generate_secrets_config.dart @@ -5,6 +5,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'; Future main(List args) async => generateSecretsConfig(args); @@ -18,8 +19,10 @@ Future generateSecretsConfig(List args) async { final configFile = File(configPath); final evmChainsConfigFile = File(evmChainsConfigPath); - final secrets = {}; + final solanaConfigFile = File(solanaConfigPath); + final secrets = {}; + secrets.addAll(extraInfo); secrets.removeWhere((key, dynamic value) { if (key.contains('--')) { @@ -49,6 +52,7 @@ Future generateSecretsConfig(List args) async { await configFile.writeAsString(secretsJson); secrets.clear(); + SecretKey.evmChainsSecrets.forEach((sec) { if (secrets[sec.name] != null) { return; @@ -60,4 +64,18 @@ Future generateSecretsConfig(List args) async { secretsJson = JsonEncoder.withIndent(' ').convert(secrets); await evmChainsConfigFile.writeAsString(secretsJson); + + secrets.clear(); + + SecretKey.solanaSecrets.forEach((sec) { + if (secrets[sec.name] != null) { + return; + } + + secrets[sec.name] = sec.generate(); + }); + + secretsJson = JsonEncoder.withIndent(' ').convert(secrets); + + await solanaConfigFile.writeAsString(secretsJson); } diff --git a/tool/import_secrets_config.dart b/tool/import_secrets_config.dart index 83e345f78..02061669b 100644 --- a/tool/import_secrets_config.dart +++ b/tool/import_secrets_config.dart @@ -8,6 +8,8 @@ const outputPath = 'lib/.secrets.g.dart'; const evmChainsConfigPath = 'tool/.evm-secrets-config.json'; const evmChainsOutputPath = 'cw_evm/lib/.secrets.g.dart'; +const solanaConfigPath = 'tool/.solana-secrets-config.json'; +const solanaOutputPath = 'cw_solana/lib/.secrets.g.dart'; Future main(List args) async => importSecretsConfig(); Future importSecretsConfig() async { @@ -21,6 +23,12 @@ Future importSecretsConfig() async { final evmChainsOutput = evmChainsInput.keys .fold('', (String acc, String val) => acc + generateConst(val, evmChainsInput)); + final solanaOutputFile = File(solanaOutputPath); + final solanaInput = + json.decode(File(solanaConfigPath).readAsStringSync()) as Map; + final solanaOutput = + solanaInput.keys.fold('', (String acc, String val) => acc + generateConst(val, solanaInput)); + if (outputFile.existsSync()) { await outputFile.delete(); } @@ -32,4 +40,10 @@ Future importSecretsConfig() async { } await evmChainsOutputFile.writeAsString(evmChainsOutput); + + if (solanaOutputFile.existsSync()) { + await solanaOutputFile.delete(); + } + + await solanaOutputFile.writeAsString(solanaOutput); } diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index f991c43cf..38b5129af 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -44,6 +44,10 @@ class SecretKey { SecretKey('polygonScanApiKey', () => ''), ]; + static final solanaSecrets = [ + SecretKey('ankrApiKey', () => ''), + ]; + final String name; final String Function() generate; }