2023-07-19 17:08:46 +00:00
|
|
|
import 'dart:async';
|
|
|
|
import 'dart:io';
|
|
|
|
|
|
|
|
import 'package:decimal/decimal.dart';
|
|
|
|
import 'package:http/http.dart' as http;
|
|
|
|
import 'package:isar/isar.dart';
|
|
|
|
import 'package:stackwallet/models/balance.dart' as SWBalance;
|
|
|
|
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart' as SWTransaction;
|
|
|
|
import 'package:stackwallet/models/isar/models/blockchain_data/address.dart' as SWAddress;
|
|
|
|
import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart';
|
|
|
|
import 'package:stackwallet/models/paymint/fee_object_model.dart';
|
|
|
|
import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart';
|
|
|
|
import 'package:stackwallet/services/coins/coin_service.dart';
|
|
|
|
import 'package:stackwallet/services/mixins/coin_control_interface.dart';
|
|
|
|
import 'package:stackwallet/utilities/amount/amount.dart';
|
|
|
|
import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart';
|
|
|
|
import 'package:tuple/tuple.dart';
|
|
|
|
|
|
|
|
import '../../../db/isar/main_db.dart';
|
|
|
|
import '../../../models/node_model.dart';
|
|
|
|
import '../../../utilities/constants.dart';
|
|
|
|
import '../../../utilities/default_nodes.dart';
|
|
|
|
import '../../../utilities/enums/coin_enum.dart';
|
|
|
|
import '../../../utilities/flutter_secure_storage_interface.dart';
|
|
|
|
import '../../../utilities/logger.dart';
|
|
|
|
import '../../../utilities/prefs.dart';
|
|
|
|
import '../../event_bus/events/global/node_connection_status_changed_event.dart';
|
|
|
|
import '../../event_bus/events/global/updated_in_background_event.dart';
|
|
|
|
import '../../event_bus/events/global/wallet_sync_status_changed_event.dart';
|
|
|
|
import '../../event_bus/global_event_bus.dart';
|
|
|
|
import '../../mixins/wallet_cache.dart';
|
|
|
|
import '../../mixins/wallet_db.dart';
|
|
|
|
import '../../node_service.dart';
|
|
|
|
import '../../transaction_notification_tracker.dart';
|
|
|
|
|
|
|
|
import 'package:stellar_flutter_sdk/stellar_flutter_sdk.dart';
|
|
|
|
|
|
|
|
const int MINIMUM_CONFIRMATIONS = 1;
|
|
|
|
|
|
|
|
class StellarWallet extends CoinServiceAPI
|
|
|
|
with WalletCache, WalletDB, CoinControlInterface {
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
late final TransactionNotificationTracker txTracker;
|
|
|
|
late SecureStorageInterface _secureStore;
|
|
|
|
|
2023-07-20 15:40:49 +00:00
|
|
|
final StellarSDK stellarSdk = StellarSDK.TESTNET;
|
2023-07-19 17:08:46 +00:00
|
|
|
|
|
|
|
@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;
|
|
|
|
|
2023-07-21 12:59:47 +00:00
|
|
|
Future<bool> accoutExists(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;
|
|
|
|
}
|
2023-07-19 17:08:46 +00:00
|
|
|
@override
|
2023-07-20 15:40:49 +00:00
|
|
|
Future<String> confirmSend({required Map<String, dynamic> txData}) async {
|
|
|
|
final secretSeed = await _secureStore.read(
|
|
|
|
key: '${_walletId}_secretSeed'
|
|
|
|
);
|
|
|
|
KeyPair senderKeyPair = KeyPair.fromSecretSeed(secretSeed!);
|
|
|
|
AccountResponse sender = await stellarSdk.accounts.account(senderKeyPair.accountId);
|
|
|
|
|
2023-07-21 12:59:47 +00:00
|
|
|
final amountToSend = txData['recipientAmt'] as Amount;
|
|
|
|
|
|
|
|
//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 accoutExists(txData['address'] as String);
|
|
|
|
Transaction transaction;
|
|
|
|
|
|
|
|
if (!validAccount) {
|
|
|
|
//Fund the account, user must ensure account is correct
|
|
|
|
CreateAccountOperationBuilder createAccBuilder =
|
|
|
|
CreateAccountOperationBuilder(
|
|
|
|
txData['address'] as String, amountToSend.decimal.toString()
|
|
|
|
);
|
|
|
|
transaction = TransactionBuilder(sender)
|
|
|
|
.addOperation(createAccBuilder.build())
|
|
|
|
.build();
|
|
|
|
} else {
|
|
|
|
transaction = TransactionBuilder(sender)
|
|
|
|
.addOperation(PaymentOperationBuilder(
|
|
|
|
txData['address'] as String, Asset.NATIVE,
|
|
|
|
amountToSend.decimal.toString())
|
|
|
|
.build()
|
|
|
|
).build();
|
|
|
|
}
|
2023-07-20 15:40:49 +00:00
|
|
|
transaction.sign(senderKeyPair, Network.TESTNET);
|
2023-07-21 12:59:47 +00:00
|
|
|
try {
|
|
|
|
SubmitTransactionResponse response = await stellarSdk.submitTransaction(transaction);
|
2023-07-20 15:40:49 +00:00
|
|
|
|
2023-07-21 12:59:47 +00:00
|
|
|
if (!response.success) {
|
|
|
|
throw("Unable to send transaction");
|
|
|
|
}
|
|
|
|
return response.hash!;
|
|
|
|
} catch(e, s) {
|
|
|
|
Logging.instance.log("Error sending TX $e - $s", level: LogLevel.Error);
|
|
|
|
rethrow;
|
2023-07-20 15:40:49 +00:00
|
|
|
}
|
2023-07-21 12:59:47 +00:00
|
|
|
|
2023-07-19 17:08:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<SWAddress.Address?> get _currentReceivingAddress =>
|
|
|
|
db.getAddresses(walletId)
|
|
|
|
.filter()
|
|
|
|
.typeEqualTo(SWAddress.AddressType.unknown)
|
|
|
|
.and()
|
|
|
|
.subTypeEqualTo(SWAddress.AddressSubType.unknown)
|
|
|
|
.sortByDerivationIndexDesc()
|
|
|
|
.findFirst();
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<String> get currentReceivingAddress async =>
|
|
|
|
(await _currentReceivingAddress)?.value ?? await getAddressSW();
|
|
|
|
|
|
|
|
Future<int> getBaseFee() async {
|
|
|
|
final nodeURI = Uri.parse("${getCurrentNode().host}:${getCurrentNode().port}");
|
|
|
|
final httpClient = http.Client();
|
|
|
|
FeeStatsResponse fsp = await FeeStatsRequestBuilder(httpClient, nodeURI).execute();
|
|
|
|
return int.parse(fsp.lastLedgerBaseFee);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<Amount> estimateFeeFor(Amount amount, int feeRate) async {
|
|
|
|
var baseFee = await getBaseFee();
|
|
|
|
int fee = 100;
|
|
|
|
switch (feeRate) {
|
|
|
|
case 0:
|
|
|
|
fee = baseFee * 10;
|
|
|
|
case 1:
|
|
|
|
case 2:
|
|
|
|
fee = baseFee * 50;
|
|
|
|
case 3:
|
|
|
|
fee = baseFee * 100;
|
|
|
|
case 4:
|
|
|
|
fee = baseFee * 200;
|
|
|
|
default:
|
|
|
|
fee = baseFee * 50;
|
|
|
|
}
|
|
|
|
return Amount(rawValue: BigInt.from(fee), fractionDigits: coin.decimals);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<void> exit() {
|
|
|
|
// TODO: implement exit
|
|
|
|
throw UnimplementedError();
|
|
|
|
}
|
|
|
|
|
|
|
|
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<FeeObject> get fees async {
|
2023-07-20 15:40:49 +00:00
|
|
|
// final nodeURI = Uri.parse("${getCurrentNode().host}:${getCurrentNode().port}");
|
|
|
|
final nodeURI = Uri.parse(getCurrentNode().host);
|
|
|
|
|
|
|
|
|
2023-07-19 17:08:46 +00:00
|
|
|
final httpClient = http.Client();
|
|
|
|
FeeStatsResponse fsp = await FeeStatsRequestBuilder(httpClient, nodeURI).execute();
|
2023-07-20 15:40:49 +00:00
|
|
|
|
2023-07-19 17:08:46 +00:00
|
|
|
return FeeObject(
|
|
|
|
numberOfBlocksFast: 0,
|
|
|
|
numberOfBlocksAverage: 0,
|
|
|
|
numberOfBlocksSlow: 0,
|
|
|
|
fast: int.parse(fsp.lastLedgerBaseFee) * 100,
|
|
|
|
medium: int.parse(fsp.lastLedgerBaseFee) * 50,
|
|
|
|
slow: int.parse(fsp.lastLedgerBaseFee) * 10
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<void> fullRescan(int maxUnusedAddressGap, int maxNumberOfIndexesToCheck) async {
|
|
|
|
await _prefs.init();
|
|
|
|
await updateTransactions();
|
|
|
|
await updateChainHeight();
|
|
|
|
await updateBalance();
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<bool> generateNewAddress() {
|
|
|
|
// TODO: implement generateNewAddress
|
|
|
|
throw UnimplementedError();
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
bool get hasCalledExit => _hasCalledExit;
|
|
|
|
bool _hasCalledExit = false;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<void> initializeExisting() async {
|
|
|
|
await _prefs.init();
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<void> initializeNew() async {
|
|
|
|
if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) {
|
|
|
|
throw Exception(
|
|
|
|
"Attempted to overwrite mnemonic on generate new wallet!");
|
|
|
|
}
|
|
|
|
|
|
|
|
await _prefs.init();
|
|
|
|
|
|
|
|
String mnemonic = await Wallet.generate24WordsMnemonic();
|
|
|
|
await _secureStore.write(
|
|
|
|
key: '${_walletId}_mnemonic',
|
2023-07-20 15:40:49 +00:00
|
|
|
value: mnemonic
|
2023-07-19 17:08:46 +00:00
|
|
|
);
|
|
|
|
await _secureStore.write(
|
|
|
|
key: '${_walletId}_mnemonicPassphrase',
|
|
|
|
value: ""
|
|
|
|
);
|
|
|
|
|
|
|
|
Wallet wallet = await Wallet.from(mnemonic);
|
|
|
|
KeyPair keyPair = await wallet.getKeyPair(index: 0);
|
|
|
|
String address = keyPair.accountId;
|
2023-07-20 15:40:49 +00:00
|
|
|
String secretSeed = keyPair.secretSeed; //This will be required for sending a tx
|
|
|
|
|
|
|
|
|
|
|
|
await _secureStore.write(
|
|
|
|
key: '${_walletId}_secretSeed',
|
|
|
|
value: secretSeed
|
|
|
|
);
|
2023-07-19 17:08:46 +00:00
|
|
|
|
|
|
|
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<String> getAddressSW() async {
|
|
|
|
var mnemonic = await _secureStore.read(
|
|
|
|
key: '${_walletId}_mnemonic'
|
|
|
|
);
|
2023-07-20 15:40:49 +00:00
|
|
|
|
|
|
|
Wallet wallet = await Wallet.from(mnemonic!);
|
|
|
|
KeyPair keyPair = await wallet.getKeyPair(index: 0);
|
|
|
|
|
2023-07-19 17:08:46 +00:00
|
|
|
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<int> get maxFee => throw UnimplementedError();
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<List<String>> get mnemonic => mnemonicString.then((value) => value!.split(" "));
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<String?> get mnemonicPassphrase => _secureStore.read(key: '${_walletId}_mnemonicPassphrase');
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<String?> get mnemonicString => _secureStore.read(key: '${_walletId}_mnemonic');
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<Map<String, dynamic>> prepareSend({required String address, required Amount amount, Map<String, dynamic>? 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<String, dynamic> txData = {
|
|
|
|
"fee": fee,
|
|
|
|
"address": address,
|
|
|
|
"recipientAmt": amount,
|
|
|
|
};
|
|
|
|
|
|
|
|
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<void> recoverFromMnemonic({required String mnemonic, String? mnemonicPassphrase, required int maxUnusedAddressGap, required int maxNumberOfIndexesToCheck, required int height}) async {
|
|
|
|
if ((await mnemonicString) != null ||
|
|
|
|
(await this.mnemonicPassphrase) != null) {
|
|
|
|
throw Exception("Attempted to overwrite mnemonic on restore!");
|
|
|
|
}
|
|
|
|
|
|
|
|
var wallet = await Wallet.from(mnemonic);
|
|
|
|
var keyPair = await wallet.getKeyPair(index: 0);
|
|
|
|
var address = keyPair.accountId;
|
2023-07-20 15:40:49 +00:00
|
|
|
var secretSeed = keyPair.secretSeed;
|
2023-07-19 17:08:46 +00:00
|
|
|
|
|
|
|
await _secureStore.write(
|
|
|
|
key: '${_walletId}_mnemonic', value: mnemonic.trim());
|
|
|
|
await _secureStore.write(
|
|
|
|
key: '${_walletId}_mnemonicPassphrase',
|
|
|
|
value: mnemonicPassphrase ?? "",
|
|
|
|
);
|
2023-07-20 15:40:49 +00:00
|
|
|
await _secureStore.write(
|
|
|
|
key: '${_walletId}_secretSeed',
|
|
|
|
value: secretSeed
|
|
|
|
);
|
2023-07-19 17:08:46 +00:00
|
|
|
|
|
|
|
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<void> updateChainHeight() async {
|
|
|
|
final height = await stellarSdk.ledgers
|
|
|
|
.order(RequestBuilderOrder.DESC)
|
|
|
|
.limit(1)
|
|
|
|
.execute()
|
|
|
|
.then((value) => value.records!.first.sequence);
|
|
|
|
await updateCachedChainHeight(height);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> updateTransactions() async {
|
2023-07-20 15:40:49 +00:00
|
|
|
|
|
|
|
try {
|
|
|
|
List<Tuple2<SWTransaction.Transaction, SWAddress.Address?>> transactionList = [];
|
|
|
|
|
|
|
|
Page<OperationResponse> payments = await stellarSdk.payments
|
|
|
|
.forAccount(await getAddressSW()).order(RequestBuilderOrder.DESC)
|
|
|
|
.execute().onError((error, stackTrace) =>
|
|
|
|
throw("Could not fetch transactions")
|
|
|
|
);
|
|
|
|
|
|
|
|
for (OperationResponse response in payments.records!) {
|
2023-07-21 12:59:47 +00:00
|
|
|
PaymentOperationResponse por;
|
2023-07-20 15:40:49 +00:00
|
|
|
if (response is PaymentOperationResponse) {
|
2023-07-21 12:59:47 +00:00
|
|
|
por = response;
|
|
|
|
|
|
|
|
Logging.instance.log(
|
|
|
|
"ALL TRANSACTIONS IS $por",
|
|
|
|
level: LogLevel.Info);
|
2023-07-20 15:40:49 +00:00
|
|
|
SWTransaction.TransactionType type;
|
|
|
|
if (por.sourceAccount == await getAddressSW()) {
|
|
|
|
type = SWTransaction.TransactionType.outgoing;
|
|
|
|
} else {
|
|
|
|
type = SWTransaction.TransactionType.incoming;
|
|
|
|
}
|
2023-07-19 17:08:46 +00:00
|
|
|
final amount = Amount(
|
2023-07-21 12:59:47 +00:00
|
|
|
rawValue: BigInt.parse(float.parse(por.amount!).toStringAsFixed(coin.decimals).replaceAll(".", "")),
|
|
|
|
fractionDigits: coin.decimals,
|
|
|
|
);
|
|
|
|
int fee = 0;
|
|
|
|
int height = 0;
|
|
|
|
var transaction = por.transaction;
|
|
|
|
if (transaction != null) {
|
|
|
|
fee = transaction.feeCharged!;
|
|
|
|
height = transaction.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<SWTransaction.Transaction, SWAddress.Address> tuple = Tuple2(theTransaction, address);
|
|
|
|
transactionList.add(tuple);
|
|
|
|
} else if (response is CreateAccountOperationResponse) {
|
|
|
|
var caor = response;
|
|
|
|
SWTransaction.TransactionType type;
|
|
|
|
if (caor.sourceAccount == await getAddressSW()) {
|
|
|
|
type = SWTransaction.TransactionType.outgoing;
|
|
|
|
} else {
|
|
|
|
type = SWTransaction.TransactionType.incoming;
|
|
|
|
}
|
|
|
|
final amount = Amount(
|
2023-07-20 15:40:49 +00:00
|
|
|
rawValue: BigInt.parse(float.parse(caor.startingBalance!).toStringAsFixed(coin.decimals).replaceAll(".", "")),
|
|
|
|
fractionDigits: coin.decimals,
|
|
|
|
);
|
|
|
|
int fee = 0;
|
|
|
|
int height = 0;
|
|
|
|
var transaction = caor.transaction;
|
|
|
|
if (transaction != null) {
|
|
|
|
fee = transaction.feeCharged!;
|
|
|
|
height = transaction.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(
|
2023-07-19 17:08:46 +00:00
|
|
|
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<SWTransaction.Transaction, SWAddress.Address> tuple = Tuple2(theTransaction, address);
|
|
|
|
transactionList.add(tuple);
|
2023-07-20 15:40:49 +00:00
|
|
|
}
|
2023-07-19 17:08:46 +00:00
|
|
|
}
|
2023-07-20 15:40:49 +00:00
|
|
|
await db.addNewTransactionData(transactionList, walletId);
|
|
|
|
} catch (e, s) {
|
|
|
|
Logging.instance.log(
|
|
|
|
"Exception rethrown from updateTransactions(): $e\n$s",
|
|
|
|
level: LogLevel.Error);
|
2023-07-19 17:08:46 +00:00
|
|
|
}
|
2023-07-20 15:40:49 +00:00
|
|
|
|
2023-07-19 17:08:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> updateBalance() async {
|
2023-07-21 12:59:47 +00:00
|
|
|
try {
|
|
|
|
AccountResponse accountResponse = await stellarSdk.accounts.account(await getAddressSW());
|
|
|
|
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 - 10000000), // Minus 1 XLM for account activation fee
|
|
|
|
fractionDigits: coin.decimals,
|
|
|
|
),
|
|
|
|
spendable: Amount(
|
|
|
|
rawValue: BigInt.from(float.parse(balance.balance) * 10000000 - 10000000), // Minus 1 XLM for account activation fee
|
|
|
|
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!);
|
|
|
|
}
|
2023-07-19 17:08:46 +00:00
|
|
|
}
|
2023-07-21 12:59:47 +00:00
|
|
|
} catch(e, s) {
|
|
|
|
Logging.instance.log(
|
|
|
|
"ERROR GETTING BALANCE $e\$s",
|
|
|
|
level: LogLevel.Info,
|
|
|
|
);
|
2023-07-19 17:08:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<void> 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<bool> testNetworkConnection() {
|
|
|
|
// TODO: implement testNetworkConnection
|
|
|
|
throw UnimplementedError();
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<List<SWTransaction.Transaction>> get transactions => db.getTransactions(walletId).findAll();
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<void> updateNode(bool shouldRefresh) async {
|
|
|
|
_xlmNode = NodeService(secureStorageInterface: _secureStore)
|
|
|
|
.getPrimaryNodeFor(coin: coin) ??
|
|
|
|
DefaultNodes.getNodeFor(coin);
|
|
|
|
|
|
|
|
if (shouldRefresh) {
|
|
|
|
unawaited(refresh());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2023-07-21 12:59:47 +00:00
|
|
|
Future<void> updateSentCachedTxData(Map<String, dynamic> 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,
|
|
|
|
);
|
2023-07-19 17:08:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
// TODO: implement utxos
|
|
|
|
Future<List<UTXO>> 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;
|
|
|
|
}
|