mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-12-23 11:59:30 +00:00
commit
1d9fb4cd06
27 changed files with 950 additions and 427 deletions
|
@ -9,6 +9,7 @@
|
|||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:stackwallet/db/hive/db.dart';
|
||||
import 'package:stackwallet/electrumx_rpc/electrumx.dart';
|
||||
|
@ -167,8 +168,10 @@ class CachedElectrumX {
|
|||
Set<String> cachedSerials =
|
||||
_list == null ? {} : List<String>.from(_list).toSet();
|
||||
|
||||
final startNumber =
|
||||
cachedSerials.length - 10; // 10 being some arbitrary buffer
|
||||
startNumber = max(
|
||||
max(0, startNumber),
|
||||
cachedSerials.length - 100, // 100 being some arbitrary buffer
|
||||
);
|
||||
|
||||
final serials = await electrumXClient.getUsedCoinSerials(
|
||||
startNumber: startNumber,
|
||||
|
|
|
@ -84,7 +84,14 @@ class TransactionV2 {
|
|||
.where((e) => e.walletOwns)
|
||||
.fold(BigInt.zero, (p, e) => p + e.value);
|
||||
|
||||
return Amount(rawValue: inSum, fractionDigits: coin.decimals);
|
||||
return Amount(
|
||||
rawValue: inSum,
|
||||
fractionDigits: coin.decimals,
|
||||
) -
|
||||
getAmountReceivedThisWallet(
|
||||
coin: coin,
|
||||
) -
|
||||
getFee(coin: coin);
|
||||
}
|
||||
|
||||
Set<String> associatedAddresses() => {
|
||||
|
|
|
@ -61,10 +61,11 @@ class _CashFusionViewState extends ConsumerState<CashFusionView> {
|
|||
FusionOption _option = FusionOption.continuous;
|
||||
|
||||
Future<void> _startFusion() async {
|
||||
final fusionWallet = ref
|
||||
final wallet = ref
|
||||
.read(walletsChangeNotifierProvider)
|
||||
.getManager(widget.walletId)
|
||||
.wallet as FusionWalletInterface;
|
||||
.wallet;
|
||||
final fusionWallet = wallet as FusionWalletInterface;
|
||||
|
||||
try {
|
||||
fusionWallet.uiState = ref.read(
|
||||
|
@ -89,7 +90,9 @@ class _CashFusionViewState extends ConsumerState<CashFusionView> {
|
|||
);
|
||||
|
||||
// update user prefs (persistent)
|
||||
ref.read(prefsChangeNotifierProvider).fusionServerInfo = newInfo;
|
||||
ref
|
||||
.read(prefsChangeNotifierProvider)
|
||||
.setFusionServerInfo(wallet.coin, newInfo);
|
||||
|
||||
unawaited(
|
||||
fusionWallet.fuse(
|
||||
|
@ -113,7 +116,11 @@ class _CashFusionViewState extends ConsumerState<CashFusionView> {
|
|||
portFocusNode = FocusNode();
|
||||
fusionRoundFocusNode = FocusNode();
|
||||
|
||||
final info = ref.read(prefsChangeNotifierProvider).fusionServerInfo;
|
||||
final info = ref.read(prefsChangeNotifierProvider).getFusionServerInfo(ref
|
||||
.read(walletsChangeNotifierProvider)
|
||||
.getManager(widget.walletId)
|
||||
.wallet
|
||||
.coin);
|
||||
serverController.text = info.host;
|
||||
portController.text = info.port.toString();
|
||||
_enableSSLCheckbox = info.ssl;
|
||||
|
@ -150,7 +157,7 @@ class _CashFusionViewState extends ConsumerState<CashFusionView> {
|
|||
automaticallyImplyLeading: false,
|
||||
leading: const AppBarBackButton(),
|
||||
title: Text(
|
||||
"CashFusion",
|
||||
"Fusion",
|
||||
style: STextStyles.navBarTitle(context),
|
||||
),
|
||||
titleSpacing: 0,
|
||||
|
@ -189,7 +196,7 @@ class _CashFusionViewState extends ConsumerState<CashFusionView> {
|
|||
children: [
|
||||
RoundedWhiteContainer(
|
||||
child: Text(
|
||||
"CashFusion allows you to anonymize your BCH coins.",
|
||||
"Fusion helps anonymize your coins by mixing them.",
|
||||
style: STextStyles.w500_12(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
|
@ -214,7 +221,11 @@ class _CashFusionViewState extends ConsumerState<CashFusionView> {
|
|||
CustomTextButton(
|
||||
text: "Default",
|
||||
onTap: () {
|
||||
const def = FusionInfo.DEFAULTS;
|
||||
final def = kFusionServerInfoDefaults[ref
|
||||
.read(walletsChangeNotifierProvider)
|
||||
.getManager(widget.walletId)
|
||||
.wallet
|
||||
.coin]!;
|
||||
serverController.text = def.host;
|
||||
portController.text = def.port.toString();
|
||||
fusionRoundController.text =
|
||||
|
|
|
@ -18,6 +18,7 @@ import 'package:stackwallet/providers/global/prefs_provider.dart';
|
|||
import 'package:stackwallet/providers/global/wallets_provider.dart';
|
||||
import 'package:stackwallet/services/mixins/fusion_wallet_interface.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
import 'package:stackwallet/utilities/show_loading.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
|
@ -43,6 +44,8 @@ class FusionProgressView extends ConsumerStatefulWidget {
|
|||
}
|
||||
|
||||
class _FusionProgressViewState extends ConsumerState<FusionProgressView> {
|
||||
late final Coin coin;
|
||||
|
||||
Future<bool> _requestAndProcessCancel() async {
|
||||
final shouldCancel = await showDialog<bool?>(
|
||||
context: context,
|
||||
|
@ -88,6 +91,16 @@ class _FusionProgressViewState extends ConsumerState<FusionProgressView> {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
coin = ref
|
||||
.read(walletsChangeNotifierProvider)
|
||||
.getManager(widget.walletId)
|
||||
.wallet
|
||||
.coin;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool _succeeded =
|
||||
|
@ -230,7 +243,8 @@ class _FusionProgressViewState extends ConsumerState<FusionProgressView> {
|
|||
.getManager(widget.walletId)
|
||||
.wallet as FusionWalletInterface;
|
||||
|
||||
final fusionInfo = ref.read(prefsChangeNotifierProvider).fusionServerInfo;
|
||||
final fusionInfo =
|
||||
ref.read(prefsChangeNotifierProvider).getFusionServerInfo(coin);
|
||||
|
||||
try {
|
||||
fusionWallet.uiState = ref.read(
|
||||
|
|
|
@ -21,6 +21,7 @@ import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dar
|
|||
import 'package:stackwallet/services/exchange/exchange.dart';
|
||||
import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart';
|
||||
import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_exchange.dart';
|
||||
import 'package:stackwallet/services/exchange/trocador/trocador_exchange.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/assets.dart';
|
||||
import 'package:stackwallet/utilities/constants.dart';
|
||||
|
@ -107,10 +108,14 @@ class _ExchangeCurrencySelectionViewState
|
|||
if (widget.pairedTicker == null) {
|
||||
return await _getCurrencies();
|
||||
}
|
||||
await ExchangeDataLoadingService.instance.initDB();
|
||||
List<Currency> currencies = await ExchangeDataLoadingService
|
||||
.instance.isar.currencies
|
||||
.where()
|
||||
.filter()
|
||||
.exchangeNameEqualTo(MajesticBankExchange.exchangeName)
|
||||
.or()
|
||||
.exchangeNameStartsWith(TrocadorExchange.exchangeName)
|
||||
.findAll();
|
||||
|
||||
final cn = await ChangeNowExchange.instance.getPairedCurrencies(
|
||||
|
@ -120,7 +125,7 @@ class _ExchangeCurrencySelectionViewState
|
|||
|
||||
if (cn.value == null) {
|
||||
if (cn.exception is UnsupportedCurrencyException) {
|
||||
return currencies;
|
||||
return _getDistinctCurrenciesFrom(currencies);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
|
@ -153,6 +158,7 @@ class _ExchangeCurrencySelectionViewState
|
|||
}
|
||||
|
||||
Future<List<Currency>> _getCurrencies() async {
|
||||
await ExchangeDataLoadingService.instance.initDB();
|
||||
final currencies = await ExchangeDataLoadingService.instance.isar.currencies
|
||||
.where()
|
||||
.filter()
|
||||
|
@ -186,7 +192,8 @@ class _ExchangeCurrencySelectionViewState
|
|||
List<Currency> _getDistinctCurrenciesFrom(List<Currency> currencies) {
|
||||
final List<Currency> distinctCurrencies = [];
|
||||
for (final currency in currencies) {
|
||||
if (!distinctCurrencies.any((e) => e.ticker == currency.ticker)) {
|
||||
if (!distinctCurrencies.any(
|
||||
(e) => e.ticker.toLowerCase() == currency.ticker.toLowerCase())) {
|
||||
distinctCurrencies.add(currency);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -88,7 +88,10 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
|
|||
|
||||
bool isStackCoin(String ticker) {
|
||||
try {
|
||||
coinFromTickerCaseInsensitive(ticker);
|
||||
try {
|
||||
coinFromTickerCaseInsensitive(ticker);
|
||||
} catch (_) {}
|
||||
coinFromPrettyName(ticker);
|
||||
return true;
|
||||
} on ArgumentError catch (_) {
|
||||
return false;
|
||||
|
@ -272,10 +275,17 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
|
|||
label: "Send from Stack",
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: () {
|
||||
final coin =
|
||||
coinFromTickerCaseInsensitive(trade.payInCurrency);
|
||||
final amount =
|
||||
sendAmount.toAmount(fractionDigits: coin.decimals);
|
||||
Coin coin;
|
||||
try {
|
||||
coin = coinFromTickerCaseInsensitive(
|
||||
trade.payInCurrency);
|
||||
} catch (_) {
|
||||
coin = coinFromPrettyName(trade.payInCurrency);
|
||||
}
|
||||
final amount = Amount.fromDecimal(
|
||||
sendAmount,
|
||||
fractionDigits: coin.decimals,
|
||||
);
|
||||
final address = trade.payInAddress;
|
||||
|
||||
Navigator.of(context).pushNamed(
|
||||
|
@ -1347,12 +1357,18 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
|
|||
SecondaryButton(
|
||||
label: "Send from Stack",
|
||||
onPressed: () {
|
||||
final amount = sendAmount;
|
||||
Coin coin;
|
||||
try {
|
||||
coin = coinFromTickerCaseInsensitive(trade.payInCurrency);
|
||||
} catch (_) {
|
||||
coin = coinFromPrettyName(trade.payInCurrency);
|
||||
}
|
||||
final amount = Amount.fromDecimal(
|
||||
sendAmount,
|
||||
fractionDigits: coin.decimals,
|
||||
);
|
||||
final address = trade.payInAddress;
|
||||
|
||||
final coin =
|
||||
coinFromTickerCaseInsensitive(trade.payInCurrency);
|
||||
|
||||
Navigator.of(context).pushNamed(
|
||||
SendFromView.routeName,
|
||||
arguments: Tuple4(
|
||||
|
|
|
@ -2007,6 +2007,7 @@ class _SendViewState extends ConsumerState<SendView> {
|
|||
),
|
||||
if (coin != Coin.epicCash &&
|
||||
coin != Coin.nano &&
|
||||
coin != Coin.tezos &&
|
||||
coin != Coin.banano)
|
||||
Text(
|
||||
"Transaction fee (estimated)",
|
||||
|
@ -2015,12 +2016,14 @@ class _SendViewState extends ConsumerState<SendView> {
|
|||
),
|
||||
if (coin != Coin.epicCash &&
|
||||
coin != Coin.nano &&
|
||||
coin != Coin.tezos &&
|
||||
coin != Coin.banano)
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (coin != Coin.epicCash &&
|
||||
coin != Coin.nano &&
|
||||
coin != Coin.tezos &&
|
||||
coin != Coin.banano)
|
||||
Stack(
|
||||
children: [
|
||||
|
|
|
@ -845,7 +845,8 @@ class _WalletViewState extends ConsumerState<WalletView> {
|
|||
onTap: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
coin == Coin.bitcoincash ||
|
||||
coin == Coin.bitcoincashTestnet
|
||||
coin == Coin.bitcoincashTestnet ||
|
||||
coin == Coin.eCash
|
||||
? AllTransactionsV2View.routeName
|
||||
: AllTransactionsView.routeName,
|
||||
arguments: walletId,
|
||||
|
@ -902,7 +903,9 @@ class _WalletViewState extends ConsumerState<WalletView> {
|
|||
children: [
|
||||
Expanded(
|
||||
child: coin == Coin.bitcoincash ||
|
||||
coin == Coin.bitcoincashTestnet
|
||||
coin ==
|
||||
Coin.bitcoincashTestnet ||
|
||||
coin == Coin.eCash
|
||||
? TransactionsV2List(
|
||||
walletId: widget.walletId,
|
||||
)
|
||||
|
|
|
@ -26,6 +26,7 @@ import 'package:stackwallet/services/mixins/fusion_wallet_interface.dart';
|
|||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/assets.dart';
|
||||
import 'package:stackwallet/utilities/constants.dart';
|
||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
|
||||
|
@ -58,6 +59,7 @@ class _DesktopCashFusion extends ConsumerState<DesktopCashFusionView> {
|
|||
late final FocusNode portFocusNode;
|
||||
late final TextEditingController fusionRoundController;
|
||||
late final FocusNode fusionRoundFocusNode;
|
||||
late final Coin coin;
|
||||
|
||||
bool _enableStartButton = false;
|
||||
bool _enableSSLCheckbox = false;
|
||||
|
@ -93,7 +95,7 @@ class _DesktopCashFusion extends ConsumerState<DesktopCashFusionView> {
|
|||
);
|
||||
|
||||
// update user prefs (persistent)
|
||||
ref.read(prefsChangeNotifierProvider).fusionServerInfo = newInfo;
|
||||
ref.read(prefsChangeNotifierProvider).setFusionServerInfo(coin, newInfo);
|
||||
|
||||
unawaited(
|
||||
fusionWallet.fuse(
|
||||
|
@ -121,8 +123,14 @@ class _DesktopCashFusion extends ConsumerState<DesktopCashFusionView> {
|
|||
serverFocusNode = FocusNode();
|
||||
portFocusNode = FocusNode();
|
||||
fusionRoundFocusNode = FocusNode();
|
||||
coin = ref
|
||||
.read(walletsChangeNotifierProvider)
|
||||
.getManager(widget.walletId)
|
||||
.wallet
|
||||
.coin;
|
||||
|
||||
final info = ref.read(prefsChangeNotifierProvider).fusionServerInfo;
|
||||
final info =
|
||||
ref.read(prefsChangeNotifierProvider).getFusionServerInfo(coin);
|
||||
serverController.text = info.host;
|
||||
portController.text = info.port.toString();
|
||||
_enableSSLCheckbox = info.ssl;
|
||||
|
@ -197,7 +205,7 @@ class _DesktopCashFusion extends ConsumerState<DesktopCashFusionView> {
|
|||
width: 12,
|
||||
),
|
||||
Text(
|
||||
"CashFusion",
|
||||
"Fusion",
|
||||
style: STextStyles.desktopH3(context),
|
||||
),
|
||||
],
|
||||
|
@ -219,7 +227,7 @@ class _DesktopCashFusion extends ConsumerState<DesktopCashFusionView> {
|
|||
),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: "What is CashFusion?",
|
||||
text: "What is Fusion?",
|
||||
style: STextStyles.richLink(context).copyWith(
|
||||
fontSize: 16,
|
||||
),
|
||||
|
@ -248,7 +256,7 @@ class _DesktopCashFusion extends ConsumerState<DesktopCashFusionView> {
|
|||
.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"What is CashFusion?",
|
||||
"What is Fusion?",
|
||||
style: STextStyles.desktopH2(
|
||||
context),
|
||||
),
|
||||
|
@ -308,7 +316,7 @@ class _DesktopCashFusion extends ConsumerState<DesktopCashFusionView> {
|
|||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
"CashFusion allows you to anonymize your BCH coins.",
|
||||
"Fusion helps anonymize your coins by mixing them.",
|
||||
style:
|
||||
STextStyles.desktopTextExtraExtraSmall(context),
|
||||
),
|
||||
|
@ -336,7 +344,11 @@ class _DesktopCashFusion extends ConsumerState<DesktopCashFusionView> {
|
|||
CustomTextButton(
|
||||
text: "Default",
|
||||
onTap: () {
|
||||
const def = FusionInfo.DEFAULTS;
|
||||
final def = kFusionServerInfoDefaults[ref
|
||||
.read(walletsChangeNotifierProvider)
|
||||
.getManager(widget.walletId)
|
||||
.wallet
|
||||
.coin]!;
|
||||
serverController.text = def.host;
|
||||
portController.text = def.port.toString();
|
||||
fusionRoundController.text =
|
||||
|
|
|
@ -283,12 +283,14 @@ class _FusionDialogViewState extends ConsumerState<FusionDialogView> {
|
|||
|
||||
/// Fuse again.
|
||||
void _fuseAgain() async {
|
||||
final fusionWallet = ref
|
||||
final wallet = ref
|
||||
.read(walletsChangeNotifierProvider)
|
||||
.getManager(widget.walletId)
|
||||
.wallet as FusionWalletInterface;
|
||||
.wallet;
|
||||
final fusionWallet = wallet as FusionWalletInterface;
|
||||
|
||||
final fusionInfo = ref.read(prefsChangeNotifierProvider).fusionServerInfo;
|
||||
final fusionInfo =
|
||||
ref.read(prefsChangeNotifierProvider).getFusionServerInfo(wallet.coin);
|
||||
|
||||
try {
|
||||
fusionWallet.uiState = ref.read(
|
||||
|
|
|
@ -482,7 +482,8 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
|
|||
} else {
|
||||
await Navigator.of(context).pushNamed(
|
||||
coin == Coin.bitcoincash ||
|
||||
coin == Coin.bitcoincashTestnet
|
||||
coin == Coin.bitcoincashTestnet ||
|
||||
coin == Coin.eCash
|
||||
? AllTransactionsV2View.routeName
|
||||
: AllTransactionsView.routeName,
|
||||
arguments: widget.walletId,
|
||||
|
@ -520,7 +521,8 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
|
|||
walletId: widget.walletId,
|
||||
)
|
||||
: coin == Coin.bitcoincash ||
|
||||
coin == Coin.bitcoincashTestnet
|
||||
coin == Coin.bitcoincashTestnet ||
|
||||
coin == Coin.eCash
|
||||
? TransactionsV2List(
|
||||
walletId: widget.walletId,
|
||||
)
|
||||
|
|
|
@ -1459,7 +1459,8 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
|||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
if (!([Coin.nano, Coin.banano, Coin.epicCash].contains(coin)))
|
||||
if (!([Coin.nano, Coin.banano, Coin.epicCash, Coin.tezos]
|
||||
.contains(coin)))
|
||||
ConditionalParent(
|
||||
condition: coin.isElectrumXCoin &&
|
||||
!(((coin == Coin.firo || coin == Coin.firoTestNet) &&
|
||||
|
@ -1510,11 +1511,13 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
|||
textAlign: TextAlign.left,
|
||||
),
|
||||
),
|
||||
if (!([Coin.nano, Coin.banano, Coin.epicCash].contains(coin)))
|
||||
if (!([Coin.nano, Coin.banano, Coin.epicCash, Coin.tezos]
|
||||
.contains(coin)))
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
if (!([Coin.nano, Coin.banano, Coin.epicCash].contains(coin)))
|
||||
if (!([Coin.nano, Coin.banano, Coin.epicCash, Coin.tezos]
|
||||
.contains(coin)))
|
||||
if (!isCustomFee)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
|
|
|
@ -125,8 +125,8 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
|
|||
),
|
||||
if (manager.hasFusionSupport)
|
||||
_MoreFeaturesItem(
|
||||
label: "CashFusion",
|
||||
detail: "Decentralized Bitcoin Cash mixing protocol",
|
||||
label: "Fusion",
|
||||
detail: "Decentralized mixing protocol",
|
||||
iconAsset: Assets.svg.cashFusion,
|
||||
onPressed: () => widget.onFusionPressed?.call(),
|
||||
),
|
||||
|
|
|
@ -26,9 +26,13 @@ import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart';
|
|||
import 'package:stackwallet/electrumx_rpc/electrumx.dart';
|
||||
import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart';
|
||||
import 'package:stackwallet/models/balance.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/isar/models/isar_models.dart' as isar_models;
|
||||
import 'package:stackwallet/models/paymint/fee_object_model.dart';
|
||||
import 'package:stackwallet/models/signing_data.dart';
|
||||
import 'package:stackwallet/services/coins/bitcoincash/bch_utils.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/refresh_percent_changed_event.dart';
|
||||
|
@ -37,6 +41,7 @@ import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_
|
|||
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
|
||||
import 'package:stackwallet/services/mixins/coin_control_interface.dart';
|
||||
import 'package:stackwallet/services/mixins/electrum_x_parsing.dart';
|
||||
import 'package:stackwallet/services/mixins/fusion_wallet_interface.dart';
|
||||
import 'package:stackwallet/services/mixins/wallet_cache.dart';
|
||||
import 'package:stackwallet/services/mixins/wallet_db.dart';
|
||||
import 'package:stackwallet/services/mixins/xpubable.dart';
|
||||
|
@ -50,6 +55,7 @@ import 'package:stackwallet/utilities/default_nodes.dart';
|
|||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart';
|
||||
import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart';
|
||||
import 'package:stackwallet/utilities/extensions/extensions.dart';
|
||||
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/prefs.dart';
|
||||
|
@ -130,7 +136,12 @@ String constructDerivePath({
|
|||
}
|
||||
|
||||
class ECashWallet extends CoinServiceAPI
|
||||
with WalletCache, WalletDB, ElectrumXParsing, CoinControlInterface
|
||||
with
|
||||
WalletCache,
|
||||
WalletDB,
|
||||
ElectrumXParsing,
|
||||
CoinControlInterface,
|
||||
FusionWalletInterface
|
||||
implements XPubAble {
|
||||
ECashWallet({
|
||||
required String walletId,
|
||||
|
@ -162,6 +173,19 @@ class ECashWallet extends CoinServiceAPI
|
|||
await updateCachedBalance(_balance!);
|
||||
},
|
||||
);
|
||||
initFusionInterface(
|
||||
walletId: walletId,
|
||||
coin: coin,
|
||||
db: db,
|
||||
getWalletCachedElectrumX: () => cachedElectrumXClient,
|
||||
getNextUnusedChangeAddress: _getUnusedChangeAddresses,
|
||||
getChainHeight: () async => chainHeight,
|
||||
updateWalletUTXOS: _updateUTXOs,
|
||||
mnemonic: mnemonicString,
|
||||
mnemonicPassphrase: mnemonicPassphrase,
|
||||
network: _network,
|
||||
convertToScriptHash: _convertToScriptHash,
|
||||
);
|
||||
}
|
||||
|
||||
static const integrationTestFlag =
|
||||
|
@ -185,6 +209,81 @@ class ECashWallet extends CoinServiceAPI
|
|||
}
|
||||
}
|
||||
|
||||
Future<List<isar_models.Address>> _getUnusedChangeAddresses({
|
||||
int numberOfAddresses = 1,
|
||||
}) async {
|
||||
if (numberOfAddresses < 1) {
|
||||
throw ArgumentError.value(
|
||||
numberOfAddresses,
|
||||
"numberOfAddresses",
|
||||
"Must not be less than 1",
|
||||
);
|
||||
}
|
||||
|
||||
final changeAddresses = await db
|
||||
.getAddresses(walletId)
|
||||
.filter()
|
||||
.typeEqualTo(isar_models.AddressType.p2pkh)
|
||||
.subTypeEqualTo(isar_models.AddressSubType.change)
|
||||
.derivationPath((q) => q.not().valueStartsWith("m/44'/0'"))
|
||||
.sortByDerivationIndex()
|
||||
.findAll();
|
||||
|
||||
final List<isar_models.Address> unused = [];
|
||||
|
||||
for (final addr in changeAddresses) {
|
||||
if (await _isUnused(addr.value)) {
|
||||
unused.add(addr);
|
||||
if (unused.length == numberOfAddresses) {
|
||||
return unused;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if not returned by now, we need to create more addresses
|
||||
int countMissing = numberOfAddresses - unused.length;
|
||||
|
||||
int nextIndex =
|
||||
changeAddresses.isEmpty ? 0 : changeAddresses.last.derivationIndex + 1;
|
||||
|
||||
while (countMissing > 0) {
|
||||
// create a new address
|
||||
final address = await _generateAddressForChain(
|
||||
1,
|
||||
nextIndex,
|
||||
DerivePathTypeExt.primaryFor(coin),
|
||||
);
|
||||
nextIndex++;
|
||||
await db.updateOrPutAddresses([address]);
|
||||
|
||||
// check if it has been used before adding
|
||||
if (await _isUnused(address.value)) {
|
||||
unused.add(address);
|
||||
countMissing--;
|
||||
}
|
||||
}
|
||||
|
||||
return unused;
|
||||
}
|
||||
|
||||
Future<bool> _isUnused(String address) async {
|
||||
final txCountInDB = await db
|
||||
.getTransactions(_walletId)
|
||||
.filter()
|
||||
.address((q) => q.valueEqualTo(address))
|
||||
.count();
|
||||
if (txCountInDB == 0) {
|
||||
// double check via electrumx
|
||||
// _getTxCountForAddress can throw!
|
||||
// final count = await getTxCount(address: address);
|
||||
// if (count == 0) {
|
||||
return true;
|
||||
// }
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
set isFavorite(bool markFavorite) {
|
||||
_isFavorite = markFavorite;
|
||||
|
@ -1160,6 +1259,8 @@ class ECashWallet extends CoinServiceAPI
|
|||
}
|
||||
}).toSet();
|
||||
|
||||
final allAddressesSet = {...receivingAddresses, ...changeAddresses};
|
||||
|
||||
final List<Map<String, dynamic>> allTxHashes =
|
||||
await _fetchHistory([...receivingAddresses, ...changeAddresses]);
|
||||
|
||||
|
@ -1194,207 +1295,168 @@ class ECashWallet extends CoinServiceAPI
|
|||
}
|
||||
}
|
||||
|
||||
final List<Tuple2<isar_models.Transaction, isar_models.Address?>> txns = [];
|
||||
final List<TransactionV2> txns = [];
|
||||
|
||||
for (final txData in allTransactions) {
|
||||
Set<String> inputAddresses = {};
|
||||
Set<String> outputAddresses = {};
|
||||
// set to true if any inputs were detected as owned by this wallet
|
||||
bool wasSentFromThisWallet = false;
|
||||
|
||||
Logging.instance.log(txData, level: LogLevel.Fatal);
|
||||
|
||||
Amount totalInputValue = Amount(
|
||||
rawValue: BigInt.from(0),
|
||||
fractionDigits: coin.decimals,
|
||||
);
|
||||
Amount totalOutputValue = Amount(
|
||||
rawValue: BigInt.from(0),
|
||||
fractionDigits: coin.decimals,
|
||||
);
|
||||
|
||||
Amount amountSentFromWallet = Amount(
|
||||
rawValue: BigInt.from(0),
|
||||
fractionDigits: coin.decimals,
|
||||
);
|
||||
Amount amountReceivedInWallet = Amount(
|
||||
rawValue: BigInt.from(0),
|
||||
fractionDigits: coin.decimals,
|
||||
);
|
||||
Amount changeAmount = Amount(
|
||||
rawValue: BigInt.from(0),
|
||||
fractionDigits: coin.decimals,
|
||||
);
|
||||
// set to true if any outputs were detected as owned by this wallet
|
||||
bool wasReceivedInThisWallet = false;
|
||||
BigInt amountReceivedInThisWallet = BigInt.zero;
|
||||
BigInt changeAmountReceivedInThisWallet = BigInt.zero;
|
||||
|
||||
// parse inputs
|
||||
for (final input in txData["vin"] as List) {
|
||||
final prevTxid = input["txid"] as String;
|
||||
final prevOut = input["vout"] as int;
|
||||
final List<InputV2> inputs = [];
|
||||
for (final jsonInput in txData["vin"] as List) {
|
||||
final map = Map<String, dynamic>.from(jsonInput as Map);
|
||||
|
||||
// fetch input tx to get address
|
||||
final inputTx = await cachedElectrumXClient.getTransaction(
|
||||
txHash: prevTxid,
|
||||
coin: coin,
|
||||
);
|
||||
final List<String> addresses = [];
|
||||
String valueStringSats = "0";
|
||||
OutpointV2? outpoint;
|
||||
|
||||
for (final output in inputTx["vout"] as List) {
|
||||
// check matching output
|
||||
if (prevOut == output["n"]) {
|
||||
// get value
|
||||
final value = Amount.fromDecimal(
|
||||
Decimal.parse(output["value"].toString()),
|
||||
fractionDigits: coin.decimals,
|
||||
);
|
||||
final coinbase = map["coinbase"] as String?;
|
||||
|
||||
// add value to total
|
||||
totalInputValue = totalInputValue + value;
|
||||
if (coinbase == null) {
|
||||
final txid = map["txid"] as String;
|
||||
final vout = map["vout"] as int;
|
||||
|
||||
// get input(prevOut) address
|
||||
final address =
|
||||
output["scriptPubKey"]?["addresses"]?[0] as String? ??
|
||||
output["scriptPubKey"]?["address"] as String?;
|
||||
|
||||
if (address != null) {
|
||||
inputAddresses.add(address);
|
||||
|
||||
// if input was from my wallet, add value to amount sent
|
||||
if (receivingAddresses.contains(address) ||
|
||||
changeAddresses.contains(address)) {
|
||||
amountSentFromWallet = amountSentFromWallet + value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse outputs
|
||||
for (final output in txData["vout"] as List) {
|
||||
// get value
|
||||
final value = Amount.fromDecimal(
|
||||
Decimal.parse(output["value"].toString()),
|
||||
fractionDigits: coin.decimals,
|
||||
);
|
||||
|
||||
// add value to total
|
||||
totalOutputValue += value;
|
||||
|
||||
// get output address
|
||||
final address = output["scriptPubKey"]?["addresses"]?[0] as String? ??
|
||||
output["scriptPubKey"]?["address"] as String?;
|
||||
if (address != null) {
|
||||
outputAddresses.add(address);
|
||||
|
||||
// if output was to my wallet, add value to amount received
|
||||
if (receivingAddresses.contains(address)) {
|
||||
amountReceivedInWallet += value;
|
||||
} else if (changeAddresses.contains(address)) {
|
||||
changeAmount += value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final mySentFromAddresses = [
|
||||
...receivingAddresses.intersection(inputAddresses),
|
||||
...changeAddresses.intersection(inputAddresses)
|
||||
];
|
||||
final myReceivedOnAddresses =
|
||||
receivingAddresses.intersection(outputAddresses);
|
||||
final myChangeReceivedOnAddresses =
|
||||
changeAddresses.intersection(outputAddresses);
|
||||
|
||||
final fee = totalInputValue - totalOutputValue;
|
||||
|
||||
// this is the address initially used to fetch the txid
|
||||
isar_models.Address transactionAddress =
|
||||
txData["address"] as isar_models.Address;
|
||||
|
||||
isar_models.TransactionType type;
|
||||
Amount amount;
|
||||
if (mySentFromAddresses.isNotEmpty && myReceivedOnAddresses.isNotEmpty) {
|
||||
// tx is sent to self
|
||||
type = isar_models.TransactionType.sentToSelf;
|
||||
amount =
|
||||
amountSentFromWallet - amountReceivedInWallet - fee - changeAmount;
|
||||
} else if (mySentFromAddresses.isNotEmpty) {
|
||||
// outgoing tx
|
||||
type = isar_models.TransactionType.outgoing;
|
||||
amount = amountSentFromWallet - changeAmount - fee;
|
||||
final possible =
|
||||
outputAddresses.difference(myChangeReceivedOnAddresses).first;
|
||||
|
||||
if (transactionAddress.value != possible) {
|
||||
transactionAddress = isar_models.Address(
|
||||
walletId: walletId,
|
||||
value: possible,
|
||||
publicKey: [],
|
||||
type: isar_models.AddressType.nonWallet,
|
||||
derivationIndex: -1,
|
||||
derivationPath: null,
|
||||
subType: isar_models.AddressSubType.nonWallet,
|
||||
final inputTx = await cachedElectrumXClient.getTransaction(
|
||||
txHash: txid,
|
||||
coin: coin,
|
||||
);
|
||||
|
||||
final prevOutJson = Map<String, dynamic>.from(
|
||||
(inputTx["vout"] as List).firstWhere((e) => e["n"] == vout)
|
||||
as Map);
|
||||
|
||||
final prevOut = OutputV2.fromElectrumXJson(
|
||||
prevOutJson,
|
||||
decimalPlaces: coin.decimals,
|
||||
walletOwns: false, // doesn't matter here as this is not saved
|
||||
);
|
||||
|
||||
outpoint = OutpointV2.isarCantDoRequiredInDefaultConstructor(
|
||||
txid: txid,
|
||||
vout: vout,
|
||||
);
|
||||
valueStringSats = prevOut.valueStringSats;
|
||||
addresses.addAll(prevOut.addresses);
|
||||
}
|
||||
} else {
|
||||
// incoming tx
|
||||
type = isar_models.TransactionType.incoming;
|
||||
amount = amountReceivedInWallet;
|
||||
}
|
||||
|
||||
List<isar_models.Input> inputs = [];
|
||||
List<isar_models.Output> outputs = [];
|
||||
|
||||
for (final json in txData["vin"] as List) {
|
||||
bool isCoinBase = json['coinbase'] != null;
|
||||
final input = isar_models.Input(
|
||||
txid: json['txid'] as String,
|
||||
vout: json['vout'] as int? ?? -1,
|
||||
scriptSig: json['scriptSig']?['hex'] as String?,
|
||||
scriptSigAsm: json['scriptSig']?['asm'] as String?,
|
||||
isCoinbase: isCoinBase ? isCoinBase : json['is_coinbase'] as bool?,
|
||||
sequence: json['sequence'] as int?,
|
||||
innerRedeemScriptAsm: json['innerRedeemscriptAsm'] as String?,
|
||||
InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor(
|
||||
scriptSigHex: map["scriptSig"]?["hex"] as String?,
|
||||
sequence: map["sequence"] as int?,
|
||||
outpoint: outpoint,
|
||||
valueStringSats: valueStringSats,
|
||||
addresses: addresses,
|
||||
witness: map["witness"] as String?,
|
||||
coinbase: coinbase,
|
||||
innerRedeemScriptAsm: map["innerRedeemscriptAsm"] as String?,
|
||||
// don't know yet if wallet owns. Need addresses first
|
||||
walletOwns: false,
|
||||
);
|
||||
|
||||
if (allAddressesSet.intersection(input.addresses.toSet()).isNotEmpty) {
|
||||
wasSentFromThisWallet = true;
|
||||
input = input.copyWith(walletOwns: true);
|
||||
}
|
||||
|
||||
inputs.add(input);
|
||||
}
|
||||
|
||||
for (final json in txData["vout"] as List) {
|
||||
final output = isar_models.Output(
|
||||
scriptPubKey: json['scriptPubKey']?['hex'] as String?,
|
||||
scriptPubKeyAsm: json['scriptPubKey']?['asm'] as String?,
|
||||
scriptPubKeyType: json['scriptPubKey']?['type'] as String?,
|
||||
scriptPubKeyAddress:
|
||||
json["scriptPubKey"]?["addresses"]?[0] as String? ??
|
||||
json['scriptPubKey']['type'] as String,
|
||||
value: Amount.fromDecimal(
|
||||
Decimal.parse(json["value"].toString()),
|
||||
fractionDigits: coin.decimals,
|
||||
).raw.toInt(),
|
||||
// parse outputs
|
||||
final List<OutputV2> outputs = [];
|
||||
for (final outputJson in txData["vout"] as List) {
|
||||
OutputV2 output = OutputV2.fromElectrumXJson(
|
||||
Map<String, dynamic>.from(outputJson as Map),
|
||||
decimalPlaces: coin.decimals,
|
||||
// don't know yet if wallet owns. Need addresses first
|
||||
walletOwns: false,
|
||||
);
|
||||
|
||||
// if output was to my wallet, add value to amount received
|
||||
if (receivingAddresses
|
||||
.intersection(output.addresses.toSet())
|
||||
.isNotEmpty) {
|
||||
wasReceivedInThisWallet = true;
|
||||
amountReceivedInThisWallet += output.value;
|
||||
output = output.copyWith(walletOwns: true);
|
||||
} else if (changeAddresses
|
||||
.intersection(output.addresses.toSet())
|
||||
.isNotEmpty) {
|
||||
wasReceivedInThisWallet = true;
|
||||
changeAmountReceivedInThisWallet += output.value;
|
||||
output = output.copyWith(walletOwns: true);
|
||||
}
|
||||
|
||||
outputs.add(output);
|
||||
}
|
||||
|
||||
final tx = isar_models.Transaction(
|
||||
final totalOut = outputs
|
||||
.map((e) => e.value)
|
||||
.fold(BigInt.zero, (value, element) => value + element);
|
||||
|
||||
isar_models.TransactionType type;
|
||||
isar_models.TransactionSubType subType =
|
||||
isar_models.TransactionSubType.none;
|
||||
|
||||
// at least one input was owned by this wallet
|
||||
if (wasSentFromThisWallet) {
|
||||
type = isar_models.TransactionType.outgoing;
|
||||
|
||||
if (wasReceivedInThisWallet) {
|
||||
if (changeAmountReceivedInThisWallet + amountReceivedInThisWallet ==
|
||||
totalOut) {
|
||||
// definitely sent all to self
|
||||
type = isar_models.TransactionType.sentToSelf;
|
||||
} else if (amountReceivedInThisWallet == BigInt.zero) {
|
||||
// most likely just a typical send
|
||||
// do nothing here yet
|
||||
}
|
||||
|
||||
// check vout 0 for special scripts
|
||||
if (outputs.isNotEmpty) {
|
||||
final output = outputs.first;
|
||||
|
||||
// check for fusion
|
||||
if (BchUtils.isFUZE(output.scriptPubKeyHex.toUint8ListFromHex)) {
|
||||
subType = isar_models.TransactionSubType.cashFusion;
|
||||
} else {
|
||||
// check other cases here such as SLP or cash tokens etc
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (wasReceivedInThisWallet) {
|
||||
// only found outputs owned by this wallet
|
||||
type = isar_models.TransactionType.incoming;
|
||||
} else {
|
||||
Logging.instance.log(
|
||||
"Unexpected tx found (ignoring it): $txData",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
final tx = TransactionV2(
|
||||
walletId: walletId,
|
||||
blockHash: txData["blockhash"] as String?,
|
||||
hash: txData["hash"] as String,
|
||||
txid: txData["txid"] as String,
|
||||
timestamp: txData["blocktime"] as int? ??
|
||||
(DateTime.now().millisecondsSinceEpoch ~/ 1000),
|
||||
type: type,
|
||||
subType: isar_models.TransactionSubType.none,
|
||||
amount: amount.raw.toInt(),
|
||||
amountString: amount.toJsonString(),
|
||||
fee: fee.raw.toInt(),
|
||||
height: txData["height"] as int?,
|
||||
isCancelled: false,
|
||||
isLelantus: false,
|
||||
slateId: null,
|
||||
otherData: null,
|
||||
nonce: null,
|
||||
inputs: inputs,
|
||||
outputs: outputs,
|
||||
numberOfMessages: null,
|
||||
version: txData["version"] as int,
|
||||
timestamp: txData["blocktime"] as int? ??
|
||||
DateTime.timestamp().millisecondsSinceEpoch ~/ 1000,
|
||||
inputs: List.unmodifiable(inputs),
|
||||
outputs: List.unmodifiable(outputs),
|
||||
type: type,
|
||||
subType: subType,
|
||||
);
|
||||
|
||||
txns.add(Tuple2(tx, transactionAddress));
|
||||
txns.add(tx);
|
||||
}
|
||||
|
||||
await db.addNewTransactionData(txns, walletId);
|
||||
await db.updateOrPutTransactionV2s(txns);
|
||||
|
||||
// quick hack to notify manager to call notifyListeners if
|
||||
// transactions changed
|
||||
|
|
|
@ -850,9 +850,10 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
int get storedChainHeight => getCachedChainHeight();
|
||||
|
||||
NodeModel getCurrentNode() {
|
||||
return _xnoNode ??
|
||||
NodeService(secureStorageInterface: _secureStore)
|
||||
.getPrimaryNodeFor(coin: coin) ??
|
||||
return
|
||||
// _xnoNode ??
|
||||
// NodeService(secureStorageInterface: _secureStore)
|
||||
// .getPrimaryNodeFor(coin: coin) ??
|
||||
DefaultNodes.getNodeFor(coin);
|
||||
}
|
||||
|
||||
|
|
58
lib/services/coins/tezos/api/tezos_account.dart
Normal file
58
lib/services/coins/tezos/api/tezos_account.dart
Normal file
|
@ -0,0 +1,58 @@
|
|||
class TezosAccount {
|
||||
final int id;
|
||||
final String type;
|
||||
final String address;
|
||||
final String? publicKey;
|
||||
final bool revealed;
|
||||
final int balance;
|
||||
final int counter;
|
||||
|
||||
TezosAccount({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.address,
|
||||
required this.publicKey,
|
||||
required this.revealed,
|
||||
required this.balance,
|
||||
required this.counter,
|
||||
});
|
||||
|
||||
TezosAccount copyWith({
|
||||
int? id,
|
||||
String? type,
|
||||
String? address,
|
||||
String? publicKey,
|
||||
bool? revealed,
|
||||
int? balance,
|
||||
int? counter,
|
||||
}) {
|
||||
return TezosAccount(
|
||||
id: id ?? this.id,
|
||||
type: type ?? this.type,
|
||||
address: address ?? this.address,
|
||||
publicKey: publicKey ?? this.publicKey,
|
||||
revealed: revealed ?? this.revealed,
|
||||
balance: balance ?? this.balance,
|
||||
counter: counter ?? this.counter,
|
||||
);
|
||||
}
|
||||
|
||||
factory TezosAccount.fromMap(Map<String, dynamic> map) {
|
||||
return TezosAccount(
|
||||
id: map['id'] as int,
|
||||
type: map['type'] as String,
|
||||
address: map['address'] as String,
|
||||
publicKey: map['publicKey'] as String?,
|
||||
revealed: map['revealed'] as bool,
|
||||
balance: map['balance'] as int,
|
||||
counter: map['counter'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UserData{id: $id, type: $type, address: $address, '
|
||||
'publicKey: $publicKey, revealed: $revealed,'
|
||||
' balance: $balance, counter: $counter}';
|
||||
}
|
||||
}
|
117
lib/services/coins/tezos/api/tezos_api.dart
Normal file
117
lib/services/coins/tezos/api/tezos_api.dart
Normal file
|
@ -0,0 +1,117 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:stackwallet/networking/http.dart';
|
||||
import 'package:stackwallet/services/coins/tezos/api/tezos_account.dart';
|
||||
import 'package:stackwallet/services/coins/tezos/api/tezos_transaction.dart';
|
||||
import 'package:stackwallet/services/tor_service.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/prefs.dart';
|
||||
|
||||
abstract final class TezosAPI {
|
||||
static final HTTP _client = HTTP();
|
||||
static const String _baseURL = 'https://api.tzkt.io';
|
||||
|
||||
static Future<int> getCounter(String address) async {
|
||||
try {
|
||||
final uriString = "$_baseURL/v1/accounts/$address/counter";
|
||||
final response = await _client.get(
|
||||
url: Uri.parse(uriString),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
proxyInfo: Prefs.instance.useTor
|
||||
? TorService.sharedInstance.getProxyInfo()
|
||||
: null,
|
||||
);
|
||||
|
||||
final result = jsonDecode(response.body);
|
||||
return result as int;
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Error occurred in TezosAPI while getting counter for $address: $e\n$s",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<TezosAccount> getAccount(String address,
|
||||
{String type = "user"}) async {
|
||||
try {
|
||||
final uriString = "$_baseURL/v1/accounts/$address?legacy=false";
|
||||
final response = await _client.get(
|
||||
url: Uri.parse(uriString),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
proxyInfo: Prefs.instance.useTor
|
||||
? TorService.sharedInstance.getProxyInfo()
|
||||
: null,
|
||||
);
|
||||
|
||||
final result = jsonDecode(response.body) as Map;
|
||||
|
||||
final account = TezosAccount.fromMap(Map<String, dynamic>.from(result));
|
||||
|
||||
return account;
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Error occurred in TezosAPI while getting account for $address: $e\n$s",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<List<TezosTransaction>> getTransactions(String address) async {
|
||||
try {
|
||||
final transactionsCall =
|
||||
"$_baseURL/v1/accounts/$address/operations?type=transaction";
|
||||
|
||||
final response = await _client.get(
|
||||
url: Uri.parse(transactionsCall),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
proxyInfo: Prefs.instance.useTor
|
||||
? TorService.sharedInstance.getProxyInfo()
|
||||
: null,
|
||||
);
|
||||
|
||||
final result = jsonDecode(response.body) as List;
|
||||
|
||||
List<TezosTransaction> txs = [];
|
||||
for (var tx in result) {
|
||||
if (tx["type"] == "transaction") {
|
||||
final theTx = TezosTransaction(
|
||||
id: tx["id"] as int,
|
||||
hash: tx["hash"] as String,
|
||||
type: tx["type"] as String,
|
||||
height: tx["level"] as int,
|
||||
timestamp: DateTime.parse(tx["timestamp"].toString())
|
||||
.toUtc()
|
||||
.millisecondsSinceEpoch ~/
|
||||
1000,
|
||||
cycle: tx["cycle"] as int?,
|
||||
counter: tx["counter"] as int,
|
||||
opN: tx["op_n"] as int?,
|
||||
opP: tx["op_p"] as int?,
|
||||
status: tx["status"] as String,
|
||||
gasLimit: tx["gasLimit"] as int,
|
||||
gasUsed: tx["gasUsed"] as int,
|
||||
storageLimit: tx["storageLimit"] as int?,
|
||||
amountInMicroTez: tx["amount"] as int,
|
||||
feeInMicroTez: (tx["bakerFee"] as int? ?? 0) +
|
||||
(tx["storageFee"] as int? ?? 0) +
|
||||
(tx["allocationFee"] as int? ?? 0),
|
||||
burnedAmountInMicroTez: tx["burned"] as int?,
|
||||
senderAddress: tx["sender"]["address"] as String,
|
||||
receiverAddress: tx["target"]["address"] as String,
|
||||
);
|
||||
txs.add(theTx);
|
||||
}
|
||||
}
|
||||
return txs;
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Error occurred in TezosAPI while getting transactions for $address: $e\n$s",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
71
lib/services/coins/tezos/api/tezos_rpc_api.dart
Normal file
71
lib/services/coins/tezos/api/tezos_rpc_api.dart
Normal file
|
@ -0,0 +1,71 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:stackwallet/networking/http.dart';
|
||||
import 'package:stackwallet/services/tor_service.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/prefs.dart';
|
||||
|
||||
abstract final class TezosRpcAPI {
|
||||
static final HTTP _client = HTTP();
|
||||
|
||||
static Future<BigInt?> getBalance({
|
||||
required ({String host, int port}) nodeInfo,
|
||||
required String address,
|
||||
}) async {
|
||||
try {
|
||||
String balanceCall =
|
||||
"${nodeInfo.host}:${nodeInfo.port}/chains/main/blocks/head/context/contracts/$address/balance";
|
||||
|
||||
final response = await _client.get(
|
||||
url: Uri.parse(balanceCall),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
proxyInfo: Prefs.instance.useTor
|
||||
? TorService.sharedInstance.getProxyInfo()
|
||||
: null,
|
||||
);
|
||||
|
||||
final balance =
|
||||
BigInt.parse(response.body.substring(1, response.body.length - 2));
|
||||
return balance;
|
||||
} catch (e) {
|
||||
Logging.instance.log(
|
||||
"Error occurred in tezos_rpc_api.dart while getting balance for $address: $e",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<int?> getChainHeight({
|
||||
required ({String host, int port}) nodeInfo,
|
||||
}) async {
|
||||
try {
|
||||
final api =
|
||||
"${nodeInfo.host}:${nodeInfo.port}/chains/main/blocks/head/header/shell";
|
||||
|
||||
final response = await _client.get(
|
||||
url: Uri.parse(api),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
proxyInfo: Prefs.instance.useTor
|
||||
? TorService.sharedInstance.getProxyInfo()
|
||||
: null,
|
||||
);
|
||||
|
||||
final jsonParsedResponse = jsonDecode(response.body);
|
||||
return int.parse(jsonParsedResponse["level"].toString());
|
||||
} catch (e) {
|
||||
Logging.instance.log(
|
||||
"Error occurred in tezos_rpc_api.dart while getting chain height for tezos: $e",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<bool> testNetworkConnection({
|
||||
required ({String host, int port}) nodeInfo,
|
||||
}) async {
|
||||
final result = await getChainHeight(nodeInfo: nodeInfo);
|
||||
return result != null;
|
||||
}
|
||||
}
|
43
lib/services/coins/tezos/api/tezos_transaction.dart
Normal file
43
lib/services/coins/tezos/api/tezos_transaction.dart
Normal file
|
@ -0,0 +1,43 @@
|
|||
class TezosTransaction {
|
||||
final int? id;
|
||||
final String hash;
|
||||
final String? type;
|
||||
final int height;
|
||||
final int timestamp;
|
||||
final int? cycle;
|
||||
final int? counter;
|
||||
final int? opN;
|
||||
final int? opP;
|
||||
final String? status;
|
||||
final bool? isSuccess;
|
||||
final int? gasLimit;
|
||||
final int? gasUsed;
|
||||
final int? storageLimit;
|
||||
final int amountInMicroTez;
|
||||
final int feeInMicroTez;
|
||||
final int? burnedAmountInMicroTez;
|
||||
final String senderAddress;
|
||||
final String receiverAddress;
|
||||
|
||||
TezosTransaction({
|
||||
this.id,
|
||||
required this.hash,
|
||||
this.type,
|
||||
required this.height,
|
||||
required this.timestamp,
|
||||
this.cycle,
|
||||
this.counter,
|
||||
this.opN,
|
||||
this.opP,
|
||||
this.status,
|
||||
this.isSuccess,
|
||||
this.gasLimit,
|
||||
this.gasUsed,
|
||||
this.storageLimit,
|
||||
required this.amountInMicroTez,
|
||||
required this.feeInMicroTez,
|
||||
this.burnedAmountInMicroTez,
|
||||
required this.senderAddress,
|
||||
required this.receiverAddress,
|
||||
});
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:decimal/decimal.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:stackwallet/db/isar/main_db.dart';
|
||||
import 'package:stackwallet/models/balance.dart';
|
||||
|
@ -10,8 +8,9 @@ import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'
|
|||
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/networking/http.dart';
|
||||
import 'package:stackwallet/services/coins/coin_service.dart';
|
||||
import 'package:stackwallet/services/coins/tezos/api/tezos_api.dart';
|
||||
import 'package:stackwallet/services/coins/tezos/api/tezos_rpc_api.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';
|
||||
|
@ -19,17 +18,15 @@ 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/tor_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:tezart/tezart.dart';
|
||||
import 'package:tezart/tezart.dart' as tezart;
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
const int MINIMUM_CONFIRMATIONS = 1;
|
||||
|
@ -62,8 +59,8 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
DefaultNodes.getNodeFor(Coin.tezos);
|
||||
}
|
||||
|
||||
Future<Keystore> getKeystore() async {
|
||||
return Keystore.fromMnemonic((await mnemonicString).toString());
|
||||
Future<tezart.Keystore> getKeystore() async {
|
||||
return tezart.Keystore.fromMnemonic((await mnemonicString).toString());
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -102,8 +99,6 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
@override
|
||||
bool get shouldAutoSync => _shouldAutoSync;
|
||||
|
||||
HTTP client = HTTP();
|
||||
|
||||
@override
|
||||
set shouldAutoSync(bool shouldAutoSync) {
|
||||
if (_shouldAutoSync != shouldAutoSync) {
|
||||
|
@ -164,69 +159,117 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
Balance get balance => _balance ??= getCachedBalance();
|
||||
Balance? _balance;
|
||||
|
||||
Future<tezart.OperationsList> _buildSendTransaction({
|
||||
required Amount amount,
|
||||
required String address,
|
||||
required int counter,
|
||||
}) async {
|
||||
try {
|
||||
final sourceKeyStore = await getKeystore();
|
||||
final server = (_xtzNode ?? getCurrentNode()).host;
|
||||
final tezartClient = tezart.TezartClient(
|
||||
server,
|
||||
);
|
||||
|
||||
final opList = await tezartClient.transferOperation(
|
||||
source: sourceKeyStore,
|
||||
destination: address,
|
||||
amount: amount.raw.toInt(),
|
||||
);
|
||||
|
||||
for (final op in opList.operations) {
|
||||
op.counter = counter;
|
||||
counter++;
|
||||
}
|
||||
|
||||
return opList;
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Error in _buildSendTransaction() in tezos_wallet.dart: $e\n$s",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> prepareSend(
|
||||
{required String address,
|
||||
required Amount amount,
|
||||
Map<String, dynamic>? args}) async {
|
||||
Future<Map<String, dynamic>> prepareSend({
|
||||
required String address,
|
||||
required Amount amount,
|
||||
Map<String, dynamic>? args,
|
||||
}) async {
|
||||
try {
|
||||
if (amount.decimals != coin.decimals) {
|
||||
throw Exception("Amount decimals do not match coin decimals!");
|
||||
}
|
||||
var fee = int.parse((await estimateFeeFor(
|
||||
amount, (args!["feeRate"] as FeeRateType).index))
|
||||
.raw
|
||||
.toString());
|
||||
|
||||
if (amount > balance.spendable) {
|
||||
throw Exception("Insufficient available balance");
|
||||
}
|
||||
|
||||
final myAddress = await currentReceivingAddress;
|
||||
final account = await TezosAPI.getAccount(
|
||||
myAddress,
|
||||
);
|
||||
|
||||
final opList = await _buildSendTransaction(
|
||||
amount: amount,
|
||||
address: address,
|
||||
counter: account.counter + 1,
|
||||
);
|
||||
|
||||
await opList.computeLimits();
|
||||
await opList.computeFees();
|
||||
await opList.simulate();
|
||||
|
||||
Map<String, dynamic> txData = {
|
||||
"fee": fee,
|
||||
"fee": Amount(
|
||||
rawValue: opList.operations
|
||||
.map(
|
||||
(e) => BigInt.from(e.fee),
|
||||
)
|
||||
.fold(
|
||||
BigInt.zero,
|
||||
(p, e) => p + e,
|
||||
),
|
||||
fractionDigits: coin.decimals,
|
||||
).raw.toInt(),
|
||||
"address": address,
|
||||
"recipientAmt": amount,
|
||||
"tezosOperationsList": opList,
|
||||
};
|
||||
return Future.value(txData);
|
||||
} catch (e) {
|
||||
return Future.error(e);
|
||||
return txData;
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Error in prepareSend() in tezos_wallet.dart: $e\n$s",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
|
||||
if (e
|
||||
.toString()
|
||||
.contains("(_operationResult['errors']): Must not be null")) {
|
||||
throw Exception("Probably insufficient balance");
|
||||
} else if (e.toString().contains(
|
||||
"The simulation of the operation: \"transaction\" failed with error(s) :"
|
||||
" contract.balance_too_low, tez.subtraction_underflow.",
|
||||
)) {
|
||||
throw Exception("Insufficient balance to pay fees");
|
||||
}
|
||||
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> confirmSend({required Map<String, dynamic> txData}) async {
|
||||
try {
|
||||
final amount = txData["recipientAmt"] as Amount;
|
||||
final amountInMicroTez = amount.decimal * Decimal.fromInt(1000000);
|
||||
final microtezToInt = int.parse(amountInMicroTez.toString());
|
||||
|
||||
final int feeInMicroTez = int.parse(txData["fee"].toString());
|
||||
final String destinationAddress = txData["address"] as String;
|
||||
final secretKey =
|
||||
Keystore.fromMnemonic((await mnemonicString)!).secretKey;
|
||||
|
||||
Logging.instance.log(secretKey, level: LogLevel.Info);
|
||||
final sourceKeyStore = Keystore.fromSecretKey(secretKey);
|
||||
final client = TezartClient(getCurrentNode().host);
|
||||
|
||||
int? sendAmount = microtezToInt;
|
||||
int gasLimit = _gasLimit;
|
||||
int thisFee = feeInMicroTez;
|
||||
|
||||
if (balance.spendable == txData["recipientAmt"] as Amount) {
|
||||
//Fee guides for emptying a tz account
|
||||
// https://github.com/TezTech/eztz/blob/master/PROTO_004_FEES.md
|
||||
thisFee = thisFee + 32;
|
||||
sendAmount = microtezToInt - thisFee;
|
||||
gasLimit = _gasLimit + 320;
|
||||
}
|
||||
|
||||
final operation = await client.transferOperation(
|
||||
source: sourceKeyStore,
|
||||
destination: destinationAddress,
|
||||
amount: sendAmount,
|
||||
customFee: feeInMicroTez,
|
||||
customGasLimit: gasLimit);
|
||||
await operation.executeAndMonitor();
|
||||
return operation.result.id as String;
|
||||
} catch (e) {
|
||||
Logging.instance.log(e.toString(), level: LogLevel.Error);
|
||||
return Future.error(e);
|
||||
final opList = txData["tezosOperationsList"] as tezart.OperationsList;
|
||||
await opList.inject();
|
||||
await opList.monitor();
|
||||
return opList.result.id!;
|
||||
} catch (e, s) {
|
||||
Logging.instance.log("ConfirmSend: $e\n$s", level: LogLevel.Error);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -236,24 +279,13 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
if (mneString == null) {
|
||||
throw Exception("No mnemonic found!");
|
||||
}
|
||||
return Future.value((Keystore.fromMnemonic(mneString)).address);
|
||||
return Future.value((tezart.Keystore.fromMnemonic(mneString)).address);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Amount> estimateFeeFor(Amount amount, int feeRate) async {
|
||||
var api = "https://api.tzstats.com/series/op?start_date=today&collapse=1d";
|
||||
var response = jsonDecode((await client.get(
|
||||
url: Uri.parse(api),
|
||||
proxyInfo:
|
||||
_prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
|
||||
))
|
||||
.body)[0];
|
||||
double totalFees = response[4] as double;
|
||||
int totalTxs = response[8] as int;
|
||||
int feePerTx = (totalFees / totalTxs * 1000000).floor();
|
||||
|
||||
return Amount(
|
||||
rawValue: BigInt.from(feePerTx),
|
||||
rawValue: BigInt.from(0),
|
||||
fractionDigits: coin.decimals,
|
||||
);
|
||||
}
|
||||
|
@ -266,18 +298,7 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
|
||||
@override
|
||||
Future<FeeObject> get fees async {
|
||||
var api = "https://api.tzstats.com/series/op?start_date=today&collapse=10d";
|
||||
var response = jsonDecode((await client.get(
|
||||
url: Uri.parse(api),
|
||||
proxyInfo:
|
||||
_prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
|
||||
))
|
||||
.body);
|
||||
double totalFees = response[0][4] as double;
|
||||
int totalTxs = response[0][8] as int;
|
||||
int feePerTx = (totalFees / totalTxs * 1000000).floor();
|
||||
Logging.instance.log("feePerTx:$feePerTx", level: LogLevel.Info);
|
||||
// TODO: fix numberOfBlocks - Since there is only one fee no need to set blocks
|
||||
int feePerTx = 0;
|
||||
return FeeObject(
|
||||
numberOfBlocksFast: 10,
|
||||
numberOfBlocksAverage: 10,
|
||||
|
@ -314,7 +335,7 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
|
||||
await _prefs.init();
|
||||
|
||||
var newKeystore = Keystore.random();
|
||||
var newKeystore = tezart.Keystore.random();
|
||||
await _secureStore.write(
|
||||
key: '${_walletId}_mnemonic',
|
||||
value: newKeystore.mnemonic,
|
||||
|
@ -380,7 +401,7 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
required String mnemonicPassphrase,
|
||||
bool isRescan = false,
|
||||
}) async {
|
||||
final keystore = Keystore.fromMnemonic(
|
||||
final keystore = tezart.Keystore.fromMnemonic(
|
||||
mnemonic,
|
||||
password: mnemonicPassphrase,
|
||||
);
|
||||
|
@ -504,18 +525,16 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
|
||||
Future<void> updateBalance() async {
|
||||
try {
|
||||
String balanceCall = "https://api.mainnet.tzkt.io/v1/accounts/"
|
||||
"${await currentReceivingAddress}/balance";
|
||||
var response = jsonDecode(await client
|
||||
.get(
|
||||
url: Uri.parse(balanceCall),
|
||||
proxyInfo:
|
||||
_prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
|
||||
)
|
||||
.then((value) => value.body));
|
||||
Amount balanceInAmount = Amount(
|
||||
rawValue: BigInt.parse(response.toString()),
|
||||
fractionDigits: coin.decimals);
|
||||
final node = getCurrentNode();
|
||||
final bal = await TezosRpcAPI.getBalance(
|
||||
address: await currentReceivingAddress,
|
||||
nodeInfo: (
|
||||
host: node.host,
|
||||
port: node.port,
|
||||
),
|
||||
);
|
||||
Amount balanceInAmount =
|
||||
Amount(rawValue: bal ?? BigInt.zero, fractionDigits: coin.decimals);
|
||||
_balance = Balance(
|
||||
total: balanceInAmount,
|
||||
spendable: balanceInAmount,
|
||||
|
@ -532,22 +551,14 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
}
|
||||
|
||||
Future<void> updateTransactions() async {
|
||||
String transactionsCall = "https://api.mainnet.tzkt.io/v1/accounts/"
|
||||
"${await currentReceivingAddress}/operations";
|
||||
var response = jsonDecode(await client
|
||||
.get(
|
||||
url: Uri.parse(transactionsCall),
|
||||
proxyInfo:
|
||||
_prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
|
||||
)
|
||||
.then((value) => value.body));
|
||||
final txns = await TezosAPI.getTransactions(await currentReceivingAddress);
|
||||
List<Tuple2<Transaction, Address>> txs = [];
|
||||
for (var tx in response as List) {
|
||||
if (tx["type"] == "transaction") {
|
||||
for (var tx in txns) {
|
||||
if (tx.type == "transaction") {
|
||||
TransactionType txType;
|
||||
final String myAddress = await currentReceivingAddress;
|
||||
final String senderAddress = tx["sender"]["address"] as String;
|
||||
final String targetAddress = tx["target"]["address"] as String;
|
||||
final String senderAddress = tx.senderAddress;
|
||||
final String targetAddress = tx.receiverAddress;
|
||||
if (senderAddress == myAddress && targetAddress == myAddress) {
|
||||
txType = TransactionType.sentToSelf;
|
||||
} else if (senderAddress == myAddress) {
|
||||
|
@ -560,21 +571,17 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
|
||||
var theTx = Transaction(
|
||||
walletId: walletId,
|
||||
txid: tx["hash"].toString(),
|
||||
timestamp: DateTime.parse(tx["timestamp"].toString())
|
||||
.toUtc()
|
||||
.millisecondsSinceEpoch ~/
|
||||
1000,
|
||||
txid: tx.hash,
|
||||
timestamp: tx.timestamp,
|
||||
type: txType,
|
||||
subType: TransactionSubType.none,
|
||||
amount: tx["amount"] as int,
|
||||
amount: tx.amountInMicroTez,
|
||||
amountString: Amount(
|
||||
rawValue:
|
||||
BigInt.parse((tx["amount"] as int).toInt().toString()),
|
||||
rawValue: BigInt.from(tx.amountInMicroTez),
|
||||
fractionDigits: coin.decimals)
|
||||
.toJsonString(),
|
||||
fee: tx["bakerFee"] as int,
|
||||
height: int.parse(tx["level"].toString()),
|
||||
fee: tx.feeInMicroTez,
|
||||
height: tx.height,
|
||||
isCancelled: false,
|
||||
isLelantus: false,
|
||||
slateId: "",
|
||||
|
@ -613,15 +620,13 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
|
||||
Future<void> updateChainHeight() async {
|
||||
try {
|
||||
var api = "${getCurrentNode().host}/chains/main/blocks/head/header/shell";
|
||||
var jsonParsedResponse = jsonDecode(await client
|
||||
.get(
|
||||
url: Uri.parse(api),
|
||||
proxyInfo:
|
||||
_prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
|
||||
)
|
||||
.then((value) => value.body));
|
||||
final int intHeight = int.parse(jsonParsedResponse["level"].toString());
|
||||
final node = getCurrentNode();
|
||||
final int intHeight = (await TezosRpcAPI.getChainHeight(
|
||||
nodeInfo: (
|
||||
host: node.host,
|
||||
port: node.port,
|
||||
),
|
||||
))!;
|
||||
Logging.instance.log("Chain height: $intHeight", level: LogLevel.Info);
|
||||
await updateCachedChainHeight(intHeight);
|
||||
} catch (e, s) {
|
||||
|
@ -700,13 +705,13 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
@override
|
||||
Future<bool> testNetworkConnection() async {
|
||||
try {
|
||||
await client.get(
|
||||
url: Uri.parse(
|
||||
"${getCurrentNode().host}:${getCurrentNode().port}/chains/main/blocks/head/header/shell"),
|
||||
proxyInfo:
|
||||
_prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
|
||||
final node = getCurrentNode();
|
||||
return await TezosRpcAPI.testNetworkConnection(
|
||||
nodeInfo: (
|
||||
host: node.host,
|
||||
port: node.port,
|
||||
),
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
@ -729,37 +734,7 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
|
||||
@override
|
||||
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
|
||||
final transaction = Transaction(
|
||||
walletId: walletId,
|
||||
txid: txData["txid"] as String,
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
type: TransactionType.outgoing,
|
||||
subType: 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,
|
||||
);
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -611,7 +611,8 @@ abstract class EthereumAPI {
|
|||
try {
|
||||
final response = await client.get(
|
||||
url: Uri.parse(
|
||||
"$stackBaseServer/tokens?addrs=$contractAddress&parts=all",
|
||||
// "$stackBaseServer/tokens?addrs=$contractAddress&parts=all",
|
||||
"$stackBaseServer/names?terms=$contractAddress",
|
||||
),
|
||||
proxyInfo: Prefs.instance.useTor
|
||||
? TorService.sharedInstance.getProxyInfo()
|
||||
|
@ -621,6 +622,10 @@ abstract class EthereumAPI {
|
|||
if (response.code == 200) {
|
||||
final json = jsonDecode(response.body) as Map;
|
||||
if (json["data"] is List) {
|
||||
if ((json["data"] as List).isEmpty) {
|
||||
throw EthApiException("Unknown token");
|
||||
}
|
||||
|
||||
final map = Map<String, dynamic>.from(json["data"].first as Map);
|
||||
EthContract? token;
|
||||
if (map["isErc20"] == true) {
|
||||
|
|
|
@ -196,6 +196,9 @@ class ExchangeDataLoadingService {
|
|||
}
|
||||
|
||||
Future<void> _loadChangeNowCurrencies() async {
|
||||
if (_isar == null) {
|
||||
await initDB();
|
||||
}
|
||||
final exchange = ChangeNowExchange.instance;
|
||||
final responseCurrencies = await exchange.getAllCurrencies(false);
|
||||
if (responseCurrencies.value != null) {
|
||||
|
@ -325,6 +328,9 @@ class ExchangeDataLoadingService {
|
|||
// }
|
||||
|
||||
Future<void> loadMajesticBankCurrencies() async {
|
||||
if (_isar == null) {
|
||||
await initDB();
|
||||
}
|
||||
final exchange = MajesticBankExchange.instance;
|
||||
final responseCurrencies = await exchange.getAllCurrencies(false);
|
||||
|
||||
|
@ -347,6 +353,9 @@ class ExchangeDataLoadingService {
|
|||
}
|
||||
|
||||
Future<void> loadTrocadorCurrencies() async {
|
||||
if (_isar == null) {
|
||||
await initDB();
|
||||
}
|
||||
final exchange = TrocadorExchange.instance;
|
||||
final responseCurrencies = await exchange.getAllCurrencies(false);
|
||||
|
||||
|
|
|
@ -22,6 +22,33 @@ import 'package:stackwallet/utilities/stack_file_system.dart';
|
|||
|
||||
const String kReservedFusionAddress = "reserved_fusion_address";
|
||||
|
||||
final kFusionServerInfoDefaults = Map<Coin, FusionInfo>.unmodifiable(const {
|
||||
Coin.bitcoincash: FusionInfo(
|
||||
host: "fusion.servo.cash",
|
||||
port: 8789,
|
||||
ssl: true,
|
||||
// host: "cashfusion.stackwallet.com",
|
||||
// port: 8787,
|
||||
// ssl: false,
|
||||
rounds: 0, // 0 is continuous
|
||||
),
|
||||
Coin.bitcoincashTestnet: FusionInfo(
|
||||
host: "fusion.servo.cash",
|
||||
port: 8789,
|
||||
ssl: true,
|
||||
// host: "cashfusion.stackwallet.com",
|
||||
// port: 8787,
|
||||
// ssl: false,
|
||||
rounds: 0, // 0 is continuous
|
||||
),
|
||||
Coin.eCash: FusionInfo(
|
||||
host: "fusion.tokamak.cash",
|
||||
port: 8788,
|
||||
ssl: true,
|
||||
rounds: 0, // 0 is continuous
|
||||
),
|
||||
});
|
||||
|
||||
class FusionInfo {
|
||||
final String host;
|
||||
final int port;
|
||||
|
@ -37,16 +64,6 @@ class FusionInfo {
|
|||
required this.rounds,
|
||||
}) : assert(rounds >= 0);
|
||||
|
||||
static const DEFAULTS = FusionInfo(
|
||||
host: "fusion.servo.cash",
|
||||
port: 8789,
|
||||
ssl: true,
|
||||
// host: "cashfusion.stackwallet.com",
|
||||
// port: 8787,
|
||||
// ssl: false,
|
||||
rounds: 0, // 0 is continuous
|
||||
);
|
||||
|
||||
factory FusionInfo.fromJsonString(String jsonString) {
|
||||
final json = jsonDecode(jsonString);
|
||||
return FusionInfo(
|
||||
|
@ -95,7 +112,7 @@ class FusionInfo {
|
|||
}
|
||||
}
|
||||
|
||||
/// A mixin for the BitcoinCashWallet class that adds CashFusion functionality.
|
||||
/// A mixin that adds CashFusion functionality.
|
||||
mixin FusionWalletInterface {
|
||||
// Passed in wallet data.
|
||||
late final String _walletId;
|
||||
|
@ -630,14 +647,25 @@ mixin FusionWalletInterface {
|
|||
// Loop through UTXOs, checking and adding valid ones.
|
||||
for (final utxo in walletUtxos) {
|
||||
final String addressString = utxo.address!;
|
||||
final List<String> possibleAddresses = [addressString];
|
||||
final Set<String> possibleAddresses = {};
|
||||
|
||||
if (bitbox.Address.detectFormat(addressString) ==
|
||||
bitbox.Address.formatCashAddr) {
|
||||
possibleAddresses
|
||||
.add(bitbox.Address.toLegacyAddress(addressString));
|
||||
possibleAddresses.add(addressString);
|
||||
possibleAddresses.add(
|
||||
bitbox.Address.toLegacyAddress(addressString),
|
||||
);
|
||||
} else {
|
||||
possibleAddresses.add(bitbox.Address.toCashAddress(addressString));
|
||||
possibleAddresses.add(addressString);
|
||||
if (_coin == Coin.eCash) {
|
||||
possibleAddresses.add(
|
||||
bitbox.Address.toECashAddress(addressString),
|
||||
);
|
||||
} else {
|
||||
possibleAddresses.add(
|
||||
bitbox.Address.toCashAddress(addressString),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch address to get pubkey
|
||||
|
@ -645,13 +673,13 @@ mixin FusionWalletInterface {
|
|||
.getAddresses(_walletId)
|
||||
.filter()
|
||||
.anyOf<String,
|
||||
QueryBuilder<Address, Address, QAfterFilterCondition>>(
|
||||
possibleAddresses, (q, e) => q.valueEqualTo(e))
|
||||
QueryBuilder<Address, Address, QAfterFilterCondition>>(
|
||||
possibleAddresses, (q, e) => q.valueEqualTo(e))
|
||||
.and()
|
||||
.group((q) => q
|
||||
.subTypeEqualTo(AddressSubType.change)
|
||||
.or()
|
||||
.subTypeEqualTo(AddressSubType.receiving))
|
||||
.subTypeEqualTo(AddressSubType.change)
|
||||
.or()
|
||||
.subTypeEqualTo(AddressSubType.receiving))
|
||||
.and()
|
||||
.typeEqualTo(AddressType.p2pkh)
|
||||
.findFirst();
|
||||
|
@ -681,6 +709,10 @@ mixin FusionWalletInterface {
|
|||
|
||||
// Fuse UTXOs.
|
||||
try {
|
||||
if (coinList.isEmpty) {
|
||||
throw Exception("Started with no coins");
|
||||
}
|
||||
|
||||
await _mainFusionObject!.fuse(
|
||||
inputsFromWallet: coinList,
|
||||
network: _coin.isTestNet
|
||||
|
@ -710,6 +742,16 @@ mixin FusionWalletInterface {
|
|||
// Do the same for the UI state.
|
||||
_uiState?.incrementFusionRoundsFailed();
|
||||
|
||||
// If we have no coins, stop trying.
|
||||
if (coinList.isEmpty ||
|
||||
e.toString().contains("Started with no coins")) {
|
||||
_updateStatus(
|
||||
status: fusion.FusionStatus.failed,
|
||||
info: "Started with no coins, stopping.");
|
||||
_stopRequested = true;
|
||||
_uiState?.setFailed(true, shouldNotify: true);
|
||||
}
|
||||
|
||||
// If we fail too many times in a row, stop trying.
|
||||
if (_failedFuseCount >= maxFailedFuseCount) {
|
||||
_updateStatus(
|
||||
|
|
|
@ -206,7 +206,8 @@ abstract class DefaultNodes {
|
|||
isDown: false);
|
||||
|
||||
static NodeModel get nano => NodeModel(
|
||||
host: "https://rainstorm.city/api",
|
||||
// host: "https://rainstorm.city/api",
|
||||
host: "https://app.natrium.io/api",
|
||||
port: 443,
|
||||
name: defaultName,
|
||||
id: _nodeId(Coin.nano),
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:stackwallet/db/hive/db.dart';
|
||||
import 'package:stackwallet/services/event_bus/events/global/tor_status_changed_event.dart';
|
||||
|
@ -936,32 +938,74 @@ class Prefs extends ChangeNotifier {
|
|||
|
||||
// fusion server info
|
||||
|
||||
FusionInfo _fusionServerInfo = FusionInfo.DEFAULTS;
|
||||
Map<Coin, FusionInfo> _fusionServerInfo = {};
|
||||
|
||||
FusionInfo get fusionServerInfo => _fusionServerInfo;
|
||||
FusionInfo getFusionServerInfo(Coin coin) {
|
||||
return _fusionServerInfo[coin] ?? kFusionServerInfoDefaults[coin]!;
|
||||
}
|
||||
|
||||
void setFusionServerInfo(Coin coin, FusionInfo fusionServerInfo) {
|
||||
if (_fusionServerInfo[coin] != fusionServerInfo) {
|
||||
_fusionServerInfo[coin] = fusionServerInfo;
|
||||
|
||||
set fusionServerInfo(FusionInfo fusionServerInfo) {
|
||||
if (this.fusionServerInfo != fusionServerInfo) {
|
||||
DB.instance.put<dynamic>(
|
||||
boxName: DB.boxNamePrefs,
|
||||
key: "fusionServerInfo",
|
||||
value: fusionServerInfo.toJsonString(),
|
||||
key: "fusionServerInfoMap",
|
||||
value: _fusionServerInfo.map(
|
||||
(key, value) => MapEntry(
|
||||
key.name,
|
||||
value.toJsonString(),
|
||||
),
|
||||
),
|
||||
);
|
||||
_fusionServerInfo = fusionServerInfo;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<FusionInfo> _getFusionServerInfo() async {
|
||||
final saved = await DB.instance.get<dynamic>(
|
||||
Future<Map<Coin, FusionInfo>> _getFusionServerInfo() async {
|
||||
final map = await DB.instance.get<dynamic>(
|
||||
boxName: DB.boxNamePrefs,
|
||||
key: "fusionServerInfo",
|
||||
) as String?;
|
||||
key: "fusionServerInfoMap",
|
||||
) as Map?;
|
||||
|
||||
try {
|
||||
return FusionInfo.fromJsonString(saved!);
|
||||
} catch (_) {
|
||||
return FusionInfo.DEFAULTS;
|
||||
if (map == null) {
|
||||
return _fusionServerInfo;
|
||||
}
|
||||
|
||||
final actualMap = Map<String, String>.from(map).map(
|
||||
(key, value) => MapEntry(
|
||||
coinFromPrettyName(key),
|
||||
FusionInfo.fromJsonString(value),
|
||||
),
|
||||
);
|
||||
|
||||
// legacy bch check
|
||||
if (actualMap[Coin.bitcoincash] == null ||
|
||||
actualMap[Coin.bitcoincashTestnet] == null) {
|
||||
final saved = await DB.instance.get<dynamic>(
|
||||
boxName: DB.boxNamePrefs,
|
||||
key: "fusionServerInfo",
|
||||
) as String?;
|
||||
|
||||
if (saved != null) {
|
||||
final bchInfo = FusionInfo.fromJsonString(saved);
|
||||
actualMap[Coin.bitcoincash] = bchInfo;
|
||||
actualMap[Coin.bitcoincashTestnet] = bchInfo;
|
||||
unawaited(
|
||||
DB.instance.put<dynamic>(
|
||||
boxName: DB.boxNamePrefs,
|
||||
key: "fusionServerInfoMap",
|
||||
value: actualMap.map(
|
||||
(key, value) => MapEntry(
|
||||
key.name,
|
||||
value.toJsonString(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return actualMap;
|
||||
}
|
||||
}
|
||||
|
|
17
pubspec.lock
17
pubspec.lock
|
@ -142,6 +142,14 @@ packages:
|
|||
url: "https://github.com/cypherstack/bitcoindart.git"
|
||||
source: git
|
||||
version: "3.0.1"
|
||||
blockchain_signer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: blockchain_signer
|
||||
sha256: aa62c62df1fec11dbce7516444715ae492862ebdf3108b8b464a1909827963cd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1649,10 +1657,11 @@ packages:
|
|||
tezart:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: tezart
|
||||
sha256: "35d526f2e6ca250c64461ebfb4fa9f64b6599fab8c4242c8e89ae27d4ac2e15a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
path: "."
|
||||
ref: main
|
||||
resolved-ref: "8a7070f533e63dd150edae99476f6853bfb25913"
|
||||
url: "https://github.com/cypherstack/tezart.git"
|
||||
source: git
|
||||
version: "2.0.5"
|
||||
time:
|
||||
dependency: transitive
|
||||
|
|
|
@ -154,7 +154,10 @@ dependencies:
|
|||
url: https://github.com/cypherstack/socks_socket.git
|
||||
ref: master
|
||||
bip340: ^0.2.0
|
||||
tezart: ^2.0.5
|
||||
tezart:
|
||||
git:
|
||||
url: https://github.com/cypherstack/tezart.git
|
||||
ref: main
|
||||
socks5_proxy: ^1.0.3+dev.3
|
||||
coinlib_flutter: ^1.0.0
|
||||
convert: ^3.1.1
|
||||
|
|
Loading…
Reference in a new issue