diff --git a/lib/bloc/transactions/transaction_builder.dart b/lib/bloc/transactions/transaction_builder.dart new file mode 100644 index 000000000..bc45e34e7 --- /dev/null +++ b/lib/bloc/transactions/transaction_builder.dart @@ -0,0 +1,467 @@ +import 'package:fixnum/fixnum.dart'; +import 'package:witnet/constants.dart'; +import 'package:witnet/data_structures.dart'; +import 'package:witnet/explorer.dart'; +import 'package:witnet/schema.dart'; +import 'package:witnet/utils.dart'; +import 'package:my_wit_wallet/bloc/crypto/api_crypto.dart'; +import 'package:my_wit_wallet/bloc/explorer/api_explorer.dart'; +import 'package:my_wit_wallet/constants.dart'; +import 'package:my_wit_wallet/shared/api_database.dart'; +import 'package:my_wit_wallet/shared/locator.dart'; +import 'package:my_wit_wallet/util/storage/database/account.dart'; +import 'package:my_wit_wallet/util/storage/database/wallet.dart'; + +class VttBuilder { + VttBuilder(); + + final Map utxoAccountMap = {}; + late Wallet wallet; + + List selectedUtxos = []; + UtxoPool utxoPool = UtxoPool(); + Account? changeAccount; + num balanceNanoWit = 0; + DateTime? selectedTimelock; + bool timelockSet = false; + UtxoSelectionStrategy utxoSelectionStrategy = + UtxoSelectionStrategy.SmallFirst; + PrioritiesEstimate? prioritiesEstimate; + Map minerFeeOptions = DEFAULT_MINER_FEE_OPTIONS; + + List inputs = []; + List outputs = []; + + /// fee + FeeType feeType = FeeType.Weighted; + int feeNanoWit = 0; + + int getFee([int additionalOutputs = 0]) { + switch (feeType) { + case FeeType.Absolute: + return feeNanoWit; + case FeeType.Weighted: + return calculatedWeightedFee(feeNanoWit); + } + } + + VTTransactionBody body() => + VTTransactionBody(inputs: inputs, outputs: outputs); + + int calculatedWeightedFee(num multiplier, {int additionalOutputs = 0}) { + num txWeight = vttWeight(inputs.length, outputs.length + additionalOutputs); + return (txWeight * multiplier).round(); + } + + void setTimeLock(DateTime dateTime) { + selectedTimelock = dateTime; + timelockSet = true; + } + + /// sign the [VTTransaction] + Future sign(Wallet currentWallet) async { + // Read the encrypted XPRV string stored in the database + Wallet walletStorage = currentWallet; + ApiCrypto apiCrypto = Locator.instance(); + try { + buildTransactionBody(currentWallet.balanceNanoWit().availableNanoWit, + currentWallet.walletType); + List signatures = await apiCrypto.signTransaction( + selectedUtxos, + walletStorage, + bytesToHex(VTTransactionBody(inputs: inputs, outputs: outputs).hash), + ); + + return VTTransaction( + body: VTTransactionBody(inputs: inputs, outputs: outputs), + signatures: signatures); + } catch (e) { + rethrow; + } + } + + /// Retrieves and sets the [prioritiesEstimate] from the Explorer. + Future setEstimatedPriorities() async { + String? nullOrError; + try { + prioritiesEstimate = await Locator.instance.get().priority(); + } catch (e) { + nullOrError = 'Error getting priority estimations $e'; + } + return nullOrError; + } + + bool validVTTransactionWeight(VTTransaction transaction) { + return transaction.weight < MAX_VT_WEIGHT; + } + + void setEstimatedWeightedFees() { + if (prioritiesEstimate != null) { + minerFeeOptions[EstimatedFeeOptions.Stinky] = + calculatedWeightedFee(prioritiesEstimate!.vttStinky.priority) + .toString(); + minerFeeOptions[EstimatedFeeOptions.Low] = + calculatedWeightedFee(prioritiesEstimate!.vttLow.priority).toString(); + minerFeeOptions[EstimatedFeeOptions.Medium] = + calculatedWeightedFee(prioritiesEstimate!.vttMedium.priority) + .toString(); + minerFeeOptions[EstimatedFeeOptions.High] = + calculatedWeightedFee(prioritiesEstimate!.vttHigh.priority) + .toString(); + minerFeeOptions[EstimatedFeeOptions.Opulent] = + calculatedWeightedFee(prioritiesEstimate!.vttOpulent.priority) + .toString(); + } + } + + void updateFee(FeeType newFeeType, [int? feeNanoWit]) { + feeType = newFeeType; + switch (feeType) { + case FeeType.Absolute: + this.feeNanoWit = feeNanoWit ?? 0; + break; + case FeeType.Weighted: + this.feeNanoWit = feeNanoWit ?? 0; + break; + } + } + + List receivers() { + return List.generate( + outputs.length, (index) => outputs[index].pkh.address); + } + + bool addOutput(ValueTransferOutput output, [bool merge = true]) { + List _recievers = receivers(); + + try { + if (merge) { + // check to see if the address is already in the list. + if (_recievers.contains(output.pkh)) { + // if the address is in the list add the value instead of + // generating a new output + outputs[_recievers.indexOf(output.pkh.address)].value += output.value; + if (selectedTimelock != null) { + outputs[_recievers.indexOf(output.pkh.address)].timeLock = + (selectedTimelock!.millisecondsSinceEpoch * 100) as Int64; + } + } else { + outputs.add(output); + } + } else { + outputs.add(output); + } + + buildTransactionBody( + wallet.balanceNanoWit().availableNanoWit, + wallet.walletType, + ); + setEstimatedWeightedFees(); + + return true; + } catch (e) { + return false; + } + } + + void setSelectionStrategy(UtxoSelectionStrategy strategy) { + utxoSelectionStrategy = strategy; + } + + Future setWallet(Wallet? wallet) async { + if (wallet != null) { + this.wallet = wallet; + balanceNanoWit = 0; + changeAccount = await wallet.getChangeAccount(); + + // update the utxo pool + utxoPool.clear(); + this.wallet.utxoMap(false).keys.forEach((utxo) { + utxoPool.insert(utxo); + }); + + // presort the utxo pool + utxoPool.sortUtxos(utxoSelectionStrategy); + } + } + + void buildTransactionBody(int balanceNanoWit, WalletType walletType) { + int valueOwedNanoWit = 0; + int valuePaidNanoWit = 0; + int valueChangeNanoWit = 0; + try { + /// calculate value owed + bool containsChangeAddress = false; + int changeIndex = 0; + int outIdx = 0; + + outputs.forEach((element) { + if (element.pkh.address == changeAccount!.address) { + /// check if a change address is already in the outputs + containsChangeAddress = true; + changeIndex = outIdx; + } + outIdx += 1; + }); + + /// + if (containsChangeAddress && walletType != WalletType.single) { + outputs.removeAt(changeIndex); + } + outputs.forEach((element) { + /// + valueOwedNanoWit += element.value.toInt(); + }); + + /// sets the fee weighted and absolute + feeNanoWit = getFee(); + valueOwedNanoWit += feeNanoWit; + + /// compare to balance + if (balanceNanoWit < valueOwedNanoWit) { + /// TODO:: throw insufficient funds exception + } else { + /// get utxos from the pool + selectedUtxos = utxoPool.cover( + amountNanoWit: valueOwedNanoWit, + utxoStrategy: utxoSelectionStrategy); + + /// convert utxo to input + inputs.clear(); + for (int i = 0; i < selectedUtxos.length; i++) { + Utxo currentUtxo = selectedUtxos[i]; + Input _input = currentUtxo.toInput(); + inputs.add(_input); + valuePaidNanoWit += currentUtxo.value; + } + } + + if (feeType == FeeType.Weighted) { + /// calculate change + valueChangeNanoWit = (valuePaidNanoWit - valueOwedNanoWit); + + /// + if (valueChangeNanoWit > 0) { + // add change + // +1 to the outputs length to include for change address + feeNanoWit = getFee(feeNanoWit); + valueChangeNanoWit = (valuePaidNanoWit - valueOwedNanoWit); + + outputs.add(ValueTransferOutput.fromJson({ + 'pkh': changeAccount?.address, + 'value': valueChangeNanoWit, + 'time_lock': 0, + })); + } + } else { + feeNanoWit = getFee(); + valueChangeNanoWit = (valuePaidNanoWit - valueOwedNanoWit); + if (valueChangeNanoWit > 0) { + outputs.add(ValueTransferOutput.fromJson({ + 'pkh': changeAccount?.address, + 'value': valueChangeNanoWit, + 'time_lock': 0, + })); + } + } + } catch (e) { + rethrow; + } + } + + String? validateAmount() { + if (utxoListValueNanoWit(utxoPool.map.values.toList()) <= + (outputListValueNanoWit(outputs) + feeNanoWit)) { + return "Insufficient Funds"; + } + return null; + } + + String? validateVttWeight() { + return null; + } + + void reset() { + selectedUtxos.clear(); + inputs.clear(); + outputs.clear(); + selectedTimelock = null; + timelockSet = false; + feeNanoWit = 0; + } + + /// Build a list of accounts + List accountsToUpdate(VTTransaction transaction) { + ApiDatabase database = Locator.instance.get(); + List _inputUtxoList = buildInputUtxoList(); + ValueTransferInfo _vti = toValueTransferInfo(transaction); + + List _accounts = []; + + // the inputs + for (int i = 0; i < _inputUtxoList.length; i++) { + InputUtxo inputUtxo = _inputUtxoList[i]; + + // get the account by address + Account account = database.walletStorage.currentWallet + .accountByAddress(inputUtxo.address)!; + + // add the vtt to the account + account.addVtt(_vti); + + // we need to remove the UTXO from the account + account.utxos.removeWhere((utxo) => + utxo.outputPointer.toString() == + inputUtxo.input.outputPointer.toString()); + + // add the account to the list we need to update + _accounts.add(account); + } + + // check outputs for accounts and update them + for (int i = 0; i < outputs.length; i++) { + ValueTransferOutput output = outputs[i]; + Account? account = Account.fromDatabase(database, output.pkh.address); + if (account != null) { + account.addVtt(_vti); + _accounts.add(account); + } + } + + return _accounts; + } + + /// Updates the database with all the data from the [vtt]. + Future updateDatabase(VTTransaction vtt) async { + ApiDatabase database = Locator.instance.get(); + // add pending tx to database + await database.addVtt(toValueTransferInfo(vtt)); + + // get the list of accounts that need to be updated + List _accountsToUpdate = accountsToUpdate(vtt); + + // update each account that was part of the transaction in the database + for (int i = 0; i < _accountsToUpdate.length; i++) { + Account account = _accountsToUpdate[i]; + await database.walletStorage.currentWallet.updateAccount( + index: account.index, + keyType: account.keyType, + account: account, + ); + } + + // refresh and reload the database + await Locator.instance().getWalletStorage(true); + await database.updateCurrentWallet(); + } + + ValueTransferInfo toValueTransferInfo(VTTransaction transaction, + [String? status]) { + List _inputUtxoList = buildInputUtxoList(); + return ValueTransferInfo( + blockHash: '', + fee: feeNanoWit, + inputs: _inputUtxoList, + outputs: outputs, + priority: 1, + status: status ?? 'pending', + txnEpoch: -1, + txnHash: transaction.transactionID, + txnTime: DateTime.now().millisecondsSinceEpoch, + type: 'ValueTransfer', + weight: transaction.weight); + } + + Map> buildSignerMap() { + Map> _signers = {}; + + /// loop through utxos + for (int i = 0; i < selectedUtxos.length; i++) { + Utxo currentUtxo = selectedUtxos.elementAt(i); + + Wallet currentWallet = + Locator.instance.get().walletStorage.currentWallet; + + /// loop though every external account + currentWallet.externalAccounts.forEach((index, account) { + if (account.utxos.contains(currentUtxo)) { + if (_signers.containsKey(currentWallet.xprv)) { + _signers[currentWallet.xprv]!.add(account.path); + } else { + _signers[currentWallet.xprv!] = [account.path]; + } + } + }); + + /// loop though every internal account + currentWallet.internalAccounts.forEach((index, account) { + if (account.utxos.contains(currentUtxo)) { + if (_signers.containsKey(currentWallet.xprv)) { + _signers[currentWallet.xprv]!.add(account.path); + } else { + _signers[currentWallet.xprv!] = [account.path]; + } + } + }); + } + + return _signers; + } + + Future send(Transaction transaction) async { + try { + var resp = await Locator.instance + .get() + .sendTransaction(transaction); + return resp['result']; + } catch (e) { + print('Error sending transaction: $e'); + return false; + } + } + + List buildInputUtxoList() { + return wallet.buildInputUtxoList(selectedUtxos); + } + + void printDebug() { + print("VTT Builder"); + + print(" Wallet:"); + print(" Balance: ${wallet.balanceNanoWit()}"); + + print("VTTransaction"); + print("Inputs: ${inputs.length}"); + if (inputs.isNotEmpty) { + inputs.forEach((Input input) { + print( + "\t${input.outputPointer.transactionId.hex}:${input.outputPointer.outputIndex}\t${utxoPool.map[input.outputPointer]!.value}"); + }); + } + print("Outputs: ${outputs.length}"); + if (outputs.isNotEmpty) { + outputs.forEach((ValueTransferOutput output) { + print( + '\taddress: "${output.pkh.address}",${output.timeLock > 0 ? " timelock: " + output.timeLock.toString() : ''} value:${output.value}'); + }); + } + } +} + +int utxoListValueNanoWit(List utxoList) { + return utxoList + .map((Utxo utxo) => utxo.value) + .toList() + .reduce((value, element) => value + element); +} + +int outputListValueNanoWit(List outputList) { + return outputList + .map((ValueTransferOutput output) => output.value.toInt()) + .toList() + .reduce((value, element) => value + element); +} + +int vttWeight(int inputsLength, int outputsLength) { + return (inputsLength * INPUT_SIZE) + (outputsLength * OUTPUT_SIZE * GAMMA); +} diff --git a/lib/bloc/transactions/value_transfer/vtt_create/vtt_create_bloc.dart b/lib/bloc/transactions/value_transfer/vtt_create/vtt_create_bloc.dart index 817927b2e..232df33fb 100644 --- a/lib/bloc/transactions/value_transfer/vtt_create/vtt_create_bloc.dart +++ b/lib/bloc/transactions/value_transfer/vtt_create/vtt_create_bloc.dart @@ -1,43 +1,22 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:fixnum/fixnum.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:my_wit_wallet/constants.dart'; -import 'package:witnet/constants.dart'; import 'package:witnet/data_structures.dart'; -import 'package:witnet/explorer.dart'; import 'package:witnet/schema.dart'; -import 'package:witnet/utils.dart'; -import 'package:my_wit_wallet/bloc/crypto/api_crypto.dart'; -import 'package:my_wit_wallet/bloc/explorer/api_explorer.dart'; -import 'package:my_wit_wallet/shared/locator.dart'; -import 'package:my_wit_wallet/shared/api_database.dart'; import 'package:my_wit_wallet/util/storage/database/wallet.dart'; -import 'package:my_wit_wallet/util/storage/database/account.dart'; + +import '../../transaction_builder.dart'; part 'vtt_create_event.dart'; part 'vtt_create_state.dart'; -/// send the transaction via the explorer. -/// returns true on success -Future _sendTransaction(Transaction transaction) async { - try { - var resp = - await Locator.instance.get().sendTransaction(transaction); - return resp['result']; - } catch (e) { - print('Error sending transaction: $e'); - return false; - } -} - class VTTCreateBloc extends Bloc { /// Create new [VTTCreateBloc]. /// /// extends [Bloc] /// [on((event, emit) => null)] /// to map [VTTCreateEvent] To [VTTCreateState] - VTTCreateBloc() + VTTCreateBloc([bool debug = true]) : super( VTTCreateState( vtTransaction: VTTransaction( @@ -48,6 +27,7 @@ class VTTCreateBloc extends Bloc { vttCreateStatus: VTTCreateStatus.initial, ), ) { + this.debug = debug; on(_addValueTransferOutputEvent); on(_setTimeLockEvent); on(_signTransactionEvent); @@ -57,605 +37,135 @@ class VTTCreateBloc extends Bloc { on(_addSourceWalletsEvent); on(_resetTransactionEvent); on(_validateTransactionEvent); - - /// - } - - final Map utxoAccountMap = {}; - late Wallet currentWallet; - List internalAddresses = []; - List externalAddresses = []; - Account? changeAccount; - List outputs = []; - List receivers = []; - List inputs = []; - List utxos = []; - Map masterUtxoPool = {}; - List selectedUtxos = []; - UtxoPool utxoPool = UtxoPool(); - FeeType feeType = FeeType.Weighted; - int feeNanoWit = 0; - num balanceNanoWit = 0; - DateTime? selectedTimelock; - bool timelockSet = false; - UtxoSelectionStrategy utxoSelectionStrategy = - UtxoSelectionStrategy.SmallFirst; - PrioritiesEstimate? prioritiesEstimate; - Map minerFeeOptions = DEFAULT_MINER_FEE_OPTIONS; - - int getFee([int additionalOutputs = 0]) { - switch (feeType) { - case FeeType.Absolute: - return feeNanoWit; - case FeeType.Weighted: - return calculatedWeightedFee(feeNanoWit); - } - } - - int calculatedWeightedFee(num multiplier, {int additionalOutputs = 0}) { - num txWeight = (inputs.length * INPUT_SIZE) + - (outputs.length + additionalOutputs * OUTPUT_SIZE * GAMMA); - return (txWeight * multiplier).round(); - } - - Future setEstimatedPriorities() async { - try { - prioritiesEstimate = await Locator.instance.get().priority(); - } catch (e) { - print('Error getting priority estimations $e'); - rethrow; - } - } - - void setEstimatedWeightedFees() { - if (prioritiesEstimate != null) { - minerFeeOptions[EstimatedFeeOptions.Stinky] = - calculatedWeightedFee(prioritiesEstimate!.vttStinky.priority) - .toString(); - minerFeeOptions[EstimatedFeeOptions.Low] = - calculatedWeightedFee(prioritiesEstimate!.vttLow.priority).toString(); - minerFeeOptions[EstimatedFeeOptions.Medium] = - calculatedWeightedFee(prioritiesEstimate!.vttMedium.priority) - .toString(); - minerFeeOptions[EstimatedFeeOptions.High] = - calculatedWeightedFee(prioritiesEstimate!.vttHigh.priority) - .toString(); - minerFeeOptions[EstimatedFeeOptions.Opulent] = - calculatedWeightedFee(prioritiesEstimate!.vttOpulent.priority) - .toString(); - } } - void updateFee(FeeType newFeeType, [int feeNanoWit = 0]) { - feeType = newFeeType; - switch (feeType) { - case FeeType.Absolute: - this.feeNanoWit = feeNanoWit; - break; - case FeeType.Weighted: - this.feeNanoWit = feeNanoWit; - break; - } - } - - bool addOutput(ValueTransferOutput output, [bool merge = true]) { - try { - if (merge) { - // check to see if the address is already in the list. - if (receivers.contains(output.pkh)) { - // if the address is in the list add the value instead of - // generating a new output - outputs[receivers.indexOf(output.pkh.address)].value += output.value; - if (selectedTimelock != null) { - outputs[receivers.indexOf(output.pkh.address)].timeLock = - (selectedTimelock!.millisecondsSinceEpoch * 100) as Int64; - } - } else { - receivers.add(output.pkh.address); - outputs.add(output); - } - } else { - // if merge is false then add an additional output. - receivers.add(output.pkh.address); - outputs.add(output); - } - return true; - } catch (e) { - return false; - } - } - - void setSelectionStrategy(UtxoSelectionStrategy strategy) { - utxoSelectionStrategy = strategy; - } - - void _setUtxo(Utxo utxo) { - if (utxo.timelock > 0) { - int _ts = utxo.timelock * 1000; - DateTime _timelock = DateTime.fromMillisecondsSinceEpoch(_ts); - - int currentTimestamp = DateTime.now().millisecondsSinceEpoch; - if (_timelock.millisecondsSinceEpoch > currentTimestamp) { - /// utxo is still locked - } else { - utxos.add(utxo); - balanceNanoWit += utxo.value; - } - } else if (utxo.timelock == 0) { - utxos.add(utxo); - balanceNanoWit += utxo.value; - } - } - - Future _setWalletBalance(Wallet wallet) async { - if (wallet.walletType == WalletType.hd) { - /// setup the external accounts - wallet.externalAccounts.forEach((index, account) { - externalAddresses.add(account.address); - account.utxos.forEach((utxo) { - _setUtxo(utxo); - }); - }); - - /// setup the internal accounts - wallet.internalAccounts.forEach((index, account) { - internalAddresses.add(account.address); - account.utxos.forEach((utxo) { - _setUtxo(utxo); - }); - }); - } else { - /// master node - wallet.masterAccount!.utxos.forEach((utxo) { - _setUtxo(utxo); - }); - } - return wallet; - } - - Future setWallet(Wallet? newWalletStorage) async { - if (newWalletStorage != null) { - utxos.clear(); - this.currentWallet = await _setWalletBalance(newWalletStorage); - balanceNanoWit = 0; - currentWallet = currentWallet; - - /// get the internal account that will be used for any change - bool changeAccountSet = false; - Wallet firstWallet = currentWallet; - - if (currentWallet.walletType == WalletType.hd) { - for (int i = 0; i < firstWallet.internalAccounts.length; i++) { - if (!changeAccountSet) { - Account account = firstWallet.internalAccounts[i]!; - if (account.vttHashes.isEmpty) { - changeAccount = account; - changeAccountSet = true; - } - } - } - - /// did we run out of change addresses? - if (!changeAccountSet) { - ApiCrypto apiCrypto = Locator.instance(); - changeAccount = await apiCrypto.generateAccount( - firstWallet, - KeyType.internal, - internalAddresses.length, - ); - } - } else { - /// master node - changeAccount = currentWallet.masterAccount!; - } - - /// update the utxo pool - utxoPool.clear(); - utxos.forEach((utxo) { - utxoPool.insert(utxo); - }); - - /// presort the utxo pool - utxoPool.sortUtxos(utxoSelectionStrategy); - } - } - - void buildTransactionBody(int balanceNanoWit, WalletType walletType) { - int valueOwedNanoWit = 0; - int valuePaidNanoWit = 0; - int valueChangeNanoWit = 0; - try { - /// calculate value owed - bool containsChangeAddress = false; - int changeIndex = 0; - int outIdx = 0; - outputs.forEach((element) { - if (element.pkh.address == changeAccount?.address) { - /// check if a change address is already in the outputs - containsChangeAddress = true; - changeIndex = outIdx; - } - outIdx += 1; - }); - - /// - if (containsChangeAddress && walletType != WalletType.single) { - outputs.removeAt(changeIndex); - } - outputs.forEach((element) { - /// - valueOwedNanoWit += element.value.toInt(); - }); - - /// sets the fee weighted and absolute - feeNanoWit = getFee(); - valueOwedNanoWit += feeNanoWit; - - /// compare to balance - if (balanceNanoWit < valueOwedNanoWit) { - /// TODO:: throw insufficient funds exception - } else { - /// get utxos from the pool - selectedUtxos = utxoPool.cover( - amountNanoWit: valueOwedNanoWit, - utxoStrategy: utxoSelectionStrategy); - - /// convert utxo to input - inputs.clear(); - for (int i = 0; i < selectedUtxos.length; i++) { - Utxo currentUtxo = selectedUtxos[i]; - Input _input = currentUtxo.toInput(); - inputs.add(_input); - valuePaidNanoWit += currentUtxo.value; - } - } - - if (feeType == FeeType.Weighted) { - /// calculate change - valueChangeNanoWit = (valuePaidNanoWit - valueOwedNanoWit); - - /// - if (valueChangeNanoWit > 0) { - // add change - // +1 to the outputs length to include for change address - feeNanoWit = getFee(feeNanoWit); - valueChangeNanoWit = (valuePaidNanoWit - valueOwedNanoWit); - - outputs.add(ValueTransferOutput.fromJson({ - 'pkh': changeAccount?.address, - 'value': valueChangeNanoWit, - 'time_lock': 0, - })); - } - } else { - feeNanoWit = getFee(); - valueChangeNanoWit = (valuePaidNanoWit - valueOwedNanoWit); - if (valueChangeNanoWit > 0) { - outputs.add(ValueTransferOutput.fromJson({ - 'pkh': changeAccount?.address, - 'value': valueChangeNanoWit, - 'time_lock': 0, - })); - } - } - } catch (e) { - rethrow; - } - } + late bool debug; + VttBuilder vttBuilder = VttBuilder(); /// add a [ValueTransferOutput] to the [VTTransaction]. void _addValueTransferOutputEvent( - AddValueTransferOutputEvent event, Emitter emit) { - emit(state.copyWith(status: VTTCreateStatus.busy)); - try { - if (event.merge) { - /// check to see if the address is already in the list. - if (receivers.contains(event.output.pkh)) { - /// if the address is in the list add the value instead of - /// generating a new output - outputs[receivers.indexOf(event.output.pkh.address)].value += - event.output.value; - if (selectedTimelock != null) { - outputs[receivers.indexOf(event.output.pkh.address)].timeLock = - (selectedTimelock!.millisecondsSinceEpoch * 100) as Int64; - } - } else { - receivers.add(event.output.pkh.address); - outputs.add(event.output); - } - } else { - // if merge is false then add an additional output. - receivers.add(event.output.pkh.address); - outputs.add(event.output); - } - } catch (e) {} - buildTransactionBody(event.currentWallet.balanceNanoWit().availableNanoWit, - event.currentWallet.walletType); - setEstimatedWeightedFees(); - emit( - state.copyWith( - inputs: inputs, outputs: outputs, status: VTTCreateStatus.building), - ); - } - - /// set the timelock for the current [ValueTransferOutput]. - void _setTimeLockEvent(SetTimelockEvent event, Emitter emit) { - selectedTimelock = event.dateTime; - timelockSet = true; - emit( - state.copyWith( - inputs: inputs, outputs: outputs, status: VTTCreateStatus.building), - ); - } - - /// sign the [VTTransaction] - Future _signTransaction( - {required Wallet currentWallet}) async { - /// Read the encrypted XPRV string stored in the database - Wallet walletStorage = currentWallet; - ApiCrypto apiCrypto = Locator.instance(); - try { - buildTransactionBody(currentWallet.balanceNanoWit().availableNanoWit, - currentWallet.walletType); - List signatures = await apiCrypto.signTransaction( - selectedUtxos, - walletStorage, - bytesToHex(VTTransactionBody(inputs: inputs, outputs: outputs).hash), - ); - - return VTTransaction( - body: VTTransactionBody(inputs: inputs, outputs: outputs), - signatures: signatures); - } catch (e) { - rethrow; + AddValueTransferOutputEvent event, + Emitter emit, + ) { + if (debug) { + print(event.runtimeType); + print(''); } - } - - Map> buildSignerMap() { - Map> _signers = {}; - - /// loop through utxos - for (int i = 0; i < selectedUtxos.length; i++) { - Utxo currentUtxo = selectedUtxos.elementAt(i); - Wallet currentWallet = - Locator.instance.get().walletStorage.currentWallet; + emit(VTTCreateState.busy(state)); + vttBuilder.addOutput(event.output, event.merge); - /// loop though every external account - currentWallet.externalAccounts.forEach((index, account) { - if (account.utxos.contains(currentUtxo)) { - if (_signers.containsKey(currentWallet.xprv)) { - _signers[currentWallet.xprv]!.add(account.path); - } else { - _signers[currentWallet.xprv!] = [account.path]; - } - } - }); - - /// loop though every internal account - currentWallet.internalAccounts.forEach((index, account) { - if (account.utxos.contains(currentUtxo)) { - if (_signers.containsKey(currentWallet.xprv)) { - _signers[currentWallet.xprv]!.add(account.path); - } else { - _signers[currentWallet.xprv!] = [account.path]; - } - } - }); + if (debug) { + vttBuilder.printDebug(); + print(''); } - - return _signers; + emit(VTTCreateState.building(state, vttBuilder)); } - List buildInputUtxoList() { - List _inputs = []; - - /// loop through utxos - for (int i = 0; i < selectedUtxos.length; i++) { - Utxo currentUtxo = selectedUtxos.elementAt(i); - - /// loop though every external account - currentWallet.externalAccounts.forEach((index, account) { - if (account.utxos.contains(currentUtxo)) { - _inputs.add(InputUtxo( - address: account.address, - input: currentUtxo.toInput(), - value: currentUtxo.value)); - } - }); - - /// loop though every internal account - currentWallet.internalAccounts.forEach((index, account) { - if (account.utxos.contains(currentUtxo)) { - _inputs.add(InputUtxo( - address: account.address, - input: currentUtxo.toInput(), - value: currentUtxo.value)); - } - }); - - if (currentWallet.walletType == WalletType.single && - currentWallet.masterAccount != null) { - _inputs.add(InputUtxo( - address: currentWallet.masterAccount!.address, - input: currentUtxo.toInput(), - value: currentUtxo.value)); - } - } - return _inputs; + /// set the timelock for the current [ValueTransferOutput]. + void _setTimeLockEvent( + SetTimelockEvent event, + Emitter emit, + ) { + print(event.runtimeType); + vttBuilder.setTimeLock(event.dateTime); + emit(VTTCreateState.building(state, vttBuilder)); } /// sign the transaction Future _signTransactionEvent( - SignTransactionEvent event, Emitter emit) async { - emit(state.copyWith(status: VTTCreateStatus.signing)); + SignTransactionEvent event, + Emitter emit, + ) async { + print(event.runtimeType); + emit(VTTCreateState.signing(state)); try { - VTTransaction vtTransaction = - await _signTransaction(currentWallet: event.currentWallet); - emit(VTTCreateState( - vtTransaction: vtTransaction, - vttCreateStatus: VTTCreateStatus.finished, - message: null, - )); + VTTransaction vtTransaction = await vttBuilder.sign(event.currentWallet); + emit(VTTCreateState.finished(state, vtTransaction)); } catch (e) { - print('Error signing the transaction :: $e'); - emit(state.copyWith(status: VTTCreateStatus.exception, message: '$e')); + emit(VTTCreateState.exception( + state, "Error signing the transaction :: $e")); rethrow; } } /// send the transaction to the explorer Future _sendVttTransactionEvent( - SendTransactionEvent event, Emitter emit) async { - emit(state.copyWith(status: VTTCreateStatus.sending)); - ApiDatabase database = Locator.instance.get(); + SendTransactionEvent event, + Emitter emit, + ) async { + print(event.runtimeType); + emit(VTTCreateState.sending(state)); + VTTransaction vtt = event.transaction; bool transactionAccepted = - await _sendTransaction(Transaction(valueTransfer: event.transaction)); + await vttBuilder.send(Transaction(valueTransfer: vtt)); if (transactionAccepted) { - /// add pending transaction - /// - List _inputUtxoList = buildInputUtxoList(); - ValueTransferInfo vti = ValueTransferInfo( - blockHash: '', - fee: feeNanoWit, - inputs: _inputUtxoList, - outputs: outputs, - priority: 1, - status: 'pending', - txnEpoch: -1, - txnHash: event.transaction.transactionID, - txnTime: DateTime.now().millisecondsSinceEpoch, - type: 'ValueTransfer', - weight: event.transaction.weight); - - /// add pending tx to database - await database.addVtt(vti); - - /// update the accounts transaction list - - /// the inputs - for (int i = 0; i < _inputUtxoList.length; i++) { - InputUtxo inputUtxo = _inputUtxoList[i]; - Account account = database.walletStorage.currentWallet - .accountByAddress(inputUtxo.address)!; - account.vttHashes.add(event.transaction.transactionID); - account.vtts.add(vti); - await database.walletStorage.currentWallet.updateAccount( - index: account.index, - keyType: account.keyType, - account: account, - ); - } - - /// check outputs for accounts and update them - for (int i = 0; i < outputs.length; i++) { - ValueTransferOutput output = outputs[i]; - Account? account = database.walletStorage.currentWallet - .accountByAddress(output.pkh.address); - if (account != null) { - account.vttHashes.add(event.transaction.transactionID); - account.vtts.add(vti); - await database.walletStorage.currentWallet.updateAccount( - index: account.index, - keyType: account.keyType, - account: account, - ); - } - } - - emit(state.copyWith(status: VTTCreateStatus.accepted)); - List utxoListToUpdate = []; - selectedUtxos.forEach((selectedUtxo) { - event.currentWallet.externalAccounts.forEach((index, value) { - if (value.utxos.contains(selectedUtxo)) { - value.utxos.remove(selectedUtxo); - utxoListToUpdate.add(value); - } - }); - - event.currentWallet.internalAccounts.forEach((index, value) { - if (value.utxos.contains(selectedUtxo)) { - value.utxos.remove(selectedUtxo); - utxoListToUpdate.add(value); - } - }); - }); - for (int i = 0; i < utxoListToUpdate.length; i++) { - Account account = utxoListToUpdate[i]; - await Locator.instance() - .walletStorage - .currentWallet - .updateAccount( - index: account.index, - keyType: account.keyType, - account: account); - } - await Locator.instance().getWalletStorage(true); - await database.updateCurrentWallet(); + await vttBuilder.updateDatabase(vtt); + emit(VTTCreateState.accepted(state)); } else { - emit(state.copyWith(status: VTTCreateStatus.exception)); + emit(VTTCreateState.exception(state, "Transaction was not accepted")); } } void _updateFeeEvent(UpdateFeeEvent event, Emitter emit) { - if (event.feeNanoWit != null) { - updateFee(event.feeType, event.feeNanoWit!); - } else { - updateFee(event.feeType); - } + print(event.runtimeType); + vttBuilder.updateFee(event.feeType, event.feeNanoWit); } void _updateUtxoSelectionStrategyEvent( UpdateUtxoSelectionStrategyEvent event, Emitter emit) { - utxoSelectionStrategy = event.strategy; + print(event.runtimeType); + vttBuilder.setSelectionStrategy(event.strategy); } Future _addSourceWalletsEvent( AddSourceWalletsEvent event, Emitter emit) async { - await setWallet(event.currentWallet); - emit(state.copyWith( - inputs: inputs, outputs: outputs, status: VTTCreateStatus.building)); + print(event.runtimeType); + await vttBuilder.setWallet(event.currentWallet); + vttBuilder.printDebug(); + emit(VTTCreateState.building(state, vttBuilder)); try { - await setEstimatedPriorities(); + await vttBuilder.setEstimatedPriorities(); } catch (err) { - print('Error setting estimated priorities $err'); - emit(state.copyWith(status: VTTCreateStatus.exception)); + String error = 'Error setting estimated priorities $err'; + emit(VTTCreateState.exception(state, error)); rethrow; } } void _resetTransactionEvent( - ResetTransactionEvent event, Emitter emit) { - selectedUtxos.clear(); - inputs.clear(); - outputs.clear(); - receivers.clear(); - selectedTimelock = null; - timelockSet = false; - feeNanoWit = 0; - emit(state.copyWith(status: VTTCreateStatus.initial)); + ResetTransactionEvent event, + Emitter emit, + ) { + if (debug) { + print(event.runtimeType); + } + vttBuilder.reset(); + if (debug) { + vttBuilder.printDebug(); + } + emit(VTTCreateState.initial(state)); } void _validateTransactionEvent( - ValidateTransactionEvent event, Emitter emit) { + ValidateTransactionEvent event, + Emitter emit, + ) { + print(event.runtimeType); + /// ensure that the wallet has sufficient funds - int utxoValueNanoWit = selectedUtxos - .map((Utxo utxo) => utxo.value) - .toList() - .reduce((value, element) => value + element); - int outputValueNanoWit = outputs - .map((ValueTransferOutput output) => output.value.toInt()) - .toList() - .reduce((value, element) => value + element); - int feeValueNanoWit = feeNanoWit; - if (utxoValueNanoWit <= (outputValueNanoWit + feeValueNanoWit)) { - emit(state.copyWith( - inputs: inputs, outputs: outputs, status: VTTCreateStatus.building)); - } else { - emit(state.copyWith( - status: VTTCreateStatus.exception, message: 'Insufficient Funds')); - } + String? nullOrError = vttBuilder.validateAmount(); + print(nullOrError); + emit( + (nullOrError == null) + ? VTTCreateState.building(state, vttBuilder) + : VTTCreateState.exception(state, nullOrError), + ); } } + +typedef String? VttValidation(); + +List validations = []; diff --git a/lib/bloc/transactions/value_transfer/vtt_create/vtt_create_state.dart b/lib/bloc/transactions/value_transfer/vtt_create/vtt_create_state.dart index 01e838d5e..3904ee02b 100644 --- a/lib/bloc/transactions/value_transfer/vtt_create/vtt_create_state.dart +++ b/lib/bloc/transactions/value_transfer/vtt_create/vtt_create_state.dart @@ -8,8 +8,6 @@ enum VTTCreateStatus { sending, accepted, finished, - recipientSet, - inputSet, exception, } @@ -22,12 +20,14 @@ class VTTCreateState extends Equatable { final VTTCreateStatus vttCreateStatus; final VTTransaction vtTransaction; final String? message; + VTTCreateState copyWith({ List? inputs, List? outputs, List? signatures, VTTCreateStatus? status, String? message, + VTTransaction? vtTransaction, }) { return VTTCreateState( vtTransaction: VTTransaction( @@ -42,6 +42,43 @@ class VTTCreateState extends Equatable { ); } + static VTTCreateState initial(state) => + state.copyWith(status: VTTCreateStatus.initial); + + static VTTCreateState building( + VTTCreateState state, + VttBuilder builder, + ) { + return state.copyWith( + inputs: builder.inputs, + outputs: builder.outputs, + status: VTTCreateStatus.building, + ); + } + + static VTTCreateState busy(VTTCreateState state) => + state.copyWith(status: VTTCreateStatus.busy); + + static VTTCreateState signing(VTTCreateState state) => + state.copyWith(status: VTTCreateStatus.signing); + + static VTTCreateState sending(VTTCreateState state) => + state.copyWith(status: VTTCreateStatus.sending); + + static VTTCreateState accepted(VTTCreateState state) => + state.copyWith(status: VTTCreateStatus.accepted); + + static VTTCreateState finished( + VTTCreateState state, VTTransaction vtTransaction) => + state.copyWith( + vtTransaction: vtTransaction, + status: VTTCreateStatus.finished, + message: null, + ); + + static VTTCreateState exception(VTTCreateState state, String error) => + state.copyWith(status: VTTCreateStatus.exception, message: error); + @override List get props => [vtTransaction, vttCreateStatus]; } diff --git a/lib/screens/send_transaction/send_vtt_screen.dart b/lib/screens/send_transaction/send_vtt_screen.dart index ec64e0cda..e74ed3c3e 100644 --- a/lib/screens/send_transaction/send_vtt_screen.dart +++ b/lib/screens/send_transaction/send_vtt_screen.dart @@ -122,8 +122,8 @@ class CreateVttScreenState extends State try { setState(() => { currentTxOutput = vttBloc.state.vtTransaction.body.outputs.first, - savedFeeAmount = vttBloc.feeNanoWit.toString(), - savedFeeType = vttBloc.feeType, + savedFeeAmount = vttBloc.vttBuilder.feeNanoWit.toString(), + savedFeeType = vttBloc.vttBuilder.feeType, }); } catch (err) { // There is no saved transaction details diff --git a/lib/util/storage/database/account.dart b/lib/util/storage/database/account.dart index 17d795114..16d4b2df9 100644 --- a/lib/util/storage/database/account.dart +++ b/lib/util/storage/database/account.dart @@ -1,5 +1,7 @@ import 'dart:core'; +import 'package:flutter/foundation.dart'; +import 'package:my_wit_wallet/shared/api_database.dart'; import 'package:my_wit_wallet/util/storage/database/transaction_adapter.dart'; import 'package:my_wit_wallet/util/storage/database/wallet.dart'; import 'package:my_wit_wallet/util/utxo_list_to_string.dart'; @@ -170,6 +172,10 @@ class Account extends _Account { address == other.address; } + static Account? fromDatabase(ApiDatabase database, String address) { + return database.walletStorage.currentWallet.accountByAddress(address); + } + @override int get hashCode => hash4(walletName.hashCode, address.hashCode, vttHashes.hashCode, utxos.hashCode); diff --git a/lib/util/storage/database/wallet.dart b/lib/util/storage/database/wallet.dart index 27648f436..b96ca914c 100644 --- a/lib/util/storage/database/wallet.dart +++ b/lib/util/storage/database/wallet.dart @@ -15,6 +15,7 @@ import 'package:witnet/explorer.dart'; import 'package:witnet/utils.dart'; import 'package:witnet/witnet.dart'; +import '../../../bloc/crypto/api_crypto.dart'; import 'account.dart'; import 'balance_info.dart'; @@ -74,6 +75,7 @@ class Wallet { Map externalAccounts = {}; Map internalAccounts = {}; Account? masterAccount; + Account? changeAccount; Map accountMap(KeyType keyType) { Map _accounts = {}; @@ -309,20 +311,8 @@ class Wallet { } } - BalanceInfo balanceNanoWit() { - List _utxos = []; - - internalAccounts.forEach((address, account) { - _utxos.addAll(account.utxos); - }); - externalAccounts.forEach((address, account) { - _utxos.addAll(account.utxos); - }); - if (masterAccount != null) { - _utxos.addAll(masterAccount!.utxos); - } - return BalanceInfo.fromUtxoList(_utxos); - } + BalanceInfo balanceNanoWit() => + BalanceInfo.fromUtxoList(utxoMap().keys.toList()); Future generateKey({ required int index, @@ -600,6 +590,119 @@ class Wallet { } } + Map utxoMap([bool includeTimeLocked = true]) { + Map _utxoMap = {}; + + void updateMapByAccount(Account account) { + account.utxos.forEach((utxo) { + if (includeTimeLocked) { + _utxoMap[utxo] = account.address; + } else { + if (!utxoLocked(utxo)) { + _utxoMap[utxo] = account.address; + } + } + }); + } + + internalAccounts.forEach((address, account) { + updateMapByAccount(account); + }); + externalAccounts.forEach((address, account) { + updateMapByAccount(account); + }); + if (masterAccount != null) { + updateMapByAccount(masterAccount!); + } + + return _utxoMap; + } + + bool utxoLocked(Utxo utxo) { + if (utxo.timelock > 0) { + int _ts = utxo.timelock * 1000; + DateTime _timelock = DateTime.fromMillisecondsSinceEpoch(_ts); + int currentTimestamp = DateTime.now().millisecondsSinceEpoch; + if (_timelock.millisecondsSinceEpoch > currentTimestamp) return true; + return false; + } + return false; + } + + Account? utxoOwner(Utxo utxo) { + return null; + } + + List buildInputUtxoList(List utxos) { + List _inputs = []; + + /// loop through utxos + for (int i = 0; i < utxos.length; i++) { + Utxo currentUtxo = utxos.elementAt(i); + + /// loop though every external account + externalAccounts.forEach((index, account) { + if (account.utxos.contains(currentUtxo)) { + _inputs.add(InputUtxo( + address: account.address, + input: currentUtxo.toInput(), + value: currentUtxo.value)); + } + }); + + /// loop though every internal account + internalAccounts.forEach((index, account) { + if (account.utxos.contains(currentUtxo)) { + _inputs.add(InputUtxo( + address: account.address, + input: currentUtxo.toInput(), + value: currentUtxo.value)); + } + }); + + if (walletType == WalletType.single && masterAccount != null) { + _inputs.add(InputUtxo( + address: masterAccount!.address, + input: currentUtxo.toInput(), + value: currentUtxo.value)); + } + } + + return _inputs; + } + + Future getChangeAccount() async { + /// get the internal account that will be used for any change + bool changeAccountSet = false; + + if (walletType == WalletType.hd) { + for (int i = 0; i < internalAccounts.keys.length; i++) { + if (!changeAccountSet) { + Account account = internalAccounts[i]!; + if (account.vttHashes.isEmpty) { + changeAccount = account; + changeAccountSet = true; + } + } + } + + /// did we run out of change addresses? + if (!changeAccountSet) { + ApiCrypto apiCrypto = Locator.instance(); + changeAccount = await apiCrypto.generateAccount( + this, + KeyType.internal, + internalAccounts.keys.length, + ); + } + } else { + /// master node + changeAccount = masterAccount!; + } + + return changeAccount!; + } + void printDebug() { print('Wallet'); print(' ID: $id'); diff --git a/lib/widgets/witnet/transactions/value_transfer/create_dialog_box/qr_scanner.dart b/lib/widgets/witnet/transactions/value_transfer/create_dialog_box/qr_scanner.dart index 69e6e174c..3ec78de3c 100644 --- a/lib/widgets/witnet/transactions/value_transfer/create_dialog_box/qr_scanner.dart +++ b/lib/widgets/witnet/transactions/value_transfer/create_dialog_box/qr_scanner.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; @@ -13,6 +15,29 @@ class QrScanner extends StatelessWidget { required this.onChanged, }) : super(key: key); + static Color iconColor(bool isScanQrFocused, ThemeData theme) => + isScanQrFocused + ? theme.textSelectionTheme.cursorColor! + : theme.inputDecorationTheme.enabledBorder!.borderSide.color; + + static Icon icon = Icon(FontAwesomeIcons.qrcode); + + static Widget? iconButton({ + FocusNode? focusNode, + void Function()? onPressed, + required bool isScanQrFocused, + required ThemeData theme, + }) { + return !Platform.isWindows && !Platform.isLinux + ? IconButton( + focusNode: focusNode, + splashRadius: 1, + icon: QrScanner.icon, + onPressed: () => onPressed, + color: QrScanner.iconColor(isScanQrFocused, theme)) + : null; + } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/lib/widgets/witnet/transactions/value_transfer/create_dialog_box/vtt_builder/01_recipient_step.dart b/lib/widgets/witnet/transactions/value_transfer/create_dialog_box/vtt_builder/01_recipient_step.dart index a17c199e6..dc3f3ffad 100644 --- a/lib/widgets/witnet/transactions/value_transfer/create_dialog_box/vtt_builder/01_recipient_step.dart +++ b/lib/widgets/witnet/transactions/value_transfer/create_dialog_box/vtt_builder/01_recipient_step.dart @@ -4,7 +4,6 @@ import 'package:my_wit_wallet/screens/send_transaction/send_vtt_screen.dart'; import 'package:my_wit_wallet/util/extensions/num_extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:my_wit_wallet/util/showTxConnectionError.dart'; import 'package:my_wit_wallet/widgets/snack_bars.dart'; import 'package:my_wit_wallet/widgets/validations/address_input.dart'; @@ -17,7 +16,6 @@ import 'package:my_wit_wallet/util/storage/database/balance_info.dart'; import 'package:my_wit_wallet/util/storage/database/wallet.dart'; import 'package:my_wit_wallet/widgets/input_amount.dart'; import 'package:my_wit_wallet/util/extensions/text_input_formatter.dart'; -import 'dart:io' show Platform; import 'package:my_wit_wallet/widgets/witnet/transactions/value_transfer/create_dialog_box/qr_scanner.dart'; class RecipientStep extends StatefulWidget { @@ -146,9 +144,17 @@ class RecipientStepState extends State BlocProvider.of(context).add(ResetTransactionEvent()); } + bool _validate() { + final vttBloc = BlocProvider.of(context); + vttBloc.add(ValidateTransactionEvent()); + + return true; + } + void nextAction() { final theme = Theme.of(context); final vttBloc = BlocProvider.of(context); + if (_connectionError) { ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).showSnackBar(buildErrorSnackbar( @@ -171,6 +177,7 @@ class RecipientStepState extends State }), merge: true)); } + _validate(); } NavAction next() { @@ -180,6 +187,52 @@ class RecipientStepState extends State ); } + _activateQrScanner(BuildContext context, ThemeData theme) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => QrScanner( + currentRoute: CreateVttScreen.route, + onChanged: (String value) => { + Navigator.popUntil( + context, ModalRoute.withName(CreateVttScreen.route)), + _addressController.text = value, + setAddress(value), + }, + ), + ), + ); + } + + _recipientAddressTextFormField(BuildContext context, ThemeData theme) { + return TextFormField( + style: theme.textTheme.bodyLarge, + decoration: InputDecoration( + hintText: 'Recipient address', + suffixIcon: QrScanner.iconButton( + focusNode: _scanQrFocusNode, + isScanQrFocused: isScanQrFocused, + onPressed: () => _activateQrScanner(context, theme), + theme: theme, + ), + errorText: _address.error, + ), + controller: _addressController, + focusNode: _addressFocusNode, + keyboardType: TextInputType.text, + inputFormatters: [WitAddressFormatter()], + onChanged: (String value) { + setAddress(value); + }, + onFieldSubmitted: (String value) { + _amountFocusNode.requestFocus(); + }, + onTap: () { + _addressFocusNode.requestFocus(); + }, + ); + } + _buildForm(BuildContext context, ThemeData theme) { _addressFocusNode.addListener(() => validateForm()); _amountFocusNode.addListener(() => validateForm()); @@ -196,53 +249,7 @@ class RecipientStepState extends State style: theme.textTheme.titleSmall, ), SizedBox(height: 8), - TextFormField( - style: theme.textTheme.bodyLarge, - decoration: InputDecoration( - hintText: 'Recipient address', - suffixIcon: !Platform.isWindows && !Platform.isLinux - ? IconButton( - focusNode: _scanQrFocusNode, - splashRadius: 1, - icon: Icon(FontAwesomeIcons.qrcode), - onPressed: () => { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => QrScanner( - currentRoute: CreateVttScreen.route, - onChanged: (String value) => { - Navigator.popUntil( - context, - ModalRoute.withName( - CreateVttScreen - .route)), - _addressController.text = - value, - setAddress(value) - }))) - }, - color: isScanQrFocused - ? theme.textSelectionTheme.cursorColor - : theme.inputDecorationTheme.enabledBorder - ?.borderSide.color) - : null, - errorText: _address.error, - ), - controller: _addressController, - focusNode: _addressFocusNode, - keyboardType: TextInputType.text, - inputFormatters: [WitAddressFormatter()], - onChanged: (String value) { - setAddress(value); - }, - onFieldSubmitted: (String value) { - _amountFocusNode.requestFocus(); - }, - onTap: () { - _addressFocusNode.requestFocus(); - }, - ), + _recipientAddressTextFormField(context, theme), SizedBox(height: 16), Text( 'Amount', diff --git a/lib/widgets/witnet/transactions/value_transfer/create_dialog_box/vtt_builder/02_select_miner_fee.dart b/lib/widgets/witnet/transactions/value_transfer/create_dialog_box/vtt_builder/02_select_miner_fee.dart index 5c84e765e..d0c967156 100644 --- a/lib/widgets/witnet/transactions/value_transfer/create_dialog_box/vtt_builder/02_select_miner_fee.dart +++ b/lib/widgets/witnet/transactions/value_transfer/create_dialog_box/vtt_builder/02_select_miner_fee.dart @@ -63,7 +63,7 @@ class SelectMinerFeeStepState extends State duration: const Duration(milliseconds: 400), ); _minerFeeOptionsNanoWit = - BlocProvider.of(context).minerFeeOptions; + BlocProvider.of(context).vttBuilder.minerFeeOptions; WidgetsBinding.instance.addPostFrameCallback( (_) => {widget.nextAction(next), _setSavedFeeData()}); } @@ -118,6 +118,7 @@ class SelectMinerFeeStepState extends State void setMinerFeeValue(String amount, {bool? validate}) { int weightedFeeAmount = BlocProvider.of(context) + .vttBuilder .calculatedWeightedFee(_minerFeeWitToNanoWitNumber()); _minerFeeWit = VttAmountInput.dirty( allowValidation: diff --git a/lib/widgets/witnet/transactions/value_transfer/create_dialog_box/vtt_builder/03_review_step.dart b/lib/widgets/witnet/transactions/value_transfer/create_dialog_box/vtt_builder/03_review_step.dart index f761532a0..ddcc04806 100644 --- a/lib/widgets/witnet/transactions/value_transfer/create_dialog_box/vtt_builder/03_review_step.dart +++ b/lib/widgets/witnet/transactions/value_transfer/create_dialog_box/vtt_builder/03_review_step.dart @@ -72,7 +72,7 @@ class ReviewStepState extends State @override Widget build(BuildContext context) { final theme = Theme.of(context); - int fee = BlocProvider.of(context).getFee(); + int fee = BlocProvider.of(context).vttBuilder.getFee(); return BlocBuilder( builder: (context, state) { if (state.vttCreateStatus == VTTCreateStatus.exception) {