mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-01-09 12:19:24 +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? privateKey;
|
||||
|
||||
// TODO: [prio=high] finish fleshing this out
|
||||
if (coin.hasMnemonicPassphraseSupport) {
|
||||
wordCount =
|
||||
Constants.defaultSeedPhraseLengthFor(
|
||||
coin: info.coin,
|
||||
);
|
||||
if (wordCount > 0) {
|
||||
if (ref
|
||||
.read(pNewWalletOptions.state)
|
||||
.state !=
|
||||
null) {
|
||||
if (coin.hasMnemonicPassphraseSupport) {
|
||||
mnemonicPassphrase = ref
|
||||
.read(pNewWalletOptions.state)
|
||||
.state!
|
||||
.mnemonicPassphrase;
|
||||
} else {}
|
||||
|
||||
wordCount = ref
|
||||
.read(pNewWalletOptions.state)
|
||||
.state!
|
||||
.mnemonicWordsCount;
|
||||
} else {
|
||||
wordCount = 12;
|
||||
mnemonicPassphrase = "";
|
||||
}
|
||||
final int strength;
|
||||
if (wordCount == 12) {
|
||||
strength = 128;
|
||||
} else if (wordCount == 24) {
|
||||
strength = 256;
|
||||
} else {
|
||||
|
||||
if (wordCount < 12 ||
|
||||
24 < wordCount ||
|
||||
wordCount % 3 != 0) {
|
||||
throw Exception("Invalid word count");
|
||||
}
|
||||
|
||||
final strength = (wordCount ~/ 3) * 32;
|
||||
|
||||
mnemonic = bip39.generateMnemonic(
|
||||
strength: strength);
|
||||
strength: strength,
|
||||
);
|
||||
}
|
||||
|
||||
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:stackwallet/notifications/show_flush_bar.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/themes/coin_icon_provider.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/util.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/conditional_parent.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:stackwallet/notifications/show_flush_bar.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/utilities/assets.dart';
|
||||
import 'package:stackwallet/utilities/clipboard_interface.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/text_styles.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/conditional_parent.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";
|
||||
|
||||
@override
|
||||
ConsumerState<ChangeRepresentativeView> createState() => _XPubViewState();
|
||||
ConsumerState<ChangeRepresentativeView> createState() =>
|
||||
_ChangeRepresentativeViewState();
|
||||
}
|
||||
|
||||
class _XPubViewState extends ConsumerState<ChangeRepresentativeView> {
|
||||
class _ChangeRepresentativeViewState
|
||||
extends ConsumerState<ChangeRepresentativeView> {
|
||||
final _textController = TextEditingController();
|
||||
final _textFocusNode = FocusNode();
|
||||
final bool isDesktop = Util.isDesktop;
|
||||
|
@ -65,23 +65,18 @@ class _XPubViewState extends ConsumerState<ChangeRepresentativeView> {
|
|||
|
||||
Future<String> loadRepresentative() async {
|
||||
final wallet = ref.read(pWallets).getWallet(widget.walletId);
|
||||
final coin = wallet.info.coin;
|
||||
|
||||
if (coin == Coin.nano) {
|
||||
return (wallet as NanoWallet).getCurrentRepresentative();
|
||||
} else if (coin == Coin.banano) {
|
||||
return (wallet as BananoWallet).getCurrentRepresentative();
|
||||
}
|
||||
if (wallet is NanoBased) {
|
||||
return wallet.getCurrentRepresentative();
|
||||
} else {
|
||||
throw Exception("Unsupported wallet attempted to show representative!");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
final wallet = ref.read(pWallets).getWallet(widget.walletId);
|
||||
final coin = wallet.info.coin;
|
||||
final wallet = ref.read(pWallets).getWallet(widget.walletId) as NanoBased;
|
||||
|
||||
final changeFuture = coin == Coin.nano
|
||||
? (wallet as NanoWallet).changeRepresentative
|
||||
: (wallet as BananoWallet).changeRepresentative;
|
||||
final changeFuture = wallet.changeRepresentative;
|
||||
|
||||
final result = await showLoading(
|
||||
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/providers.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/event_bus/events/global/wallet_sync_status_changed_event.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/coin_enum.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/conditional_parent.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 walletInfo = wallet.info;
|
||||
|
||||
final monke = wallet.info.coin == Coin.banano
|
||||
? (wallet as BananoWallet).getMonkeyImageBytes()
|
||||
: null;
|
||||
final monke = wallet is BananoWallet ? wallet.getMonkeyImageBytes() : null;
|
||||
|
||||
return ConditionalParent(
|
||||
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/node_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/ethereum/ethereum_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/monero/monero_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/stellar/stellar_wallet.dart';
|
||||
import 'package:stackwallet/services/coins/tezos/tezos_wallet.dart';
|
||||
|
@ -217,20 +215,10 @@ abstract class CoinServiceAPI {
|
|||
);
|
||||
|
||||
case Coin.nano:
|
||||
return NanoWallet(
|
||||
walletId: walletId,
|
||||
walletName: walletName,
|
||||
coin: coin,
|
||||
tracker: tracker,
|
||||
secureStore: secureStorageInterface);
|
||||
throw UnimplementedError("moved");
|
||||
|
||||
case Coin.banano:
|
||||
return BananoWallet(
|
||||
walletId: walletId,
|
||||
walletName: walletName,
|
||||
coin: coin,
|
||||
tracker: tracker,
|
||||
secureStore: secureStorageInterface);
|
||||
throw UnimplementedError("moved");
|
||||
|
||||
case Coin.dogecoinTestNet:
|
||||
throw UnimplementedError("moved");
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -277,6 +277,9 @@ abstract class Constants {
|
|||
|
||||
case Coin.monero:
|
||||
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
|
||||
Future<void> setMnemonicVerified({
|
||||
required Isar isar,
|
||||
|
@ -317,4 +337,5 @@ abstract class WalletInfoKeys {
|
|||
static const String tokenContractAddresses = "tokenContractAddressesKey";
|
||||
static const String cachedSecondaryBalance = "cachedSecondaryBalanceKey";
|
||||
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/isar/models/wallet_info.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/bitcoincash_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/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/mixins/electrumx.dart';
|
||||
import 'package:stackwallet/wallets/wallet/mixins/mnemonic_based_wallet.dart';
|
||||
|
@ -238,6 +240,9 @@ abstract class Wallet<T extends CryptoCurrency> {
|
|||
required WalletInfo walletInfo,
|
||||
}) {
|
||||
switch (walletInfo.coin) {
|
||||
case Coin.banano:
|
||||
return BananoWallet(CryptoCurrencyNetwork.main);
|
||||
|
||||
case Coin.bitcoin:
|
||||
return BitcoinWallet(CryptoCurrencyNetwork.main);
|
||||
case Coin.bitcoinTestNet:
|
||||
|
@ -259,6 +264,9 @@ abstract class Wallet<T extends CryptoCurrency> {
|
|||
case Coin.epicCash:
|
||||
return EpiccashWallet(CryptoCurrencyNetwork.main);
|
||||
|
||||
case Coin.nano:
|
||||
return NanoWallet(CryptoCurrencyNetwork.main);
|
||||
|
||||
case Coin.wownero:
|
||||
return WowneroWallet(CryptoCurrencyNetwork.main);
|
||||
|
||||
|
|
Loading…
Reference in a new issue