mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-12-27 05:49:21 +00:00
612 lines
17 KiB
Dart
612 lines
17 KiB
Dart
import 'package:isar/isar.dart';
|
|
import 'package:stackwallet/models/balance.dart';
|
|
import 'package:stackwallet/models/isar/models/blockchain_data/address.dart';
|
|
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart';
|
|
import 'package:stackwallet/models/node_model.dart';
|
|
import 'package:stackwallet/models/paymint/fee_object_model.dart';
|
|
import 'package:stackwallet/services/node_service.dart';
|
|
import 'package:stackwallet/utilities/amount/amount.dart';
|
|
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';
|
|
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
|
|
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
|
|
import 'package:stackwallet/wallets/models/tx_data.dart';
|
|
import 'package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart';
|
|
import 'package:tezart/tezart.dart' as tezart;
|
|
import 'package:tuple/tuple.dart';
|
|
|
|
// const kDefaultTransactionStorageLimit = 496;
|
|
// const kDefaultTransactionGasLimit = 10600;
|
|
//
|
|
// const kDefaultKeyRevealFee = 1270;
|
|
// const kDefaultKeyRevealStorageLimit = 0;
|
|
// const kDefaultKeyRevealGasLimit = 1100;
|
|
|
|
class TezosWallet extends Bip39Wallet<Tezos> {
|
|
TezosWallet(CryptoCurrencyNetwork network) : super(Tezos(network));
|
|
|
|
NodeModel? _xtzNode;
|
|
|
|
String get derivationPath =>
|
|
info.otherData[WalletInfoKeys.tezosDerivationPath] as String? ?? "";
|
|
|
|
Future<DerivationPath> _scanPossiblePaths({
|
|
required String mnemonic,
|
|
String passphrase = "",
|
|
}) async {
|
|
try {
|
|
for (final path in Tezos.possibleDerivationPaths) {
|
|
final ks = await _getKeyStore(path: path.value);
|
|
|
|
// TODO: some kind of better check to see if the address has been used
|
|
|
|
final hasHistory =
|
|
(await TezosAPI.getTransactions(ks.address)).isNotEmpty;
|
|
|
|
if (hasHistory) {
|
|
return path;
|
|
}
|
|
}
|
|
|
|
return Tezos.standardDerivationPath;
|
|
} catch (e, s) {
|
|
Logging.instance.log(
|
|
"Error in _scanPossiblePaths() in tezos_wallet.dart: $e\n$s",
|
|
level: LogLevel.Error,
|
|
);
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
Future<tezart.Keystore> _getKeyStore({String? path}) async {
|
|
final mnemonic = await getMnemonic();
|
|
final passphrase = await getMnemonicPassphrase();
|
|
|
|
return Tezos.mnemonicToKeyStore(
|
|
mnemonic: mnemonic,
|
|
mnemonicPassphrase: passphrase,
|
|
derivationPath: path ?? derivationPath,
|
|
);
|
|
}
|
|
|
|
Future<Address> _getAddressFromMnemonic() async {
|
|
final keyStore = await _getKeyStore();
|
|
return Address(
|
|
walletId: walletId,
|
|
value: keyStore.address,
|
|
publicKey: keyStore.publicKey.toUint8ListFromBase58CheckEncoded,
|
|
derivationIndex: 0,
|
|
derivationPath: DerivationPath()..value = derivationPath,
|
|
type: info.coin.primaryAddressType,
|
|
subType: AddressSubType.receiving,
|
|
);
|
|
}
|
|
|
|
Future<tezart.OperationsList> _buildSendTransaction({
|
|
required Amount amount,
|
|
required String address,
|
|
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(
|
|
server,
|
|
);
|
|
|
|
final opList = await tezartClient.transferOperation(
|
|
source: sourceKeyStore,
|
|
destination: address,
|
|
amount: amount.raw.toInt(),
|
|
// customFee: customFee?.raw.toInt(),
|
|
// customGasLimit: customGasLimit,
|
|
// reveal: false,
|
|
);
|
|
|
|
// if (reveal) {
|
|
// opList.prependOperation(
|
|
// tezart.RevealOperation(
|
|
// customGasLimit: customGasLimit,
|
|
// customFee: customRevealFee?.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<void> checkSaveInitialReceivingAddress() async {
|
|
try {
|
|
final _address = await getCurrentReceivingAddress();
|
|
if (_address == null) {
|
|
final address = await _getAddressFromMnemonic();
|
|
await mainDB.updateOrPutAddresses([address]);
|
|
}
|
|
} catch (e, s) {
|
|
// do nothing, still allow user into wallet
|
|
Logging.instance.log(
|
|
"$runtimeType checkSaveInitialReceivingAddress() failed: $e\n$s",
|
|
level: LogLevel.Error,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
FilterOperation? get changeAddressFilterOperation =>
|
|
throw UnimplementedError("Not used for $runtimeType");
|
|
|
|
@override
|
|
FilterOperation? get receivingAddressFilterOperation =>
|
|
FilterGroup.and(standardReceivingAddressFilters);
|
|
|
|
@override
|
|
Future<TxData> prepareSend({required TxData txData}) async {
|
|
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 myAddress = (await getCurrentReceivingAddress())!;
|
|
final account = await TezosAPI.getAccount(
|
|
myAddress.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: 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,
|
|
isChange: txData.recipients!.first.isChange,
|
|
)
|
|
],
|
|
// 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<TxData> 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<Amount> 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 myAddress = (await getCurrentReceivingAddress())!;
|
|
final account = await TezosAPI.getAccount(
|
|
myAddress.value,
|
|
);
|
|
|
|
try {
|
|
final fees = await _estimate(account, recipientAddress);
|
|
|
|
final fee = Amount(
|
|
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",
|
|
level: LogLevel.Error,
|
|
);
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Not really used (yet)
|
|
@override
|
|
Future<FeeObject> get fees async {
|
|
const feePerTx = 1;
|
|
return FeeObject(
|
|
numberOfBlocksFast: 10,
|
|
numberOfBlocksAverage: 10,
|
|
numberOfBlocksSlow: 10,
|
|
fast: feePerTx,
|
|
medium: feePerTx,
|
|
slow: feePerTx,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<bool> pingCheck() async {
|
|
final currentNode = getCurrentNode();
|
|
return await TezosRpcAPI.testNetworkConnection(
|
|
nodeInfo: (
|
|
host: currentNode.host,
|
|
port: currentNode.port,
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> recover({required bool isRescan}) async {
|
|
await refreshMutex.protect(() async {
|
|
if (isRescan) {
|
|
await mainDB.deleteWalletBlockchainData(walletId);
|
|
} else {
|
|
final derivationPath = await _scanPossiblePaths(
|
|
mnemonic: await getMnemonic(),
|
|
passphrase: await getMnemonicPassphrase(),
|
|
);
|
|
|
|
await info.updateOtherData(
|
|
newEntries: {
|
|
WalletInfoKeys.tezosDerivationPath: derivationPath.value,
|
|
},
|
|
isar: mainDB.isar,
|
|
);
|
|
}
|
|
|
|
final address = await _getAddressFromMnemonic();
|
|
|
|
await mainDB.updateOrPutAddresses([address]);
|
|
|
|
// ensure we only have a single address
|
|
await mainDB.isar.writeTxn(() async {
|
|
await mainDB.isar.addresses
|
|
.where()
|
|
.walletIdEqualTo(walletId)
|
|
.filter()
|
|
.not()
|
|
.derivationPath((q) => q.valueEqualTo(derivationPath))
|
|
.deleteAll();
|
|
});
|
|
|
|
if (info.cachedReceivingAddress != address.value) {
|
|
await info.updateReceivingAddress(
|
|
newAddress: address.value,
|
|
isar: mainDB.isar,
|
|
);
|
|
}
|
|
|
|
await Future.wait([
|
|
updateBalance(),
|
|
updateTransactions(),
|
|
updateChainHeight(),
|
|
]);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<void> updateBalance() async {
|
|
try {
|
|
final currentNode = _xtzNode ?? getCurrentNode();
|
|
final balance = await TezosRpcAPI.getBalance(
|
|
nodeInfo: (host: currentNode.host, port: currentNode.port),
|
|
address: (await getCurrentReceivingAddress())!.value,
|
|
);
|
|
|
|
final balanceInAmount = Amount(
|
|
rawValue: balance!,
|
|
fractionDigits: cryptoCurrency.fractionDigits,
|
|
);
|
|
final newBalance = Balance(
|
|
total: balanceInAmount,
|
|
spendable: balanceInAmount,
|
|
blockedTotal: Amount(
|
|
rawValue: BigInt.zero,
|
|
fractionDigits: cryptoCurrency.fractionDigits,
|
|
),
|
|
pendingSpendable: Amount(
|
|
rawValue: BigInt.zero,
|
|
fractionDigits: cryptoCurrency.fractionDigits,
|
|
),
|
|
);
|
|
|
|
await info.updateBalance(newBalance: newBalance, isar: mainDB.isar);
|
|
} catch (e, s) {
|
|
Logging.instance.log(
|
|
"Error getting balance in tezos_wallet.dart: $e\n$s",
|
|
level: LogLevel.Error,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> updateChainHeight() async {
|
|
try {
|
|
final currentNode = _xtzNode ?? getCurrentNode();
|
|
final height = await TezosRpcAPI.getChainHeight(
|
|
nodeInfo: (
|
|
host: currentNode.host,
|
|
port: currentNode.port,
|
|
),
|
|
);
|
|
|
|
await info.updateCachedChainHeight(
|
|
newHeight: height!,
|
|
isar: mainDB.isar,
|
|
);
|
|
} catch (e, s) {
|
|
Logging.instance.log(
|
|
"Error occurred in tezos_wallet.dart while getting"
|
|
" chain height for tezos: $e\n$s",
|
|
level: LogLevel.Error,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> updateNode() async {
|
|
_xtzNode = NodeService(secureStorageInterface: secureStorageInterface)
|
|
.getPrimaryNodeFor(coin: info.coin) ??
|
|
DefaultNodes.getNodeFor(info.coin);
|
|
|
|
await refresh();
|
|
}
|
|
|
|
@override
|
|
NodeModel getCurrentNode() {
|
|
return _xtzNode ??
|
|
NodeService(secureStorageInterface: secureStorageInterface)
|
|
.getPrimaryNodeFor(coin: info.coin) ??
|
|
DefaultNodes.getNodeFor(info.coin);
|
|
}
|
|
|
|
@override
|
|
Future<void> updateTransactions() async {
|
|
// TODO: optimize updateTransactions and use V2
|
|
|
|
final myAddress = (await getCurrentReceivingAddress())!;
|
|
final txs = await TezosAPI.getTransactions(myAddress.value);
|
|
|
|
if (txs.isEmpty) {
|
|
return;
|
|
}
|
|
|
|
List<Tuple2<Transaction, Address>> transactions = [];
|
|
for (final theTx in txs) {
|
|
final TransactionType txType;
|
|
|
|
if (myAddress.value == theTx.senderAddress) {
|
|
txType = TransactionType.outgoing;
|
|
} else if (myAddress.value == theTx.receiverAddress) {
|
|
if (myAddress.value == theTx.senderAddress) {
|
|
txType = TransactionType.sentToSelf;
|
|
} else {
|
|
txType = TransactionType.incoming;
|
|
}
|
|
} else {
|
|
txType = TransactionType.unknown;
|
|
}
|
|
|
|
var transaction = Transaction(
|
|
walletId: walletId,
|
|
txid: theTx.hash,
|
|
timestamp: theTx.timestamp,
|
|
type: txType,
|
|
subType: TransactionSubType.none,
|
|
amount: theTx.amountInMicroTez,
|
|
amountString: Amount(
|
|
rawValue: BigInt.from(theTx.amountInMicroTez),
|
|
fractionDigits: cryptoCurrency.fractionDigits,
|
|
).toJsonString(),
|
|
fee: theTx.feeInMicroTez,
|
|
height: theTx.height,
|
|
isCancelled: false,
|
|
isLelantus: false,
|
|
slateId: "",
|
|
otherData: "",
|
|
inputs: [],
|
|
outputs: [],
|
|
nonce: 0,
|
|
numberOfMessages: null,
|
|
);
|
|
|
|
final Address theAddress;
|
|
switch (txType) {
|
|
case TransactionType.incoming:
|
|
case TransactionType.sentToSelf:
|
|
theAddress = myAddress;
|
|
break;
|
|
case TransactionType.outgoing:
|
|
case TransactionType.unknown:
|
|
theAddress = Address(
|
|
walletId: walletId,
|
|
value: theTx.receiverAddress,
|
|
publicKey: [],
|
|
derivationIndex: 0,
|
|
derivationPath: null,
|
|
type: AddressType.tezos,
|
|
subType: AddressSubType.unknown,
|
|
);
|
|
break;
|
|
}
|
|
transactions.add(Tuple2(transaction, theAddress));
|
|
}
|
|
await mainDB.addNewTransactionData(transactions, walletId);
|
|
}
|
|
|
|
@override
|
|
Future<bool> updateUTXOs() async {
|
|
// do nothing. Not used in tezos
|
|
return false;
|
|
}
|
|
}
|