mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-01-24 19:25:52 +00:00
refactored ba/nano wallets
This commit is contained in:
parent
8ba998af8f
commit
c381326dd5
16 changed files with 851 additions and 2097 deletions
|
@ -486,34 +486,41 @@ class _NewWalletRecoveryPhraseWarningViewState
|
||||||
String? mnemonic;
|
String? mnemonic;
|
||||||
String? privateKey;
|
String? privateKey;
|
||||||
|
|
||||||
// TODO: [prio=high] finish fleshing this out
|
wordCount =
|
||||||
if (coin.hasMnemonicPassphraseSupport) {
|
Constants.defaultSeedPhraseLengthFor(
|
||||||
|
coin: info.coin,
|
||||||
|
);
|
||||||
|
if (wordCount > 0) {
|
||||||
if (ref
|
if (ref
|
||||||
.read(pNewWalletOptions.state)
|
.read(pNewWalletOptions.state)
|
||||||
.state !=
|
.state !=
|
||||||
null) {
|
null) {
|
||||||
|
if (coin.hasMnemonicPassphraseSupport) {
|
||||||
mnemonicPassphrase = ref
|
mnemonicPassphrase = ref
|
||||||
.read(pNewWalletOptions.state)
|
.read(pNewWalletOptions.state)
|
||||||
.state!
|
.state!
|
||||||
.mnemonicPassphrase;
|
.mnemonicPassphrase;
|
||||||
|
} else {}
|
||||||
|
|
||||||
wordCount = ref
|
wordCount = ref
|
||||||
.read(pNewWalletOptions.state)
|
.read(pNewWalletOptions.state)
|
||||||
.state!
|
.state!
|
||||||
.mnemonicWordsCount;
|
.mnemonicWordsCount;
|
||||||
} else {
|
} else {
|
||||||
wordCount = 12;
|
|
||||||
mnemonicPassphrase = "";
|
mnemonicPassphrase = "";
|
||||||
}
|
}
|
||||||
final int strength;
|
|
||||||
if (wordCount == 12) {
|
if (wordCount < 12 ||
|
||||||
strength = 128;
|
24 < wordCount ||
|
||||||
} else if (wordCount == 24) {
|
wordCount % 3 != 0) {
|
||||||
strength = 256;
|
|
||||||
} else {
|
|
||||||
throw Exception("Invalid word count");
|
throw Exception("Invalid word count");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final strength = (wordCount ~/ 3) * 32;
|
||||||
|
|
||||||
mnemonic = bip39.generateMnemonic(
|
mnemonic = bip39.generateMnemonic(
|
||||||
strength: strength);
|
strength: strength,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final wallet = await Wallet.create(
|
final wallet = await Wallet.create(
|
||||||
|
|
|
@ -8,7 +8,6 @@ import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:stackwallet/notifications/show_flush_bar.dart';
|
import 'package:stackwallet/notifications/show_flush_bar.dart';
|
||||||
import 'package:stackwallet/providers/global/wallets_provider.dart';
|
import 'package:stackwallet/providers/global/wallets_provider.dart';
|
||||||
import 'package:stackwallet/services/coins/banano/banano_wallet.dart';
|
|
||||||
import 'package:stackwallet/services/monkey_service.dart';
|
import 'package:stackwallet/services/monkey_service.dart';
|
||||||
import 'package:stackwallet/themes/coin_icon_provider.dart';
|
import 'package:stackwallet/themes/coin_icon_provider.dart';
|
||||||
import 'package:stackwallet/themes/stack_colors.dart';
|
import 'package:stackwallet/themes/stack_colors.dart';
|
||||||
|
@ -17,6 +16,7 @@ import 'package:stackwallet/utilities/show_loading.dart';
|
||||||
import 'package:stackwallet/utilities/text_styles.dart';
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
import 'package:stackwallet/utilities/util.dart';
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
|
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
|
||||||
|
import 'package:stackwallet/wallets/wallet/impl/banano_wallet.dart';
|
||||||
import 'package:stackwallet/widgets/background.dart';
|
import 'package:stackwallet/widgets/background.dart';
|
||||||
import 'package:stackwallet/widgets/conditional_parent.dart';
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
|
|
@ -16,16 +16,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:stackwallet/notifications/show_flush_bar.dart';
|
import 'package:stackwallet/notifications/show_flush_bar.dart';
|
||||||
import 'package:stackwallet/providers/global/wallets_provider.dart';
|
import 'package:stackwallet/providers/global/wallets_provider.dart';
|
||||||
import 'package:stackwallet/services/coins/banano/banano_wallet.dart';
|
|
||||||
import 'package:stackwallet/services/coins/nano/nano_wallet.dart';
|
|
||||||
import 'package:stackwallet/themes/stack_colors.dart';
|
import 'package:stackwallet/themes/stack_colors.dart';
|
||||||
import 'package:stackwallet/utilities/assets.dart';
|
import 'package:stackwallet/utilities/assets.dart';
|
||||||
import 'package:stackwallet/utilities/clipboard_interface.dart';
|
import 'package:stackwallet/utilities/clipboard_interface.dart';
|
||||||
import 'package:stackwallet/utilities/constants.dart';
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
|
||||||
import 'package:stackwallet/utilities/show_loading.dart';
|
import 'package:stackwallet/utilities/show_loading.dart';
|
||||||
import 'package:stackwallet/utilities/text_styles.dart';
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
import 'package:stackwallet/utilities/util.dart';
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
import 'package:stackwallet/wallets/wallet/mixins/nano_based.dart';
|
||||||
import 'package:stackwallet/widgets/background.dart';
|
import 'package:stackwallet/widgets/background.dart';
|
||||||
import 'package:stackwallet/widgets/conditional_parent.dart';
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
@ -51,10 +49,12 @@ class ChangeRepresentativeView extends ConsumerStatefulWidget {
|
||||||
static const String routeName = "/changeRepresentative";
|
static const String routeName = "/changeRepresentative";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<ChangeRepresentativeView> createState() => _XPubViewState();
|
ConsumerState<ChangeRepresentativeView> createState() =>
|
||||||
|
_ChangeRepresentativeViewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _XPubViewState extends ConsumerState<ChangeRepresentativeView> {
|
class _ChangeRepresentativeViewState
|
||||||
|
extends ConsumerState<ChangeRepresentativeView> {
|
||||||
final _textController = TextEditingController();
|
final _textController = TextEditingController();
|
||||||
final _textFocusNode = FocusNode();
|
final _textFocusNode = FocusNode();
|
||||||
final bool isDesktop = Util.isDesktop;
|
final bool isDesktop = Util.isDesktop;
|
||||||
|
@ -65,23 +65,18 @@ class _XPubViewState extends ConsumerState<ChangeRepresentativeView> {
|
||||||
|
|
||||||
Future<String> loadRepresentative() async {
|
Future<String> loadRepresentative() async {
|
||||||
final wallet = ref.read(pWallets).getWallet(widget.walletId);
|
final wallet = ref.read(pWallets).getWallet(widget.walletId);
|
||||||
final coin = wallet.info.coin;
|
|
||||||
|
|
||||||
if (coin == Coin.nano) {
|
if (wallet is NanoBased) {
|
||||||
return (wallet as NanoWallet).getCurrentRepresentative();
|
return wallet.getCurrentRepresentative();
|
||||||
} else if (coin == Coin.banano) {
|
} else {
|
||||||
return (wallet as BananoWallet).getCurrentRepresentative();
|
|
||||||
}
|
|
||||||
throw Exception("Unsupported wallet attempted to show representative!");
|
throw Exception("Unsupported wallet attempted to show representative!");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _save() async {
|
Future<void> _save() async {
|
||||||
final wallet = ref.read(pWallets).getWallet(widget.walletId);
|
final wallet = ref.read(pWallets).getWallet(widget.walletId) as NanoBased;
|
||||||
final coin = wallet.info.coin;
|
|
||||||
|
|
||||||
final changeFuture = coin == Coin.nano
|
final changeFuture = wallet.changeRepresentative;
|
||||||
? (wallet as NanoWallet).changeRepresentative
|
|
||||||
: (wallet as BananoWallet).changeRepresentative;
|
|
||||||
|
|
||||||
final result = await showLoading(
|
final result = await showLoading(
|
||||||
whileFuture: changeFuture(_textController.text),
|
whileFuture: changeFuture(_textController.text),
|
||||||
|
|
|
@ -33,7 +33,6 @@ import 'package:stackwallet/providers/global/active_wallet_provider.dart';
|
||||||
import 'package:stackwallet/providers/global/auto_swb_service_provider.dart';
|
import 'package:stackwallet/providers/global/auto_swb_service_provider.dart';
|
||||||
import 'package:stackwallet/providers/providers.dart';
|
import 'package:stackwallet/providers/providers.dart';
|
||||||
import 'package:stackwallet/providers/ui/transaction_filter_provider.dart';
|
import 'package:stackwallet/providers/ui/transaction_filter_provider.dart';
|
||||||
import 'package:stackwallet/services/coins/banano/banano_wallet.dart';
|
|
||||||
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
|
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
|
||||||
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
|
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
|
||||||
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
|
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
|
||||||
|
@ -43,6 +42,7 @@ import 'package:stackwallet/utilities/assets.dart';
|
||||||
import 'package:stackwallet/utilities/enums/backup_frequency_type.dart';
|
import 'package:stackwallet/utilities/enums/backup_frequency_type.dart';
|
||||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||||
import 'package:stackwallet/utilities/text_styles.dart';
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/wallets/wallet/impl/banano_wallet.dart';
|
||||||
import 'package:stackwallet/widgets/background.dart';
|
import 'package:stackwallet/widgets/background.dart';
|
||||||
import 'package:stackwallet/widgets/conditional_parent.dart';
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
@ -193,9 +193,7 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
|
||||||
final wallet = ref.watch(pWallets).getWallet(widget.walletId);
|
final wallet = ref.watch(pWallets).getWallet(widget.walletId);
|
||||||
final walletInfo = wallet.info;
|
final walletInfo = wallet.info;
|
||||||
|
|
||||||
final monke = wallet.info.coin == Coin.banano
|
final monke = wallet is BananoWallet ? wallet.getMonkeyImageBytes() : null;
|
||||||
? (wallet as BananoWallet).getMonkeyImageBytes()
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return ConditionalParent(
|
return ConditionalParent(
|
||||||
condition: _rescanningOnOpen,
|
condition: _rescanningOnOpen,
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -14,14 +14,12 @@ import 'package:stackwallet/models/balance.dart';
|
||||||
import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models;
|
import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models;
|
||||||
import 'package:stackwallet/models/node_model.dart';
|
import 'package:stackwallet/models/node_model.dart';
|
||||||
import 'package:stackwallet/models/paymint/fee_object_model.dart';
|
import 'package:stackwallet/models/paymint/fee_object_model.dart';
|
||||||
import 'package:stackwallet/services/coins/banano/banano_wallet.dart';
|
|
||||||
import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart';
|
import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart';
|
||||||
import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart';
|
import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart';
|
||||||
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
|
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
|
||||||
import 'package:stackwallet/services/coins/litecoin/litecoin_wallet.dart';
|
import 'package:stackwallet/services/coins/litecoin/litecoin_wallet.dart';
|
||||||
import 'package:stackwallet/services/coins/monero/monero_wallet.dart';
|
import 'package:stackwallet/services/coins/monero/monero_wallet.dart';
|
||||||
import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart';
|
import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart';
|
||||||
import 'package:stackwallet/services/coins/nano/nano_wallet.dart';
|
|
||||||
import 'package:stackwallet/services/coins/particl/particl_wallet.dart';
|
import 'package:stackwallet/services/coins/particl/particl_wallet.dart';
|
||||||
import 'package:stackwallet/services/coins/stellar/stellar_wallet.dart';
|
import 'package:stackwallet/services/coins/stellar/stellar_wallet.dart';
|
||||||
import 'package:stackwallet/services/coins/tezos/tezos_wallet.dart';
|
import 'package:stackwallet/services/coins/tezos/tezos_wallet.dart';
|
||||||
|
@ -217,20 +215,10 @@ abstract class CoinServiceAPI {
|
||||||
);
|
);
|
||||||
|
|
||||||
case Coin.nano:
|
case Coin.nano:
|
||||||
return NanoWallet(
|
throw UnimplementedError("moved");
|
||||||
walletId: walletId,
|
|
||||||
walletName: walletName,
|
|
||||||
coin: coin,
|
|
||||||
tracker: tracker,
|
|
||||||
secureStore: secureStorageInterface);
|
|
||||||
|
|
||||||
case Coin.banano:
|
case Coin.banano:
|
||||||
return BananoWallet(
|
throw UnimplementedError("moved");
|
||||||
walletId: walletId,
|
|
||||||
walletName: walletName,
|
|
||||||
coin: coin,
|
|
||||||
tracker: tracker,
|
|
||||||
secureStore: secureStorageInterface);
|
|
||||||
|
|
||||||
case Coin.dogecoinTestNet:
|
case Coin.dogecoinTestNet:
|
||||||
throw UnimplementedError("moved");
|
throw UnimplementedError("moved");
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -277,6 +277,9 @@ abstract class Constants {
|
||||||
|
|
||||||
case Coin.monero:
|
case Coin.monero:
|
||||||
return 25;
|
return 25;
|
||||||
|
//
|
||||||
|
// default:
|
||||||
|
// -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
25
lib/wallets/crypto_currency/coins/banano.dart
Normal file
25
lib/wallets/crypto_currency/coins/banano.dart
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import 'package:nanodart/nanodart.dart';
|
||||||
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||||
|
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
|
||||||
|
import 'package:stackwallet/wallets/crypto_currency/intermediate/nano_currency.dart';
|
||||||
|
|
||||||
|
class Banano extends NanoCurrency {
|
||||||
|
Banano(super.network) {
|
||||||
|
switch (network) {
|
||||||
|
case CryptoCurrencyNetwork.main:
|
||||||
|
coin = Coin.banano;
|
||||||
|
default:
|
||||||
|
throw Exception("Unsupported network: $network");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get minConfirms => 1;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get defaultRepresentative =>
|
||||||
|
"ban_1ka1ium4pfue3uxtntqsrib8mumxgazsjf58gidh1xeo5te3whsq8z476goo";
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get nanoAccountType => NanoAccountType.BANANO;
|
||||||
|
}
|
25
lib/wallets/crypto_currency/coins/nano.dart
Normal file
25
lib/wallets/crypto_currency/coins/nano.dart
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import 'package:nanodart/nanodart.dart';
|
||||||
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||||
|
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
|
||||||
|
import 'package:stackwallet/wallets/crypto_currency/intermediate/nano_currency.dart';
|
||||||
|
|
||||||
|
class Nano extends NanoCurrency {
|
||||||
|
Nano(super.network) {
|
||||||
|
switch (network) {
|
||||||
|
case CryptoCurrencyNetwork.main:
|
||||||
|
coin = Coin.nano;
|
||||||
|
default:
|
||||||
|
throw Exception("Unsupported network: $network");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get minConfirms => 1;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get defaultRepresentative =>
|
||||||
|
"nano_38713x95zyjsqzx6nm1dsom1jmm668owkeb9913ax6nfgj15az3nu8xkx579";
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get nanoAccountType => NanoAccountType.NANO;
|
||||||
|
}
|
21
lib/wallets/crypto_currency/intermediate/nano_currency.dart
Normal file
21
lib/wallets/crypto_currency/intermediate/nano_currency.dart
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import 'package:nanodart/nanodart.dart';
|
||||||
|
import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_currency.dart';
|
||||||
|
|
||||||
|
abstract class NanoCurrency extends Bip39Currency {
|
||||||
|
NanoCurrency(super.network);
|
||||||
|
|
||||||
|
String get defaultRepresentative;
|
||||||
|
|
||||||
|
int get nanoAccountType;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool validateAddress(String address) => NanoAccounts.isValid(
|
||||||
|
nanoAccountType,
|
||||||
|
address,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get genesisHash => throw UnimplementedError(
|
||||||
|
"Not used in nano based coins",
|
||||||
|
);
|
||||||
|
}
|
|
@ -215,6 +215,26 @@ class WalletInfo implements IsarId {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// update [otherData] with the map entries in [newEntries]
|
||||||
|
Future<void> updateOtherData({
|
||||||
|
required Map<String, dynamic> newEntries,
|
||||||
|
required Isar isar,
|
||||||
|
}) async {
|
||||||
|
final Map<String, dynamic> newMap = {};
|
||||||
|
newMap.addAll(otherData);
|
||||||
|
newMap.addAll(newEntries);
|
||||||
|
final encodedNew = jsonEncode(newMap);
|
||||||
|
|
||||||
|
// only update if there were changes
|
||||||
|
if (_otherDataJsonString != encodedNew) {
|
||||||
|
_otherDataJsonString = encodedNew;
|
||||||
|
await isar.writeTxn(() async {
|
||||||
|
await isar.walletInfo.deleteByWalletId(walletId);
|
||||||
|
await isar.walletInfo.put(this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// copies this with a new name and updates the db
|
/// copies this with a new name and updates the db
|
||||||
Future<void> setMnemonicVerified({
|
Future<void> setMnemonicVerified({
|
||||||
required Isar isar,
|
required Isar isar,
|
||||||
|
@ -317,4 +337,5 @@ abstract class WalletInfoKeys {
|
||||||
static const String tokenContractAddresses = "tokenContractAddressesKey";
|
static const String tokenContractAddresses = "tokenContractAddressesKey";
|
||||||
static const String cachedSecondaryBalance = "cachedSecondaryBalanceKey";
|
static const String cachedSecondaryBalance = "cachedSecondaryBalanceKey";
|
||||||
static const String epiccashData = "epiccashDataKey";
|
static const String epiccashData = "epiccashDataKey";
|
||||||
|
static const String bananoMonkeyImageBytes = "monkeyImageBytesKey";
|
||||||
}
|
}
|
||||||
|
|
27
lib/wallets/wallet/impl/banano_wallet.dart
Normal file
27
lib/wallets/wallet/impl/banano_wallet.dart
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import 'package:stackwallet/wallets/crypto_currency/coins/banano.dart';
|
||||||
|
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
|
||||||
|
import 'package:stackwallet/wallets/crypto_currency/intermediate/nano_currency.dart';
|
||||||
|
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
|
||||||
|
import 'package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart';
|
||||||
|
import 'package:stackwallet/wallets/wallet/mixins/nano_based.dart';
|
||||||
|
|
||||||
|
class BananoWallet extends Bip39Wallet<NanoCurrency> with NanoBased {
|
||||||
|
BananoWallet(CryptoCurrencyNetwork network) : super(Banano(network));
|
||||||
|
|
||||||
|
Future<void> updateMonkeyImageBytes(List<int> bytes) async {
|
||||||
|
await info.updateOtherData(
|
||||||
|
newEntries: {
|
||||||
|
WalletInfoKeys.bananoMonkeyImageBytes: bytes,
|
||||||
|
},
|
||||||
|
isar: mainDB.isar,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int>? getMonkeyImageBytes() {
|
||||||
|
final list = info.otherData[WalletInfoKeys.bananoMonkeyImageBytes] as List?;
|
||||||
|
if (list == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return List<int>.from(list);
|
||||||
|
}
|
||||||
|
}
|
9
lib/wallets/wallet/impl/nano_wallet.dart
Normal file
9
lib/wallets/wallet/impl/nano_wallet.dart
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import 'package:stackwallet/wallets/crypto_currency/coins/nano.dart';
|
||||||
|
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
|
||||||
|
import 'package:stackwallet/wallets/crypto_currency/intermediate/nano_currency.dart';
|
||||||
|
import 'package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart';
|
||||||
|
import 'package:stackwallet/wallets/wallet/mixins/nano_based.dart';
|
||||||
|
|
||||||
|
class NanoWallet extends Bip39Wallet<NanoCurrency> with NanoBased {
|
||||||
|
NanoWallet(CryptoCurrencyNetwork network) : super(Nano(network));
|
||||||
|
}
|
675
lib/wallets/wallet/mixins/nano_based.dart
Normal file
675
lib/wallets/wallet/mixins/nano_based.dart
Normal file
|
@ -0,0 +1,675 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:isar/isar.dart';
|
||||||
|
import 'package:nanodart/nanodart.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/networking/http.dart';
|
||||||
|
import 'package:stackwallet/services/nano_api.dart';
|
||||||
|
import 'package:stackwallet/services/node_service.dart';
|
||||||
|
import 'package:stackwallet/services/tor_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/crypto_currency/intermediate/nano_currency.dart';
|
||||||
|
import 'package:stackwallet/wallets/models/tx_data.dart';
|
||||||
|
import 'package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart';
|
||||||
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
|
const _kWorkServer = "https://rpc.nano.to";
|
||||||
|
|
||||||
|
mixin NanoBased<T extends NanoCurrency> on Bip39Wallet<T> {
|
||||||
|
// since nano based coins only have a single address/account we can cache
|
||||||
|
// the address instead of fetching from db every time we need it in certain
|
||||||
|
// cases
|
||||||
|
Address? _cachedAddress;
|
||||||
|
|
||||||
|
NodeModel? _cachedNode;
|
||||||
|
|
||||||
|
final _httpClient = HTTP();
|
||||||
|
|
||||||
|
Future<String?> _requestWork(String hash) async {
|
||||||
|
return _httpClient
|
||||||
|
.post(
|
||||||
|
url: Uri.parse(_kWorkServer), // this should be a
|
||||||
|
headers: {'Content-type': 'application/json'},
|
||||||
|
body: json.encode(
|
||||||
|
{
|
||||||
|
"action": "work_generate",
|
||||||
|
"hash": hash,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
proxyInfo: prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
|
||||||
|
)
|
||||||
|
.then((_httpClient) {
|
||||||
|
if (_httpClient.code == 200) {
|
||||||
|
final Map<String, dynamic> decoded =
|
||||||
|
json.decode(_httpClient.body) as Map<String, dynamic>;
|
||||||
|
if (decoded.containsKey("error")) {
|
||||||
|
throw Exception("Received error ${decoded["error"]}");
|
||||||
|
}
|
||||||
|
return decoded["work"] as String?;
|
||||||
|
} else {
|
||||||
|
throw Exception("Received error ${_httpClient.code}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _getPrivateKeyFromMnemonic() async {
|
||||||
|
final mnemonicList = await getMnemonicAsWords();
|
||||||
|
final seed = NanoMnemomics.mnemonicListToSeed(mnemonicList);
|
||||||
|
return NanoKeys.seedToPrivate(seed, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Address> _getAddressFromMnemonic() async {
|
||||||
|
final publicKey = NanoKeys.createPublicKey(
|
||||||
|
await _getPrivateKeyFromMnemonic(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final addressString =
|
||||||
|
NanoAccounts.createAccount(cryptoCurrency.nanoAccountType, publicKey);
|
||||||
|
|
||||||
|
return Address(
|
||||||
|
walletId: walletId,
|
||||||
|
value: addressString,
|
||||||
|
publicKey: publicKey.toUint8ListFromHex,
|
||||||
|
derivationIndex: 0,
|
||||||
|
derivationPath: null,
|
||||||
|
type: cryptoCurrency.coin.primaryAddressType,
|
||||||
|
subType: AddressSubType.receiving,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _receiveBlock(
|
||||||
|
String blockHash,
|
||||||
|
String source,
|
||||||
|
String amountRaw,
|
||||||
|
String publicAddress,
|
||||||
|
) async {
|
||||||
|
// TODO: the opening block of an account is a special case
|
||||||
|
bool openBlock = false;
|
||||||
|
|
||||||
|
final headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
// first check if the account is open:
|
||||||
|
// get the account info (we need the frontier and representative):
|
||||||
|
final infoBody = jsonEncode({
|
||||||
|
"action": "account_info",
|
||||||
|
"representative": "true",
|
||||||
|
"account": publicAddress,
|
||||||
|
});
|
||||||
|
final infoResponse = await _httpClient.post(
|
||||||
|
url: Uri.parse(getCurrentNode().host),
|
||||||
|
headers: headers,
|
||||||
|
body: infoBody,
|
||||||
|
proxyInfo: prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
|
||||||
|
);
|
||||||
|
final infoData = jsonDecode(infoResponse.body);
|
||||||
|
|
||||||
|
if (infoData["error"] != null) {
|
||||||
|
// account is not open yet, we need to create an open block:
|
||||||
|
openBlock = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// first get the account balance:
|
||||||
|
final balanceBody = jsonEncode({
|
||||||
|
"action": "account_balance",
|
||||||
|
"account": publicAddress,
|
||||||
|
});
|
||||||
|
|
||||||
|
final balanceResponse = await _httpClient.post(
|
||||||
|
url: Uri.parse(getCurrentNode().host),
|
||||||
|
headers: headers,
|
||||||
|
body: balanceBody,
|
||||||
|
proxyInfo: prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
final balanceData = jsonDecode(balanceResponse.body);
|
||||||
|
final BigInt currentBalance =
|
||||||
|
BigInt.parse(balanceData["balance"].toString());
|
||||||
|
final BigInt txAmount = BigInt.parse(amountRaw);
|
||||||
|
final BigInt balanceAfterTx = currentBalance + txAmount;
|
||||||
|
|
||||||
|
String frontier = infoData["frontier"].toString();
|
||||||
|
String representative = infoData["representative"].toString();
|
||||||
|
|
||||||
|
if (openBlock) {
|
||||||
|
// we don't have a representative set yet:
|
||||||
|
representative = cryptoCurrency.defaultRepresentative;
|
||||||
|
}
|
||||||
|
|
||||||
|
// link = send block hash:
|
||||||
|
final String link = blockHash;
|
||||||
|
// this "linkAsAccount" is meaningless:
|
||||||
|
final String linkAsAccount =
|
||||||
|
NanoAccounts.createAccount(NanoAccountType.BANANO, blockHash);
|
||||||
|
|
||||||
|
// construct the receive block:
|
||||||
|
Map<String, String> receiveBlock = {
|
||||||
|
"type": "state",
|
||||||
|
"account": publicAddress,
|
||||||
|
"previous": openBlock
|
||||||
|
? "0000000000000000000000000000000000000000000000000000000000000000"
|
||||||
|
: frontier,
|
||||||
|
"representative": representative,
|
||||||
|
"balance": balanceAfterTx.toString(),
|
||||||
|
"link": link,
|
||||||
|
"link_as_account": linkAsAccount,
|
||||||
|
};
|
||||||
|
|
||||||
|
// sign the receive block:
|
||||||
|
final String hash = NanoBlocks.computeStateHash(
|
||||||
|
NanoAccountType.BANANO,
|
||||||
|
receiveBlock["account"]!,
|
||||||
|
receiveBlock["previous"]!,
|
||||||
|
receiveBlock["representative"]!,
|
||||||
|
BigInt.parse(receiveBlock["balance"]!),
|
||||||
|
receiveBlock["link"]!,
|
||||||
|
);
|
||||||
|
final String privateKey = await _getPrivateKeyFromMnemonic();
|
||||||
|
final String signature = NanoSignatures.signBlock(hash, privateKey);
|
||||||
|
|
||||||
|
// get PoW for the receive block:
|
||||||
|
String? work;
|
||||||
|
if (openBlock) {
|
||||||
|
work = await _requestWork(NanoAccounts.extractPublicKey(publicAddress));
|
||||||
|
} else {
|
||||||
|
work = await _requestWork(frontier);
|
||||||
|
}
|
||||||
|
if (work == null) {
|
||||||
|
throw Exception("Failed to get PoW for receive block");
|
||||||
|
}
|
||||||
|
receiveBlock["link_as_account"] = linkAsAccount;
|
||||||
|
receiveBlock["signature"] = signature;
|
||||||
|
receiveBlock["work"] = work;
|
||||||
|
|
||||||
|
// process the receive block:
|
||||||
|
|
||||||
|
final processBody = jsonEncode({
|
||||||
|
"action": "process",
|
||||||
|
"json_block": "true",
|
||||||
|
"subtype": "receive",
|
||||||
|
"block": receiveBlock,
|
||||||
|
});
|
||||||
|
final processResponse = await _httpClient.post(
|
||||||
|
url: Uri.parse(getCurrentNode().host),
|
||||||
|
headers: headers,
|
||||||
|
body: processBody,
|
||||||
|
proxyInfo: prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
final Map<String, dynamic> decoded =
|
||||||
|
json.decode(processResponse.body) as Map<String, dynamic>;
|
||||||
|
if (decoded.containsKey("error")) {
|
||||||
|
throw Exception("Received error ${decoded["error"]}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _confirmAllReceivable(String accountAddress) async {
|
||||||
|
final receivableResponse = await _httpClient.post(
|
||||||
|
url: Uri.parse(getCurrentNode().host),
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: jsonEncode({
|
||||||
|
"action": "receivable",
|
||||||
|
"source": "true",
|
||||||
|
"account": accountAddress,
|
||||||
|
"count": "-1",
|
||||||
|
}),
|
||||||
|
proxyInfo: prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
final receivableData = await jsonDecode(receivableResponse.body);
|
||||||
|
if (receivableData["blocks"] == "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final blocks = receivableData["blocks"] as Map<String, dynamic>;
|
||||||
|
// confirm all receivable blocks:
|
||||||
|
for (final blockHash in blocks.keys) {
|
||||||
|
final block = blocks[blockHash];
|
||||||
|
final String amountRaw = block["amount"] as String;
|
||||||
|
final String source = block["source"] as String;
|
||||||
|
await _receiveBlock(blockHash, source, amountRaw, accountAddress);
|
||||||
|
// a bit of a hack:
|
||||||
|
await Future<void>.delayed(const Duration(seconds: 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//========= public ===========================================================
|
||||||
|
|
||||||
|
Future<String> getCurrentRepresentative() async {
|
||||||
|
final serverURI = Uri.parse(getCurrentNode().host);
|
||||||
|
final address =
|
||||||
|
(_cachedAddress ?? await getCurrentReceivingAddress())!.value;
|
||||||
|
|
||||||
|
final response = await NanoAPI.getAccountInfo(
|
||||||
|
server: serverURI,
|
||||||
|
representative: true,
|
||||||
|
account: address,
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.accountInfo?.representative ??
|
||||||
|
cryptoCurrency.defaultRepresentative;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> changeRepresentative(String newRepresentative) async {
|
||||||
|
try {
|
||||||
|
final serverURI = Uri.parse(getCurrentNode().host);
|
||||||
|
await updateBalance();
|
||||||
|
final balance = info.cachedBalance.spendable.raw.toString();
|
||||||
|
final String privateKey = await _getPrivateKeyFromMnemonic();
|
||||||
|
final address =
|
||||||
|
(_cachedAddress ?? await getCurrentReceivingAddress())!.value;
|
||||||
|
|
||||||
|
final response = await NanoAPI.getAccountInfo(
|
||||||
|
server: serverURI,
|
||||||
|
representative: true,
|
||||||
|
account: address,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.accountInfo == null) {
|
||||||
|
throw response.exception ?? Exception("Failed to get account info");
|
||||||
|
}
|
||||||
|
|
||||||
|
final work = await _requestWork(response.accountInfo!.frontier);
|
||||||
|
|
||||||
|
return await NanoAPI.changeRepresentative(
|
||||||
|
server: serverURI,
|
||||||
|
accountType: NanoAccountType.BANANO,
|
||||||
|
account: address,
|
||||||
|
newRepresentative: newRepresentative,
|
||||||
|
previousBlock: response.accountInfo!.frontier,
|
||||||
|
balance: balance,
|
||||||
|
privateKey: privateKey,
|
||||||
|
work: work!,
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//========= overrides ========================================================
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> updateNode() async {
|
||||||
|
_cachedNode = NodeService(secureStorageInterface: secureStorageInterface)
|
||||||
|
.getPrimaryNodeFor(coin: info.coin) ??
|
||||||
|
DefaultNodes.getNodeFor(info.coin);
|
||||||
|
|
||||||
|
unawaited(refresh());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
NodeModel getCurrentNode() {
|
||||||
|
return _cachedNode ??
|
||||||
|
NodeService(secureStorageInterface: secureStorageInterface)
|
||||||
|
.getPrimaryNodeFor(coin: info.coin) ??
|
||||||
|
DefaultNodes.getNodeFor(info.coin);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> init() async {
|
||||||
|
_cachedAddress = await getCurrentReceivingAddress();
|
||||||
|
if (_cachedAddress == null) {
|
||||||
|
_cachedAddress = await _getAddressFromMnemonic();
|
||||||
|
await mainDB.putAddress(_cachedAddress!);
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> pingCheck() async {
|
||||||
|
final uri = Uri.parse(getCurrentNode().host);
|
||||||
|
|
||||||
|
final response = await _httpClient.post(
|
||||||
|
url: uri,
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: jsonEncode(
|
||||||
|
{
|
||||||
|
"action": "version",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
proxyInfo: prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.code == 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<TxData> prepareSend({required TxData txData}) async {
|
||||||
|
if (txData.recipients!.length != 1) {
|
||||||
|
throw ArgumentError(
|
||||||
|
"${cryptoCurrency.runtimeType} currently only "
|
||||||
|
"supports one recipient per transaction",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return txData.copyWith(
|
||||||
|
fee: Amount(
|
||||||
|
rawValue: BigInt.zero,
|
||||||
|
fractionDigits: cryptoCurrency.fractionDigits,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<TxData> confirmSend({required TxData txData}) async {
|
||||||
|
try {
|
||||||
|
// our address:
|
||||||
|
final String publicAddress =
|
||||||
|
(_cachedAddress ?? await getCurrentReceivingAddress())!.value;
|
||||||
|
|
||||||
|
// first update to get latest account balance:
|
||||||
|
|
||||||
|
final currentBalance = info.cachedBalance.spendable;
|
||||||
|
final txAmount = txData.amount!;
|
||||||
|
final BigInt balanceAfterTx = (currentBalance - txAmount).raw;
|
||||||
|
|
||||||
|
// get the account info (we need the frontier and representative):
|
||||||
|
final infoBody = jsonEncode({
|
||||||
|
"action": "account_info",
|
||||||
|
"representative": "true",
|
||||||
|
"account": publicAddress,
|
||||||
|
});
|
||||||
|
|
||||||
|
final headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
final infoResponse = await _httpClient.post(
|
||||||
|
url: Uri.parse(getCurrentNode().host),
|
||||||
|
headers: headers,
|
||||||
|
body: infoBody,
|
||||||
|
proxyInfo:
|
||||||
|
prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
final String frontier =
|
||||||
|
jsonDecode(infoResponse.body)["frontier"].toString();
|
||||||
|
final String representative =
|
||||||
|
jsonDecode(infoResponse.body)["representative"].toString();
|
||||||
|
// link = destination address:
|
||||||
|
final String linkAsAccount = txData.recipients!.first.address;
|
||||||
|
final String link = NanoAccounts.extractPublicKey(linkAsAccount);
|
||||||
|
|
||||||
|
// construct the send block:
|
||||||
|
Map<String, String> sendBlock = {
|
||||||
|
"type": "state",
|
||||||
|
"account": publicAddress,
|
||||||
|
"previous": frontier,
|
||||||
|
"representative": representative,
|
||||||
|
"balance": balanceAfterTx.toString(),
|
||||||
|
"link": link,
|
||||||
|
};
|
||||||
|
|
||||||
|
// sign the send block:
|
||||||
|
final String hash = NanoBlocks.computeStateHash(
|
||||||
|
NanoAccountType.BANANO,
|
||||||
|
sendBlock["account"]!,
|
||||||
|
sendBlock["previous"]!,
|
||||||
|
sendBlock["representative"]!,
|
||||||
|
BigInt.parse(sendBlock["balance"]!),
|
||||||
|
sendBlock["link"]!,
|
||||||
|
);
|
||||||
|
final String privateKey = await _getPrivateKeyFromMnemonic();
|
||||||
|
final String signature = NanoSignatures.signBlock(hash, privateKey);
|
||||||
|
|
||||||
|
// get PoW for the send block:
|
||||||
|
final String? work = await _requestWork(frontier);
|
||||||
|
if (work == null) {
|
||||||
|
throw Exception("Failed to get PoW for send block");
|
||||||
|
}
|
||||||
|
|
||||||
|
sendBlock["link_as_account"] = linkAsAccount;
|
||||||
|
sendBlock["signature"] = signature;
|
||||||
|
sendBlock["work"] = work;
|
||||||
|
|
||||||
|
final processBody = jsonEncode({
|
||||||
|
"action": "process",
|
||||||
|
"json_block": "true",
|
||||||
|
"subtype": "send",
|
||||||
|
"block": sendBlock,
|
||||||
|
});
|
||||||
|
final processResponse = await _httpClient.post(
|
||||||
|
url: Uri.parse(getCurrentNode().host),
|
||||||
|
headers: headers,
|
||||||
|
body: processBody,
|
||||||
|
proxyInfo:
|
||||||
|
prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
final Map<String, dynamic> decoded =
|
||||||
|
json.decode(processResponse.body) as Map<String, dynamic>;
|
||||||
|
if (decoded.containsKey("error")) {
|
||||||
|
throw Exception("Received error ${decoded["error"]}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the hash of the transaction:
|
||||||
|
return txData.copyWith(
|
||||||
|
txid: decoded["hash"].toString(),
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance
|
||||||
|
.log("Error sending transaction $e - $s", level: LogLevel.Error);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> recover({required bool isRescan}) async {
|
||||||
|
try {
|
||||||
|
await refreshMutex.protect(() async {
|
||||||
|
if (isRescan) {
|
||||||
|
await mainDB.deleteWalletBlockchainData(walletId);
|
||||||
|
}
|
||||||
|
_cachedAddress = await _getAddressFromMnemonic();
|
||||||
|
|
||||||
|
await mainDB.putAddress(_cachedAddress!);
|
||||||
|
});
|
||||||
|
|
||||||
|
await refresh();
|
||||||
|
} catch (e) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> updateTransactions() async {
|
||||||
|
final receivingAddress =
|
||||||
|
(_cachedAddress ?? await getCurrentReceivingAddress())!;
|
||||||
|
final String publicAddress = receivingAddress.value;
|
||||||
|
await _confirmAllReceivable(publicAddress);
|
||||||
|
final response = await _httpClient.post(
|
||||||
|
url: Uri.parse(getCurrentNode().host),
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: jsonEncode({
|
||||||
|
"action": "account_history",
|
||||||
|
"account": publicAddress,
|
||||||
|
"count": "-1",
|
||||||
|
}),
|
||||||
|
proxyInfo: prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
|
||||||
|
);
|
||||||
|
final data = await jsonDecode(response.body);
|
||||||
|
final transactions =
|
||||||
|
data["history"] is List ? data["history"] as List<dynamic> : [];
|
||||||
|
if (transactions.isEmpty) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
List<Tuple2<Transaction, Address?>> transactionList = [];
|
||||||
|
for (var tx in transactions) {
|
||||||
|
var typeString = tx["type"].toString();
|
||||||
|
TransactionType transactionType = TransactionType.unknown;
|
||||||
|
if (typeString == "send") {
|
||||||
|
transactionType = TransactionType.outgoing;
|
||||||
|
} else if (typeString == "receive") {
|
||||||
|
transactionType = TransactionType.incoming;
|
||||||
|
}
|
||||||
|
final amount = Amount(
|
||||||
|
rawValue: BigInt.parse(tx["amount"].toString()),
|
||||||
|
fractionDigits: cryptoCurrency.fractionDigits,
|
||||||
|
);
|
||||||
|
|
||||||
|
var transaction = Transaction(
|
||||||
|
walletId: walletId,
|
||||||
|
txid: tx["hash"].toString(),
|
||||||
|
timestamp: int.parse(tx["local_timestamp"].toString()),
|
||||||
|
type: transactionType,
|
||||||
|
subType: TransactionSubType.none,
|
||||||
|
amount: 0,
|
||||||
|
amountString: amount.toJsonString(),
|
||||||
|
fee: 0,
|
||||||
|
height: int.parse(tx["height"].toString()),
|
||||||
|
isCancelled: false,
|
||||||
|
isLelantus: false,
|
||||||
|
slateId: "",
|
||||||
|
otherData: "",
|
||||||
|
inputs: [],
|
||||||
|
outputs: [],
|
||||||
|
nonce: 0,
|
||||||
|
numberOfMessages: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
Address address = transactionType == TransactionType.incoming
|
||||||
|
? receivingAddress
|
||||||
|
: Address(
|
||||||
|
walletId: walletId,
|
||||||
|
publicKey: [],
|
||||||
|
value: tx["account"].toString(),
|
||||||
|
derivationIndex: 0,
|
||||||
|
derivationPath: null,
|
||||||
|
type: info.coin.primaryAddressType,
|
||||||
|
subType: AddressSubType.nonWallet,
|
||||||
|
);
|
||||||
|
Tuple2<Transaction, Address> tuple = Tuple2(transaction, address);
|
||||||
|
transactionList.add(tuple);
|
||||||
|
}
|
||||||
|
|
||||||
|
await mainDB.addNewTransactionData(transactionList, walletId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> updateBalance() async {
|
||||||
|
try {
|
||||||
|
final addressString =
|
||||||
|
(_cachedAddress ??= (await getCurrentReceivingAddress())!).value;
|
||||||
|
final body = jsonEncode({
|
||||||
|
"action": "account_balance",
|
||||||
|
"account": addressString,
|
||||||
|
});
|
||||||
|
final headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
final response = await _httpClient.post(
|
||||||
|
url: Uri.parse(getCurrentNode().host),
|
||||||
|
headers: headers,
|
||||||
|
body: body,
|
||||||
|
proxyInfo:
|
||||||
|
prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
|
||||||
|
);
|
||||||
|
final data = jsonDecode(response.body);
|
||||||
|
final balance = Balance(
|
||||||
|
total: Amount(
|
||||||
|
rawValue: (BigInt.parse(data["balance"].toString()) +
|
||||||
|
BigInt.parse(data["receivable"].toString())),
|
||||||
|
fractionDigits: cryptoCurrency.fractionDigits,
|
||||||
|
),
|
||||||
|
spendable: Amount(
|
||||||
|
rawValue: BigInt.parse(data["balance"].toString()),
|
||||||
|
fractionDigits: cryptoCurrency.fractionDigits,
|
||||||
|
),
|
||||||
|
blockedTotal: Amount(
|
||||||
|
rawValue: BigInt.parse("0"),
|
||||||
|
fractionDigits: cryptoCurrency.fractionDigits,
|
||||||
|
),
|
||||||
|
pendingSpendable: Amount(
|
||||||
|
rawValue: BigInt.parse(data["receivable"].toString()),
|
||||||
|
fractionDigits: cryptoCurrency.fractionDigits,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await info.updateBalance(newBalance: balance, isar: mainDB.isar);
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"Failed to update ${cryptoCurrency.runtimeType} balance: $e\n$s",
|
||||||
|
level: LogLevel.Warning,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> updateChainHeight() async {
|
||||||
|
try {
|
||||||
|
final String publicAddress =
|
||||||
|
(_cachedAddress ??= (await getCurrentReceivingAddress())!).value;
|
||||||
|
|
||||||
|
final infoBody = jsonEncode({
|
||||||
|
"action": "account_info",
|
||||||
|
"account": publicAddress,
|
||||||
|
});
|
||||||
|
final headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
final infoResponse = await _httpClient.post(
|
||||||
|
url: Uri.parse(getCurrentNode().host),
|
||||||
|
headers: headers,
|
||||||
|
body: infoBody,
|
||||||
|
proxyInfo:
|
||||||
|
prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
|
||||||
|
);
|
||||||
|
final infoData = jsonDecode(infoResponse.body);
|
||||||
|
|
||||||
|
final height = int.tryParse(
|
||||||
|
infoData["confirmation_height"].toString(),
|
||||||
|
) ??
|
||||||
|
0;
|
||||||
|
|
||||||
|
await info.updateCachedChainHeight(newHeight: height, isar: mainDB.isar);
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"Failed to update ${cryptoCurrency.runtimeType} chain height: $e\n$s",
|
||||||
|
level: LogLevel.Warning,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FilterOperation? get changeAddressFilterOperation =>
|
||||||
|
FilterGroup.and(standardChangeAddressFilters);
|
||||||
|
|
||||||
|
@override
|
||||||
|
FilterOperation? get receivingAddressFilterOperation =>
|
||||||
|
FilterGroup.and(standardReceivingAddressFilters);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> updateUTXOs() async {
|
||||||
|
// do nothing for nano based coins
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
// nano has no fees
|
||||||
|
Future<Amount> estimateFeeFor(Amount amount, int feeRate) async => Amount(
|
||||||
|
rawValue: BigInt.from(0),
|
||||||
|
fractionDigits: cryptoCurrency.fractionDigits,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
// nano has no fees
|
||||||
|
Future<FeeObject> get fees async => FeeObject(
|
||||||
|
numberOfBlocksFast: 1,
|
||||||
|
numberOfBlocksAverage: 1,
|
||||||
|
numberOfBlocksSlow: 1,
|
||||||
|
fast: 0,
|
||||||
|
medium: 0,
|
||||||
|
slow: 0,
|
||||||
|
);
|
||||||
|
}
|
|
@ -22,11 +22,13 @@ import 'package:stackwallet/utilities/prefs.dart';
|
||||||
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
|
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
|
||||||
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
|
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
|
||||||
import 'package:stackwallet/wallets/models/tx_data.dart';
|
import 'package:stackwallet/wallets/models/tx_data.dart';
|
||||||
|
import 'package:stackwallet/wallets/wallet/impl/banano_wallet.dart';
|
||||||
import 'package:stackwallet/wallets/wallet/impl/bitcoin_wallet.dart';
|
import 'package:stackwallet/wallets/wallet/impl/bitcoin_wallet.dart';
|
||||||
import 'package:stackwallet/wallets/wallet/impl/bitcoincash_wallet.dart';
|
import 'package:stackwallet/wallets/wallet/impl/bitcoincash_wallet.dart';
|
||||||
import 'package:stackwallet/wallets/wallet/impl/dogecoin_wallet.dart';
|
import 'package:stackwallet/wallets/wallet/impl/dogecoin_wallet.dart';
|
||||||
import 'package:stackwallet/wallets/wallet/impl/ecash_wallet.dart';
|
import 'package:stackwallet/wallets/wallet/impl/ecash_wallet.dart';
|
||||||
import 'package:stackwallet/wallets/wallet/impl/epiccash_wallet.dart';
|
import 'package:stackwallet/wallets/wallet/impl/epiccash_wallet.dart';
|
||||||
|
import 'package:stackwallet/wallets/wallet/impl/nano_wallet.dart';
|
||||||
import 'package:stackwallet/wallets/wallet/impl/wownero_wallet.dart';
|
import 'package:stackwallet/wallets/wallet/impl/wownero_wallet.dart';
|
||||||
import 'package:stackwallet/wallets/wallet/mixins/electrumx.dart';
|
import 'package:stackwallet/wallets/wallet/mixins/electrumx.dart';
|
||||||
import 'package:stackwallet/wallets/wallet/mixins/mnemonic_based_wallet.dart';
|
import 'package:stackwallet/wallets/wallet/mixins/mnemonic_based_wallet.dart';
|
||||||
|
@ -238,6 +240,9 @@ abstract class Wallet<T extends CryptoCurrency> {
|
||||||
required WalletInfo walletInfo,
|
required WalletInfo walletInfo,
|
||||||
}) {
|
}) {
|
||||||
switch (walletInfo.coin) {
|
switch (walletInfo.coin) {
|
||||||
|
case Coin.banano:
|
||||||
|
return BananoWallet(CryptoCurrencyNetwork.main);
|
||||||
|
|
||||||
case Coin.bitcoin:
|
case Coin.bitcoin:
|
||||||
return BitcoinWallet(CryptoCurrencyNetwork.main);
|
return BitcoinWallet(CryptoCurrencyNetwork.main);
|
||||||
case Coin.bitcoinTestNet:
|
case Coin.bitcoinTestNet:
|
||||||
|
@ -259,6 +264,9 @@ abstract class Wallet<T extends CryptoCurrency> {
|
||||||
case Coin.epicCash:
|
case Coin.epicCash:
|
||||||
return EpiccashWallet(CryptoCurrencyNetwork.main);
|
return EpiccashWallet(CryptoCurrencyNetwork.main);
|
||||||
|
|
||||||
|
case Coin.nano:
|
||||||
|
return NanoWallet(CryptoCurrencyNetwork.main);
|
||||||
|
|
||||||
case Coin.wownero:
|
case Coin.wownero:
|
||||||
return WowneroWallet(CryptoCurrencyNetwork.main);
|
return WowneroWallet(CryptoCurrencyNetwork.main);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue