diff --git a/lib/db/migrate_wallets_to_isar.dart b/lib/db/migrate_wallets_to_isar.dart index ddaea3f21..78d7308c7 100644 --- a/lib/db/migrate_wallets_to_isar.dart +++ b/lib/db/migrate_wallets_to_isar.dart @@ -86,6 +86,11 @@ Future migrateWalletsToIsar({ } } + // reset stellar address type + if (old.coin == Coin.stellar || old.coin == Coin.stellarTestnet) { + await MainDB.instance.deleteWalletBlockchainData(old.walletId); + } + // // Set other data values // diff --git a/lib/models/isar/models/blockchain_data/address.dart b/lib/models/isar/models/blockchain_data/address.dart index 3ada44746..43c732491 100644 --- a/lib/models/isar/models/blockchain_data/address.dart +++ b/lib/models/isar/models/blockchain_data/address.dart @@ -161,6 +161,7 @@ enum AddressType { nano, banano, spark, + stellar, ; String get readableName { @@ -187,6 +188,8 @@ enum AddressType { return "Banano"; case AddressType.spark: return "Spark"; + case AddressType.stellar: + return "Stellar"; } } } diff --git a/lib/models/isar/models/blockchain_data/address.g.dart b/lib/models/isar/models/blockchain_data/address.g.dart index 6cff9d525..3ffd073d3 100644 --- a/lib/models/isar/models/blockchain_data/address.g.dart +++ b/lib/models/isar/models/blockchain_data/address.g.dart @@ -264,6 +264,7 @@ const _AddresstypeEnumValueMap = { 'nano': 8, 'banano': 9, 'spark': 10, + 'stellar': 11, }; const _AddresstypeValueEnumMap = { 0: AddressType.p2pkh, @@ -277,6 +278,7 @@ const _AddresstypeValueEnumMap = { 8: AddressType.nano, 9: AddressType.banano, 10: AddressType.spark, + 11: AddressType.stellar, }; Id _addressGetId(Address object) { diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index 8a79f7830..f272c9762 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -14,7 +14,6 @@ import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; -import 'package:stackwallet/services/coins/stellar/stellar_wallet.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -109,22 +108,10 @@ abstract class CoinServiceAPI { throw UnimplementedError("moved"); case Coin.stellar: - return StellarWallet( - walletId: walletId, - walletName: walletName, - coin: coin, - secureStore: secureStorageInterface, - tracker: tracker, - ); + throw UnimplementedError("moved"); case Coin.stellarTestnet: - return StellarWallet( - walletId: walletId, - walletName: walletName, - coin: coin, - secureStore: secureStorageInterface, - tracker: tracker, - ); + throw UnimplementedError("moved"); case Coin.tezos: throw UnimplementedError("moved"); diff --git a/lib/services/coins/stellar/stellar_wallet.dart b/lib/services/coins/stellar/stellar_wallet.dart index 8efe7fbdd..482093529 100644 --- a/lib/services/coins/stellar/stellar_wallet.dart +++ b/lib/services/coins/stellar/stellar_wallet.dart @@ -1,933 +1,227 @@ -import 'dart:async'; - -import 'package:bip39/bip39.dart' as bip39; -import 'package:isar/isar.dart'; -import 'package:stackwallet/db/isar/main_db.dart'; -import 'package:stackwallet/models/balance.dart' as SWBalance; -import 'package:stackwallet/models/isar/models/blockchain_data/address.dart' - as SWAddress; -import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart' - as SWTransaction; -import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; -import 'package:stackwallet/models/node_model.dart'; -import 'package:stackwallet/models/paymint/fee_object_model.dart'; -import 'package:stackwallet/services/coins/coin_service.dart'; -import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; -import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; -import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; -import 'package:stackwallet/services/event_bus/global_event_bus.dart'; -import 'package:stackwallet/services/mixins/wallet_cache.dart'; -import 'package:stackwallet/services/mixins/wallet_db.dart'; -import 'package:stackwallet/services/node_service.dart'; -import 'package:stackwallet/services/transaction_notification_tracker.dart'; -import 'package:stackwallet/utilities/amount/amount.dart'; -import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/default_nodes.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; -import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/prefs.dart'; -import 'package:stackwallet/utilities/test_stellar_node_connection.dart'; -import 'package:stellar_flutter_sdk/stellar_flutter_sdk.dart'; -import 'package:tuple/tuple.dart'; - -const int MINIMUM_CONFIRMATIONS = 1; - -class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB { - late StellarSDK stellarSdk; - late Network stellarNetwork; - - StellarWallet({ - required String walletId, - required String walletName, - required Coin coin, - required TransactionNotificationTracker tracker, - required SecureStorageInterface secureStore, - MainDB? mockableOverride, - }) { - txTracker = tracker; - _walletId = walletId; - _walletName = walletName; - _coin = coin; - _secureStore = secureStore; - initCache(walletId, coin); - initWalletDB(mockableOverride: mockableOverride); - - if (coin.isTestNet) { - stellarNetwork = Network.TESTNET; - } else { - stellarNetwork = Network.PUBLIC; - } - - _updateNode(); - } - - void _updateNode() { - _xlmNode = NodeService(secureStorageInterface: _secureStore) - .getPrimaryNodeFor(coin: coin) ?? - DefaultNodes.getNodeFor(coin); - stellarSdk = StellarSDK("${_xlmNode!.host}:${_xlmNode!.port}"); - } - - late final TransactionNotificationTracker txTracker; - late SecureStorageInterface _secureStore; - - @override - bool get isFavorite => _isFavorite ??= getCachedIsFavorite(); - bool? _isFavorite; - - @override - set isFavorite(bool isFavorite) { - _isFavorite = isFavorite; - updateCachedIsFavorite(isFavorite); - } - - @override - bool get shouldAutoSync => _shouldAutoSync; - bool _shouldAutoSync = true; - - Timer? timer; - - final _prefs = Prefs.instance; - - @override - set shouldAutoSync(bool shouldAutoSync) { - if (_shouldAutoSync != shouldAutoSync) { - _shouldAutoSync = shouldAutoSync; - if (!shouldAutoSync) { - timer?.cancel(); - timer = null; - stopNetworkAlivePinging(); - } else { - startNetworkAlivePinging(); - refresh(); - } - } - } - - Timer? _networkAliveTimer; - - void startNetworkAlivePinging() { - // call once on start right away - _periodicPingCheck(); - - // then periodically check - _networkAliveTimer = Timer.periodic( - Constants.networkAliveTimerDuration, - (_) async { - _periodicPingCheck(); - }, - ); - } - - void stopNetworkAlivePinging() { - _networkAliveTimer?.cancel(); - _networkAliveTimer = null; - } - - void _periodicPingCheck() async { - bool hasNetwork = await testNetworkConnection(); - - if (_isConnected != hasNetwork) { - NodeConnectionStatus status = hasNetwork - ? NodeConnectionStatus.connected - : NodeConnectionStatus.disconnected; - - GlobalEventBus.instance.fire( - NodeConnectionStatusChangedEvent( - status, - walletId, - coin, - ), - ); - - _isConnected = hasNetwork; - if (hasNetwork) { - unawaited(refresh()); - } - } - } - - @override - String get walletName => _walletName; - late String _walletName; - - @override - set walletName(String name) => _walletName = name; - - @override - SWBalance.Balance get balance => _balance ??= getCachedBalance(); - SWBalance.Balance? _balance; - - @override - Coin get coin => _coin; - late Coin _coin; - - Future _accountExists(String accountId) async { - bool exists = false; - - try { - AccountResponse receiverAccount = - await stellarSdk.accounts.account(accountId); - if (receiverAccount.accountId != "") { - exists = true; - } - } catch (e, s) { - Logging.instance.log( - "Error getting account ${e.toString()} - ${s.toString()}", - level: LogLevel.Error); - } - return exists; - } - - @override - Future> prepareSend( - {required String address, - required Amount amount, - Map? args}) async { - try { - final feeRate = args?["feeRate"]; - var fee = 1000; - if (feeRate is FeeRateType) { - final theFees = await fees; - switch (feeRate) { - case FeeRateType.fast: - fee = theFees.fast; - case FeeRateType.slow: - fee = theFees.slow; - case FeeRateType.average: - default: - fee = theFees.medium; - } - } - Map txData = { - "fee": fee, - "address": address, - "recipientAmt": amount, - "memo": args?["memo"] as String?, - }; - - Logging.instance.log("prepare send: $txData", level: LogLevel.Info); - return txData; - } catch (e, s) { - Logging.instance.log("Error getting fees $e - $s", level: LogLevel.Error); - rethrow; - } - } - - @override - Future confirmSend({required Map txData}) async { - final secretSeed = await _secureStore.read(key: '${_walletId}_secretSeed'); - KeyPair senderKeyPair = KeyPair.fromSecretSeed(secretSeed!); - AccountResponse sender = - await stellarSdk.accounts.account(senderKeyPair.accountId); - final amountToSend = txData['recipientAmt'] as Amount; - final memo = txData["memo"] as String?; - - //First check if account exists, can be skipped, but if the account does not exist, - // the transaction fee will be charged when the transaction fails. - bool validAccount = await _accountExists(txData['address'] as String); - TransactionBuilder transactionBuilder; - - if (!validAccount) { - //Fund the account, user must ensure account is correct - CreateAccountOperationBuilder createAccBuilder = - CreateAccountOperationBuilder( - txData['address'] as String, amountToSend.decimal.toString()); - transactionBuilder = - TransactionBuilder(sender).addOperation(createAccBuilder.build()); - } else { - transactionBuilder = TransactionBuilder(sender).addOperation( - PaymentOperationBuilder(txData['address'] as String, Asset.NATIVE, - amountToSend.decimal.toString()) - .build()); - } - - if (memo != null) { - transactionBuilder.addMemo(MemoText(memo)); - } - - final transaction = transactionBuilder.build(); - - transaction.sign(senderKeyPair, stellarNetwork); - try { - SubmitTransactionResponse response = await stellarSdk - .submitTransaction(transaction) - .onError((error, stackTrace) => throw (error.toString())); - if (!response.success) { - throw ("${response.extras?.resultCodes?.transactionResultCode}" - " ::: ${response.extras?.resultCodes?.operationsResultCodes}"); - } - return response.hash!; - } catch (e, s) { - Logging.instance.log("Error sending TX $e - $s", level: LogLevel.Error); - rethrow; - } - } - - Future get _currentReceivingAddress => db - .getAddresses(walletId) - .filter() - .typeEqualTo(SWAddress.AddressType.unknown) - .and() - .subTypeEqualTo(SWAddress.AddressSubType.unknown) - .sortByDerivationIndexDesc() - .findFirst(); - - @override - Future get currentReceivingAddress async => - (await _currentReceivingAddress)?.value ?? await getAddressSW(); - - Future getBaseFee() async { - var fees = await stellarSdk.feeStats.execute(); - return int.parse(fees.lastLedgerBaseFee); - } - - @override - Future estimateFeeFor(Amount amount, int feeRate) async { - var baseFee = await getBaseFee(); - return Amount( - rawValue: BigInt.from(baseFee), fractionDigits: coin.decimals); - } - - @override - Future exit() async { - _hasCalledExit = true; - timer?.cancel(); - timer = null; - stopNetworkAlivePinging(); - } - - NodeModel? _xlmNode; - - NodeModel getCurrentNode() { - if (_xlmNode != null) { - return _xlmNode!; - } else if (NodeService(secureStorageInterface: _secureStore) - .getPrimaryNodeFor(coin: coin) != - null) { - return NodeService(secureStorageInterface: _secureStore) - .getPrimaryNodeFor(coin: coin)!; - } else { - return DefaultNodes.getNodeFor(coin); - } - } - - @override - Future get fees async { - int fee = await getBaseFee(); - return FeeObject( - numberOfBlocksFast: 10, - numberOfBlocksAverage: 10, - numberOfBlocksSlow: 10, - fast: fee, - medium: fee, - slow: fee); - } - - @override - Future fullRescan( - int maxUnusedAddressGap, - int maxNumberOfIndexesToCheck, - ) async { - try { - Logging.instance.log("Starting full rescan!", level: LogLevel.Info); - longMutex = true; - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - walletId, - coin, - ), - ); - - final _mnemonic = await mnemonicString; - final _mnemonicPassphrase = await mnemonicPassphrase; - - await db.deleteWalletBlockchainData(walletId); - - await _recoverWalletFromBIP32SeedPhrase( - mnemonic: _mnemonic!, - mnemonicPassphrase: _mnemonicPassphrase!, - isRescan: true, - ); - - await refresh(); - Logging.instance.log("Full rescan complete!", level: LogLevel.Info); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.synced, - walletId, - coin, - ), - ); - } catch (e, s) { - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.unableToSync, - walletId, - coin, - ), - ); - - Logging.instance.log( - "Exception rethrown from fullRescan(): $e\n$s", - level: LogLevel.Error, - ); - rethrow; - } finally { - longMutex = false; - } - } - - @override - Future generateNewAddress() { - // not used for stellar(?) - throw UnimplementedError(); - } - - @override - bool get hasCalledExit => _hasCalledExit; - bool _hasCalledExit = false; - - @override - Future initializeExisting() async { - await _prefs.init(); - } - - @override - Future initializeNew( - ({String mnemonicPassphrase, int wordCount})? data, - ) async { - if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) { - throw Exception( - "Attempted to overwrite mnemonic on generate new wallet!"); - } - - await _prefs.init(); - - final int strength; - if (data == null || data.wordCount == 12) { - strength = 128; - } else if (data.wordCount == 24) { - strength = 256; - } else { - throw Exception("Invalid word count"); - } - final String mnemonic = bip39.generateMnemonic(strength: strength); - final String passphrase = data?.mnemonicPassphrase ?? ""; - await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic); - await _secureStore.write( - key: '${_walletId}_mnemonicPassphrase', - value: passphrase, - ); - - Wallet wallet = await Wallet.from( - mnemonic, - passphrase: passphrase, - ); - KeyPair keyPair = await wallet.getKeyPair(index: 0); - String address = keyPair.accountId; - String secretSeed = - keyPair.secretSeed; //This will be required for sending a tx - - await _secureStore.write(key: '${_walletId}_secretSeed', value: secretSeed); - - final swAddress = SWAddress.Address( - walletId: walletId, - value: address, - publicKey: keyPair.publicKey, - derivationIndex: 0, - derivationPath: null, - type: SWAddress.AddressType.unknown, // TODO: set type - subType: SWAddress.AddressSubType.unknown); - - await db.putAddress(swAddress); - - await Future.wait( - [updateCachedId(walletId), updateCachedIsFavorite(false)]); - } - - Future getAddressSW() async { - var mnemonic = await _secureStore.read(key: '${_walletId}_mnemonic'); - - Wallet wallet = await Wallet.from(mnemonic!); - KeyPair keyPair = await wallet.getKeyPair(index: 0); - - return Future.value(keyPair.accountId); - } - - @override - bool get isConnected => _isConnected; - bool _isConnected = false; - - @override - bool get isRefreshing => refreshMutex; - bool refreshMutex = false; - - @override - // TODO: implement maxFee - Future get maxFee => throw UnimplementedError(); - - @override - Future> get mnemonic => - mnemonicString.then((value) => value!.split(" ")); - - @override - Future get mnemonicPassphrase => - _secureStore.read(key: '${_walletId}_mnemonicPassphrase'); - - @override - Future get mnemonicString => - _secureStore.read(key: '${_walletId}_mnemonic'); - - Future _recoverWalletFromBIP32SeedPhrase({ - required String mnemonic, - required String mnemonicPassphrase, - bool isRescan = false, - }) async { - final Wallet wallet = await Wallet.from( - mnemonic, - passphrase: mnemonicPassphrase, - ); - final KeyPair keyPair = await wallet.getKeyPair(index: 0); - final String address = keyPair.accountId; - String secretSeed = - keyPair.secretSeed; //This will be required for sending a tx - - await _secureStore.write( - key: '${_walletId}_secretSeed', - value: secretSeed, - ); - - final swAddress = SWAddress.Address( - walletId: walletId, - value: address, - publicKey: keyPair.publicKey, - derivationIndex: 0, - derivationPath: null, - type: SWAddress.AddressType.unknown, - subType: SWAddress.AddressSubType.unknown, - ); - - if (isRescan) { - await db.updateOrPutAddresses([swAddress]); - } else { - await db.putAddress(swAddress); - } - } - - bool longMutex = false; - - @override - Future recoverFromMnemonic({ - required String mnemonic, - String? mnemonicPassphrase, - required int maxUnusedAddressGap, - required int maxNumberOfIndexesToCheck, - required int height, - }) async { - longMutex = true; - try { - if ((await mnemonicString) != null || - (await this.mnemonicPassphrase) != null) { - throw Exception("Attempted to overwrite mnemonic on restore!"); - } - - await _secureStore.write( - key: '${_walletId}_mnemonic', - value: mnemonic.trim(), - ); - await _secureStore.write( - key: '${_walletId}_mnemonicPassphrase', - value: mnemonicPassphrase ?? "", - ); - - await _recoverWalletFromBIP32SeedPhrase( - mnemonic: mnemonic, - mnemonicPassphrase: mnemonicPassphrase ?? "", - isRescan: false, - ); - - await Future.wait([ - updateCachedId(walletId), - updateCachedIsFavorite(false), - ]); - } catch (e, s) { - Logging.instance.log( - "Exception rethrown from recoverFromMnemonic(): $e\n$s", - level: LogLevel.Error); - - rethrow; - } finally { - longMutex = false; - } - } - - Future updateChainHeight() async { - final height = await stellarSdk.ledgers - .order(RequestBuilderOrder.DESC) - .limit(1) - .execute() - .then((value) => value.records!.first.sequence) - .onError((error, stackTrace) => throw ("Error getting chain height")); - await updateCachedChainHeight(height); - } - - Future updateTransactions() async { - try { - List> - transactionList = []; - Page payments; - try { - payments = await stellarSdk.payments - .forAccount(await getAddressSW()) - .order(RequestBuilderOrder.DESC) - .execute() - .onError((error, stackTrace) => throw error!); - } catch (e) { - if (e is ErrorResponse && - e.body.contains("The resource at the url requested was not found. " - "This usually occurs for one of two reasons: " - "The url requested is not valid, or no data in our database " - "could be found with the parameters provided.")) { - // probably just doesn't have any history yet or whatever stellar needs - return; - } else { - Logging.instance.log( - "Stellar $walletName $walletId failed to fetch transactions", - level: LogLevel.Warning, - ); - rethrow; - } - } - for (OperationResponse response in payments.records!) { - // PaymentOperationResponse por; - if (response is PaymentOperationResponse) { - PaymentOperationResponse por = response; - - SWTransaction.TransactionType type; - if (por.sourceAccount == await getAddressSW()) { - type = SWTransaction.TransactionType.outgoing; - } else { - type = SWTransaction.TransactionType.incoming; - } - final amount = Amount( - rawValue: BigInt.parse(float - .parse(por.amount!) - .toStringAsFixed(coin.decimals) - .replaceAll(".", "")), - fractionDigits: coin.decimals, - ); - int fee = 0; - int height = 0; - //Query the transaction linked to the payment, - // por.transaction returns a null sometimes - TransactionResponse tx = - await stellarSdk.transactions.transaction(por.transactionHash!); - - if (tx.hash.isNotEmpty) { - fee = tx.feeCharged!; - height = tx.ledger; - } - var theTransaction = SWTransaction.Transaction( - walletId: walletId, - txid: por.transactionHash!, - timestamp: - DateTime.parse(por.createdAt!).millisecondsSinceEpoch ~/ 1000, - type: type, - subType: SWTransaction.TransactionSubType.none, - amount: 0, - amountString: amount.toJsonString(), - fee: fee, - height: height, - isCancelled: false, - isLelantus: false, - slateId: "", - otherData: "", - inputs: [], - outputs: [], - nonce: 0, - numberOfMessages: null, - ); - SWAddress.Address? receivingAddress = await _currentReceivingAddress; - SWAddress.Address address = - type == SWTransaction.TransactionType.incoming - ? receivingAddress! - : SWAddress.Address( - walletId: walletId, - value: por.sourceAccount!, - publicKey: - KeyPair.fromAccountId(por.sourceAccount!).publicKey, - derivationIndex: 0, - derivationPath: null, - type: SWAddress.AddressType.unknown, // TODO: set type - subType: SWAddress.AddressSubType.unknown); - Tuple2 tuple = - Tuple2(theTransaction, address); - transactionList.add(tuple); - } else if (response is CreateAccountOperationResponse) { - CreateAccountOperationResponse caor = response; - SWTransaction.TransactionType type; - if (caor.sourceAccount == await getAddressSW()) { - type = SWTransaction.TransactionType.outgoing; - } else { - type = SWTransaction.TransactionType.incoming; - } - final amount = Amount( - rawValue: BigInt.parse(float - .parse(caor.startingBalance!) - .toStringAsFixed(coin.decimals) - .replaceAll(".", "")), - fractionDigits: coin.decimals, - ); - int fee = 0; - int height = 0; - TransactionResponse tx = - await stellarSdk.transactions.transaction(caor.transactionHash!); - if (tx.hash.isNotEmpty) { - fee = tx.feeCharged!; - height = tx.ledger; - } - var theTransaction = SWTransaction.Transaction( - walletId: walletId, - txid: caor.transactionHash!, - timestamp: - DateTime.parse(caor.createdAt!).millisecondsSinceEpoch ~/ 1000, - type: type, - subType: SWTransaction.TransactionSubType.none, - amount: 0, - amountString: amount.toJsonString(), - fee: fee, - height: height, - isCancelled: false, - isLelantus: false, - slateId: "", - otherData: "", - inputs: [], - outputs: [], - nonce: 0, - numberOfMessages: null, - ); - SWAddress.Address? receivingAddress = await _currentReceivingAddress; - SWAddress.Address address = - type == SWTransaction.TransactionType.incoming - ? receivingAddress! - : SWAddress.Address( - walletId: walletId, - value: caor.sourceAccount!, - publicKey: - KeyPair.fromAccountId(caor.sourceAccount!).publicKey, - derivationIndex: 0, - derivationPath: null, - type: SWAddress.AddressType.unknown, // TODO: set type - subType: SWAddress.AddressSubType.unknown); - Tuple2 tuple = - Tuple2(theTransaction, address); - transactionList.add(tuple); - } - } - await db.addNewTransactionData(transactionList, walletId); - } catch (e, s) { - Logging.instance.log( - "Exception rethrown from updateTransactions(): $e\n$s", - level: LogLevel.Error); - rethrow; - } - } - - Future updateBalance() async { - try { - AccountResponse accountResponse; - - try { - accountResponse = await stellarSdk.accounts - .account(await getAddressSW()) - .onError((error, stackTrace) => throw error!); - } catch (e) { - if (e is ErrorResponse && - e.body.contains("The resource at the url requested was not found. " - "This usually occurs for one of two reasons: " - "The url requested is not valid, or no data in our database " - "could be found with the parameters provided.")) { - // probably just doesn't have any history yet or whatever stellar needs - return; - } else { - Logging.instance.log( - "Stellar $walletName $walletId failed to fetch transactions", - level: LogLevel.Warning, - ); - rethrow; - } - } - - for (Balance balance in accountResponse.balances) { - switch (balance.assetType) { - case Asset.TYPE_NATIVE: - _balance = SWBalance.Balance( - total: Amount( - rawValue: BigInt.from(float.parse(balance.balance) * 10000000), - fractionDigits: coin.decimals, - ), - spendable: Amount( - rawValue: BigInt.from(float.parse(balance.balance) * 10000000), - fractionDigits: coin.decimals, - ), - blockedTotal: Amount( - rawValue: BigInt.from(0), - fractionDigits: coin.decimals, - ), - pendingSpendable: Amount( - rawValue: BigInt.from(0), - fractionDigits: coin.decimals, - ), - ); - Logging.instance.log(_balance, level: LogLevel.Info); - await updateCachedBalance(_balance!); - } - } - } catch (e, s) { - Logging.instance.log( - "ERROR GETTING BALANCE $e\n$s", - level: LogLevel.Info, - ); - rethrow; - } - } - - @override - Future refresh() async { - if (refreshMutex) { - Logging.instance.log( - "$walletId $walletName refreshMutex denied", - level: LogLevel.Info, - ); - return; - } else { - refreshMutex = true; - } - - try { - await _prefs.init(); - - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - walletId, - coin, - ), - ); - - await updateChainHeight(); - await updateTransactions(); - await updateBalance(); - - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.synced, - walletId, - coin, - ), - ); - - if (shouldAutoSync) { - timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async { - Logging.instance.log( - "Periodic refresh check for $walletId $walletName in object instance: $hashCode", - level: LogLevel.Info); - - await refresh(); - GlobalEventBus.instance.fire( - UpdatedInBackgroundEvent( - "New data found in $walletId $walletName in background!", - walletId, - ), - ); - }); - } - } catch (e, s) { - Logging.instance.log( - "Failed to refresh stellar wallet $walletId: '$walletName': $e\n$s", - level: LogLevel.Warning, - ); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.unableToSync, - walletId, - coin, - ), - ); - } - - refreshMutex = false; - } - - @override - int get storedChainHeight => getCachedChainHeight(); - - @override - Future testNetworkConnection() async { - return await testStellarNodeConnection(_xlmNode!.host, _xlmNode!.port); - } - - @override - Future> get transactions => - db.getTransactions(walletId).findAll(); - - @override - Future updateNode(bool shouldRefresh) async { - _updateNode(); - if (shouldRefresh) { - unawaited(refresh()); - } - } - - @override - Future updateSentCachedTxData(Map txData) async { - final transaction = SWTransaction.Transaction( - walletId: walletId, - txid: txData["txid"] as String, - timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, - type: SWTransaction.TransactionType.outgoing, - subType: SWTransaction.TransactionSubType.none, - // precision may be lost here hence the following amountString - amount: (txData["recipientAmt"] as Amount).raw.toInt(), - amountString: (txData["recipientAmt"] as Amount).toJsonString(), - fee: txData["fee"] as int, - height: null, - isCancelled: false, - isLelantus: false, - otherData: null, - slateId: null, - nonce: null, - inputs: [], - outputs: [], - numberOfMessages: null, - ); - - final address = txData["address"] is String - ? await db.getAddress(walletId, txData["address"] as String) - : null; - - await db.addNewTransactionData( - [ - Tuple2(transaction, address), - ], - walletId, - ); - } - - @override - // not used - Future> get utxos => throw UnimplementedError(); - - @override - bool validateAddress(String address) { - return RegExp(r"^[G][A-Z0-9]{55}$").hasMatch(address); - } - - @override - String get walletId => _walletId; - late String _walletId; -} +// import 'dart:async'; +// +// import 'package:bip39/bip39.dart' as bip39; +// import 'package:isar/isar.dart'; +// import 'package:stackwallet/db/isar/main_db.dart'; +// import 'package:stackwallet/models/balance.dart' as SWBalance; +// import 'package:stackwallet/models/isar/models/blockchain_data/address.dart' +// as SWAddress; +// import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart' +// as SWTransaction; +// import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; +// import 'package:stackwallet/models/node_model.dart'; +// import 'package:stackwallet/models/paymint/fee_object_model.dart'; +// import 'package:stackwallet/services/coins/coin_service.dart'; +// import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; +// import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; +// import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +// import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +// import 'package:stackwallet/services/mixins/wallet_cache.dart'; +// import 'package:stackwallet/services/mixins/wallet_db.dart'; +// import 'package:stackwallet/services/node_service.dart'; +// import 'package:stackwallet/services/transaction_notification_tracker.dart'; +// import 'package:stackwallet/utilities/amount/amount.dart'; +// import 'package:stackwallet/utilities/constants.dart'; +// import 'package:stackwallet/utilities/default_nodes.dart'; +// import 'package:stackwallet/utilities/enums/coin_enum.dart'; +// import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; +// import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +// import 'package:stackwallet/utilities/logger.dart'; +// import 'package:stackwallet/utilities/prefs.dart'; +// import 'package:stackwallet/utilities/test_stellar_node_connection.dart'; +// import 'package:stellar_flutter_sdk/stellar_flutter_sdk.dart'; +// import 'package:tuple/tuple.dart'; +// +// const int MINIMUM_CONFIRMATIONS = 1; +// +// class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB { +// late StellarSDK stellarSdk; +// late Network stellarNetwork; +// +// StellarWallet({ +// required String walletId, +// required String walletName, +// required Coin coin, +// required TransactionNotificationTracker tracker, +// required SecureStorageInterface secureStore, +// MainDB? mockableOverride, +// }) { +// txTracker = tracker; +// _walletId = walletId; +// _walletName = walletName; +// _coin = coin; +// _secureStore = secureStore; +// initCache(walletId, coin); +// initWalletDB(mockableOverride: mockableOverride); +// +// if (coin.isTestNet) { +// stellarNetwork = Network.TESTNET; +// } else { +// stellarNetwork = Network.PUBLIC; +// } +// +// _updateNode(); +// } +// +// Future updateTransactions() async { +// try { +// List> +// transactionList = []; +// Page payments; +// try { +// payments = await stellarSdk.payments +// .forAccount(await getAddressSW()) +// .order(RequestBuilderOrder.DESC) +// .execute() +// .onError((error, stackTrace) => throw error!); +// } catch (e) { +// if (e is ErrorResponse && +// e.body.contains("The resource at the url requested was not found. " +// "This usually occurs for one of two reasons: " +// "The url requested is not valid, or no data in our database " +// "could be found with the parameters provided.")) { +// // probably just doesn't have any history yet or whatever stellar needs +// return; +// } else { +// Logging.instance.log( +// "Stellar $walletName $walletId failed to fetch transactions", +// level: LogLevel.Warning, +// ); +// rethrow; +// } +// } +// for (OperationResponse response in payments.records!) { +// // PaymentOperationResponse por; +// if (response is PaymentOperationResponse) { +// PaymentOperationResponse por = response; +// +// SWTransaction.TransactionType type; +// if (por.sourceAccount == await getAddressSW()) { +// type = SWTransaction.TransactionType.outgoing; +// } else { +// type = SWTransaction.TransactionType.incoming; +// } +// final amount = Amount( +// rawValue: BigInt.parse(float +// .parse(por.amount!) +// .toStringAsFixed(coin.decimals) +// .replaceAll(".", "")), +// fractionDigits: coin.decimals, +// ); +// int fee = 0; +// int height = 0; +// //Query the transaction linked to the payment, +// // por.transaction returns a null sometimes +// TransactionResponse tx = +// await stellarSdk.transactions.transaction(por.transactionHash!); +// +// if (tx.hash.isNotEmpty) { +// fee = tx.feeCharged!; +// height = tx.ledger; +// } +// var theTransaction = SWTransaction.Transaction( +// walletId: walletId, +// txid: por.transactionHash!, +// timestamp: +// DateTime.parse(por.createdAt!).millisecondsSinceEpoch ~/ 1000, +// type: type, +// subType: SWTransaction.TransactionSubType.none, +// amount: 0, +// amountString: amount.toJsonString(), +// fee: fee, +// height: height, +// isCancelled: false, +// isLelantus: false, +// slateId: "", +// otherData: "", +// inputs: [], +// outputs: [], +// nonce: 0, +// numberOfMessages: null, +// ); +// SWAddress.Address? receivingAddress = await _currentReceivingAddress; +// SWAddress.Address address = +// type == SWTransaction.TransactionType.incoming +// ? receivingAddress! +// : SWAddress.Address( +// walletId: walletId, +// value: por.sourceAccount!, +// publicKey: +// KeyPair.fromAccountId(por.sourceAccount!).publicKey, +// derivationIndex: 0, +// derivationPath: null, +// type: SWAddress.AddressType.unknown, // TODO: set type +// subType: SWAddress.AddressSubType.unknown); +// Tuple2 tuple = +// Tuple2(theTransaction, address); +// transactionList.add(tuple); +// } else if (response is CreateAccountOperationResponse) { +// CreateAccountOperationResponse caor = response; +// SWTransaction.TransactionType type; +// if (caor.sourceAccount == await getAddressSW()) { +// type = SWTransaction.TransactionType.outgoing; +// } else { +// type = SWTransaction.TransactionType.incoming; +// } +// final amount = Amount( +// rawValue: BigInt.parse(float +// .parse(caor.startingBalance!) +// .toStringAsFixed(coin.decimals) +// .replaceAll(".", "")), +// fractionDigits: coin.decimals, +// ); +// int fee = 0; +// int height = 0; +// TransactionResponse tx = +// await stellarSdk.transactions.transaction(caor.transactionHash!); +// if (tx.hash.isNotEmpty) { +// fee = tx.feeCharged!; +// height = tx.ledger; +// } +// var theTransaction = SWTransaction.Transaction( +// walletId: walletId, +// txid: caor.transactionHash!, +// timestamp: +// DateTime.parse(caor.createdAt!).millisecondsSinceEpoch ~/ 1000, +// type: type, +// subType: SWTransaction.TransactionSubType.none, +// amount: 0, +// amountString: amount.toJsonString(), +// fee: fee, +// height: height, +// isCancelled: false, +// isLelantus: false, +// slateId: "", +// otherData: "", +// inputs: [], +// outputs: [], +// nonce: 0, +// numberOfMessages: null, +// ); +// SWAddress.Address? receivingAddress = await _currentReceivingAddress; +// SWAddress.Address address = +// type == SWTransaction.TransactionType.incoming +// ? receivingAddress! +// : SWAddress.Address( +// walletId: walletId, +// value: caor.sourceAccount!, +// publicKey: +// KeyPair.fromAccountId(caor.sourceAccount!).publicKey, +// derivationIndex: 0, +// derivationPath: null, +// type: SWAddress.AddressType.unknown, // TODO: set type +// subType: SWAddress.AddressSubType.unknown); +// Tuple2 tuple = +// Tuple2(theTransaction, address); +// transactionList.add(tuple); +// } +// } +// await db.addNewTransactionData(transactionList, walletId); +// } catch (e, s) { +// Logging.instance.log( +// "Exception rethrown from updateTransactions(): $e\n$s", +// level: LogLevel.Error); +// rethrow; +// } +// } +// } diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 7105dcd26..cfa26dd19 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -399,9 +399,7 @@ extension CoinExt on Coin { case Coin.stellar: case Coin.stellarTestnet: - // should not be unknown but since already used in prod changing - // this requires a migrate - return AddressType.unknown; + return AddressType.stellar; } } } diff --git a/lib/wallets/crypto_currency/coins/stellar.dart b/lib/wallets/crypto_currency/coins/stellar.dart new file mode 100644 index 000000000..c7bbc3d0a --- /dev/null +++ b/lib/wallets/crypto_currency/coins/stellar.dart @@ -0,0 +1,42 @@ +import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_currency.dart'; + +class Stellar extends Bip39Currency { + Stellar(super.network) { + switch (network) { + case CryptoCurrencyNetwork.main: + coin = Coin.stellar; + case CryptoCurrencyNetwork.test: + coin = Coin.stellarTestnet; + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + int get minConfirms => 1; + + @override + String get genesisHash => throw UnimplementedError( + "Not used for stellar", + ); + + @override + NodeModel get defaultNode { + switch (network) { + case CryptoCurrencyNetwork.main: + return DefaultNodes.stellar; + case CryptoCurrencyNetwork.test: + return DefaultNodes.stellarTestnet; + default: + throw Exception("Unsupported network"); + } + } + + @override + bool validateAddress(String address) => + RegExp(r"^[G][A-Z0-9]{55}$").hasMatch(address); +} diff --git a/lib/wallets/isar/models/wallet_info.g.dart b/lib/wallets/isar/models/wallet_info.g.dart index 28d16c5a5..bf4f00ff5 100644 --- a/lib/wallets/isar/models/wallet_info.g.dart +++ b/lib/wallets/isar/models/wallet_info.g.dart @@ -263,6 +263,7 @@ const _WalletInfomainAddressTypeEnumValueMap = { 'nano': 8, 'banano': 9, 'spark': 10, + 'stellar': 11, }; const _WalletInfomainAddressTypeValueEnumMap = { 0: AddressType.p2pkh, @@ -276,6 +277,7 @@ const _WalletInfomainAddressTypeValueEnumMap = { 8: AddressType.nano, 9: AddressType.banano, 10: AddressType.spark, + 11: AddressType.stellar, }; Id _walletInfoGetId(WalletInfo object) { diff --git a/lib/wallets/wallet/impl/stellar_wallet.dart b/lib/wallets/wallet/impl/stellar_wallet.dart new file mode 100644 index 000000000..5d138aa3d --- /dev/null +++ b/lib/wallets/wallet/impl/stellar_wallet.dart @@ -0,0 +1,561 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:isar/isar.dart'; +import 'package:stackwallet/models/balance.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; +import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/test_stellar_node_connection.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/stellar.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/models/tx_data.dart'; +import 'package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart'; +import 'package:stellar_flutter_sdk/stellar_flutter_sdk.dart' as stellar; + +class StellarWallet extends Bip39Wallet { + StellarWallet(CryptoCurrencyNetwork network) : super(Stellar(network)); + + stellar.StellarSDK get stellarSdk { + if (_stellarSdk == null) { + _updateSdk(); + } + return _stellarSdk!; + } + + stellar.Network get stellarNetwork { + switch (cryptoCurrency.network) { + case CryptoCurrencyNetwork.main: + return stellar.Network.PUBLIC; + case CryptoCurrencyNetwork.test: + return stellar.Network.TESTNET; + default: + throw Exception("Unsupported network"); + } + } + + // ============== Private ==================================================== + + stellar.StellarSDK? _stellarSdk; + + Future _getBaseFee() async { + final fees = await stellarSdk.feeStats.execute(); + return int.parse(fees.lastLedgerBaseFee); + } + + void _updateSdk() { + final currentNode = getCurrentNode(); + _stellarSdk = stellar.StellarSDK("${currentNode.host}:${currentNode.port}"); + } + + Future _accountExists(String accountId) async { + bool exists = false; + + try { + final receiverAccount = await stellarSdk.accounts.account(accountId); + if (receiverAccount.accountId != "") { + exists = true; + } + } catch (e, s) { + Logging.instance.log( + "Error getting account ${e.toString()} - ${s.toString()}", + level: LogLevel.Error); + } + return exists; + } + + Future _getStellarWallet() async { + return await stellar.Wallet.from( + await getMnemonic(), + passphrase: await getMnemonicPassphrase(), + ); + } + + Future _getSenderKeyPair({required int index}) async { + final wallet = await _getStellarWallet(); + return await wallet.getKeyPair(index: index); + } + + Future
_fetchStellarAddress({required int index}) async { + final stellar.KeyPair keyPair = await _getSenderKeyPair(index: index); + final String address = keyPair.accountId; + + return Address( + walletId: walletId, + value: address, + publicKey: keyPair.publicKey, + derivationIndex: index, + derivationPath: null, + type: AddressType.stellar, + subType: AddressSubType.receiving, + ); + } + + // ============== Overrides ================================================== + + @override + int get isarTransactionVersion => 2; + + @override + FilterOperation? get changeAddressFilterOperation => + FilterGroup.and(standardChangeAddressFilters); + + @override + FilterOperation? get receivingAddressFilterOperation => + FilterGroup.and(standardReceivingAddressFilters); + + @override + Future init() async { + try { + final address = await getCurrentReceivingAddress(); + if (address == null) { + await mainDB + .updateOrPutAddresses([await _fetchStellarAddress(index: 0)]); + } + } catch (_) { + // do nothing, still allow user into wallet + } + return super.init(); + } + + @override + Future prepareSend({required TxData txData}) async { + try { + if (txData.recipients?.length != 1) { + throw Exception("Missing recipient"); + } + + final feeRate = txData.feeRateType; + var fee = 1000; + if (feeRate is FeeRateType) { + final theFees = await fees; + switch (feeRate) { + case FeeRateType.fast: + fee = theFees.fast; + case FeeRateType.slow: + fee = theFees.slow; + case FeeRateType.average: + default: + fee = theFees.medium; + } + } + + return txData.copyWith( + fee: Amount( + rawValue: BigInt.from(fee), + fractionDigits: cryptoCurrency.fractionDigits, + ), + ); + } catch (e, s) { + Logging.instance.log("$runtimeType prepareSend() failed: $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + @override + Future confirmSend({required TxData txData}) async { + final senderKeyPair = await _getSenderKeyPair(index: 0); + final sender = await stellarSdk.accounts.account(senderKeyPair.accountId); + + final address = txData.recipients!.first.address; + final amountToSend = txData.recipients!.first.amount; + final memo = txData.memo; + + //First check if account exists, can be skipped, but if the account does not exist, + // the transaction fee will be charged when the transaction fails. + final validAccount = await _accountExists(address); + final stellar.TransactionBuilder transactionBuilder; + + if (!validAccount) { + //Fund the account, user must ensure account is correct + final createAccBuilder = stellar.CreateAccountOperationBuilder( + address, + amountToSend.decimal.toString(), + ); + transactionBuilder = stellar.TransactionBuilder(sender).addOperation( + createAccBuilder.build(), + ); + } else { + transactionBuilder = stellar.TransactionBuilder(sender).addOperation( + stellar.PaymentOperationBuilder( + address, + stellar.Asset.NATIVE, + amountToSend.decimal.toString(), + ).build(), + ); + } + + if (memo != null) { + transactionBuilder.addMemo(stellar.MemoText(memo)); + } + + final transaction = transactionBuilder.build(); + + transaction.sign(senderKeyPair, stellarNetwork); + try { + final response = await stellarSdk.submitTransaction(transaction); + if (!response.success) { + throw Exception("${response.extras?.resultCodes?.transactionResultCode}" + " ::: ${response.extras?.resultCodes?.operationsResultCodes}"); + } + + return txData.copyWith( + txHash: response.hash!, + txid: response.hash!, + ); + } catch (e, s) { + Logging.instance.log("Error sending TX $e - $s", level: LogLevel.Error); + rethrow; + } + } + + @override + Future estimateFeeFor(Amount amount, int feeRate) async { + final baseFee = await _getBaseFee(); + return Amount( + rawValue: BigInt.from(baseFee), + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + + @override + Future get fees async { + int fee = await _getBaseFee(); + return FeeObject( + numberOfBlocksFast: 1, + numberOfBlocksAverage: 1, + numberOfBlocksSlow: 1, + fast: fee, + medium: fee, + slow: fee, + ); + } + + @override + Future pingCheck() async { + final currentNode = getCurrentNode(); + return await testStellarNodeConnection(currentNode.host, currentNode.port); + } + + @override + Future recover({required bool isRescan}) async { + await refreshMutex.protect(() async { + if (isRescan) { + await mainDB.deleteWalletBlockchainData(walletId); + } + + await mainDB.updateOrPutAddresses([await _fetchStellarAddress(index: 0)]); + }); + + if (isRescan) { + unawaited(refresh()); + } + } + + @override + Future updateBalance() async { + try { + stellar.AccountResponse accountResponse; + + try { + accountResponse = await stellarSdk.accounts + .account((await getCurrentReceivingAddress())!.value) + .onError((error, stackTrace) => throw error!); + } catch (e) { + if (e is stellar.ErrorResponse && + e.body.contains("The resource at the url requested was not found. " + "This usually occurs for one of two reasons: " + "The url requested is not valid, or no data in our database " + "could be found with the parameters provided.")) { + // probably just doesn't have any history yet or whatever stellar needs + return; + } else { + Logging.instance.log( + "$runtimeType ${info.name} $walletId " + "failed to fetch account to updateBalance", + level: LogLevel.Warning, + ); + rethrow; + } + } + + for (stellar.Balance balance in accountResponse.balances) { + switch (balance.assetType) { + case stellar.Asset.TYPE_NATIVE: + final swBalance = Balance( + total: Amount( + rawValue: BigInt.from(float.parse(balance.balance) * 10000000), + fractionDigits: cryptoCurrency.fractionDigits, + ), + spendable: Amount( + rawValue: BigInt.from(float.parse(balance.balance) * 10000000), + fractionDigits: cryptoCurrency.fractionDigits, + ), + blockedTotal: Amount( + rawValue: BigInt.from(0), + fractionDigits: cryptoCurrency.fractionDigits, + ), + pendingSpendable: Amount( + rawValue: BigInt.from(0), + fractionDigits: cryptoCurrency.fractionDigits, + ), + ); + await info.updateBalance(newBalance: swBalance, isar: mainDB.isar); + } + } + } catch (e, s) { + Logging.instance.log( + "$runtimeType ${info.name} $walletId " + "updateBalance() failed: $e\n$s", + level: LogLevel.Warning, + ); + rethrow; + } + } + + @override + Future updateChainHeight() async { + try { + final height = await stellarSdk.ledgers + .order(stellar.RequestBuilderOrder.DESC) + .limit(1) + .execute() + .then((value) => value.records!.first.sequence); + await info.updateCachedChainHeight(newHeight: height, isar: mainDB.isar); + } catch (e, s) { + Logging.instance.log( + "$runtimeType updateChainHeight() failed: $e\n$s", + level: LogLevel.Error, + ); + + rethrow; + } + } + + @override + Future updateNode() async { + _updateSdk(); + } + + @override + Future updateTransactions() async { + try { + final myAddress = (await getCurrentReceivingAddress())!; + + List transactionList = []; + stellar.Page payments; + try { + payments = await stellarSdk.payments + .forAccount(myAddress.value) + .order(stellar.RequestBuilderOrder.DESC) + .execute(); + } catch (e) { + if (e is stellar.ErrorResponse && + e.body.contains("The resource at the url requested was not found. " + "This usually occurs for one of two reasons: " + "The url requested is not valid, or no data in our database " + "could be found with the parameters provided.")) { + // probably just doesn't have any history yet or whatever stellar needs + return; + } else { + Logging.instance.log( + "Stellar ${info.name} $walletId failed to fetch transactions", + level: LogLevel.Warning, + ); + rethrow; + } + } + for (stellar.OperationResponse response in payments.records!) { + // PaymentOperationResponse por; + if (response is stellar.PaymentOperationResponse) { + final por = response; + + final addressTo = por.to!.accountId; + final addressFrom = por.from!.accountId; + + final TransactionType type; + if (addressFrom == myAddress.value) { + if (addressTo == myAddress.value) { + type = TransactionType.sentToSelf; + } else { + type = TransactionType.outgoing; + } + } else { + type = TransactionType.incoming; + } + final amount = Amount( + rawValue: BigInt.parse(float + .parse(por.amount!) + .toStringAsFixed(cryptoCurrency.fractionDigits) + .replaceAll(".", "")), + fractionDigits: cryptoCurrency.fractionDigits, + ); + + // hack eth tx data into inputs and outputs + final List outputs = []; + final List inputs = []; + + OutputV2 output = OutputV2.isarCantDoRequiredInDefaultConstructor( + scriptPubKeyHex: "00", + valueStringSats: amount.raw.toString(), + addresses: [ + addressTo, + ], + walletOwns: addressTo == myAddress.value, + ); + InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: null, + sequence: null, + outpoint: null, + addresses: [addressFrom], + valueStringSats: amount.raw.toString(), + witness: null, + innerRedeemScriptAsm: null, + coinbase: null, + walletOwns: addressFrom == myAddress.value, + ); + + outputs.add(output); + inputs.add(input); + + int fee = 0; + int height = 0; + //Query the transaction linked to the payment, + // por.transaction returns a null sometimes + stellar.TransactionResponse tx = + await stellarSdk.transactions.transaction(por.transactionHash!); + + if (tx.hash.isNotEmpty) { + fee = tx.feeCharged!; + height = tx.ledger; + } + + final otherData = { + "overrideFee": Amount( + rawValue: BigInt.from(fee), + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), + }; + + final theTransaction = TransactionV2( + walletId: walletId, + blockHash: "", + hash: por.transactionHash!, + txid: por.transactionHash!, + timestamp: + DateTime.parse(por.createdAt!).millisecondsSinceEpoch ~/ 1000, + height: height, + inputs: inputs, + outputs: outputs, + version: -1, + type: type, + subType: TransactionSubType.none, + otherData: jsonEncode(otherData), + ); + + transactionList.add(theTransaction); + } else if (response is stellar.CreateAccountOperationResponse) { + final caor = response; + final TransactionType type; + if (caor.sourceAccount == myAddress.value) { + type = TransactionType.outgoing; + } else { + type = TransactionType.incoming; + } + final amount = Amount( + rawValue: BigInt.parse(float + .parse(caor.startingBalance!) + .toStringAsFixed(cryptoCurrency.fractionDigits) + .replaceAll(".", "")), + fractionDigits: cryptoCurrency.fractionDigits, + ); + + // hack eth tx data into inputs and outputs + final List outputs = []; + final List inputs = []; + + OutputV2 output = OutputV2.isarCantDoRequiredInDefaultConstructor( + scriptPubKeyHex: "00", + valueStringSats: amount.raw.toString(), + addresses: [ + // this is what the previous code was doing and I don't think its correct + caor.sourceAccount!, + ], + walletOwns: caor.sourceAccount! == myAddress.value, + ); + InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: null, + sequence: null, + outpoint: null, + addresses: [ + // this is what the previous code was doing and I don't think its correct + caor.sourceAccount!, + ], + valueStringSats: amount.raw.toString(), + witness: null, + innerRedeemScriptAsm: null, + coinbase: null, + walletOwns: caor.sourceAccount! == myAddress.value, + ); + + outputs.add(output); + inputs.add(input); + + int fee = 0; + int height = 0; + final tx = + await stellarSdk.transactions.transaction(caor.transactionHash!); + if (tx.hash.isNotEmpty) { + fee = tx.feeCharged!; + height = tx.ledger; + } + + final otherData = { + "overrideFee": Amount( + rawValue: BigInt.from(fee), + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), + }; + + final theTransaction = TransactionV2( + walletId: walletId, + blockHash: "", + hash: caor.transactionHash!, + txid: caor.transactionHash!, + timestamp: + DateTime.parse(caor.createdAt!).millisecondsSinceEpoch ~/ 1000, + height: height, + inputs: inputs, + outputs: outputs, + version: -1, + type: type, + subType: TransactionSubType.none, + otherData: jsonEncode(otherData), + ); + + transactionList.add(theTransaction); + } + } + + await mainDB.updateOrPutTransactionV2s(transactionList); + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from updateTransactions(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + @override + Future updateUTXOs() async { + // do nothing for stellar + return false; + } +} diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index b9cf73521..037827832 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -36,6 +36,7 @@ import 'package:stackwallet/wallets/wallet/impl/monero_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/namecoin_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/nano_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/particl_wallet.dart'; +import 'package:stackwallet/wallets/wallet/impl/stellar_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/tezos_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/wownero_wallet.dart'; @@ -209,7 +210,7 @@ abstract class Wallet { static Wallet loadTokenWallet({ required EthereumWallet ethWallet, required EthContract contract, - }) { + }) { final Wallet wallet = EthTokenWallet( ethWallet, contract, @@ -329,6 +330,11 @@ abstract class Wallet { case Coin.particl: return ParticlWallet(CryptoCurrencyNetwork.main); + case Coin.stellar: + return StellarWallet(CryptoCurrencyNetwork.main); + case Coin.stellarTestnet: + return StellarWallet(CryptoCurrencyNetwork.test); + case Coin.tezos: return TezosWallet(CryptoCurrencyNetwork.main);