From 391304f6daf6c9b26cd715a39dd1d2ab6f705687 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 22 Nov 2023 12:30:09 -0600 Subject: [PATCH] tezos and various tweaks --- .../send_view/confirm_transaction_view.dart | 26 +- lib/pages/send_view/send_view.dart | 11 +- .../wallet_view/sub_widgets/desktop_send.dart | 11 +- .../sub_widgets/desktop_wallet_summary.dart | 4 +- lib/wallets/api/tezos/tezos_account.dart | 58 ++++ lib/wallets/api/tezos/tezos_api.dart | 29 ++ lib/wallets/crypto_currency/coins/tezos.dart | 5 +- lib/wallets/wallet/impl/tezos_wallet.dart | 309 ++++++++++++------ 8 files changed, 330 insertions(+), 123 deletions(-) create mode 100644 lib/wallets/api/tezos/tezos_account.dart diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 0ff78575b..bd1f38f91 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -443,7 +443,7 @@ class _ConfirmTransactionViewState "Amount", style: STextStyles.smallMed12(context), ), - Text( + SelectableText( ref.watch(pAmountFormatter(coin)).format( widget.txData.amount!, ethContract: ref @@ -469,7 +469,7 @@ class _ConfirmTransactionViewState "Transaction fee", style: STextStyles.smallMed12(context), ), - Text( + SelectableText( ref .watch(pAmountFormatter(coin)) .format(widget.txData.fee!), @@ -495,7 +495,7 @@ class _ConfirmTransactionViewState const SizedBox( height: 4, ), - Text( + SelectableText( "~${widget.txData.fee!.raw.toInt() ~/ widget.txData.vSize!}", style: STextStyles.itemSubtitle12(context), ), @@ -520,7 +520,7 @@ class _ConfirmTransactionViewState const SizedBox( height: 4, ), - Text( + SelectableText( widget.txData.noteOnChain!, style: STextStyles.itemSubtitle12(context), ), @@ -543,7 +543,7 @@ class _ConfirmTransactionViewState const SizedBox( height: 4, ), - Text( + SelectableText( widget.txData.note!, style: STextStyles.itemSubtitle12(context), ), @@ -664,7 +664,7 @@ class _ConfirmTransactionViewState return Row( children: [ - Text( + SelectableText( ref.watch(pAmountFormatter(coin)).format( amount, ethContract: ref @@ -687,7 +687,7 @@ class _ConfirmTransactionViewState context), ), if (externalCalls) - Text( + SelectableText( "~$fiatAmount ${ref.watch(prefsChangeNotifierProvider.select( (value) => value.currency, ))}", @@ -724,7 +724,7 @@ class _ConfirmTransactionViewState const SizedBox( height: 2, ), - Text( + SelectableText( widget.isPaynymTransaction ? widget.txData.paynymAccountLite!.nymName : widget.txData.recipients!.first.address, @@ -765,7 +765,7 @@ class _ConfirmTransactionViewState builder: (context) { final fee = widget.txData.fee!; - return Text( + return SelectableText( ref .watch(pAmountFormatter(coin)) .format(fee), @@ -884,7 +884,7 @@ class _ConfirmTransactionViewState const SizedBox( height: 12, ), - Text( + SelectableText( (coin == Coin.epicCash) ? "Local Note (optional)" : "Note (optional)", @@ -987,7 +987,7 @@ class _ConfirmTransactionViewState builder: (context) { final fee = widget.txData.fee!; - return Text( + return SelectableText( ref.watch(pAmountFormatter(coin)).format(fee), style: STextStyles.itemSubtitle(context), ); @@ -1026,7 +1026,7 @@ class _ConfirmTransactionViewState color: Theme.of(context) .extension()! .textFieldDefaultBG, - child: Text( + child: SelectableText( "~${widget.txData.fee!.raw.toInt() ~/ widget.txData.vSize!}", style: STextStyles.itemSubtitle(context), ), @@ -1075,7 +1075,7 @@ class _ConfirmTransactionViewState final fee = widget.txData.fee!; final amount = widget.txData.amount!; - return Text( + return SelectableText( ref .watch(pAmountFormatter(coin)) .format(amount + fee), diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 78b094bc0..68139499e 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -1486,7 +1486,7 @@ class _SendViewState extends ConsumerState { style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - if (coin != Coin.ethereum) + if (coin != Coin.ethereum && coin != Coin.tezos) CustomTextButton( text: "Send all ${coin.ticker}", onTap: () async { @@ -1898,7 +1898,8 @@ class _SendViewState extends ConsumerState { ), if (coin != Coin.epicCash && coin != Coin.nano && - coin != Coin.banano) + coin != Coin.banano && + coin != Coin.tezos) Text( "Transaction fee (estimated)", style: STextStyles.smallMed12(context), @@ -1906,13 +1907,15 @@ class _SendViewState extends ConsumerState { ), if (coin != Coin.epicCash && coin != Coin.nano && - coin != Coin.banano) + coin != Coin.banano && + coin != Coin.tezos) const SizedBox( height: 8, ), if (coin != Coin.epicCash && coin != Coin.nano && - coin != Coin.banano) + coin != Coin.banano && + coin != Coin.tezos) Stack( children: [ TextField( diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index 60d31ef17..76ce17c8e 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -1062,7 +1062,7 @@ class _DesktopSendState extends ConsumerState { ), textAlign: TextAlign.left, ), - if (coin != Coin.ethereum) + if (coin != Coin.ethereum && coin != Coin.tezos) CustomTextButton( text: "Send all ${coin.ticker}", onTap: sendAllTapped, @@ -1481,7 +1481,8 @@ class _DesktopSendState extends ConsumerState { 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) && @@ -1532,11 +1533,13 @@ class _DesktopSendState extends ConsumerState { 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), diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart index bde0c7a6f..26f556fb9 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart @@ -117,7 +117,7 @@ class _WDesktopWalletSummaryState extends ConsumerState { children: [ FittedBox( fit: BoxFit.scaleDown, - child: Text( + child: SelectableText( ref .watch(pAmountFormatter(coin)) .format(balanceToShow, ethContract: tokenContract), @@ -125,7 +125,7 @@ class _WDesktopWalletSummaryState extends ConsumerState { ), ), if (externalCalls) - Text( + SelectableText( "${Amount.fromDecimal( priceTuple.item1 * balanceToShow.decimal, fractionDigits: 2, diff --git a/lib/wallets/api/tezos/tezos_account.dart b/lib/wallets/api/tezos/tezos_account.dart new file mode 100644 index 000000000..8bf9a9f64 --- /dev/null +++ b/lib/wallets/api/tezos/tezos_account.dart @@ -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 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}'; + } +} diff --git a/lib/wallets/api/tezos/tezos_api.dart b/lib/wallets/api/tezos/tezos_api.dart index 6c2ba9ae6..a798fb01b 100644 --- a/lib/wallets/api/tezos/tezos_api.dart +++ b/lib/wallets/api/tezos/tezos_api.dart @@ -4,6 +4,7 @@ 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'; +import 'package:stackwallet/wallets/api/tezos/tezos_account.dart'; import 'package:stackwallet/wallets/api/tezos/tezos_transaction.dart'; abstract final class TezosAPI { @@ -32,6 +33,34 @@ abstract final class TezosAPI { } } + static Future 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.from(result)); + + print("Get account =================== $account"); + + 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> getTransactions(String address) async { try { final transactionsCall = diff --git a/lib/wallets/crypto_currency/coins/tezos.dart b/lib/wallets/crypto_currency/coins/tezos.dart index 548422f5c..587cba407 100644 --- a/lib/wallets/crypto_currency/coins/tezos.dart +++ b/lib/wallets/crypto_currency/coins/tezos.dart @@ -15,8 +15,9 @@ class Tezos extends Bip39Currency { } @override - // TODO: implement genesisHash - String get genesisHash => throw UnimplementedError(); + String get genesisHash => throw UnimplementedError( + "Not used in tezos at the moment", + ); @override int get minConfirms => 1; diff --git a/lib/wallets/wallet/impl/tezos_wallet.dart b/lib/wallets/wallet/impl/tezos_wallet.dart index 557286ef3..f4ae7f66f 100644 --- a/lib/wallets/wallet/impl/tezos_wallet.dart +++ b/lib/wallets/wallet/impl/tezos_wallet.dart @@ -10,6 +10,7 @@ import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/extensions/impl/string.dart'; import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/wallets/api/tezos/tezos_account.dart'; import 'package:stackwallet/wallets/api/tezos/tezos_api.dart'; import 'package:stackwallet/wallets/api/tezos/tezos_rpc_api.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/tezos.dart'; @@ -20,8 +21,8 @@ import 'package:tezart/tezart.dart' as tezart; import 'package:tuple/tuple.dart'; // const kDefaultTransactionStorageLimit = 496; -const kDefaultTransactionGasLimit = 10600; - +// const kDefaultTransactionGasLimit = 10600; +// // const kDefaultKeyRevealFee = 1270; // const kDefaultKeyRevealStorageLimit = 0; // const kDefaultKeyRevealGasLimit = 1100; @@ -53,45 +54,51 @@ class TezosWallet extends Bip39Wallet { Future _buildSendTransaction({ required Amount amount, required String address, - int? customGasLimit, - Amount? customFee, + required int counter, + // required bool reveal, + // int? customGasLimit, + // Amount? customFee, + // Amount? customRevealFee, }) async { try { final sourceKeyStore = await _getKeyStore(); + final server = (_xtzNode ?? getCurrentNode()).host; + // if (kDebugMode) { + // print("SERVER: $server"); + // print("COUNTER: $counter"); + // print("customFee: $customFee"); + // } final tezartClient = tezart.TezartClient( - (_xtzNode ?? getCurrentNode()).host, + server, ); final opList = await tezartClient.transferOperation( source: sourceKeyStore, destination: address, amount: amount.raw.toInt(), - customGasLimit: customGasLimit, - customFee: customFee?.raw.toInt(), + // customFee: customFee?.raw.toInt(), + // customGasLimit: customGasLimit, + // reveal: false, ); - final counter = (await TezosAPI.getCounter( - (await getCurrentReceivingAddress())!.value, - )) + - 1; + // if (reveal) { + // opList.prependOperation( + // tezart.RevealOperation( + // customGasLimit: customGasLimit, + // customFee: customRevealFee?.raw.toInt(), + // ), + // ); + // } for (final op in opList.operations) { - if (op is tezart.RevealOperation) { - // op.storageLimit = kDefaultKeyRevealStorageLimit; - // op.gasLimit = kDefaultKeyRevealGasLimit; - // op.fee = kDefaultKeyRevealFee; - op.counter = counter; - } else if (op is tezart.TransactionOperation) { - op.counter = counter + 1; - // op.storageLimit = kDefaultTransactionStorageLimit; - // op.gasLimit = kDefaultTransactionGasLimit; - } + op.counter = counter; + counter++; } return opList; } catch (e, s) { Logging.instance.log( - "Error in estimateFeeFor() in tezos_wallet.dart: $e\n$s}", + "Error in _buildSendTransaction() in tezos_wallet.dart: $e\n$s}", level: LogLevel.Error, ); rethrow; @@ -122,97 +129,203 @@ class TezosWallet extends Bip39Wallet { @override Future prepareSend({required TxData txData}) async { - if (txData.recipients == null || txData.recipients!.length != 1) { - throw Exception("$runtimeType prepareSend requires 1 recipient"); - } - - Amount sendAmount = txData.amount!; - - if (sendAmount > info.cachedBalance.spendable) { - throw Exception("Insufficient available balance"); - } - - final bool isSendAll = sendAmount == info.cachedBalance.spendable; - - Amount fee = await estimateFeeFor(sendAmount, -1); - - int? customGasLimit; - - if (isSendAll) { - //Fee guides for emptying a tz account - // https://github.com/TezTech/eztz/blob/master/PROTO_004_FEES.md - customGasLimit = kDefaultTransactionGasLimit + 320; - fee = Amount( - rawValue: BigInt.from(fee.raw.toInt() + 32), - fractionDigits: cryptoCurrency.fractionDigits, - ); - sendAmount = sendAmount - fee; - } - - final opList = await _buildSendTransaction( - amount: sendAmount, - address: txData.recipients!.first.address, - customFee: fee, - customGasLimit: customGasLimit, - ); - - return txData.copyWith( - recipients: [ - ( - amount: sendAmount, - address: txData.recipients!.first.address, - ) - ], - fee: fee, - tezosOperationsList: opList, - ); - } - - @override - Future confirmSend({required TxData txData}) async { - await txData.tezosOperationsList!.executeAndMonitor(); - return txData.copyWith( - txid: txData.tezosOperationsList!.result.id, - ); - } - - @override - Future estimateFeeFor(Amount amount, int feeRate) async { - if (amount.raw == BigInt.zero) { - amount = Amount( - rawValue: BigInt.one, - fractionDigits: cryptoCurrency.fractionDigits, - ); - } - - final myAddressForSimulation = (await getCurrentReceivingAddress())!.value; - try { + if (txData.recipients == null || txData.recipients!.length != 1) { + throw Exception("$runtimeType prepareSend requires 1 recipient"); + } + + Amount sendAmount = txData.amount!; + + if (sendAmount > info.cachedBalance.spendable) { + throw Exception("Insufficient available balance"); + } + final account = await TezosAPI.getAccount( + (await getCurrentReceivingAddress())!.value, + ); + + // final bool isSendAll = sendAmount == info.cachedBalance.spendable; + // + // int? customGasLimit; + // Amount? fee; + // Amount? revealFee; + // + // if (isSendAll) { + // final fees = await _estimate( + // account, + // txData.recipients!.first.address, + // ); + // //Fee guides for emptying a tz account + // // https://github.com/TezTech/eztz/blob/master/PROTO_004_FEES.md + // // customGasLimit = kDefaultTransactionGasLimit + 320; + // fee = Amount( + // rawValue: BigInt.from(fees.transfer + 32), + // fractionDigits: cryptoCurrency.fractionDigits, + // ); + // + // BigInt rawAmount = sendAmount.raw - fee.raw; + // + // if (!account.revealed) { + // revealFee = Amount( + // rawValue: BigInt.from(fees.reveal + 32), + // fractionDigits: cryptoCurrency.fractionDigits, + // ); + // + // rawAmount = rawAmount - revealFee.raw; + // } + // + // sendAmount = Amount( + // rawValue: rawAmount, + // fractionDigits: cryptoCurrency.fractionDigits, + // ); + // } + final opList = await _buildSendTransaction( - amount: amount, - address: myAddressForSimulation, + amount: sendAmount, + address: txData.recipients!.first.address, + counter: account.counter + 1, + // reveal: !account.revealed, + // customFee: isSendAll ? fee : null, + // customRevealFee: isSendAll ? revealFee : null, + // customGasLimit: customGasLimit, ); await opList.computeLimits(); await opList.computeFees(); await opList.simulate(); + return txData.copyWith( + recipients: [ + ( + amount: sendAmount, + address: txData.recipients!.first.address, + ) + ], + // fee: fee, + fee: Amount( + rawValue: opList.operations + .map( + (e) => BigInt.from(e.fee), + ) + .fold( + BigInt.zero, + (p, e) => p + e, + ), + fractionDigits: cryptoCurrency.fractionDigits, + ), + tezosOperationsList: opList, + ); + } 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 confirmSend({required TxData txData}) async { + await txData.tezosOperationsList!.inject(); + await txData.tezosOperationsList!.monitor(); + return txData.copyWith( + txid: txData.tezosOperationsList!.result.id, + ); + } + + int _estCount = 0; + + Future<({int reveal, int transfer})> _estimate( + TezosAccount account, String recipientAddress) async { + try { + final opList = await _buildSendTransaction( + amount: Amount( + rawValue: BigInt.one, + fractionDigits: cryptoCurrency.fractionDigits, + ), + address: recipientAddress, + counter: account.counter + 1, + // reveal: !account.revealed, + ); + + await opList.computeLimits(); + await opList.computeFees(); + await opList.simulate(); + + int reveal = 0; + int transfer = 0; + + for (final op in opList.operations) { + if (op is tezart.TransactionOperation) { + transfer += op.fee; + } else if (op is tezart.RevealOperation) { + reveal += op.fee; + } + } + + return (reveal: reveal, transfer: transfer); + } catch (e, s) { + if (_estCount > 3) { + _estCount = 0; + Logging.instance.log( + " Error in _estimate in tezos_wallet.dart: $e\n$s}", + level: LogLevel.Error, + ); + rethrow; + } else { + _estCount++; + Logging.instance.log( + "_estimate() retry _estCount=$_estCount", + level: LogLevel.Warning, + ); + return await _estimate( + account, + recipientAddress, + ); + } + } + } + + @override + Future estimateFeeFor( + Amount amount, + int feeRate, { + String recipientAddress = "tz1MXvDCyXSqBqXPNDcsdmVZKfoxL9FTHmp2", + }) async { + if (info.cachedBalance.spendable.raw == BigInt.zero) { + return Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + + final account = await TezosAPI.getAccount( + (await getCurrentReceivingAddress())!.value, + ); + + try { + final fees = await _estimate(account, recipientAddress); + final fee = Amount( - rawValue: opList.operations - .map( - (e) => BigInt.from(e.fee), - ) - .fold( - BigInt.zero, - (p, e) => p + e, - ), + rawValue: BigInt.from(fees.reveal + fees.transfer), fractionDigits: cryptoCurrency.fractionDigits, ); return fee; } catch (e, s) { Logging.instance.log( - "Error in estimateFeeFor() in tezos_wallet.dart: $e\n$s}", + " Error in estimateFeeFor() in tezos_wallet.dart: $e\n$s}", level: LogLevel.Error, ); rethrow;