mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-12-26 05:19:22 +00:00
933 lines
28 KiB
Dart
933 lines
28 KiB
Dart
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<bool> _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<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,
|
|
"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<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);
|
|
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<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 {
|
|
var fees = await stellarSdk.feeStats.execute();
|
|
return int.parse(fees.lastLedgerBaseFee);
|
|
}
|
|
|
|
@override
|
|
Future<Amount> estimateFeeFor(Amount amount, int feeRate) async {
|
|
var baseFee = await getBaseFee();
|
|
return Amount(
|
|
rawValue: BigInt.from(baseFee), fractionDigits: coin.decimals);
|
|
}
|
|
|
|
@override
|
|
Future<void> 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<FeeObject> get fees async {
|
|
int fee = await getBaseFee();
|
|
return FeeObject(
|
|
numberOfBlocksFast: 10,
|
|
numberOfBlocksAverage: 10,
|
|
numberOfBlocksSlow: 10,
|
|
fast: fee,
|
|
medium: fee,
|
|
slow: fee);
|
|
}
|
|
|
|
@override
|
|
Future<void> 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<bool> generateNewAddress() {
|
|
// not used for stellar(?)
|
|
throw UnimplementedError();
|
|
}
|
|
|
|
@override
|
|
bool get hasCalledExit => _hasCalledExit;
|
|
bool _hasCalledExit = false;
|
|
|
|
@override
|
|
Future<void> initializeExisting() async {
|
|
await _prefs.init();
|
|
}
|
|
|
|
@override
|
|
Future<void> 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<String> 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<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');
|
|
|
|
Future<void> _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<void> 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<void> 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<void> updateTransactions() async {
|
|
try {
|
|
List<Tuple2<SWTransaction.Transaction, SWAddress.Address?>>
|
|
transactionList = [];
|
|
Page<OperationResponse> 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<SWTransaction.Transaction, SWAddress.Address> 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<SWTransaction.Transaction, SWAddress.Address> 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<void> 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<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() async {
|
|
return await testStellarNodeConnection(_xlmNode!.host, _xlmNode!.port);
|
|
}
|
|
|
|
@override
|
|
Future<List<SWTransaction.Transaction>> get transactions =>
|
|
db.getTransactions(walletId).findAll();
|
|
|
|
@override
|
|
Future<void> updateNode(bool shouldRefresh) async {
|
|
_updateNode();
|
|
if (shouldRefresh) {
|
|
unawaited(refresh());
|
|
}
|
|
}
|
|
|
|
@override
|
|
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,
|
|
);
|
|
}
|
|
|
|
@override
|
|
// not used
|
|
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;
|
|
}
|